OSDN Git Service

Give CropActivity minimal touchpad support.
[android-x86/packages-apps-Gallery2.git] / src / com / android / gallery3d / filtershow / crop / CropActivity.java
1 /*
2  * Copyright (C) 2013 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.filtershow.crop;
18
19 import android.app.ActionBar;
20 import android.app.Activity;
21 import android.app.WallpaperManager;
22 import android.content.Context;
23 import android.content.Intent;
24 import android.content.res.Configuration;
25 import android.graphics.Bitmap;
26 import android.graphics.Bitmap.CompressFormat;
27 import android.graphics.BitmapFactory;
28 import android.graphics.BitmapRegionDecoder;
29 import android.graphics.Canvas;
30 import android.graphics.Matrix;
31 import android.graphics.Paint;
32 import android.graphics.Rect;
33 import android.graphics.RectF;
34 import android.net.Uri;
35 import android.os.AsyncTask;
36 import android.os.Bundle;
37 import android.provider.MediaStore;
38 import android.util.DisplayMetrics;
39 import android.util.Log;
40 import android.view.InputDevice;
41 import android.view.InputDevice.MotionRange;
42 import android.view.KeyEvent;
43 import android.view.MotionEvent;
44 import android.view.View;
45 import android.view.View.OnClickListener;
46 import android.view.WindowManager;
47 import android.widget.Toast;
48
49 import com.android.gallery3d.R;
50 import com.android.gallery3d.common.Utils;
51 import com.android.gallery3d.filtershow.cache.ImageLoader;
52 import com.android.gallery3d.filtershow.tools.SaveImage;
53 import com.google.android.pano.util.TouchNavGestureDetector;
54 import com.google.android.pano.util.TouchNavGestureDetector.OnGestureListener;
55
56 import java.io.ByteArrayInputStream;
57 import java.io.ByteArrayOutputStream;
58 import java.io.FileNotFoundException;
59 import java.io.IOException;
60 import java.io.InputStream;
61 import java.io.OutputStream;
62
63 /**
64  * Activity for cropping an image.
65  */
66 public class CropActivity extends Activity implements OnGestureListener {
67     private static final String LOGTAG = "CropActivity";
68     public static final String CROP_ACTION = "com.android.camera.action.CROP";
69     private static final int MIN_SCROLL_LENGTH = 5;
70     private CropExtras mCropExtras = null;
71     private LoadBitmapTask mLoadBitmapTask = null;
72
73     private int mOutputX = 0;
74     private int mOutputY = 0;
75     private Bitmap mOriginalBitmap = null;
76     private RectF mOriginalBounds = null;
77     private int mOriginalRotation = 0;
78     private Uri mSourceUri = null;
79     private CropView mCropView = null;
80     private View mSaveButton = null;
81     private boolean finalIOGuard = false;
82
83     private static final int SELECT_PICTURE = 1; // request code for picker
84
85     private static final int DEFAULT_COMPRESS_QUALITY = 90;
86     /**
87      * The maximum bitmap size we allow to be returned through the intent.
88      * Intents have a maximum of 1MB in total size. However, the Bitmap seems to
89      * have some overhead to hit so that we go way below the limit here to make
90      * sure the intent stays below 1MB.We should consider just returning a byte
91      * array instead of a Bitmap instance to avoid overhead.
92      */
93     public static final int MAX_BMAP_IN_INTENT = 750000;
94
95     // Flags
96     private static final int DO_SET_WALLPAPER = 1;
97     private static final int DO_RETURN_DATA = 1 << 1;
98     private static final int DO_EXTRA_OUTPUT = 1 << 2;
99
100     private static final int FLAG_CHECK = DO_SET_WALLPAPER | DO_RETURN_DATA | DO_EXTRA_OUTPUT;
101
102     private TouchNavGestureDetector mGestureDetector;
103     private float mTouchPadToViewRatioX;
104     private float mTouchPadToViewRatioY;
105     private float mResolutionX;
106     private float mResolutionY;
107
108     /**
109      * Used for click filtering. Any scroll less than MIN_SCROLL_LENGTH will not
110      * be registered. This will make clicking motions not trigger a scroll. Once
111      * you get past MIN_SCROLL_LENGTH, you can go below it again and register
112      * scrolls. This is to allow for starting a scroll and then moving back to
113      * the origin of the scroll.
114      */
115     private boolean mDidStartDrag;
116
117     @Override
118     public void onCreate(Bundle savedInstanceState) {
119         super.onCreate(savedInstanceState);
120         mGestureDetector = new TouchNavGestureDetector(this, this);
121         Intent intent = getIntent();
122         setResult(RESULT_CANCELED, new Intent());
123         mCropExtras = getExtrasFromIntent(intent);
124         if (mCropExtras != null && mCropExtras.getShowWhenLocked()) {
125             getWindow().addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED);
126         }
127
128         setContentView(R.layout.crop_activity);
129         mCropView = (CropView) findViewById(R.id.cropView);
130
131         ActionBar actionBar = getActionBar();
132         if (actionBar != null) {
133             actionBar.setDisplayOptions(ActionBar.DISPLAY_SHOW_CUSTOM);
134             actionBar.setCustomView(R.layout.filtershow_actionbar);
135
136             mSaveButton = actionBar.getCustomView();
137             mSaveButton.setOnClickListener(new OnClickListener() {
138                 @Override
139                 public void onClick(View view) {
140                     startFinishOutput();
141                 }
142             });
143         }
144
145         if (intent.getData() != null) {
146             mSourceUri = intent.getData();
147             startLoadBitmap(mSourceUri);
148         } else {
149             pickImage();
150         }
151     }
152
153     private void enableSave(boolean enable) {
154         if (mSaveButton != null) {
155             mSaveButton.setEnabled(enable);
156         }
157     }
158
159     @Override
160     protected void onDestroy() {
161         if (mLoadBitmapTask != null) {
162             mLoadBitmapTask.cancel(false);
163         }
164         super.onDestroy();
165     }
166
167     @Override
168     public void onConfigurationChanged (Configuration newConfig) {
169         super.onConfigurationChanged(newConfig);
170         mCropView.configChanged();
171     }
172
173     /**
174      * Opens a selector in Gallery to chose an image for use when none was given
175      * in the CROP intent.
176      */
177     private void pickImage() {
178         Intent intent = new Intent();
179         intent.setType("image/*");
180         intent.setAction(Intent.ACTION_GET_CONTENT);
181         startActivityForResult(Intent.createChooser(intent, getString(R.string.select_image)),
182                 SELECT_PICTURE);
183     }
184
185     /**
186      * Callback for pickImage().
187      */
188     @Override
189     public void onActivityResult(int requestCode, int resultCode, Intent data) {
190         if (resultCode == RESULT_OK && requestCode == SELECT_PICTURE) {
191             mSourceUri = data.getData();
192             startLoadBitmap(mSourceUri);
193         }
194     }
195
196     /**
197      * Gets screen size metric.
198      */
199     private int getScreenImageSize() {
200         DisplayMetrics outMetrics = new DisplayMetrics();
201         getWindowManager().getDefaultDisplay().getMetrics(outMetrics);
202         return (int) Math.max(outMetrics.heightPixels, outMetrics.widthPixels);
203     }
204
205     /**
206      * Method that loads a bitmap in an async task.
207      */
208     private void startLoadBitmap(Uri uri) {
209         if (uri != null) {
210             enableSave(false);
211             final View loading = findViewById(R.id.loading);
212             loading.setVisibility(View.VISIBLE);
213             mLoadBitmapTask = new LoadBitmapTask();
214             mLoadBitmapTask.execute(uri);
215         } else {
216             cannotLoadImage();
217             done();
218         }
219     }
220
221     /**
222      * Method called on UI thread with loaded bitmap.
223      */
224     private void doneLoadBitmap(Bitmap bitmap, RectF bounds, int orientation) {
225         final View loading = findViewById(R.id.loading);
226         loading.setVisibility(View.GONE);
227         mOriginalBitmap = bitmap;
228         mOriginalBounds = bounds;
229         mOriginalRotation = orientation;
230         if (bitmap != null && bitmap.getWidth() != 0 && bitmap.getHeight() != 0) {
231             RectF imgBounds = new RectF(0, 0, bitmap.getWidth(), bitmap.getHeight());
232             mCropView.initialize(bitmap, imgBounds, imgBounds, orientation);
233             if (mCropExtras != null) {
234                 int aspectX = mCropExtras.getAspectX();
235                 int aspectY = mCropExtras.getAspectY();
236                 mOutputX = mCropExtras.getOutputX();
237                 mOutputY = mCropExtras.getOutputY();
238                 if (mOutputX > 0 && mOutputY > 0) {
239                     mCropView.applyAspect(mOutputX, mOutputY);
240
241                 }
242                 float spotX = mCropExtras.getSpotlightX();
243                 float spotY = mCropExtras.getSpotlightY();
244                 if (spotX > 0 && spotY > 0) {
245                     mCropView.setWallpaperSpotlight(spotX, spotY);
246                 }
247                 if (aspectX > 0 && aspectY > 0) {
248                     mCropView.applyAspect(aspectX, aspectY);
249                 }
250             }
251             enableSave(true);
252         } else {
253             Log.w(LOGTAG, "could not load image for cropping");
254             cannotLoadImage();
255             setResult(RESULT_CANCELED, new Intent());
256             done();
257         }
258     }
259
260     /**
261      * Display toast for image loading failure.
262      */
263     private void cannotLoadImage() {
264         CharSequence text = getString(R.string.cannot_load_image);
265         Toast toast = Toast.makeText(this, text, Toast.LENGTH_SHORT);
266         toast.show();
267     }
268
269     /**
270      * AsyncTask for loading a bitmap into memory.
271      *
272      * @see #startLoadBitmap(Uri)
273      * @see #doneLoadBitmap(Bitmap)
274      */
275     private class LoadBitmapTask extends AsyncTask<Uri, Void, Bitmap> {
276         int mBitmapSize;
277         Context mContext;
278         Rect mOriginalBounds;
279         int mOrientation;
280
281         public LoadBitmapTask() {
282             mBitmapSize = getScreenImageSize();
283             mContext = getApplicationContext();
284             mOriginalBounds = new Rect();
285             mOrientation = 0;
286         }
287
288         @Override
289         protected Bitmap doInBackground(Uri... params) {
290             Uri uri = params[0];
291             Bitmap bmap = ImageLoader.loadConstrainedBitmap(uri, mContext, mBitmapSize,
292                     mOriginalBounds, false);
293             mOrientation = ImageLoader.getMetadataRotation(mContext, uri);
294             return bmap;
295         }
296
297         @Override
298         protected void onPostExecute(Bitmap result) {
299             doneLoadBitmap(result, new RectF(mOriginalBounds), mOrientation);
300         }
301     }
302
303     private void startFinishOutput() {
304         if (finalIOGuard) {
305             return;
306         } else {
307             finalIOGuard = true;
308         }
309         enableSave(false);
310         Uri destinationUri = null;
311         int flags = 0;
312         if (mOriginalBitmap != null && mCropExtras != null) {
313             if (mCropExtras.getExtraOutput() != null) {
314                 destinationUri = mCropExtras.getExtraOutput();
315                 if (destinationUri != null) {
316                     flags |= DO_EXTRA_OUTPUT;
317                 }
318             }
319             if (mCropExtras.getSetAsWallpaper()) {
320                 flags |= DO_SET_WALLPAPER;
321             }
322             if (mCropExtras.getReturnData()) {
323                 flags |= DO_RETURN_DATA;
324             }
325         }
326         if (flags == 0) {
327             destinationUri = SaveImage.makeAndInsertUri(this, mSourceUri);
328             if (destinationUri != null) {
329                 flags |= DO_EXTRA_OUTPUT;
330             }
331         }
332         if ((flags & FLAG_CHECK) != 0 && mOriginalBitmap != null) {
333             RectF photo = new RectF(0, 0, mOriginalBitmap.getWidth(), mOriginalBitmap.getHeight());
334             RectF crop = getBitmapCrop(photo);
335             startBitmapIO(flags, mOriginalBitmap, mSourceUri, destinationUri, crop,
336                     photo, mOriginalBounds,
337                     (mCropExtras == null) ? null : mCropExtras.getOutputFormat(), mOriginalRotation);
338             return;
339         }
340         setResult(RESULT_CANCELED, new Intent());
341         done();
342         return;
343     }
344
345     private void startBitmapIO(int flags, Bitmap currentBitmap, Uri sourceUri, Uri destUri,
346             RectF cropBounds, RectF photoBounds, RectF currentBitmapBounds, String format,
347             int rotation) {
348         if (cropBounds == null || photoBounds == null || currentBitmap == null
349                 || currentBitmap.getWidth() == 0 || currentBitmap.getHeight() == 0
350                 || cropBounds.width() == 0 || cropBounds.height() == 0 || photoBounds.width() == 0
351                 || photoBounds.height() == 0) {
352             return; // fail fast
353         }
354         if ((flags & FLAG_CHECK) == 0) {
355             return; // no output options
356         }
357         if ((flags & DO_SET_WALLPAPER) != 0) {
358             Toast.makeText(this, R.string.setting_wallpaper, Toast.LENGTH_LONG).show();
359         }
360
361         final View loading = findViewById(R.id.loading);
362         loading.setVisibility(View.VISIBLE);
363         BitmapIOTask ioTask = new BitmapIOTask(sourceUri, destUri, format, flags, cropBounds,
364                 photoBounds, currentBitmapBounds, rotation, mOutputX, mOutputY);
365         ioTask.execute(currentBitmap);
366     }
367
368     private void doneBitmapIO(boolean success, Intent intent) {
369         final View loading = findViewById(R.id.loading);
370         loading.setVisibility(View.GONE);
371         if (success) {
372             setResult(RESULT_OK, intent);
373         } else {
374             setResult(RESULT_CANCELED, intent);
375         }
376         done();
377     }
378
379     private class BitmapIOTask extends AsyncTask<Bitmap, Void, Boolean> {
380
381         private final WallpaperManager mWPManager;
382         InputStream mInStream = null;
383         OutputStream mOutStream = null;
384         String mOutputFormat = null;
385         Uri mOutUri = null;
386         Uri mInUri = null;
387         int mFlags = 0;
388         RectF mCrop = null;
389         RectF mPhoto = null;
390         RectF mOrig = null;
391         Intent mResultIntent = null;
392         int mRotation = 0;
393
394         // Helper to setup input stream
395         private void regenerateInputStream() {
396             if (mInUri == null) {
397                 Log.w(LOGTAG, "cannot read original file, no input URI given");
398             } else {
399                 Utils.closeSilently(mInStream);
400                 try {
401                     mInStream = getContentResolver().openInputStream(mInUri);
402                 } catch (FileNotFoundException e) {
403                     Log.w(LOGTAG, "cannot read file: " + mInUri.toString(), e);
404                 }
405             }
406         }
407
408         public BitmapIOTask(Uri sourceUri, Uri destUri, String outputFormat, int flags,
409                 RectF cropBounds, RectF photoBounds, RectF originalBitmapBounds, int rotation,
410                 int outputX, int outputY) {
411             mOutputFormat = outputFormat;
412             mOutStream = null;
413             mOutUri = destUri;
414             mInUri = sourceUri;
415             mFlags = flags;
416             mCrop = cropBounds;
417             mPhoto = photoBounds;
418             mOrig = originalBitmapBounds;
419             mWPManager = WallpaperManager.getInstance(getApplicationContext());
420             mResultIntent = new Intent();
421             mRotation = (rotation < 0) ? -rotation : rotation;
422             mRotation %= 360;
423             mRotation = 90 * (int) (mRotation / 90);  // now mRotation is a multiple of 90
424             mOutputX = outputX;
425             mOutputY = outputY;
426
427             if ((flags & DO_EXTRA_OUTPUT) != 0) {
428                 if (mOutUri == null) {
429                     Log.w(LOGTAG, "cannot write file, no output URI given");
430                 } else {
431                     try {
432                         mOutStream = getContentResolver().openOutputStream(mOutUri);
433                     } catch (FileNotFoundException e) {
434                         Log.w(LOGTAG, "cannot write file: " + mOutUri.toString(), e);
435                     }
436                 }
437             }
438
439             if ((flags & (DO_EXTRA_OUTPUT | DO_SET_WALLPAPER)) != 0) {
440                 regenerateInputStream();
441             }
442         }
443
444         @Override
445         protected Boolean doInBackground(Bitmap... params) {
446             boolean failure = false;
447             Bitmap img = params[0];
448
449             // Set extra for crop bounds
450             if (mCrop != null && mPhoto != null && mOrig != null) {
451                 RectF trueCrop = CropMath.getScaledCropBounds(mCrop, mPhoto, mOrig);
452                 Matrix m = new Matrix();
453                 m.setRotate(mRotation);
454                 m.mapRect(trueCrop);
455                 if (trueCrop != null) {
456                     Rect rounded = new Rect();
457                     trueCrop.roundOut(rounded);
458                     mResultIntent.putExtra(CropExtras.KEY_CROPPED_RECT, rounded);
459                 }
460             }
461
462             // Find the small cropped bitmap that is returned in the intent
463             if ((mFlags & DO_RETURN_DATA) != 0) {
464                 assert (img != null);
465                 Bitmap ret = getCroppedImage(img, mCrop, mPhoto);
466                 if (ret != null) {
467                     ret = getDownsampledBitmap(ret, MAX_BMAP_IN_INTENT);
468                 }
469                 if (ret == null) {
470                     Log.w(LOGTAG, "could not downsample bitmap to return in data");
471                     failure = true;
472                 } else {
473                     if (mRotation > 0) {
474                         Matrix m = new Matrix();
475                         m.setRotate(mRotation);
476                         Bitmap tmp = Bitmap.createBitmap(ret, 0, 0, ret.getWidth(),
477                                 ret.getHeight(), m, true);
478                         if (tmp != null) {
479                             ret = tmp;
480                         }
481                     }
482                     mResultIntent.putExtra(CropExtras.KEY_DATA, ret);
483                 }
484             }
485
486             // Do the large cropped bitmap and/or set the wallpaper
487             if ((mFlags & (DO_EXTRA_OUTPUT | DO_SET_WALLPAPER)) != 0 && mInStream != null) {
488                 // Find crop bounds (scaled to original image size)
489                 RectF trueCrop = CropMath.getScaledCropBounds(mCrop, mPhoto, mOrig);
490                 if (trueCrop == null) {
491                     Log.w(LOGTAG, "cannot find crop for full size image");
492                     failure = true;
493                     return false;
494                 }
495                 Rect roundedTrueCrop = new Rect();
496                 trueCrop.roundOut(roundedTrueCrop);
497
498                 if (roundedTrueCrop.width() <= 0 || roundedTrueCrop.height() <= 0) {
499                     Log.w(LOGTAG, "crop has bad values for full size image");
500                     failure = true;
501                     return false;
502                 }
503
504                 // Attempt to open a region decoder
505                 BitmapRegionDecoder decoder = null;
506                 try {
507                     decoder = BitmapRegionDecoder.newInstance(mInStream, true);
508                 } catch (IOException e) {
509                     Log.w(LOGTAG, "cannot open region decoder for file: " + mInUri.toString(), e);
510                 }
511
512                 Bitmap crop = null;
513                 if (decoder != null) {
514                     // Do region decoding to get crop bitmap
515                     BitmapFactory.Options options = new BitmapFactory.Options();
516                     options.inMutable = true;
517                     crop = decoder.decodeRegion(roundedTrueCrop, options);
518                     decoder.recycle();
519                 }
520
521                 if (crop == null) {
522                     // BitmapRegionDecoder has failed, try to crop in-memory
523                     regenerateInputStream();
524                     Bitmap fullSize = null;
525                     if (mInStream != null) {
526                         fullSize = BitmapFactory.decodeStream(mInStream);
527                     }
528                     if (fullSize != null) {
529                         crop = Bitmap.createBitmap(fullSize, roundedTrueCrop.left,
530                                 roundedTrueCrop.top, roundedTrueCrop.width(),
531                                 roundedTrueCrop.height());
532                     }
533                 }
534
535                 if (crop == null) {
536                     Log.w(LOGTAG, "cannot decode file: " + mInUri.toString());
537                     failure = true;
538                     return false;
539                 }
540                 if (mOutputX > 0 && mOutputY > 0) {
541                     Matrix m = new Matrix();
542                     RectF cropRect = new RectF(0, 0, crop.getWidth(), crop.getHeight());
543                     if (mRotation > 0) {
544                         m.setRotate(mRotation);
545                         m.mapRect(cropRect);
546                     }
547                     RectF returnRect = new RectF(0, 0, mOutputX, mOutputY);
548                     m.setRectToRect(cropRect, returnRect, Matrix.ScaleToFit.FILL);
549                     m.preRotate(mRotation);
550                     Bitmap tmp = Bitmap.createBitmap((int) returnRect.width(),
551                             (int) returnRect.height(), Bitmap.Config.ARGB_8888);
552                     if (tmp != null) {
553                         Canvas c = new Canvas(tmp);
554                         c.drawBitmap(crop, m, new Paint());
555                         crop = tmp;
556                     }
557                 } else if (mRotation > 0) {
558                     Matrix m = new Matrix();
559                     m.setRotate(mRotation);
560                     Bitmap tmp = Bitmap.createBitmap(crop, 0, 0, crop.getWidth(),
561                             crop.getHeight(), m, true);
562                     if (tmp != null) {
563                         crop = tmp;
564                     }
565                 }
566                 // Get output compression format
567                 CompressFormat cf =
568                         convertExtensionToCompressFormat(getFileExtension(mOutputFormat));
569
570                 // If we only need to output to a URI, compress straight to file
571                 if (mFlags == DO_EXTRA_OUTPUT) {
572                     if (mOutStream == null
573                             || !crop.compress(cf, DEFAULT_COMPRESS_QUALITY, mOutStream)) {
574                         Log.w(LOGTAG, "failed to compress bitmap to file: " + mOutUri.toString());
575                         failure = true;
576                     } else {
577                         mResultIntent.setData(mOutUri);
578                     }
579                 } else {
580                     // Compress to byte array
581                     ByteArrayOutputStream tmpOut = new ByteArrayOutputStream(2048);
582                     if (crop.compress(cf, DEFAULT_COMPRESS_QUALITY, tmpOut)) {
583
584                         // If we need to output to a Uri, write compressed
585                         // bitmap out
586                         if ((mFlags & DO_EXTRA_OUTPUT) != 0) {
587                             if (mOutStream == null) {
588                                 Log.w(LOGTAG,
589                                         "failed to compress bitmap to file: " + mOutUri.toString());
590                                 failure = true;
591                             } else {
592                                 try {
593                                     mOutStream.write(tmpOut.toByteArray());
594                                     mResultIntent.setData(mOutUri);
595                                 } catch (IOException e) {
596                                     Log.w(LOGTAG,
597                                             "failed to compress bitmap to file: "
598                                                     + mOutUri.toString(), e);
599                                     failure = true;
600                                 }
601                             }
602                         }
603
604                         // If we need to set to the wallpaper, set it
605                         if ((mFlags & DO_SET_WALLPAPER) != 0 && mWPManager != null) {
606                             if (mWPManager == null) {
607                                 Log.w(LOGTAG, "no wallpaper manager");
608                                 failure = true;
609                             } else {
610                                 try {
611                                     mWPManager.setStream(new ByteArrayInputStream(tmpOut
612                                             .toByteArray()));
613                                 } catch (IOException e) {
614                                     Log.w(LOGTAG, "cannot write stream to wallpaper", e);
615                                     failure = true;
616                                 }
617                             }
618                         }
619                     } else {
620                         Log.w(LOGTAG, "cannot compress bitmap");
621                         failure = true;
622                     }
623                 }
624             }
625             return !failure; // True if any of the operations failed
626         }
627
628         @Override
629         protected void onPostExecute(Boolean result) {
630             Utils.closeSilently(mOutStream);
631             Utils.closeSilently(mInStream);
632             doneBitmapIO(result.booleanValue(), mResultIntent);
633         }
634
635     }
636
637     private void done() {
638         finish();
639     }
640
641     protected static Bitmap getCroppedImage(Bitmap image, RectF cropBounds, RectF photoBounds) {
642         RectF imageBounds = new RectF(0, 0, image.getWidth(), image.getHeight());
643         RectF crop = CropMath.getScaledCropBounds(cropBounds, photoBounds, imageBounds);
644         if (crop == null) {
645             return null;
646         }
647         Rect intCrop = new Rect();
648         crop.roundOut(intCrop);
649         return Bitmap.createBitmap(image, intCrop.left, intCrop.top, intCrop.width(),
650                 intCrop.height());
651     }
652
653     protected static Bitmap getDownsampledBitmap(Bitmap image, int max_size) {
654         if (image == null || image.getWidth() == 0 || image.getHeight() == 0 || max_size < 16) {
655             throw new IllegalArgumentException("Bad argument to getDownsampledBitmap()");
656         }
657         int shifts = 0;
658         int size = CropMath.getBitmapSize(image);
659         while (size > max_size) {
660             shifts++;
661             size /= 4;
662         }
663         Bitmap ret = Bitmap.createScaledBitmap(image, image.getWidth() >> shifts,
664                 image.getHeight() >> shifts, true);
665         if (ret == null) {
666             return null;
667         }
668         // Handle edge case for rounding.
669         if (CropMath.getBitmapSize(ret) > max_size) {
670             return Bitmap.createScaledBitmap(ret, ret.getWidth() >> 1, ret.getHeight() >> 1, true);
671         }
672         return ret;
673     }
674
675     /**
676      * Gets the crop extras from the intent, or null if none exist.
677      */
678     protected static CropExtras getExtrasFromIntent(Intent intent) {
679         Bundle extras = intent.getExtras();
680         if (extras != null) {
681             return new CropExtras(extras.getInt(CropExtras.KEY_OUTPUT_X, 0),
682                     extras.getInt(CropExtras.KEY_OUTPUT_Y, 0),
683                     extras.getBoolean(CropExtras.KEY_SCALE, true) &&
684                             extras.getBoolean(CropExtras.KEY_SCALE_UP_IF_NEEDED, false),
685                     extras.getInt(CropExtras.KEY_ASPECT_X, 0),
686                     extras.getInt(CropExtras.KEY_ASPECT_Y, 0),
687                     extras.getBoolean(CropExtras.KEY_SET_AS_WALLPAPER, false),
688                     extras.getBoolean(CropExtras.KEY_RETURN_DATA, false),
689                     (Uri) extras.getParcelable(MediaStore.EXTRA_OUTPUT),
690                     extras.getString(CropExtras.KEY_OUTPUT_FORMAT),
691                     extras.getBoolean(CropExtras.KEY_SHOW_WHEN_LOCKED, false),
692                     extras.getFloat(CropExtras.KEY_SPOTLIGHT_X),
693                     extras.getFloat(CropExtras.KEY_SPOTLIGHT_Y));
694         }
695         return null;
696     }
697
698     protected static CompressFormat convertExtensionToCompressFormat(String extension) {
699         return extension.equals("png") ? CompressFormat.PNG : CompressFormat.JPEG;
700     }
701
702     protected static String getFileExtension(String requestFormat) {
703         String outputFormat = (requestFormat == null)
704                 ? "jpg"
705                 : requestFormat;
706         outputFormat = outputFormat.toLowerCase();
707         return (outputFormat.equals("png") || outputFormat.equals("gif"))
708                 ? "png" // We don't support gif compression.
709                 : "jpg";
710     }
711
712     private RectF getBitmapCrop(RectF imageBounds) {
713         RectF crop = mCropView.getCrop();
714         RectF photo = mCropView.getPhoto();
715         if (crop == null || photo == null) {
716             Log.w(LOGTAG, "could not get crop");
717             return null;
718         }
719         RectF scaledCrop = CropMath.getScaledCropBounds(crop, photo, imageBounds);
720         return scaledCrop;
721     }
722
723     @Override
724     public boolean onGenericMotionEvent(MotionEvent event) {
725         boolean handled = super.onGenericMotionEvent(event);
726         if (!handled) {
727             handled = mGestureDetector.onGenericMotionEvent(event);
728             if (!handled
729                     && (event.getSource() & TouchNavGestureDetector.SOURCE_TOUCH_NAVIGATION)
730                         == TouchNavGestureDetector.SOURCE_TOUCH_NAVIGATION) {
731                 int action = event.getActionMasked();
732                 switch (action) {
733                     case MotionEvent.ACTION_UP:
734                     case MotionEvent.ACTION_CANCEL:
735                         sendScaledMotionEvent(event);
736                         handled = true;
737                         break;
738                 }
739             }
740         }
741         return handled;
742     }
743
744     @Override
745     public boolean onKeyDown(int keyCode, KeyEvent event) {
746         boolean handled = super.onKeyDown(keyCode, event);
747         if (!handled) {
748             if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER || keyCode == KeyEvent.KEYCODE_ENTER) {
749                 startFinishOutput();
750                 handled = true;
751             }
752         }
753         return handled;
754     }
755
756     /**
757      * Returns whether a scroll happened.
758      */
759     @Override
760     public boolean onScroll(MotionEvent downEvent, MotionEvent currentEvent, float deltaX,
761             float deltaY) {
762         // If <5mm from the start, consider it a click. Used for smoother
763         // clicks.
764         if (!mDidStartDrag) {
765             float distanceX = Math.abs((currentEvent.getX() - downEvent.getX()) / mResolutionX);
766             float distanceY = Math.abs((currentEvent.getY() - downEvent.getY()) / mResolutionY);
767             if (distanceX < MIN_SCROLL_LENGTH && distanceY < MIN_SCROLL_LENGTH) {
768                 // Not moved enough to scroll.
769                 return false;
770             }
771         }
772         // Moved enough to scroll.
773         mDidStartDrag = true;
774         sendScaledMotionEvent(currentEvent);
775         return true;
776     }
777
778     @Override
779     public boolean onFling(MotionEvent downEvent, MotionEvent currentEvent, float velX,
780             float velY) {
781         sendScaledMotionEvent(currentEvent);
782         return true;
783     }
784
785     @Override
786     public boolean onDown(MotionEvent downEvent) {
787         mDidStartDrag = false;
788         InputDevice device = downEvent.getDevice();
789         MotionRange motionRangeX = device.getMotionRange(MotionEvent.AXIS_X);
790         MotionRange motionRangeY = device.getMotionRange(MotionEvent.AXIS_Y);
791
792         if (motionRangeX == null || motionRangeY == null) {
793             return false;
794         }
795
796         mResolutionX = motionRangeX.getResolution();
797         mResolutionY = motionRangeY.getResolution();
798
799         if (mResolutionX == 0) {
800             mResolutionX = TouchNavGestureDetector.DEFAULT_TOUCH_RESOLUTION;
801         }
802         if (mResolutionY == 0) {
803             mResolutionY = TouchNavGestureDetector.DEFAULT_TOUCH_RESOLUTION;
804         }
805
806         // This is the conversion between a drag on the device vs. a drag on
807         // the screen.
808         mTouchPadToViewRatioX = mCropView.getWidth() / motionRangeX.getRange();
809         mTouchPadToViewRatioY = mCropView.getHeight() / motionRangeY.getRange();
810
811         sendScaledMotionEvent(downEvent);
812         return true;
813     }
814
815     private void sendScaledMotionEvent(MotionEvent event) {
816         final int scaledX = (int) (mTouchPadToViewRatioX * event.getX());
817         final int scaledY = (int) (mTouchPadToViewRatioY * event.getY());
818         final MotionEvent ev = MotionEvent.obtain(event.getEventTime(), event.getEventTime(),
819                 event.getActionMasked(), scaledX, scaledY, 0);
820         mCropView.onTouchEvent(ev);
821         ev.recycle();
822     }
823
824 }