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