OSDN Git Service

Merge "Import translations. DO NOT MERGE" into gb-ub-photos-arches
[android-x86/packages-apps-Gallery2.git] / src / com / android / gallery3d / app / CropImage.java
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.annotation.TargetApi;
20 import android.app.ActionBar;
21 import android.app.ProgressDialog;
22 import android.app.WallpaperManager;
23 import android.content.ContentValues;
24 import android.content.Intent;
25 import android.graphics.Bitmap;
26 import android.graphics.Bitmap.CompressFormat;
27 import android.graphics.Bitmap.Config;
28 import android.graphics.BitmapFactory;
29 import android.graphics.BitmapRegionDecoder;
30 import android.graphics.Canvas;
31 import android.graphics.Paint;
32 import android.graphics.Rect;
33 import android.graphics.RectF;
34 import android.media.ExifInterface;
35 import android.net.Uri;
36 import android.os.Build;
37 import android.os.Bundle;
38 import android.os.Environment;
39 import android.os.Handler;
40 import android.os.Message;
41 import android.provider.MediaStore;
42 import android.provider.MediaStore.Images;
43 import android.util.FloatMath;
44 import android.view.Menu;
45 import android.view.MenuItem;
46 import android.view.Window;
47 import android.widget.Toast;
48
49 import com.android.gallery3d.R;
50 import com.android.gallery3d.common.ApiHelper;
51 import com.android.gallery3d.common.BitmapUtils;
52 import com.android.gallery3d.common.ExifTags;
53 import com.android.gallery3d.common.Utils;
54 import com.android.gallery3d.data.DataManager;
55 import com.android.gallery3d.data.LocalImage;
56 import com.android.gallery3d.data.MediaItem;
57 import com.android.gallery3d.data.MediaObject;
58 import com.android.gallery3d.data.Path;
59 import com.android.gallery3d.picasasource.PicasaSource;
60 import com.android.gallery3d.ui.BitmapTileProvider;
61 import com.android.gallery3d.ui.CropView;
62 import com.android.gallery3d.ui.GLRoot;
63 import com.android.gallery3d.ui.SynchronizedHandler;
64 import com.android.gallery3d.ui.TileImageViewAdapter;
65 import com.android.gallery3d.util.BucketNames;
66 import com.android.gallery3d.util.Future;
67 import com.android.gallery3d.util.FutureListener;
68 import com.android.gallery3d.util.GalleryUtils;
69 import com.android.gallery3d.util.InterruptableOutputStream;
70 import com.android.gallery3d.util.ThreadPool.CancelListener;
71 import com.android.gallery3d.util.ThreadPool.Job;
72 import com.android.gallery3d.util.ThreadPool.JobContext;
73
74 import java.io.File;
75 import java.io.FileNotFoundException;
76 import java.io.FileOutputStream;
77 import java.io.IOException;
78 import java.io.OutputStream;
79 import java.text.SimpleDateFormat;
80 import java.util.Date;
81
82 /**
83  * The activity can crop specific region of interest from an image.
84  */
85 public class CropImage extends AbstractGalleryActivity {
86     private static final String TAG = "CropImage";
87     public static final String ACTION_CROP = "com.android.camera.action.CROP";
88
89     private static final int MAX_PIXEL_COUNT = 5 * 1000000; // 5M pixels
90     private static final int MAX_FILE_INDEX = 1000;
91     private static final int TILE_SIZE = 512;
92     private static final int BACKUP_PIXEL_COUNT = 480000; // around 800x600
93
94     private static final int MSG_LARGE_BITMAP = 1;
95     private static final int MSG_BITMAP = 2;
96     private static final int MSG_SAVE_COMPLETE = 3;
97     private static final int MSG_SHOW_SAVE_ERROR = 4;
98
99     private static final int MAX_BACKUP_IMAGE_SIZE = 320;
100     private static final int DEFAULT_COMPRESS_QUALITY = 90;
101     private static final String TIME_STAMP_NAME = "'IMG'_yyyyMMdd_HHmmss";
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     @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
375     private static void setImageSize(ContentValues values, int width, int height) {
376         // The two fields are available since ICS but got published in JB
377         if (ApiHelper.HAS_MEDIA_COLUMNS_WIDTH_AND_HEIGHT) {
378             values.put(Images.Media.WIDTH, width);
379             values.put(Images.Media.HEIGHT, height);
380         }
381     }
382
383     private Uri savePicasaImage(JobContext jc, Bitmap cropped) {
384         if (!DOWNLOAD_BUCKET.isDirectory() && !DOWNLOAD_BUCKET.mkdirs()) {
385             throw new RuntimeException("cannot create download folder");
386         }
387
388         String filename = PicasaSource.getImageTitle(mMediaItem);
389         int pos = filename.lastIndexOf('.');
390         if (pos >= 0) filename = filename.substring(0, pos);
391         File output = saveMedia(jc, cropped, DOWNLOAD_BUCKET, filename);
392         if (output == null) return null;
393
394         copyExif(mMediaItem, output.getAbsolutePath(), cropped.getWidth(), cropped.getHeight());
395
396         long now = System.currentTimeMillis() / 1000;
397         ContentValues values = new ContentValues();
398         values.put(Images.Media.TITLE, PicasaSource.getImageTitle(mMediaItem));
399         values.put(Images.Media.DISPLAY_NAME, output.getName());
400         values.put(Images.Media.DATE_TAKEN, PicasaSource.getDateTaken(mMediaItem));
401         values.put(Images.Media.DATE_MODIFIED, now);
402         values.put(Images.Media.DATE_ADDED, now);
403         values.put(Images.Media.MIME_TYPE, getOutputMimeType());
404         values.put(Images.Media.ORIENTATION, 0);
405         values.put(Images.Media.DATA, output.getAbsolutePath());
406         values.put(Images.Media.SIZE, output.length());
407         setImageSize(values, cropped.getWidth(), cropped.getHeight());
408
409         double latitude = PicasaSource.getLatitude(mMediaItem);
410         double longitude = PicasaSource.getLongitude(mMediaItem);
411         if (GalleryUtils.isValidLocation(latitude, longitude)) {
412             values.put(Images.Media.LATITUDE, latitude);
413             values.put(Images.Media.LONGITUDE, longitude);
414         }
415         return getContentResolver().insert(
416                 Images.Media.EXTERNAL_CONTENT_URI, values);
417     }
418
419     private Uri saveLocalImage(JobContext jc, Bitmap cropped) {
420         LocalImage localImage = (LocalImage) mMediaItem;
421
422         File oldPath = new File(localImage.filePath);
423         File directory = new File(oldPath.getParent());
424
425         String filename = oldPath.getName();
426         int pos = filename.lastIndexOf('.');
427         if (pos >= 0) filename = filename.substring(0, pos);
428         File output = saveMedia(jc, cropped, directory, filename);
429         if (output == null) return null;
430
431         copyExif(oldPath.getAbsolutePath(), output.getAbsolutePath(),
432                 cropped.getWidth(), cropped.getHeight());
433
434         long now = System.currentTimeMillis() / 1000;
435         ContentValues values = new ContentValues();
436         values.put(Images.Media.TITLE, localImage.caption);
437         values.put(Images.Media.DISPLAY_NAME, output.getName());
438         values.put(Images.Media.DATE_TAKEN, localImage.dateTakenInMs);
439         values.put(Images.Media.DATE_MODIFIED, now);
440         values.put(Images.Media.DATE_ADDED, now);
441         values.put(Images.Media.MIME_TYPE, getOutputMimeType());
442         values.put(Images.Media.ORIENTATION, 0);
443         values.put(Images.Media.DATA, output.getAbsolutePath());
444         values.put(Images.Media.SIZE, output.length());
445
446         setImageSize(values, cropped.getWidth(), cropped.getHeight());
447
448         if (GalleryUtils.isValidLocation(localImage.latitude, localImage.longitude)) {
449             values.put(Images.Media.LATITUDE, localImage.latitude);
450             values.put(Images.Media.LONGITUDE, localImage.longitude);
451         }
452         return getContentResolver().insert(
453                 Images.Media.EXTERNAL_CONTENT_URI, values);
454     }
455
456     private Uri saveGenericImage(JobContext jc, Bitmap cropped) {
457         if (!DOWNLOAD_BUCKET.isDirectory() && !DOWNLOAD_BUCKET.mkdirs()) {
458             throw new RuntimeException("cannot create download folder");
459         }
460
461         long now = System.currentTimeMillis();
462         String filename = new SimpleDateFormat(TIME_STAMP_NAME).
463                 format(new Date(now));
464
465         File output = saveMedia(jc, cropped, DOWNLOAD_BUCKET, filename);
466         if (output == null) return null;
467
468         ContentValues values = new ContentValues();
469         values.put(Images.Media.TITLE, filename);
470         values.put(Images.Media.DISPLAY_NAME, output.getName());
471         values.put(Images.Media.DATE_TAKEN, now);
472         values.put(Images.Media.DATE_MODIFIED, now / 1000);
473         values.put(Images.Media.DATE_ADDED, now / 1000);
474         values.put(Images.Media.MIME_TYPE, getOutputMimeType());
475         values.put(Images.Media.ORIENTATION, 0);
476         values.put(Images.Media.DATA, output.getAbsolutePath());
477         values.put(Images.Media.SIZE, output.length());
478
479         setImageSize(values, cropped.getWidth(), cropped.getHeight());
480
481         return getContentResolver().insert(
482                 Images.Media.EXTERNAL_CONTENT_URI, values);
483     }
484
485     private boolean saveBitmapToOutputStream(
486             JobContext jc, Bitmap bitmap, CompressFormat format, OutputStream os) {
487         // We wrap the OutputStream so that it can be interrupted.
488         final InterruptableOutputStream ios = new InterruptableOutputStream(os);
489         jc.setCancelListener(new CancelListener() {
490                 public void onCancel() {
491                     ios.interrupt();
492                 }
493             });
494         try {
495             bitmap.compress(format, DEFAULT_COMPRESS_QUALITY, os);
496             return !jc.isCancelled();
497         } finally {
498             jc.setCancelListener(null);
499             Utils.closeSilently(os);
500         }
501     }
502
503     private boolean saveBitmapToUri(JobContext jc, Bitmap bitmap, Uri uri) {
504         try {
505             return saveBitmapToOutputStream(jc, bitmap,
506                     convertExtensionToCompressFormat(getFileExtension()),
507                     getContentResolver().openOutputStream(uri));
508         } catch (FileNotFoundException e) {
509             Log.w(TAG, "cannot write output", e);
510         }
511         return true;
512     }
513
514     private CompressFormat convertExtensionToCompressFormat(String extension) {
515         return extension.equals("png")
516                 ? CompressFormat.PNG
517                 : CompressFormat.JPEG;
518     }
519
520     private String getOutputMimeType() {
521         return getFileExtension().equals("png") ? "image/png" : "image/jpeg";
522     }
523
524     private String getFileExtension() {
525         String requestFormat = getIntent().getStringExtra(KEY_OUTPUT_FORMAT);
526         String outputFormat = (requestFormat == null)
527                 ? determineCompressFormat(mMediaItem)
528                 : requestFormat;
529
530         outputFormat = outputFormat.toLowerCase();
531         return (outputFormat.equals("png") || outputFormat.equals("gif"))
532                 ? "png" // We don't support gif compression.
533                 : "jpg";
534     }
535
536     private void onSaveClicked() {
537         Bundle extra = getIntent().getExtras();
538         RectF cropRect = mCropView.getCropRectangle();
539         if (cropRect == null) return;
540         mState = STATE_SAVING;
541         int messageId = extra != null && extra.getBoolean(KEY_SET_AS_WALLPAPER)
542                 ? R.string.wallpaper
543                 : R.string.saving_image;
544         mProgressDialog = ProgressDialog.show(
545                 this, null, getString(messageId), true, false);
546         mSaveTask = getThreadPool().submit(new SaveOutput(cropRect),
547                 new FutureListener<Intent>() {
548             public void onFutureDone(Future<Intent> future) {
549                 mSaveTask = null;
550                 if (future.isCancelled()) return;
551                 Intent intent = future.get();
552                 if (intent != null) {
553                     mMainHandler.sendMessage(mMainHandler.obtainMessage(
554                             MSG_SAVE_COMPLETE, intent));
555                 } else {
556                     mMainHandler.sendEmptyMessage(MSG_SHOW_SAVE_ERROR);
557                 }
558             }
559         });
560     }
561
562     private Bitmap getCroppedImage(Rect rect) {
563         Utils.assertTrue(rect.width() > 0 && rect.height() > 0);
564
565         Bundle extras = getIntent().getExtras();
566         // (outputX, outputY) = the width and height of the returning bitmap.
567         int outputX = rect.width();
568         int outputY = rect.height();
569         if (extras != null) {
570             outputX = extras.getInt(KEY_OUTPUT_X, outputX);
571             outputY = extras.getInt(KEY_OUTPUT_Y, outputY);
572         }
573
574         if (outputX * outputY > MAX_PIXEL_COUNT) {
575             float scale = FloatMath.sqrt((float) MAX_PIXEL_COUNT / outputX / outputY);
576             Log.w(TAG, "scale down the cropped image: " + scale);
577             outputX = Math.round(scale * outputX);
578             outputY = Math.round(scale * outputY);
579         }
580
581         // (rect.width() * scaleX, rect.height() * scaleY) =
582         // the size of drawing area in output bitmap
583         float scaleX = 1;
584         float scaleY = 1;
585         Rect dest = new Rect(0, 0, outputX, outputY);
586         if (extras == null || extras.getBoolean(KEY_SCALE, true)) {
587             scaleX = (float) outputX / rect.width();
588             scaleY = (float) outputY / rect.height();
589             if (extras == null || !extras.getBoolean(
590                     KEY_SCALE_UP_IF_NEEDED, false)) {
591                 if (scaleX > 1f) scaleX = 1;
592                 if (scaleY > 1f) scaleY = 1;
593             }
594         }
595
596         // Keep the content in the center (or crop the content)
597         int rectWidth = Math.round(rect.width() * scaleX);
598         int rectHeight = Math.round(rect.height() * scaleY);
599         dest.set(Math.round((outputX - rectWidth) / 2f),
600                 Math.round((outputY - rectHeight) / 2f),
601                 Math.round((outputX + rectWidth) / 2f),
602                 Math.round((outputY + rectHeight) / 2f));
603
604         if (mBitmapInIntent != null) {
605             Bitmap source = mBitmapInIntent;
606             Bitmap result = Bitmap.createBitmap(
607                     outputX, outputY, Config.ARGB_8888);
608             Canvas canvas = new Canvas(result);
609             canvas.drawBitmap(source, rect, dest, null);
610             return result;
611         }
612
613         if (mUseRegionDecoder) {
614             int rotation = mMediaItem.getFullImageRotation();
615             rotateRectangle(rect, mCropView.getImageWidth(),
616                     mCropView.getImageHeight(), 360 - rotation);
617             rotateRectangle(dest, outputX, outputY, 360 - rotation);
618
619             BitmapFactory.Options options = new BitmapFactory.Options();
620             int sample = BitmapUtils.computeSampleSizeLarger(
621                     Math.max(scaleX, scaleY));
622             options.inSampleSize = sample;
623
624             // The decoding result is what we want if
625             //   1. The size of the decoded bitmap match the destination's size
626             //   2. The destination covers the whole output bitmap
627             //   3. No rotation
628             if ((rect.width() / sample) == dest.width()
629                     && (rect.height() / sample) == dest.height()
630                     && (outputX == dest.width()) && (outputY == dest.height())
631                     && rotation == 0) {
632                 // To prevent concurrent access in GLThread
633                 synchronized (mRegionDecoder) {
634                     return mRegionDecoder.decodeRegion(rect, options);
635                 }
636             }
637             Bitmap result = Bitmap.createBitmap(
638                     outputX, outputY, Config.ARGB_8888);
639             Canvas canvas = new Canvas(result);
640             rotateCanvas(canvas, outputX, outputY, rotation);
641             drawInTiles(canvas, mRegionDecoder, rect, dest, sample);
642             return result;
643         } else {
644             int rotation = mMediaItem.getRotation();
645             rotateRectangle(rect, mCropView.getImageWidth(),
646                     mCropView.getImageHeight(), 360 - rotation);
647             rotateRectangle(dest, outputX, outputY, 360 - rotation);
648             Bitmap result = Bitmap.createBitmap(outputX, outputY, Config.ARGB_8888);
649             Canvas canvas = new Canvas(result);
650             rotateCanvas(canvas, outputX, outputY, rotation);
651             canvas.drawBitmap(mBitmap,
652                     rect, dest, new Paint(Paint.FILTER_BITMAP_FLAG));
653             return result;
654         }
655     }
656
657     private static void rotateCanvas(
658             Canvas canvas, int width, int height, int rotation) {
659         canvas.translate(width / 2, height / 2);
660         canvas.rotate(rotation);
661         if (((rotation / 90) & 0x01) == 0) {
662             canvas.translate(-width / 2, -height / 2);
663         } else {
664             canvas.translate(-height / 2, -width / 2);
665         }
666     }
667
668     private static void rotateRectangle(
669             Rect rect, int width, int height, int rotation) {
670         if (rotation == 0 || rotation == 360) return;
671
672         int w = rect.width();
673         int h = rect.height();
674         switch (rotation) {
675             case 90: {
676                 rect.top = rect.left;
677                 rect.left = height - rect.bottom;
678                 rect.right = rect.left + h;
679                 rect.bottom = rect.top + w;
680                 return;
681             }
682             case 180: {
683                 rect.left = width - rect.right;
684                 rect.top = height - rect.bottom;
685                 rect.right = rect.left + w;
686                 rect.bottom = rect.top + h;
687                 return;
688             }
689             case 270: {
690                 rect.left = rect.top;
691                 rect.top = width - rect.right;
692                 rect.right = rect.left + h;
693                 rect.bottom = rect.top + w;
694                 return;
695             }
696             default: throw new AssertionError();
697         }
698     }
699
700     private void drawInTiles(Canvas canvas,
701             BitmapRegionDecoder decoder, Rect rect, Rect dest, int sample) {
702         int tileSize = TILE_SIZE * sample;
703         Rect tileRect = new Rect();
704         BitmapFactory.Options options = new BitmapFactory.Options();
705         options.inPreferredConfig = Config.ARGB_8888;
706         options.inSampleSize = sample;
707         canvas.translate(dest.left, dest.top);
708         canvas.scale((float) sample * dest.width() / rect.width(),
709                 (float) sample * dest.height() / rect.height());
710         Paint paint = new Paint(Paint.FILTER_BITMAP_FLAG);
711         for (int tx = rect.left, x = 0;
712                 tx < rect.right; tx += tileSize, x += TILE_SIZE) {
713             for (int ty = rect.top, y = 0;
714                     ty < rect.bottom; ty += tileSize, y += TILE_SIZE) {
715                 tileRect.set(tx, ty, tx + tileSize, ty + tileSize);
716                 if (tileRect.intersect(rect)) {
717                     Bitmap bitmap;
718
719                     // To prevent concurrent access in GLThread
720                     synchronized (decoder) {
721                         bitmap = decoder.decodeRegion(tileRect, options);
722                     }
723                     canvas.drawBitmap(bitmap, x, y, paint);
724                     bitmap.recycle();
725                 }
726             }
727         }
728     }
729
730     private void onBitmapRegionDecoderAvailable(
731             BitmapRegionDecoder regionDecoder) {
732
733         if (regionDecoder == null) {
734             Toast.makeText(this, R.string.fail_to_load_image, Toast.LENGTH_SHORT).show();
735             finish();
736             return;
737         }
738         mRegionDecoder = regionDecoder;
739         mUseRegionDecoder = true;
740         mState = STATE_LOADED;
741
742         BitmapFactory.Options options = new BitmapFactory.Options();
743         int width = regionDecoder.getWidth();
744         int height = regionDecoder.getHeight();
745         options.inSampleSize = BitmapUtils.computeSampleSize(width, height,
746                 BitmapUtils.UNCONSTRAINED, BACKUP_PIXEL_COUNT);
747         mBitmap = regionDecoder.decodeRegion(
748                 new Rect(0, 0, width, height), options);
749         mCropView.setDataModel(new TileImageViewAdapter(
750                 mBitmap, regionDecoder), mMediaItem.getFullImageRotation());
751         if (mDoFaceDetection) {
752             mCropView.detectFaces(mBitmap);
753         } else {
754             mCropView.initializeHighlightRectangle();
755         }
756     }
757
758     private void onBitmapAvailable(Bitmap bitmap) {
759         if (bitmap == null) {
760             Toast.makeText(this, R.string.fail_to_load_image, Toast.LENGTH_SHORT).show();
761             finish();
762             return;
763         }
764         mUseRegionDecoder = false;
765         mState = STATE_LOADED;
766
767         mBitmap = bitmap;
768         BitmapFactory.Options options = new BitmapFactory.Options();
769         mCropView.setDataModel(new BitmapTileProvider(bitmap, 512),
770                 mMediaItem.getRotation());
771         if (mDoFaceDetection) {
772             mCropView.detectFaces(bitmap);
773         } else {
774             mCropView.initializeHighlightRectangle();
775         }
776     }
777
778     private void setCropParameters() {
779         Bundle extras = getIntent().getExtras();
780         if (extras == null)
781             return;
782         int aspectX = extras.getInt(KEY_ASPECT_X, 0);
783         int aspectY = extras.getInt(KEY_ASPECT_Y, 0);
784         if (aspectX != 0 && aspectY != 0) {
785             mCropView.setAspectRatio((float) aspectX / aspectY);
786         }
787
788         float spotlightX = extras.getFloat(KEY_SPOTLIGHT_X, 0);
789         float spotlightY = extras.getFloat(KEY_SPOTLIGHT_Y, 0);
790         if (spotlightX != 0 && spotlightY != 0) {
791             mCropView.setSpotlightRatio(spotlightX, spotlightY);
792         }
793     }
794
795     private void initializeData() {
796         Bundle extras = getIntent().getExtras();
797
798         if (extras != null) {
799             if (extras.containsKey(KEY_NO_FACE_DETECTION)) {
800                 mDoFaceDetection = !extras.getBoolean(KEY_NO_FACE_DETECTION);
801             }
802
803             mBitmapInIntent = extras.getParcelable(KEY_DATA);
804
805             if (mBitmapInIntent != null) {
806                 mBitmapTileProvider =
807                         new BitmapTileProvider(mBitmapInIntent, MAX_BACKUP_IMAGE_SIZE);
808                 mCropView.setDataModel(mBitmapTileProvider, 0);
809                 if (mDoFaceDetection) {
810                     mCropView.detectFaces(mBitmapInIntent);
811                 } else {
812                     mCropView.initializeHighlightRectangle();
813                 }
814                 mState = STATE_LOADED;
815                 return;
816             }
817         }
818
819         mProgressDialog = ProgressDialog.show(
820                 this, null, getString(R.string.loading_image), true, false);
821
822         mMediaItem = getMediaItemFromIntentData();
823         if (mMediaItem == null) return;
824
825         boolean supportedByBitmapRegionDecoder =
826             (mMediaItem.getSupportedOperations() & MediaItem.SUPPORT_FULL_IMAGE) != 0;
827         if (supportedByBitmapRegionDecoder) {
828             mLoadTask = getThreadPool().submit(new LoadDataTask(mMediaItem),
829                     new FutureListener<BitmapRegionDecoder>() {
830                 public void onFutureDone(Future<BitmapRegionDecoder> future) {
831                     mLoadTask = null;
832                     BitmapRegionDecoder decoder = future.get();
833                     if (future.isCancelled()) {
834                         if (decoder != null) decoder.recycle();
835                         return;
836                     }
837                     mMainHandler.sendMessage(mMainHandler.obtainMessage(
838                             MSG_LARGE_BITMAP, decoder));
839                 }
840             });
841         } else {
842             mLoadBitmapTask = getThreadPool().submit(new LoadBitmapDataTask(mMediaItem),
843                     new FutureListener<Bitmap>() {
844                 public void onFutureDone(Future<Bitmap> future) {
845                     mLoadBitmapTask = null;
846                     Bitmap bitmap = future.get();
847                     if (future.isCancelled()) {
848                         if (bitmap != null) bitmap.recycle();
849                         return;
850                     }
851                     mMainHandler.sendMessage(mMainHandler.obtainMessage(
852                             MSG_BITMAP, bitmap));
853                 }
854             });
855         }
856     }
857
858     @Override
859     protected void onResume() {
860         super.onResume();
861         if (mState == STATE_INIT) initializeData();
862         if (mState == STATE_SAVING) onSaveClicked();
863
864         // TODO: consider to do it in GLView system
865         GLRoot root = getGLRoot();
866         root.lockRenderThread();
867         try {
868             mCropView.resume();
869         } finally {
870             root.unlockRenderThread();
871         }
872     }
873
874     @Override
875     protected void onPause() {
876         super.onPause();
877
878         Future<BitmapRegionDecoder> loadTask = mLoadTask;
879         if (loadTask != null && !loadTask.isDone()) {
880             // load in progress, try to cancel it
881             loadTask.cancel();
882             loadTask.waitDone();
883             mProgressDialog.dismiss();
884         }
885
886         Future<Bitmap> loadBitmapTask = mLoadBitmapTask;
887         if (loadBitmapTask != null && !loadBitmapTask.isDone()) {
888             // load in progress, try to cancel it
889             loadBitmapTask.cancel();
890             loadBitmapTask.waitDone();
891             mProgressDialog.dismiss();
892         }
893
894         Future<Intent> saveTask = mSaveTask;
895         if (saveTask != null && !saveTask.isDone()) {
896             // save in progress, try to cancel it
897             saveTask.cancel();
898             saveTask.waitDone();
899             mProgressDialog.dismiss();
900         }
901         GLRoot root = getGLRoot();
902         root.lockRenderThread();
903         try {
904             mCropView.pause();
905         } finally {
906             root.unlockRenderThread();
907         }
908     }
909
910     private MediaItem getMediaItemFromIntentData() {
911         Uri uri = getIntent().getData();
912         DataManager manager = getDataManager();
913         Path path = manager.findPathByUri(uri, getIntent().getType());
914         if (path == null) {
915             Log.w(TAG, "cannot get path for: " + uri + ", or no data given");
916             return null;
917         }
918         return (MediaItem) manager.getMediaObject(path);
919     }
920
921     private class LoadDataTask implements Job<BitmapRegionDecoder> {
922         MediaItem mItem;
923
924         public LoadDataTask(MediaItem item) {
925             mItem = item;
926         }
927
928         public BitmapRegionDecoder run(JobContext jc) {
929             return mItem == null ? null : mItem.requestLargeImage().run(jc);
930         }
931     }
932
933     private class LoadBitmapDataTask implements Job<Bitmap> {
934         MediaItem mItem;
935
936         public LoadBitmapDataTask(MediaItem item) {
937             mItem = item;
938         }
939         public Bitmap run(JobContext jc) {
940             return mItem == null
941                     ? null
942                     : mItem.requestImage(MediaItem.TYPE_THUMBNAIL).run(jc);
943         }
944     }
945
946     private static final String[] EXIF_TAGS = {
947             ExifInterface.TAG_DATETIME,
948             ExifInterface.TAG_MAKE,
949             ExifInterface.TAG_MODEL,
950             ExifInterface.TAG_FLASH,
951             ExifInterface.TAG_GPS_LATITUDE,
952             ExifInterface.TAG_GPS_LONGITUDE,
953             ExifInterface.TAG_GPS_LATITUDE_REF,
954             ExifInterface.TAG_GPS_LONGITUDE_REF,
955             ExifInterface.TAG_GPS_ALTITUDE,
956             ExifInterface.TAG_GPS_ALTITUDE_REF,
957             ExifInterface.TAG_GPS_TIMESTAMP,
958             ExifInterface.TAG_GPS_DATESTAMP,
959             ExifInterface.TAG_WHITE_BALANCE,
960             ExifInterface.TAG_FOCAL_LENGTH,
961             ExifInterface.TAG_GPS_PROCESSING_METHOD};
962
963     private static void copyExif(MediaItem item, String destination, int newWidth, int newHeight) {
964         try {
965             ExifInterface newExif = new ExifInterface(destination);
966             PicasaSource.extractExifValues(item, newExif);
967             newExif.setAttribute(ExifInterface.TAG_IMAGE_WIDTH, String.valueOf(newWidth));
968             newExif.setAttribute(ExifInterface.TAG_IMAGE_LENGTH, String.valueOf(newHeight));
969             newExif.setAttribute(ExifInterface.TAG_ORIENTATION, String.valueOf(0));
970             newExif.saveAttributes();
971         } catch (Throwable t) {
972             Log.w(TAG, "cannot copy exif: " + item, t);
973         }
974     }
975
976     private static void copyExif(String source, String destination, int newWidth, int newHeight) {
977         try {
978             ExifInterface oldExif = new ExifInterface(source);
979             ExifInterface newExif = new ExifInterface(destination);
980
981             newExif.setAttribute(ExifInterface.TAG_IMAGE_WIDTH, String.valueOf(newWidth));
982             newExif.setAttribute(ExifInterface.TAG_IMAGE_LENGTH, String.valueOf(newHeight));
983             newExif.setAttribute(ExifInterface.TAG_ORIENTATION, String.valueOf(0));
984
985             for (String tag : EXIF_TAGS) {
986                 String value = oldExif.getAttribute(tag);
987                 if (value != null) {
988                     newExif.setAttribute(tag, value);
989                 }
990             }
991
992             // Handle some special values here
993             String value = oldExif.getAttribute(ExifTags.TAG_APERTURE);
994             if (value != null) {
995                 try {
996                     float aperture = Float.parseFloat(value);
997                     newExif.setAttribute(ExifTags.TAG_APERTURE,
998                             String.valueOf((int) (aperture * 10 + 0.5f)) + "/10");
999                 } catch (NumberFormatException e) {
1000                     Log.w(TAG, "cannot parse aperture: " + value);
1001                 }
1002             }
1003
1004             // TODO: The code is broken, need to fix the JHEAD lib
1005             /*
1006             value = oldExif.getAttribute(ExifTags.TAG_EXPOSURE_TIME);
1007             if (value != null) {
1008                 try {
1009                     double exposure = Double.parseDouble(value);
1010                     testToRational("test exposure", exposure);
1011                     newExif.setAttribute(ExifTags.TAG_EXPOSURE_TIME, value);
1012                 } catch (NumberFormatException e) {
1013                     Log.w(TAG, "cannot parse exposure time: " + value);
1014                 }
1015             }
1016
1017             value = oldExif.getAttribute(ExifTags.TAG_ISO);
1018             if (value != null) {
1019                 try {
1020                     int iso = Integer.parseInt(value);
1021                     newExif.setAttribute(ExifTags.TAG_ISO, String.valueOf(iso) + "/1");
1022                 } catch (NumberFormatException e) {
1023                     Log.w(TAG, "cannot parse exposure time: " + value);
1024                 }
1025             }*/
1026             newExif.saveAttributes();
1027         } catch (Throwable t) {
1028             Log.w(TAG, "cannot copy exif: " + source, t);
1029         }
1030     }
1031 }