Home | History | Annotate | Download | only in app
      1 /*
      2  * Copyright (C) 2010 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.gallery3d.app;
     18 
     19 import android.app.ActionBar;
     20 import android.app.ProgressDialog;
     21 import android.app.WallpaperManager;
     22 import android.content.ContentValues;
     23 import android.content.Intent;
     24 import android.graphics.Bitmap;
     25 import android.graphics.Bitmap.CompressFormat;
     26 import android.graphics.Bitmap.Config;
     27 import android.graphics.BitmapFactory;
     28 import android.graphics.BitmapRegionDecoder;
     29 import android.graphics.Canvas;
     30 import android.graphics.Paint;
     31 import android.graphics.Rect;
     32 import android.graphics.RectF;
     33 import android.media.ExifInterface;
     34 import android.net.Uri;
     35 import android.os.Bundle;
     36 import android.os.Environment;
     37 import android.os.Handler;
     38 import android.os.Message;
     39 import android.provider.MediaStore;
     40 import android.provider.MediaStore.Images;
     41 import android.view.Menu;
     42 import android.view.MenuItem;
     43 import android.view.Window;
     44 import android.widget.Toast;
     45 
     46 import com.android.gallery3d.R;
     47 import com.android.gallery3d.common.BitmapUtils;
     48 import com.android.gallery3d.common.Utils;
     49 import com.android.gallery3d.data.DataManager;
     50 import com.android.gallery3d.data.LocalImage;
     51 import com.android.gallery3d.data.MediaItem;
     52 import com.android.gallery3d.data.MediaObject;
     53 import com.android.gallery3d.data.Path;
     54 import com.android.gallery3d.picasasource.PicasaSource;
     55 import com.android.gallery3d.ui.BitmapTileProvider;
     56 import com.android.gallery3d.ui.CropView;
     57 import com.android.gallery3d.ui.GLRoot;
     58 import com.android.gallery3d.ui.SynchronizedHandler;
     59 import com.android.gallery3d.ui.TileImageViewAdapter;
     60 import com.android.gallery3d.util.Future;
     61 import com.android.gallery3d.util.FutureListener;
     62 import com.android.gallery3d.util.GalleryUtils;
     63 import com.android.gallery3d.util.InterruptableOutputStream;
     64 import com.android.gallery3d.util.ThreadPool.CancelListener;
     65 import com.android.gallery3d.util.ThreadPool.Job;
     66 import com.android.gallery3d.util.ThreadPool.JobContext;
     67 
     68 import java.io.File;
     69 import java.io.FileNotFoundException;
     70 import java.io.FileOutputStream;
     71 import java.io.IOException;
     72 import java.io.OutputStream;
     73 import java.text.SimpleDateFormat;
     74 import java.util.Date;
     75 
     76 /**
     77  * The activity can crop specific region of interest from an image.
     78  */
     79 public class CropImage extends AbstractGalleryActivity {
     80     private static final String TAG = "CropImage";
     81     public static final String ACTION_CROP = "com.android.camera.action.CROP";
     82 
     83     private static final int MAX_PIXEL_COUNT = 5 * 1000000; // 5M pixels
     84     private static final int MAX_FILE_INDEX = 1000;
     85     private static final int TILE_SIZE = 512;
     86     private static final int BACKUP_PIXEL_COUNT = 480000; // around 800x600
     87 
     88     private static final int MSG_LARGE_BITMAP = 1;
     89     private static final int MSG_BITMAP = 2;
     90     private static final int MSG_SAVE_COMPLETE = 3;
     91     private static final int MSG_SHOW_SAVE_ERROR = 4;
     92 
     93     private static final int MAX_BACKUP_IMAGE_SIZE = 320;
     94     private static final int DEFAULT_COMPRESS_QUALITY = 90;
     95     private static final String TIME_STAMP_NAME = "'IMG'_yyyyMMdd_HHmmss";
     96 
     97     // Change these to Images.Media.WIDTH/HEIGHT after they are unhidden.
     98     private static final String WIDTH = "width";
     99     private static final String HEIGHT = "height";
    100 
    101     public static final String KEY_RETURN_DATA = "return-data";
    102     public static final String KEY_CROPPED_RECT = "cropped-rect";
    103     public static final String KEY_ASPECT_X = "aspectX";
    104     public static final String KEY_ASPECT_Y = "aspectY";
    105     public static final String KEY_SPOTLIGHT_X = "spotlightX";
    106     public static final String KEY_SPOTLIGHT_Y = "spotlightY";
    107     public static final String KEY_OUTPUT_X = "outputX";
    108     public static final String KEY_OUTPUT_Y = "outputY";
    109     public static final String KEY_SCALE = "scale";
    110     public static final String KEY_DATA = "data";
    111     public static final String KEY_SCALE_UP_IF_NEEDED = "scaleUpIfNeeded";
    112     public static final String KEY_OUTPUT_FORMAT = "outputFormat";
    113     public static final String KEY_SET_AS_WALLPAPER = "set-as-wallpaper";
    114     public static final String KEY_NO_FACE_DETECTION = "noFaceDetection";
    115 
    116     private static final String KEY_STATE = "state";
    117 
    118     private static final int STATE_INIT = 0;
    119     private static final int STATE_LOADED = 1;
    120     private static final int STATE_SAVING = 2;
    121 
    122     public static final String DOWNLOAD_STRING = "download";
    123     public static final File DOWNLOAD_BUCKET = new File(
    124             Environment.getExternalStorageDirectory(), DOWNLOAD_STRING);
    125 
    126     public static final String CROP_ACTION = "com.android.camera.action.CROP";
    127 
    128     private int mState = STATE_INIT;
    129 
    130     private CropView mCropView;
    131 
    132     private boolean mDoFaceDetection = true;
    133 
    134     private Handler mMainHandler;
    135 
    136     // We keep the following members so that we can free them
    137 
    138     // mBitmap is the unrotated bitmap we pass in to mCropView for detect faces.
    139     // mCropView is responsible for rotating it to the way that it is viewed by users.
    140     private Bitmap mBitmap;
    141     private BitmapTileProvider mBitmapTileProvider;
    142     private BitmapRegionDecoder mRegionDecoder;
    143     private Bitmap mBitmapInIntent;
    144     private boolean mUseRegionDecoder = false;
    145 
    146     private ProgressDialog mProgressDialog;
    147     private Future<BitmapRegionDecoder> mLoadTask;
    148     private Future<Bitmap> mLoadBitmapTask;
    149     private Future<Intent> mSaveTask;
    150 
    151     private MediaItem mMediaItem;
    152 
    153     @Override
    154     public void onCreate(Bundle bundle) {
    155         super.onCreate(bundle);
    156         requestWindowFeature(Window.FEATURE_ACTION_BAR);
    157         requestWindowFeature(Window.FEATURE_ACTION_BAR_OVERLAY);
    158 
    159         // Initialize UI
    160         setContentView(R.layout.cropimage);
    161         mCropView = new CropView(this);
    162         getGLRoot().setContentPane(mCropView);
    163 
    164         ActionBar actionBar = getActionBar();
    165         actionBar.setDisplayOptions(ActionBar.DISPLAY_HOME_AS_UP,
    166                 ActionBar.DISPLAY_HOME_AS_UP);
    167 
    168         mMainHandler = new SynchronizedHandler(getGLRoot()) {
    169             @Override
    170             public void handleMessage(Message message) {
    171                 switch (message.what) {
    172                     case MSG_LARGE_BITMAP: {
    173                         mProgressDialog.dismiss();
    174                         onBitmapRegionDecoderAvailable((BitmapRegionDecoder) message.obj);
    175                         break;
    176                     }
    177                     case MSG_BITMAP: {
    178                         mProgressDialog.dismiss();
    179                         onBitmapAvailable((Bitmap) message.obj);
    180                         break;
    181                     }
    182                     case MSG_SHOW_SAVE_ERROR: {
    183                         mProgressDialog.dismiss();
    184                         setResult(RESULT_CANCELED);
    185                         Toast.makeText(CropImage.this,
    186                                 CropImage.this.getString(R.string.save_error),
    187                                 Toast.LENGTH_LONG).show();
    188                         finish();
    189                     }
    190                     case MSG_SAVE_COMPLETE: {
    191                         mProgressDialog.dismiss();
    192                         setResult(RESULT_OK, (Intent) message.obj);
    193                         finish();
    194                         break;
    195                     }
    196                 }
    197             }
    198         };
    199 
    200         setCropParameters();
    201     }
    202 
    203     @Override
    204     protected void onSaveInstanceState(Bundle saveState) {
    205         saveState.putInt(KEY_STATE, mState);
    206     }
    207 
    208     @Override
    209     public boolean onCreateOptionsMenu(Menu menu) {
    210         super.onCreateOptionsMenu(menu);
    211         getMenuInflater().inflate(R.menu.crop, menu);
    212         return true;
    213     }
    214 
    215     @Override
    216     public boolean onOptionsItemSelected(MenuItem item) {
    217         switch (item.getItemId()) {
    218             case android.R.id.home: {
    219                 finish();
    220                 break;
    221             }
    222             case R.id.cancel: {
    223                 setResult(RESULT_CANCELED);
    224                 finish();
    225                 break;
    226             }
    227             case R.id.save: {
    228                 onSaveClicked();
    229                 break;
    230             }
    231         }
    232         return true;
    233     }
    234 
    235     private class SaveOutput implements Job<Intent> {
    236         private final RectF mCropRect;
    237 
    238         public SaveOutput(RectF cropRect) {
    239             mCropRect = cropRect;
    240         }
    241 
    242         public Intent run(JobContext jc) {
    243             RectF cropRect = mCropRect;
    244             Bundle extra = getIntent().getExtras();
    245 
    246             Rect rect = new Rect(
    247                     Math.round(cropRect.left), Math.round(cropRect.top),
    248                     Math.round(cropRect.right), Math.round(cropRect.bottom));
    249 
    250             Intent result = new Intent();
    251             result.putExtra(KEY_CROPPED_RECT, rect);
    252             Bitmap cropped = null;
    253             boolean outputted = false;
    254             if (extra != null) {
    255                 Uri uri = (Uri) extra.getParcelable(MediaStore.EXTRA_OUTPUT);
    256                 if (uri != null) {
    257                     if (jc.isCancelled()) return null;
    258                     outputted = true;
    259                     cropped = getCroppedImage(rect);
    260                     if (!saveBitmapToUri(jc, cropped, uri)) return null;
    261                 }
    262                 if (extra.getBoolean(KEY_RETURN_DATA, false)) {
    263                     if (jc.isCancelled()) return null;
    264                     outputted = true;
    265                     if (cropped == null) cropped = getCroppedImage(rect);
    266                     result.putExtra(KEY_DATA, cropped);
    267                 }
    268                 if (extra.getBoolean(KEY_SET_AS_WALLPAPER, false)) {
    269                     if (jc.isCancelled()) return null;
    270                     outputted = true;
    271                     if (cropped == null) cropped = getCroppedImage(rect);
    272                     if (!setAsWallpaper(jc, cropped)) return null;
    273                 }
    274             }
    275             if (!outputted) {
    276                 if (jc.isCancelled()) return null;
    277                 if (cropped == null) cropped = getCroppedImage(rect);
    278                 Uri data = saveToMediaProvider(jc, cropped);
    279                 if (data != null) result.setData(data);
    280             }
    281             return result;
    282         }
    283     }
    284 
    285     public static String determineCompressFormat(MediaObject obj) {
    286         String compressFormat = "JPEG";
    287         if (obj instanceof MediaItem) {
    288             String mime = ((MediaItem) obj).getMimeType();
    289             if (mime.contains("png") || mime.contains("gif")) {
    290               // Set the compress format to PNG for png and gif images
    291               // because they may contain alpha values.
    292               compressFormat = "PNG";
    293             }
    294         }
    295         return compressFormat;
    296     }
    297 
    298     private boolean setAsWallpaper(JobContext jc, Bitmap wallpaper) {
    299         try {
    300             WallpaperManager.getInstance(this).setBitmap(wallpaper);
    301         } catch (IOException e) {
    302             Log.w(TAG, "fail to set wall paper", e);
    303         }
    304         return true;
    305     }
    306 
    307     private File saveMedia(
    308             JobContext jc, Bitmap cropped, File directory, String filename) {
    309         // Try file-1.jpg, file-2.jpg, ... until we find a filename
    310         // which does not exist yet.
    311         File candidate = null;
    312         String fileExtension = getFileExtension();
    313         for (int i = 1; i < MAX_FILE_INDEX; ++i) {
    314             candidate = new File(directory, filename + "-" + i + "."
    315                     + fileExtension);
    316             try {
    317                 if (candidate.createNewFile()) break;
    318             } catch (IOException e) {
    319                 Log.e(TAG, "fail to create new file: "
    320                         + candidate.getAbsolutePath(), e);
    321                 return null;
    322             }
    323         }
    324         if (!candidate.exists() || !candidate.isFile()) {
    325             throw new RuntimeException("cannot create file: " + filename);
    326         }
    327 
    328         candidate.setReadable(true, false);
    329         candidate.setWritable(true, false);
    330 
    331         try {
    332             FileOutputStream fos = new FileOutputStream(candidate);
    333             try {
    334                 saveBitmapToOutputStream(jc, cropped,
    335                         convertExtensionToCompressFormat(fileExtension), fos);
    336             } finally {
    337                 fos.close();
    338             }
    339         } catch (IOException e) {
    340             Log.e(TAG, "fail to save image: "
    341                     + candidate.getAbsolutePath(), e);
    342             candidate.delete();
    343             return null;
    344         }
    345 
    346         if (jc.isCancelled()) {
    347             candidate.delete();
    348             return null;
    349         }
    350 
    351         return candidate;
    352     }
    353 
    354     private Uri saveToMediaProvider(JobContext jc, Bitmap cropped) {
    355         if (PicasaSource.isPicasaImage(mMediaItem)) {
    356             return savePicasaImage(jc, cropped);
    357         } else if (mMediaItem instanceof LocalImage) {
    358             return saveLocalImage(jc, cropped);
    359         } else {
    360             return saveGenericImage(jc, cropped);
    361         }
    362     }
    363 
    364     private Uri savePicasaImage(JobContext jc, Bitmap cropped) {
    365         if (!DOWNLOAD_BUCKET.isDirectory() && !DOWNLOAD_BUCKET.mkdirs()) {
    366             throw new RuntimeException("cannot create download folder");
    367         }
    368 
    369         String filename = PicasaSource.getImageTitle(mMediaItem);
    370         int pos = filename.lastIndexOf('.');
    371         if (pos >= 0) filename = filename.substring(0, pos);
    372         File output = saveMedia(jc, cropped, DOWNLOAD_BUCKET, filename);
    373         if (output == null) return null;
    374 
    375         copyExif(mMediaItem, output.getAbsolutePath(), cropped.getWidth(), cropped.getHeight());
    376 
    377         long now = System.currentTimeMillis() / 1000;
    378         ContentValues values = new ContentValues();
    379         values.put(Images.Media.TITLE, PicasaSource.getImageTitle(mMediaItem));
    380         values.put(Images.Media.DISPLAY_NAME, output.getName());
    381         values.put(Images.Media.DATE_TAKEN, PicasaSource.getDateTaken(mMediaItem));
    382         values.put(Images.Media.DATE_MODIFIED, now);
    383         values.put(Images.Media.DATE_ADDED, now);
    384         values.put(Images.Media.MIME_TYPE, getOutputMimeType());
    385         values.put(Images.Media.ORIENTATION, 0);
    386         values.put(Images.Media.DATA, output.getAbsolutePath());
    387         values.put(Images.Media.SIZE, output.length());
    388         values.put(WIDTH, cropped.getWidth());
    389         values.put(HEIGHT, cropped.getHeight());
    390 
    391         double latitude = PicasaSource.getLatitude(mMediaItem);
    392         double longitude = PicasaSource.getLongitude(mMediaItem);
    393         if (GalleryUtils.isValidLocation(latitude, longitude)) {
    394             values.put(Images.Media.LATITUDE, latitude);
    395             values.put(Images.Media.LONGITUDE, longitude);
    396         }
    397         return getContentResolver().insert(
    398                 Images.Media.EXTERNAL_CONTENT_URI, values);
    399     }
    400 
    401     private Uri saveLocalImage(JobContext jc, Bitmap cropped) {
    402         LocalImage localImage = (LocalImage) mMediaItem;
    403 
    404         File oldPath = new File(localImage.filePath);
    405         File directory = new File(oldPath.getParent());
    406 
    407         String filename = oldPath.getName();
    408         int pos = filename.lastIndexOf('.');
    409         if (pos >= 0) filename = filename.substring(0, pos);
    410         File output = saveMedia(jc, cropped, directory, filename);
    411         if (output == null) return null;
    412 
    413         copyExif(oldPath.getAbsolutePath(), output.getAbsolutePath(),
    414                 cropped.getWidth(), cropped.getHeight());
    415 
    416         long now = System.currentTimeMillis() / 1000;
    417         ContentValues values = new ContentValues();
    418         values.put(Images.Media.TITLE, localImage.caption);
    419         values.put(Images.Media.DISPLAY_NAME, output.getName());
    420         values.put(Images.Media.DATE_TAKEN, localImage.dateTakenInMs);
    421         values.put(Images.Media.DATE_MODIFIED, now);
    422         values.put(Images.Media.DATE_ADDED, now);
    423         values.put(Images.Media.MIME_TYPE, getOutputMimeType());
    424         values.put(Images.Media.ORIENTATION, 0);
    425         values.put(Images.Media.DATA, output.getAbsolutePath());
    426         values.put(Images.Media.SIZE, output.length());
    427         values.put(WIDTH, cropped.getWidth());
    428         values.put(HEIGHT, cropped.getHeight());
    429 
    430         if (GalleryUtils.isValidLocation(localImage.latitude, localImage.longitude)) {
    431             values.put(Images.Media.LATITUDE, localImage.latitude);
    432             values.put(Images.Media.LONGITUDE, localImage.longitude);
    433         }
    434         return getContentResolver().insert(
    435                 Images.Media.EXTERNAL_CONTENT_URI, values);
    436     }
    437 
    438     private Uri saveGenericImage(JobContext jc, Bitmap cropped) {
    439         if (!DOWNLOAD_BUCKET.isDirectory() && !DOWNLOAD_BUCKET.mkdirs()) {
    440             throw new RuntimeException("cannot create download folder");
    441         }
    442 
    443         long now = System.currentTimeMillis();
    444         String filename = new SimpleDateFormat(TIME_STAMP_NAME).
    445                 format(new Date(now));
    446 
    447         File output = saveMedia(jc, cropped, DOWNLOAD_BUCKET, filename);
    448         if (output == null) return null;
    449 
    450         ContentValues values = new ContentValues();
    451         values.put(Images.Media.TITLE, filename);
    452         values.put(Images.Media.DISPLAY_NAME, output.getName());
    453         values.put(Images.Media.DATE_TAKEN, now);
    454         values.put(Images.Media.DATE_MODIFIED, now / 1000);
    455         values.put(Images.Media.DATE_ADDED, now / 1000);
    456         values.put(Images.Media.MIME_TYPE, getOutputMimeType());
    457         values.put(Images.Media.ORIENTATION, 0);
    458         values.put(Images.Media.DATA, output.getAbsolutePath());
    459         values.put(Images.Media.SIZE, output.length());
    460         values.put(WIDTH, cropped.getWidth());
    461         values.put(HEIGHT, cropped.getHeight());
    462 
    463         return getContentResolver().insert(
    464                 Images.Media.EXTERNAL_CONTENT_URI, values);
    465     }
    466 
    467     private boolean saveBitmapToOutputStream(
    468             JobContext jc, Bitmap bitmap, CompressFormat format, OutputStream os) {
    469         // We wrap the OutputStream so that it can be interrupted.
    470         final InterruptableOutputStream ios = new InterruptableOutputStream(os);
    471         jc.setCancelListener(new CancelListener() {
    472                 public void onCancel() {
    473                     ios.interrupt();
    474                 }
    475             });
    476         try {
    477             bitmap.compress(format, DEFAULT_COMPRESS_QUALITY, os);
    478             return !jc.isCancelled();
    479         } finally {
    480             jc.setCancelListener(null);
    481             Utils.closeSilently(os);
    482         }
    483     }
    484 
    485     private boolean saveBitmapToUri(JobContext jc, Bitmap bitmap, Uri uri) {
    486         try {
    487             return saveBitmapToOutputStream(jc, bitmap,
    488                     convertExtensionToCompressFormat(getFileExtension()),
    489                     getContentResolver().openOutputStream(uri));
    490         } catch (FileNotFoundException e) {
    491             Log.w(TAG, "cannot write output", e);
    492         }
    493         return true;
    494     }
    495 
    496     private CompressFormat convertExtensionToCompressFormat(String extension) {
    497         return extension.equals("png")
    498                 ? CompressFormat.PNG
    499                 : CompressFormat.JPEG;
    500     }
    501 
    502     private String getOutputMimeType() {
    503         return getFileExtension().equals("png") ? "image/png" : "image/jpeg";
    504     }
    505 
    506     private String getFileExtension() {
    507         String requestFormat = getIntent().getStringExtra(KEY_OUTPUT_FORMAT);
    508         String outputFormat = (requestFormat == null)
    509                 ? determineCompressFormat(mMediaItem)
    510                 : requestFormat;
    511 
    512         outputFormat = outputFormat.toLowerCase();
    513         return (outputFormat.equals("png") || outputFormat.equals("gif"))
    514                 ? "png" // We don't support gif compression.
    515                 : "jpg";
    516     }
    517 
    518     private void onSaveClicked() {
    519         Bundle extra = getIntent().getExtras();
    520         RectF cropRect = mCropView.getCropRectangle();
    521         if (cropRect == null) return;
    522         mState = STATE_SAVING;
    523         int messageId = extra != null && extra.getBoolean(KEY_SET_AS_WALLPAPER)
    524                 ? R.string.wallpaper
    525                 : R.string.saving_image;
    526         mProgressDialog = ProgressDialog.show(
    527                 this, null, getString(messageId), true, false);
    528         mSaveTask = getThreadPool().submit(new SaveOutput(cropRect),
    529                 new FutureListener<Intent>() {
    530             public void onFutureDone(Future<Intent> future) {
    531                 mSaveTask = null;
    532                 if (future.isCancelled()) return;
    533                 Intent intent = future.get();
    534                 if (intent != null) {
    535                     mMainHandler.sendMessage(mMainHandler.obtainMessage(
    536                             MSG_SAVE_COMPLETE, intent));
    537                 } else {
    538                     mMainHandler.sendEmptyMessage(MSG_SHOW_SAVE_ERROR);
    539                 }
    540             }
    541         });
    542     }
    543 
    544     private Bitmap getCroppedImage(Rect rect) {
    545         Utils.assertTrue(rect.width() > 0 && rect.height() > 0);
    546 
    547         Bundle extras = getIntent().getExtras();
    548         // (outputX, outputY) = the width and height of the returning bitmap.
    549         int outputX = rect.width();
    550         int outputY = rect.height();
    551         if (extras != null) {
    552             outputX = extras.getInt(KEY_OUTPUT_X, outputX);
    553             outputY = extras.getInt(KEY_OUTPUT_Y, outputY);
    554         }
    555 
    556         if (outputX * outputY > MAX_PIXEL_COUNT) {
    557             float scale = (float) Math.sqrt(
    558                     (double) MAX_PIXEL_COUNT / outputX / outputY);
    559             Log.w(TAG, "scale down the cropped image: " + scale);
    560             outputX = Math.round(scale * outputX);
    561             outputY = Math.round(scale * outputY);
    562         }
    563 
    564         // (rect.width() * scaleX, rect.height() * scaleY) =
    565         // the size of drawing area in output bitmap
    566         float scaleX = 1;
    567         float scaleY = 1;
    568         Rect dest = new Rect(0, 0, outputX, outputY);
    569         if (extras == null || extras.getBoolean(KEY_SCALE, true)) {
    570             scaleX = (float) outputX / rect.width();
    571             scaleY = (float) outputY / rect.height();
    572             if (extras == null || !extras.getBoolean(
    573                     KEY_SCALE_UP_IF_NEEDED, false)) {
    574                 if (scaleX > 1f) scaleX = 1;
    575                 if (scaleY > 1f) scaleY = 1;
    576             }
    577         }
    578 
    579         // Keep the content in the center (or crop the content)
    580         int rectWidth = Math.round(rect.width() * scaleX);
    581         int rectHeight = Math.round(rect.height() * scaleY);
    582         dest.set(Math.round((outputX - rectWidth) / 2f),
    583                 Math.round((outputY - rectHeight) / 2f),
    584                 Math.round((outputX + rectWidth) / 2f),
    585                 Math.round((outputY + rectHeight) / 2f));
    586 
    587         if (mBitmapInIntent != null) {
    588             Bitmap source = mBitmapInIntent;
    589             Bitmap result = Bitmap.createBitmap(
    590                     outputX, outputY, Config.ARGB_8888);
    591             Canvas canvas = new Canvas(result);
    592             canvas.drawBitmap(source, rect, dest, null);
    593             return result;
    594         }
    595 
    596         if (mUseRegionDecoder) {
    597             int rotation = mMediaItem.getFullImageRotation();
    598             rotateRectangle(rect, mCropView.getImageWidth(),
    599                     mCropView.getImageHeight(), 360 - rotation);
    600             rotateRectangle(dest, outputX, outputY, 360 - rotation);
    601 
    602             BitmapFactory.Options options = new BitmapFactory.Options();
    603             int sample = BitmapUtils.computeSampleSizeLarger(
    604                     Math.max(scaleX, scaleY));
    605             options.inSampleSize = sample;
    606             if ((rect.width() / sample) == dest.width()
    607                     && (rect.height() / sample) == dest.height()
    608                     && rotation == 0) {
    609                 // To prevent concurrent access in GLThread
    610                 synchronized (mRegionDecoder) {
    611                     return mRegionDecoder.decodeRegion(rect, options);
    612                 }
    613             }
    614             Bitmap result = Bitmap.createBitmap(
    615                     outputX, outputY, Config.ARGB_8888);
    616             Canvas canvas = new Canvas(result);
    617             rotateCanvas(canvas, outputX, outputY, rotation);
    618             drawInTiles(canvas, mRegionDecoder, rect, dest, sample);
    619             return result;
    620         } else {
    621             int rotation = mMediaItem.getRotation();
    622             rotateRectangle(rect, mCropView.getImageWidth(),
    623                     mCropView.getImageHeight(), 360 - rotation);
    624             rotateRectangle(dest, outputX, outputY, 360 - rotation);
    625             Bitmap result = Bitmap.createBitmap(outputX, outputY, Config.ARGB_8888);
    626             Canvas canvas = new Canvas(result);
    627             rotateCanvas(canvas, outputX, outputY, rotation);
    628             canvas.drawBitmap(mBitmap,
    629                     rect, dest, new Paint(Paint.FILTER_BITMAP_FLAG));
    630             return result;
    631         }
    632     }
    633 
    634     private static void rotateCanvas(
    635             Canvas canvas, int width, int height, int rotation) {
    636         canvas.translate(width / 2, height / 2);
    637         canvas.rotate(rotation);
    638         if (((rotation / 90) & 0x01) == 0) {
    639             canvas.translate(-width / 2, -height / 2);
    640         } else {
    641             canvas.translate(-height / 2, -width / 2);
    642         }
    643     }
    644 
    645     private static void rotateRectangle(
    646             Rect rect, int width, int height, int rotation) {
    647         if (rotation == 0 || rotation == 360) return;
    648 
    649         int w = rect.width();
    650         int h = rect.height();
    651         switch (rotation) {
    652             case 90: {
    653                 rect.top = rect.left;
    654                 rect.left = height - rect.bottom;
    655                 rect.right = rect.left + h;
    656                 rect.bottom = rect.top + w;
    657                 return;
    658             }
    659             case 180: {
    660                 rect.left = width - rect.right;
    661                 rect.top = height - rect.bottom;
    662                 rect.right = rect.left + w;
    663                 rect.bottom = rect.top + h;
    664                 return;
    665             }
    666             case 270: {
    667                 rect.left = rect.top;
    668                 rect.top = width - rect.right;
    669                 rect.right = rect.left + h;
    670                 rect.bottom = rect.top + w;
    671                 return;
    672             }
    673             default: throw new AssertionError();
    674         }
    675     }
    676 
    677     private void drawInTiles(Canvas canvas,
    678             BitmapRegionDecoder decoder, Rect rect, Rect dest, int sample) {
    679         int tileSize = TILE_SIZE * sample;
    680         Rect tileRect = new Rect();
    681         BitmapFactory.Options options = new BitmapFactory.Options();
    682         options.inPreferredConfig = Config.ARGB_8888;
    683         options.inSampleSize = sample;
    684         canvas.translate(dest.left, dest.top);
    685         canvas.scale((float) sample * dest.width() / rect.width(),
    686                 (float) sample * dest.height() / rect.height());
    687         Paint paint = new Paint(Paint.FILTER_BITMAP_FLAG);
    688         for (int tx = rect.left, x = 0;
    689                 tx < rect.right; tx += tileSize, x += TILE_SIZE) {
    690             for (int ty = rect.top, y = 0;
    691                     ty < rect.bottom; ty += tileSize, y += TILE_SIZE) {
    692                 tileRect.set(tx, ty, tx + tileSize, ty + tileSize);
    693                 if (tileRect.intersect(rect)) {
    694                     Bitmap bitmap;
    695 
    696                     // To prevent concurrent access in GLThread
    697                     synchronized (decoder) {
    698                         bitmap = decoder.decodeRegion(tileRect, options);
    699                     }
    700                     canvas.drawBitmap(bitmap, x, y, paint);
    701                     bitmap.recycle();
    702                 }
    703             }
    704         }
    705     }
    706 
    707     private void onBitmapRegionDecoderAvailable(
    708             BitmapRegionDecoder regionDecoder) {
    709 
    710         if (regionDecoder == null) {
    711             Toast.makeText(this, "fail to load image", Toast.LENGTH_SHORT).show();
    712             finish();
    713             return;
    714         }
    715         mRegionDecoder = regionDecoder;
    716         mUseRegionDecoder = true;
    717         mState = STATE_LOADED;
    718 
    719         BitmapFactory.Options options = new BitmapFactory.Options();
    720         int width = regionDecoder.getWidth();
    721         int height = regionDecoder.getHeight();
    722         options.inSampleSize = BitmapUtils.computeSampleSize(width, height,
    723                 BitmapUtils.UNCONSTRAINED, BACKUP_PIXEL_COUNT);
    724         mBitmap = regionDecoder.decodeRegion(
    725                 new Rect(0, 0, width, height), options);
    726         mCropView.setDataModel(new TileImageViewAdapter(
    727                 mBitmap, regionDecoder), mMediaItem.getFullImageRotation());
    728         if (mDoFaceDetection) {
    729             mCropView.detectFaces(mBitmap);
    730         } else {
    731             mCropView.initializeHighlightRectangle();
    732         }
    733     }
    734 
    735     private void onBitmapAvailable(Bitmap bitmap) {
    736         if (bitmap == null) {
    737             Toast.makeText(this, "fail to load image", Toast.LENGTH_SHORT).show();
    738             finish();
    739             return;
    740         }
    741         mUseRegionDecoder = false;
    742         mState = STATE_LOADED;
    743 
    744         mBitmap = bitmap;
    745         BitmapFactory.Options options = new BitmapFactory.Options();
    746         mCropView.setDataModel(new BitmapTileProvider(bitmap, 512),
    747                 mMediaItem.getRotation());
    748         if (mDoFaceDetection) {
    749             mCropView.detectFaces(bitmap);
    750         } else {
    751             mCropView.initializeHighlightRectangle();
    752         }
    753     }
    754 
    755     private void setCropParameters() {
    756         Bundle extras = getIntent().getExtras();
    757         if (extras == null)
    758             return;
    759         int aspectX = extras.getInt(KEY_ASPECT_X, 0);
    760         int aspectY = extras.getInt(KEY_ASPECT_Y, 0);
    761         if (aspectX != 0 && aspectY != 0) {
    762             mCropView.setAspectRatio((float) aspectX / aspectY);
    763         }
    764 
    765         float spotlightX = extras.getFloat(KEY_SPOTLIGHT_X, 0);
    766         float spotlightY = extras.getFloat(KEY_SPOTLIGHT_Y, 0);
    767         if (spotlightX != 0 && spotlightY != 0) {
    768             mCropView.setSpotlightRatio(spotlightX, spotlightY);
    769         }
    770     }
    771 
    772     private void initializeData() {
    773         Bundle extras = getIntent().getExtras();
    774 
    775         if (extras != null) {
    776             if (extras.containsKey(KEY_NO_FACE_DETECTION)) {
    777                 mDoFaceDetection = !extras.getBoolean(KEY_NO_FACE_DETECTION);
    778             }
    779 
    780             mBitmapInIntent = extras.getParcelable(KEY_DATA);
    781 
    782             if (mBitmapInIntent != null) {
    783                 mBitmapTileProvider =
    784                         new BitmapTileProvider(mBitmapInIntent, MAX_BACKUP_IMAGE_SIZE);
    785                 mCropView.setDataModel(mBitmapTileProvider, 0);
    786                 if (mDoFaceDetection) {
    787                     mCropView.detectFaces(mBitmapInIntent);
    788                 } else {
    789                     mCropView.initializeHighlightRectangle();
    790                 }
    791                 mState = STATE_LOADED;
    792                 return;
    793             }
    794         }
    795 
    796         mProgressDialog = ProgressDialog.show(
    797                 this, null, getString(R.string.loading_image), true, false);
    798 
    799         mMediaItem = getMediaItemFromIntentData();
    800         if (mMediaItem == null) return;
    801 
    802         boolean supportedByBitmapRegionDecoder =
    803             (mMediaItem.getSupportedOperations() & MediaItem.SUPPORT_FULL_IMAGE) != 0;
    804         if (supportedByBitmapRegionDecoder) {
    805             mLoadTask = getThreadPool().submit(new LoadDataTask(mMediaItem),
    806                     new FutureListener<BitmapRegionDecoder>() {
    807                 public void onFutureDone(Future<BitmapRegionDecoder> future) {
    808                     mLoadTask = null;
    809                     BitmapRegionDecoder decoder = future.get();
    810                     if (future.isCancelled()) {
    811                         if (decoder != null) decoder.recycle();
    812                         return;
    813                     }
    814                     mMainHandler.sendMessage(mMainHandler.obtainMessage(
    815                             MSG_LARGE_BITMAP, decoder));
    816                 }
    817             });
    818         } else {
    819             mLoadBitmapTask = getThreadPool().submit(new LoadBitmapDataTask(mMediaItem),
    820                     new FutureListener<Bitmap>() {
    821                 public void onFutureDone(Future<Bitmap> future) {
    822                     mLoadBitmapTask = null;
    823                     Bitmap bitmap = future.get();
    824                     if (future.isCancelled()) {
    825                         if (bitmap != null) bitmap.recycle();
    826                         return;
    827                     }
    828                     mMainHandler.sendMessage(mMainHandler.obtainMessage(
    829                             MSG_BITMAP, bitmap));
    830                 }
    831             });
    832         }
    833     }
    834 
    835     @Override
    836     protected void onResume() {
    837         super.onResume();
    838         if (mState == STATE_INIT) initializeData();
    839         if (mState == STATE_SAVING) onSaveClicked();
    840 
    841         // TODO: consider to do it in GLView system
    842         GLRoot root = getGLRoot();
    843         root.lockRenderThread();
    844         try {
    845             mCropView.resume();
    846         } finally {
    847             root.unlockRenderThread();
    848         }
    849     }
    850 
    851     @Override
    852     protected void onPause() {
    853         super.onPause();
    854 
    855         Future<BitmapRegionDecoder> loadTask = mLoadTask;
    856         if (loadTask != null && !loadTask.isDone()) {
    857             // load in progress, try to cancel it
    858             loadTask.cancel();
    859             loadTask.waitDone();
    860             mProgressDialog.dismiss();
    861         }
    862 
    863         Future<Bitmap> loadBitmapTask = mLoadBitmapTask;
    864         if (loadBitmapTask != null && !loadBitmapTask.isDone()) {
    865             // load in progress, try to cancel it
    866             loadBitmapTask.cancel();
    867             loadBitmapTask.waitDone();
    868             mProgressDialog.dismiss();
    869         }
    870 
    871         Future<Intent> saveTask = mSaveTask;
    872         if (saveTask != null && !saveTask.isDone()) {
    873             // save in progress, try to cancel it
    874             saveTask.cancel();
    875             saveTask.waitDone();
    876             mProgressDialog.dismiss();
    877         }
    878         GLRoot root = getGLRoot();
    879         root.lockRenderThread();
    880         try {
    881             mCropView.pause();
    882         } finally {
    883             root.unlockRenderThread();
    884         }
    885     }
    886 
    887     private MediaItem getMediaItemFromIntentData() {
    888         Uri uri = getIntent().getData();
    889         DataManager manager = getDataManager();
    890         if (uri == null) {
    891             Log.w(TAG, "no data given");
    892             return null;
    893         }
    894         Path path = manager.findPathByUri(uri);
    895         if (path == null) {
    896             Log.w(TAG, "cannot get path for: " + uri);
    897             return null;
    898         }
    899         return (MediaItem) manager.getMediaObject(path);
    900     }
    901 
    902     private class LoadDataTask implements Job<BitmapRegionDecoder> {
    903         MediaItem mItem;
    904 
    905         public LoadDataTask(MediaItem item) {
    906             mItem = item;
    907         }
    908 
    909         public BitmapRegionDecoder run(JobContext jc) {
    910             return mItem == null ? null : mItem.requestLargeImage().run(jc);
    911         }
    912     }
    913 
    914     private class LoadBitmapDataTask implements Job<Bitmap> {
    915         MediaItem mItem;
    916 
    917         public LoadBitmapDataTask(MediaItem item) {
    918             mItem = item;
    919         }
    920         public Bitmap run(JobContext jc) {
    921             return mItem == null
    922                     ? null
    923                     : mItem.requestImage(MediaItem.TYPE_THUMBNAIL).run(jc);
    924         }
    925     }
    926 
    927     private static final String[] EXIF_TAGS = {
    928             ExifInterface.TAG_DATETIME,
    929             ExifInterface.TAG_MAKE,
    930             ExifInterface.TAG_MODEL,
    931             ExifInterface.TAG_FLASH,
    932             ExifInterface.TAG_GPS_LATITUDE,
    933             ExifInterface.TAG_GPS_LONGITUDE,
    934             ExifInterface.TAG_GPS_LATITUDE_REF,
    935             ExifInterface.TAG_GPS_LONGITUDE_REF,
    936             ExifInterface.TAG_GPS_ALTITUDE,
    937             ExifInterface.TAG_GPS_ALTITUDE_REF,
    938             ExifInterface.TAG_GPS_TIMESTAMP,
    939             ExifInterface.TAG_GPS_DATESTAMP,
    940             ExifInterface.TAG_WHITE_BALANCE,
    941             ExifInterface.TAG_FOCAL_LENGTH,
    942             ExifInterface.TAG_GPS_PROCESSING_METHOD};
    943 
    944     private static void copyExif(MediaItem item, String destination, int newWidth, int newHeight) {
    945         try {
    946             ExifInterface newExif = new ExifInterface(destination);
    947             PicasaSource.extractExifValues(item, newExif);
    948             newExif.setAttribute(ExifInterface.TAG_IMAGE_WIDTH, String.valueOf(newWidth));
    949             newExif.setAttribute(ExifInterface.TAG_IMAGE_LENGTH, String.valueOf(newHeight));
    950             newExif.setAttribute(ExifInterface.TAG_ORIENTATION, String.valueOf(0));
    951             newExif.saveAttributes();
    952         } catch (Throwable t) {
    953             Log.w(TAG, "cannot copy exif: " + item, t);
    954         }
    955     }
    956 
    957     private static void copyExif(String source, String destination, int newWidth, int newHeight) {
    958         try {
    959             ExifInterface oldExif = new ExifInterface(source);
    960             ExifInterface newExif = new ExifInterface(destination);
    961 
    962             newExif.setAttribute(ExifInterface.TAG_IMAGE_WIDTH, String.valueOf(newWidth));
    963             newExif.setAttribute(ExifInterface.TAG_IMAGE_LENGTH, String.valueOf(newHeight));
    964             newExif.setAttribute(ExifInterface.TAG_ORIENTATION, String.valueOf(0));
    965 
    966             for (String tag : EXIF_TAGS) {
    967                 String value = oldExif.getAttribute(tag);
    968                 if (value != null) {
    969                     newExif.setAttribute(tag, value);
    970                 }
    971             }
    972 
    973             // Handle some special values here
    974             String value = oldExif.getAttribute(ExifInterface.TAG_APERTURE);
    975             if (value != null) {
    976                 try {
    977                     float aperture = Float.parseFloat(value);
    978                     newExif.setAttribute(ExifInterface.TAG_APERTURE,
    979                             String.valueOf((int) (aperture * 10 + 0.5f)) + "/10");
    980                 } catch (NumberFormatException e) {
    981                     Log.w(TAG, "cannot parse aperture: " + value);
    982                 }
    983             }
    984 
    985             // TODO: The code is broken, need to fix the JHEAD lib
    986             /*
    987             value = oldExif.getAttribute(ExifInterface.TAG_EXPOSURE_TIME);
    988             if (value != null) {
    989                 try {
    990                     double exposure = Double.parseDouble(value);
    991                     testToRational("test exposure", exposure);
    992                     newExif.setAttribute(ExifInterface.TAG_EXPOSURE_TIME, value);
    993                 } catch (NumberFormatException e) {
    994                     Log.w(TAG, "cannot parse exposure time: " + value);
    995                 }
    996             }
    997 
    998             value = oldExif.getAttribute(ExifInterface.TAG_ISO);
    999             if (value != null) {
   1000                 try {
   1001                     int iso = Integer.parseInt(value);
   1002                     newExif.setAttribute(ExifInterface.TAG_ISO, String.valueOf(iso) + "/1");
   1003                 } catch (NumberFormatException e) {
   1004                     Log.w(TAG, "cannot parse exposure time: " + value);
   1005                 }
   1006             }*/
   1007             newExif.saveAttributes();
   1008         } catch (Throwable t) {
   1009             Log.w(TAG, "cannot copy exif: " + source, t);
   1010         }
   1011     }
   1012 }
   1013