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