• 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.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
onCreate(Bundle bundle)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
onSaveInstanceState(Bundle saveState)209     protected void onSaveInstanceState(Bundle saveState) {
210         saveState.putInt(KEY_STATE, mState);
211     }
212 
213     @Override
onCreateOptionsMenu(Menu menu)214     public boolean onCreateOptionsMenu(Menu menu) {
215         super.onCreateOptionsMenu(menu);
216         getMenuInflater().inflate(R.menu.crop, menu);
217         return true;
218     }
219 
220     @Override
onOptionsItemSelected(MenuItem item)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
onBackPressed()241     public void onBackPressed() {
242         finish();
243     }
244 
245     private class SaveOutput implements Job<Intent> {
246         private final RectF mCropRect;
247 
SaveOutput(RectF cropRect)248         public SaveOutput(RectF cropRect) {
249             mCropRect = cropRect;
250         }
251 
run(JobContext jc)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 
determineCompressFormat(MediaObject obj)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 
setAsWallpaper(JobContext jc, Bitmap wallpaper)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 
saveMedia( JobContext jc, Bitmap cropped, File directory, String filename)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 
saveToMediaProvider(JobContext jc, Bitmap cropped)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 
savePicasaImage(JobContext jc, Bitmap cropped)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 
saveLocalImage(JobContext jc, Bitmap cropped)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 
saveGenericImage(JobContext jc, Bitmap cropped)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 
saveBitmapToOutputStream( JobContext jc, Bitmap bitmap, CompressFormat format, OutputStream os)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 
saveBitmapToUri(JobContext jc, Bitmap bitmap, Uri uri)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 
convertExtensionToCompressFormat(String extension)506     private CompressFormat convertExtensionToCompressFormat(String extension) {
507         return extension.equals("png")
508                 ? CompressFormat.PNG
509                 : CompressFormat.JPEG;
510     }
511 
getOutputMimeType()512     private String getOutputMimeType() {
513         return getFileExtension().equals("png") ? "image/png" : "image/jpeg";
514     }
515 
getFileExtension()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 
onSaveClicked()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 
getCroppedImage(Rect rect)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 
rotateCanvas( Canvas canvas, int width, int height, int rotation)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 
rotateRectangle( Rect rect, int width, int height, int rotation)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 
drawInTiles(Canvas canvas, BitmapRegionDecoder decoder, Rect rect, Rect dest, int sample)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 
onBitmapRegionDecoderAvailable( BitmapRegionDecoder regionDecoder)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 
onBitmapAvailable(Bitmap bitmap)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 
setCropParameters()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 
initializeData()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
onResume()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
onPause()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 
getMediaItemFromIntentData()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 
LoadDataTask(MediaItem item)916         public LoadDataTask(MediaItem item) {
917             mItem = item;
918         }
919 
run(JobContext jc)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 
LoadBitmapDataTask(MediaItem item)928         public LoadBitmapDataTask(MediaItem item) {
929             mItem = item;
930         }
run(JobContext jc)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 
copyExif(MediaItem item, String destination, int newWidth, int newHeight)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 
copyExif(String source, String destination, int newWidth, int newHeight)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