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 /* Copied from Launcher3 */
17 package com.android.wallpapercropper;
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.SharedPreferences;
25 import android.content.res.Configuration;
26 import android.content.res.Resources;
27 import android.graphics.Bitmap;
28 import android.graphics.Bitmap.CompressFormat;
29 import android.graphics.BitmapFactory;
30 import android.graphics.BitmapRegionDecoder;
31 import android.graphics.Canvas;
32 import android.graphics.Matrix;
33 import android.graphics.Paint;
34 import android.graphics.Point;
35 import android.graphics.Rect;
36 import android.graphics.RectF;
37 import android.net.Uri;
38 import android.os.AsyncTask;
39 import android.os.Bundle;
40 import android.util.Log;
41 import android.view.Display;
42 import android.view.View;
43 import android.view.WindowManager;
44 import android.widget.Toast;
45 
46 import com.android.gallery3d.common.Utils;
47 import com.android.gallery3d.exif.ExifInterface;
48 import com.android.photos.BitmapRegionTileSource;
49 import com.android.photos.BitmapRegionTileSource.BitmapSource;
50 
51 import java.io.BufferedInputStream;
52 import java.io.ByteArrayInputStream;
53 import java.io.ByteArrayOutputStream;
54 import java.io.FileNotFoundException;
55 import java.io.IOException;
56 import java.io.InputStream;
57 
58 public class WallpaperCropActivity extends Activity {
59     private static final String LOGTAG = "Launcher3.CropActivity";
60 
61     protected static final String WALLPAPER_WIDTH_KEY = "wallpaper.width";
62     protected static final String WALLPAPER_HEIGHT_KEY = "wallpaper.height";
63     private static final int DEFAULT_COMPRESS_QUALITY = 90;
64     /**
65      * The maximum bitmap size we allow to be returned through the intent.
66      * Intents have a maximum of 1MB in total size. However, the Bitmap seems to
67      * have some overhead to hit so that we go way below the limit here to make
68      * sure the intent stays below 1MB.We should consider just returning a byte
69      * array instead of a Bitmap instance to avoid overhead.
70      */
71     public static final int MAX_BMAP_IN_INTENT = 750000;
72     private static final float WALLPAPER_SCREENS_SPAN = 2f;
73 
74     protected static Point sDefaultWallpaperSize;
75 
76     protected CropView mCropView;
77     protected Uri mUri;
78     private View mSetWallpaperButton;
79 
80     @Override
onCreate(Bundle savedInstanceState)81     protected void onCreate(Bundle savedInstanceState) {
82         super.onCreate(savedInstanceState);
83         init();
84         if (!enableRotation()) {
85             setRequestedOrientation(Configuration.ORIENTATION_PORTRAIT);
86         }
87     }
88 
init()89     protected void init() {
90         setContentView(R.layout.wallpaper_cropper);
91 
92         mCropView = (CropView) findViewById(R.id.cropView);
93 
94         Intent cropIntent = getIntent();
95         final Uri imageUri = cropIntent.getData();
96 
97         if (imageUri == null) {
98             Log.e(LOGTAG, "No URI passed in intent, exiting WallpaperCropActivity");
99             finish();
100             return;
101         }
102 
103         // Action bar
104         // Show the custom action bar view
105         final ActionBar actionBar = getActionBar();
106         actionBar.setCustomView(R.layout.actionbar_set_wallpaper);
107         actionBar.getCustomView().setOnClickListener(
108                 new View.OnClickListener() {
109                     @Override
110                     public void onClick(View v) {
111                         boolean finishActivityWhenDone = true;
112                         cropImageAndSetWallpaper(imageUri, null, finishActivityWhenDone);
113                     }
114                 });
115         mSetWallpaperButton = findViewById(R.id.set_wallpaper_button);
116 
117         // Load image in background
118         final BitmapRegionTileSource.UriBitmapSource bitmapSource =
119                 new BitmapRegionTileSource.UriBitmapSource(this, imageUri, 1024);
120         mSetWallpaperButton.setVisibility(View.INVISIBLE);
121         Runnable onLoad = new Runnable() {
122             public void run() {
123                 if (bitmapSource.getLoadingState() != BitmapSource.State.LOADED) {
124                     Toast.makeText(WallpaperCropActivity.this,
125                             getString(R.string.wallpaper_load_fail),
126                             Toast.LENGTH_LONG).show();
127                     finish();
128                 } else {
129                     mSetWallpaperButton.setVisibility(View.VISIBLE);
130                 }
131             }
132         };
133         setCropViewTileSource(bitmapSource, true, false, onLoad);
134     }
135 
136     @Override
onDestroy()137     protected void onDestroy() {
138         if (mCropView != null) {
139             mCropView.destroy();
140         }
141         super.onDestroy();
142     }
143 
setCropViewTileSource( final BitmapRegionTileSource.BitmapSource bitmapSource, final boolean touchEnabled, final boolean moveToLeft, final Runnable postExecute)144     public void setCropViewTileSource(
145             final BitmapRegionTileSource.BitmapSource bitmapSource, final boolean touchEnabled,
146             final boolean moveToLeft, final Runnable postExecute) {
147         final Context context = WallpaperCropActivity.this;
148         final View progressView = findViewById(R.id.loading);
149         final AsyncTask<Void, Void, Void> loadBitmapTask = new AsyncTask<Void, Void, Void>() {
150             protected Void doInBackground(Void...args) {
151                 if (!isCancelled()) {
152                     try {
153                         bitmapSource.loadInBackground();
154                     } catch (SecurityException securityException) {
155                         if (isDestroyed()) {
156                             // Temporarily granted permissions are revoked when the activity
157                             // finishes, potentially resulting in a SecurityException here.
158                             // Even though {@link #isDestroyed} might also return true in different
159                             // situations where the configuration changes, we are fine with
160                             // catching these cases here as well.
161                             cancel(false);
162                         } else {
163                             // otherwise it had a different cause and we throw it further
164                             throw securityException;
165                         }
166                     }
167                 }
168                 return null;
169             }
170             protected void onPostExecute(Void arg) {
171                 if (!isCancelled()) {
172                     progressView.setVisibility(View.INVISIBLE);
173                     if (bitmapSource.getLoadingState() == BitmapSource.State.LOADED) {
174                         mCropView.setTileSource(
175                                 new BitmapRegionTileSource(context, bitmapSource), null);
176                         mCropView.setTouchEnabled(touchEnabled);
177                         if (moveToLeft) {
178                             mCropView.moveToLeft();
179                         }
180                     }
181                 }
182                 if (postExecute != null) {
183                     postExecute.run();
184                 }
185             }
186         };
187         // We don't want to show the spinner every time we load an image, because that would be
188         // annoying; instead, only start showing the spinner if loading the image has taken
189         // longer than 1 sec (ie 1000 ms)
190         progressView.postDelayed(new Runnable() {
191             public void run() {
192                 if (loadBitmapTask.getStatus() != AsyncTask.Status.FINISHED) {
193                     progressView.setVisibility(View.VISIBLE);
194                 }
195             }
196         }, 1000);
197         loadBitmapTask.execute();
198     }
199 
enableRotation()200     public boolean enableRotation() {
201         return getResources().getBoolean(R.bool.allow_rotation);
202     }
203 
getSharedPreferencesKey()204     public static String getSharedPreferencesKey() {
205         return WallpaperCropActivity.class.getName();
206     }
207 
208     // As a ratio of screen height, the total distance we want the parallax effect to span
209     // horizontally
wallpaperTravelToScreenWidthRatio(int width, int height)210     private static float wallpaperTravelToScreenWidthRatio(int width, int height) {
211         float aspectRatio = width / (float) height;
212 
213         // At an aspect ratio of 16/10, the wallpaper parallax effect should span 1.5 * screen width
214         // At an aspect ratio of 10/16, the wallpaper parallax effect should span 1.2 * screen width
215         // We will use these two data points to extrapolate how much the wallpaper parallax effect
216         // to span (ie travel) at any aspect ratio:
217 
218         final float ASPECT_RATIO_LANDSCAPE = 16/10f;
219         final float ASPECT_RATIO_PORTRAIT = 10/16f;
220         final float WALLPAPER_WIDTH_TO_SCREEN_RATIO_LANDSCAPE = 1.5f;
221         final float WALLPAPER_WIDTH_TO_SCREEN_RATIO_PORTRAIT = 1.2f;
222 
223         // To find out the desired width at different aspect ratios, we use the following two
224         // formulas, where the coefficient on x is the aspect ratio (width/height):
225         //   (16/10)x + y = 1.5
226         //   (10/16)x + y = 1.2
227         // We solve for x and y and end up with a final formula:
228         final float x =
229             (WALLPAPER_WIDTH_TO_SCREEN_RATIO_LANDSCAPE - WALLPAPER_WIDTH_TO_SCREEN_RATIO_PORTRAIT) /
230             (ASPECT_RATIO_LANDSCAPE - ASPECT_RATIO_PORTRAIT);
231         final float y = WALLPAPER_WIDTH_TO_SCREEN_RATIO_PORTRAIT - x * ASPECT_RATIO_PORTRAIT;
232         return x * aspectRatio + y;
233     }
234 
getDefaultWallpaperSize(Resources res, WindowManager windowManager)235     static protected Point getDefaultWallpaperSize(Resources res, WindowManager windowManager) {
236         if (sDefaultWallpaperSize == null) {
237             Point minDims = new Point();
238             Point maxDims = new Point();
239             windowManager.getDefaultDisplay().getCurrentSizeRange(minDims, maxDims);
240 
241             int maxDim = Math.max(maxDims.x, maxDims.y);
242             int minDim = Math.max(minDims.x, minDims.y);
243 
244             if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN_MR1) {
245                 Point realSize = new Point();
246                 windowManager.getDefaultDisplay().getRealSize(realSize);
247                 maxDim = Math.max(realSize.x, realSize.y);
248                 minDim = Math.min(realSize.x, realSize.y);
249             }
250 
251             // We need to ensure that there is enough extra space in the wallpaper
252             // for the intended parallax effects
253             final int defaultWidth, defaultHeight;
254             if (isScreenLarge(res)) {
255                 defaultWidth = (int) (maxDim * wallpaperTravelToScreenWidthRatio(maxDim, minDim));
256                 defaultHeight = maxDim;
257             } else {
258                 defaultWidth = Math.max((int) (minDim * WALLPAPER_SCREENS_SPAN), maxDim);
259                 defaultHeight = maxDim;
260             }
261             sDefaultWallpaperSize = new Point(defaultWidth, defaultHeight);
262         }
263         return sDefaultWallpaperSize;
264     }
265 
getRotationFromExif(String path)266     public static int getRotationFromExif(String path) {
267         return getRotationFromExifHelper(path, null, 0, null, null);
268     }
269 
getRotationFromExif(Context context, Uri uri)270     public static int getRotationFromExif(Context context, Uri uri) {
271         return getRotationFromExifHelper(null, null, 0, context, uri);
272     }
273 
getRotationFromExif(Resources res, int resId)274     public static int getRotationFromExif(Resources res, int resId) {
275         return getRotationFromExifHelper(null, res, resId, null, null);
276     }
277 
getRotationFromExifHelper( String path, Resources res, int resId, Context context, Uri uri)278     private static int getRotationFromExifHelper(
279             String path, Resources res, int resId, Context context, Uri uri) {
280         ExifInterface ei = new ExifInterface();
281         InputStream is = null;
282         BufferedInputStream bis = null;
283         try {
284             if (path != null) {
285                 ei.readExif(path);
286             } else if (uri != null) {
287                 is = context.getContentResolver().openInputStream(uri);
288                 bis = new BufferedInputStream(is);
289                 ei.readExif(bis);
290             } else {
291                 is = res.openRawResource(resId);
292                 bis = new BufferedInputStream(is);
293                 ei.readExif(bis);
294             }
295             Integer ori = ei.getTagIntValue(ExifInterface.TAG_ORIENTATION);
296             if (ori != null) {
297                 return ExifInterface.getRotationForOrientationValue(ori.shortValue());
298             }
299         } catch (IOException e) {
300             Log.w(LOGTAG, "Getting exif data failed", e);
301         } catch (NullPointerException e) {
302             // Sometimes the ExifInterface has an internal NPE if Exif data isn't valid
303             Log.w(LOGTAG, "Getting exif data failed", e);
304         } finally {
305             Utils.closeSilently(bis);
306             Utils.closeSilently(is);
307         }
308         return 0;
309     }
310 
setWallpaper(String filePath, final boolean finishActivityWhenDone)311     protected void setWallpaper(String filePath, final boolean finishActivityWhenDone) {
312         int rotation = getRotationFromExif(filePath);
313         BitmapCropTask cropTask = new BitmapCropTask(
314                 this, filePath, null, rotation, 0, 0, true, false, null);
315         final Point bounds = cropTask.getImageBounds();
316         Runnable onEndCrop = new Runnable() {
317             public void run() {
318                 if (finishActivityWhenDone) {
319                     setResult(Activity.RESULT_OK);
320                     finish();
321                 }
322             }
323         };
324         cropTask.setOnEndRunnable(onEndCrop);
325         cropTask.setNoCrop(true);
326         cropTask.execute();
327     }
328 
cropImageAndSetWallpaper( Resources res, int resId, final boolean finishActivityWhenDone)329     protected void cropImageAndSetWallpaper(
330             Resources res, int resId, final boolean finishActivityWhenDone) {
331         // crop this image and scale it down to the default wallpaper size for
332         // this device
333         int rotation = getRotationFromExif(res, resId);
334         Point inSize = mCropView.getSourceDimensions();
335         Point outSize = getDefaultWallpaperSize(getResources(),
336                 getWindowManager());
337         RectF crop = getMaxCropRect(
338                 inSize.x, inSize.y, outSize.x, outSize.y, false);
339         Runnable onEndCrop = new Runnable() {
340             public void run() {
341                 if (finishActivityWhenDone) {
342                     setResult(Activity.RESULT_OK);
343                     finish();
344                 }
345             }
346         };
347         BitmapCropTask cropTask = new BitmapCropTask(this, res, resId,
348                 crop, rotation, outSize.x, outSize.y, true, false, onEndCrop);
349         cropTask.execute();
350     }
351 
isScreenLarge(Resources res)352     private static boolean isScreenLarge(Resources res) {
353         Configuration config = res.getConfiguration();
354         return config.smallestScreenWidthDp >= 720;
355     }
356 
cropImageAndSetWallpaper(Uri uri, OnBitmapCroppedHandler onBitmapCroppedHandler, final boolean finishActivityWhenDone)357     protected void cropImageAndSetWallpaper(Uri uri,
358             OnBitmapCroppedHandler onBitmapCroppedHandler, final boolean finishActivityWhenDone) {
359         boolean centerCrop = getResources().getBoolean(R.bool.center_crop);
360         // Get the crop
361         boolean ltr = mCropView.getLayoutDirection() == View.LAYOUT_DIRECTION_LTR;
362 
363         Display d = getWindowManager().getDefaultDisplay();
364 
365         Point displaySize = new Point();
366         d.getSize(displaySize);
367         boolean isPortrait = displaySize.x < displaySize.y;
368 
369         Point defaultWallpaperSize = getDefaultWallpaperSize(getResources(),
370                 getWindowManager());
371         // Get the crop
372         RectF cropRect = mCropView.getCrop();
373 
374         Point inSize = mCropView.getSourceDimensions();
375 
376         int cropRotation = mCropView.getImageRotation();
377         float cropScale = mCropView.getWidth() / (float) cropRect.width();
378 
379         Matrix rotateMatrix = new Matrix();
380         rotateMatrix.setRotate(cropRotation);
381         float[] rotatedInSize = new float[] { inSize.x, inSize.y };
382         rotateMatrix.mapPoints(rotatedInSize);
383         rotatedInSize[0] = Math.abs(rotatedInSize[0]);
384         rotatedInSize[1] = Math.abs(rotatedInSize[1]);
385 
386         // Due to rounding errors in the cropview renderer the edges can be slightly offset
387         // therefore we ensure that the boundaries are sanely defined
388         cropRect.left = Math.max(0, cropRect.left);
389         cropRect.right = Math.min(rotatedInSize[0], cropRect.right);
390         cropRect.top = Math.max(0, cropRect.top);
391         cropRect.bottom = Math.min(rotatedInSize[1], cropRect.bottom);
392 
393         // ADJUST CROP WIDTH
394         // Extend the crop all the way to the right, for parallax
395         // (or all the way to the left, in RTL)
396         float extraSpace;
397         if (centerCrop) {
398             extraSpace = 2f * Math.min(rotatedInSize[0] - cropRect.right, cropRect.left);
399         } else {
400             extraSpace = ltr ? rotatedInSize[0] - cropRect.right : cropRect.left;
401         }
402         // Cap the amount of extra width
403         float maxExtraSpace = defaultWallpaperSize.x / cropScale - cropRect.width();
404         extraSpace = Math.min(extraSpace, maxExtraSpace);
405 
406         if (centerCrop) {
407             cropRect.left -= extraSpace / 2f;
408             cropRect.right += extraSpace / 2f;
409         } else {
410             if (ltr) {
411                 cropRect.right += extraSpace;
412             } else {
413                 cropRect.left -= extraSpace;
414             }
415         }
416 
417         // ADJUST CROP HEIGHT
418         if (isPortrait) {
419             cropRect.bottom = cropRect.top + defaultWallpaperSize.y / cropScale;
420         } else { // LANDSCAPE
421             float extraPortraitHeight =
422                     defaultWallpaperSize.y / cropScale - cropRect.height();
423             float expandHeight =
424                     Math.min(Math.min(rotatedInSize[1] - cropRect.bottom, cropRect.top),
425                             extraPortraitHeight / 2);
426             cropRect.top -= expandHeight;
427             cropRect.bottom += expandHeight;
428         }
429         final int outWidth = (int) Math.round(cropRect.width() * cropScale);
430         final int outHeight = (int) Math.round(cropRect.height() * cropScale);
431 
432         Runnable onEndCrop = new Runnable() {
433             public void run() {
434                 if (finishActivityWhenDone) {
435                     setResult(Activity.RESULT_OK);
436                     finish();
437                 }
438             }
439         };
440         BitmapCropTask cropTask = new BitmapCropTask(this, uri,
441                 cropRect, cropRotation, outWidth, outHeight, true, false, onEndCrop);
442         if (onBitmapCroppedHandler != null) {
443             cropTask.setOnBitmapCropped(onBitmapCroppedHandler);
444         }
445         cropTask.execute();
446     }
447 
448     public interface OnBitmapCroppedHandler {
449         public void onBitmapCropped(byte[] imageBytes);
450     }
451 
452     protected static class BitmapCropTask extends AsyncTask<Void, Void, Boolean> {
453         Uri mInUri = null;
454         Context mContext;
455         String mInFilePath;
456         byte[] mInImageBytes;
457         int mInResId = 0;
458         RectF mCropBounds = null;
459         int mOutWidth, mOutHeight;
460         int mRotation;
461         String mOutputFormat = "jpg"; // for now
462         boolean mSetWallpaper;
463         boolean mSaveCroppedBitmap;
464         Bitmap mCroppedBitmap;
465         Runnable mOnEndRunnable;
466         Resources mResources;
467         OnBitmapCroppedHandler mOnBitmapCroppedHandler;
468         boolean mNoCrop;
469 
470         public BitmapCropTask(Context c, String filePath,
471                 RectF cropBounds, int rotation, int outWidth, int outHeight,
472                 boolean setWallpaper, boolean saveCroppedBitmap, Runnable onEndRunnable) {
473             mContext = c;
474             mInFilePath = filePath;
475             init(cropBounds, rotation,
476                     outWidth, outHeight, setWallpaper, saveCroppedBitmap, onEndRunnable);
477         }
478 
479         public BitmapCropTask(byte[] imageBytes,
480                 RectF cropBounds, int rotation, int outWidth, int outHeight,
481                 boolean setWallpaper, boolean saveCroppedBitmap, Runnable onEndRunnable) {
482             mInImageBytes = imageBytes;
483             init(cropBounds, rotation,
484                     outWidth, outHeight, setWallpaper, saveCroppedBitmap, onEndRunnable);
485         }
486 
487         public BitmapCropTask(Context c, Uri inUri,
488                 RectF cropBounds, int rotation, int outWidth, int outHeight,
489                 boolean setWallpaper, boolean saveCroppedBitmap, Runnable onEndRunnable) {
490             mContext = c;
491             mInUri = inUri;
492             init(cropBounds, rotation,
493                     outWidth, outHeight, setWallpaper, saveCroppedBitmap, onEndRunnable);
494         }
495 
496         public BitmapCropTask(Context c, Resources res, int inResId,
497                 RectF cropBounds, int rotation, int outWidth, int outHeight,
498                 boolean setWallpaper, boolean saveCroppedBitmap, Runnable onEndRunnable) {
499             mContext = c;
500             mInResId = inResId;
501             mResources = res;
502             init(cropBounds, rotation,
503                     outWidth, outHeight, setWallpaper, saveCroppedBitmap, onEndRunnable);
504         }
505 
506         private void init(RectF cropBounds, int rotation, int outWidth, int outHeight,
507                 boolean setWallpaper, boolean saveCroppedBitmap, Runnable onEndRunnable) {
508             mCropBounds = cropBounds;
509             mRotation = rotation;
510             mOutWidth = outWidth;
511             mOutHeight = outHeight;
512             mSetWallpaper = setWallpaper;
513             mSaveCroppedBitmap = saveCroppedBitmap;
514             mOnEndRunnable = onEndRunnable;
515         }
516 
517         public void setOnBitmapCropped(OnBitmapCroppedHandler handler) {
518             mOnBitmapCroppedHandler = handler;
519         }
520 
521         public void setNoCrop(boolean value) {
522             mNoCrop = value;
523         }
524 
525         public void setOnEndRunnable(Runnable onEndRunnable) {
526             mOnEndRunnable = onEndRunnable;
527         }
528 
529         // Helper to setup input stream
530         private InputStream regenerateInputStream() {
531             if (mInUri == null && mInResId == 0 && mInFilePath == null && mInImageBytes == null) {
532                 Log.w(LOGTAG, "cannot read original file, no input URI, resource ID, or " +
533                         "image byte array given");
534             } else {
535                 try {
536                     if (mInUri != null) {
537                         return new BufferedInputStream(
538                                 mContext.getContentResolver().openInputStream(mInUri));
539                     } else if (mInFilePath != null) {
540                         return mContext.openFileInput(mInFilePath);
541                     } else if (mInImageBytes != null) {
542                         return new BufferedInputStream(new ByteArrayInputStream(mInImageBytes));
543                     } else {
544                         return new BufferedInputStream(mResources.openRawResource(mInResId));
545                     }
546                 } catch (FileNotFoundException e) {
547                     Log.w(LOGTAG, "cannot read file: " + mInUri.toString(), e);
548                 }
549             }
550             return null;
551         }
552 
553         public Point getImageBounds() {
554             InputStream is = regenerateInputStream();
555             if (is != null) {
556                 BitmapFactory.Options options = new BitmapFactory.Options();
557                 options.inJustDecodeBounds = true;
558                 BitmapFactory.decodeStream(is, null, options);
559                 Utils.closeSilently(is);
560                 if (options.outWidth != 0 && options.outHeight != 0) {
561                     return new Point(options.outWidth, options.outHeight);
562                 }
563             }
564             return null;
565         }
566 
567         public void setCropBounds(RectF cropBounds) {
568             mCropBounds = cropBounds;
569         }
570 
571         public Bitmap getCroppedBitmap() {
572             return mCroppedBitmap;
573         }
574         public boolean cropBitmap() {
575             boolean failure = false;
576 
577 
578             WallpaperManager wallpaperManager = null;
579             if (mSetWallpaper) {
580                 wallpaperManager = WallpaperManager.getInstance(mContext.getApplicationContext());
581             }
582 
583 
584             if (mSetWallpaper && mNoCrop) {
585                 try {
586                     InputStream is = regenerateInputStream();
587                     if (is != null) {
588                         wallpaperManager.setStream(is);
589                         Utils.closeSilently(is);
590                     }
591                 } catch (IOException e) {
592                     Log.w(LOGTAG, "cannot write stream to wallpaper", e);
593                     failure = true;
594                 }
595                 return !failure;
596             } else {
597                 // Find crop bounds (scaled to original image size)
598                 Rect roundedTrueCrop = new Rect();
599                 Matrix rotateMatrix = new Matrix();
600                 Matrix inverseRotateMatrix = new Matrix();
601 
602                 Point bounds = getImageBounds();
603                 if (mRotation > 0) {
604                     rotateMatrix.setRotate(mRotation);
605                     inverseRotateMatrix.setRotate(-mRotation);
606 
607                     mCropBounds.roundOut(roundedTrueCrop);
608                     mCropBounds = new RectF(roundedTrueCrop);
609 
610                     if (bounds == null) {
611                         Log.w(LOGTAG, "cannot get bounds for image");
612                         failure = true;
613                         return false;
614                     }
615 
616                     float[] rotatedBounds = new float[] { bounds.x, bounds.y };
617                     rotateMatrix.mapPoints(rotatedBounds);
618                     rotatedBounds[0] = Math.abs(rotatedBounds[0]);
619                     rotatedBounds[1] = Math.abs(rotatedBounds[1]);
620 
621                     mCropBounds.offset(-rotatedBounds[0]/2, -rotatedBounds[1]/2);
622                     inverseRotateMatrix.mapRect(mCropBounds);
623                     mCropBounds.offset(bounds.x/2, bounds.y/2);
624 
625                 }
626 
627                 mCropBounds.roundOut(roundedTrueCrop);
628 
629                 if (roundedTrueCrop.width() <= 0 || roundedTrueCrop.height() <= 0) {
630                     Log.w(LOGTAG, "crop has bad values for full size image");
631                     failure = true;
632                     return false;
633                 }
634 
635                 // See how much we're reducing the size of the image
636                 int scaleDownSampleSize = Math.max(1, Math.min(roundedTrueCrop.width() / mOutWidth,
637                         roundedTrueCrop.height() / mOutHeight));
638                 // Attempt to open a region decoder
639                 BitmapRegionDecoder decoder = null;
640                 InputStream is = null;
641                 try {
642                     is = regenerateInputStream();
643                     if (is == null) {
644                         Log.w(LOGTAG, "cannot get input stream for uri=" + mInUri.toString());
645                         failure = true;
646                         return false;
647                     }
648                     decoder = BitmapRegionDecoder.newInstance(is, false);
649                     Utils.closeSilently(is);
650                 } catch (IOException e) {
651                     Log.w(LOGTAG, "cannot open region decoder for file: " + mInUri.toString(), e);
652                 } finally {
653                    Utils.closeSilently(is);
654                    is = null;
655                 }
656 
657                 Bitmap crop = null;
658                 if (decoder != null) {
659                     // Do region decoding to get crop bitmap
660                     BitmapFactory.Options options = new BitmapFactory.Options();
661                     if (scaleDownSampleSize > 1) {
662                         options.inSampleSize = scaleDownSampleSize;
663                     }
664                     crop = decoder.decodeRegion(roundedTrueCrop, options);
665                     decoder.recycle();
666                 }
667 
668                 if (crop == null) {
669                     // BitmapRegionDecoder has failed, try to crop in-memory
670                     is = regenerateInputStream();
671                     Bitmap fullSize = null;
672                     if (is != null) {
673                         BitmapFactory.Options options = new BitmapFactory.Options();
674                         if (scaleDownSampleSize > 1) {
675                             options.inSampleSize = scaleDownSampleSize;
676                         }
677                         fullSize = BitmapFactory.decodeStream(is, null, options);
678                         Utils.closeSilently(is);
679                     }
680                     if (fullSize != null) {
681                         // Find out the true sample size that was used by the decoder
682                         scaleDownSampleSize = bounds.x / fullSize.getWidth();
683                         mCropBounds.left /= scaleDownSampleSize;
684                         mCropBounds.top /= scaleDownSampleSize;
685                         mCropBounds.bottom /= scaleDownSampleSize;
686                         mCropBounds.right /= scaleDownSampleSize;
687                         mCropBounds.roundOut(roundedTrueCrop);
688 
689                         // Adjust values to account for issues related to rounding
690                         if (roundedTrueCrop.width() > fullSize.getWidth()) {
691                             // Adjust the width
692                             roundedTrueCrop.right = roundedTrueCrop.left + fullSize.getWidth();
693                         }
694                         if (roundedTrueCrop.right > fullSize.getWidth()) {
695                             // Adjust the left value
696                             int adjustment = roundedTrueCrop.left -
697                                     Math.max(0, roundedTrueCrop.right - roundedTrueCrop.width());
698                             roundedTrueCrop.left -= adjustment;
699                             roundedTrueCrop.right -= adjustment;
700                         }
701                         if (roundedTrueCrop.height() > fullSize.getHeight()) {
702                             // Adjust the height
703                             roundedTrueCrop.bottom = roundedTrueCrop.top + fullSize.getHeight();
704                         }
705                         if (roundedTrueCrop.bottom > fullSize.getHeight()) {
706                             // Adjust the top value
707                             int adjustment = roundedTrueCrop.top -
708                                     Math.max(0, roundedTrueCrop.bottom - roundedTrueCrop.height());
709                             roundedTrueCrop.top -= adjustment;
710                             roundedTrueCrop.bottom -= adjustment;
711                         }
712 
713                         crop = Bitmap.createBitmap(fullSize, roundedTrueCrop.left,
714                                 roundedTrueCrop.top, roundedTrueCrop.width(),
715                                 roundedTrueCrop.height());
716                     }
717                 }
718 
719                 if (crop == null) {
720                     Log.w(LOGTAG, "cannot decode file: " + mInUri.toString());
721                     failure = true;
722                     return false;
723                 }
724                 if (mOutWidth > 0 && mOutHeight > 0 || mRotation > 0) {
725                     float[] dimsAfter = new float[] { crop.getWidth(), crop.getHeight() };
726                     rotateMatrix.mapPoints(dimsAfter);
727                     dimsAfter[0] = Math.abs(dimsAfter[0]);
728                     dimsAfter[1] = Math.abs(dimsAfter[1]);
729 
730                     if (!(mOutWidth > 0 && mOutHeight > 0)) {
731                         mOutWidth = Math.round(dimsAfter[0]);
732                         mOutHeight = Math.round(dimsAfter[1]);
733                     }
734 
735                     RectF cropRect = new RectF(0, 0, dimsAfter[0], dimsAfter[1]);
736                     RectF returnRect = new RectF(0, 0, mOutWidth, mOutHeight);
737 
738                     Matrix m = new Matrix();
739                     if (mRotation == 0) {
740                         m.setRectToRect(cropRect, returnRect, Matrix.ScaleToFit.FILL);
741                     } else {
742                         Matrix m1 = new Matrix();
743                         m1.setTranslate(-crop.getWidth() / 2f, -crop.getHeight() / 2f);
744                         Matrix m2 = new Matrix();
745                         m2.setRotate(mRotation);
746                         Matrix m3 = new Matrix();
747                         m3.setTranslate(dimsAfter[0] / 2f, dimsAfter[1] / 2f);
748                         Matrix m4 = new Matrix();
749                         m4.setRectToRect(cropRect, returnRect, Matrix.ScaleToFit.FILL);
750 
751                         Matrix c1 = new Matrix();
752                         c1.setConcat(m2, m1);
753                         Matrix c2 = new Matrix();
754                         c2.setConcat(m4, m3);
755                         m.setConcat(c2, c1);
756                     }
757 
758                     Bitmap tmp = Bitmap.createBitmap((int) returnRect.width(),
759                             (int) returnRect.height(), Bitmap.Config.ARGB_8888);
760                     if (tmp != null) {
761                         Canvas c = new Canvas(tmp);
762                         Paint p = new Paint();
763                         p.setFilterBitmap(true);
764                         c.drawBitmap(crop, m, p);
765                         crop = tmp;
766                     }
767                 }
768 
769                 if (mSaveCroppedBitmap) {
770                     mCroppedBitmap = crop;
771                 }
772 
773                 // Get output compression format
774                 CompressFormat cf =
775                         convertExtensionToCompressFormat(getFileExtension(mOutputFormat));
776 
777                 // Compress to byte array
778                 ByteArrayOutputStream tmpOut = new ByteArrayOutputStream(2048);
779                 if (crop.compress(cf, DEFAULT_COMPRESS_QUALITY, tmpOut)) {
780                     // If we need to set to the wallpaper, set it
781                     if (mSetWallpaper && wallpaperManager != null) {
782                         try {
783                             byte[] outByteArray = tmpOut.toByteArray();
784                             wallpaperManager.setStream(new ByteArrayInputStream(outByteArray));
785                             if (mOnBitmapCroppedHandler != null) {
786                                 mOnBitmapCroppedHandler.onBitmapCropped(outByteArray);
787                             }
788                         } catch (IOException e) {
789                             Log.w(LOGTAG, "cannot write stream to wallpaper", e);
790                             failure = true;
791                         }
792                     }
793                 } else {
794                     Log.w(LOGTAG, "cannot compress bitmap");
795                     failure = true;
796                 }
797             }
798             return !failure; // True if any of the operations failed
799         }
800 
801         @Override
doInBackground(Void... params)802         protected Boolean doInBackground(Void... params) {
803             return cropBitmap();
804         }
805 
806         @Override
onPostExecute(Boolean result)807         protected void onPostExecute(Boolean result) {
808             if (mOnEndRunnable != null) {
809                 mOnEndRunnable.run();
810             }
811         }
812     }
813 
814     protected static RectF getMaxCropRect(
815             int inWidth, int inHeight, int outWidth, int outHeight, boolean leftAligned) {
816         RectF cropRect = new RectF();
817         // Get a crop rect that will fit this
818         if (inWidth / (float) inHeight > outWidth / (float) outHeight) {
819              cropRect.top = 0;
820              cropRect.bottom = inHeight;
821              cropRect.left = (inWidth - (outWidth / (float) outHeight) * inHeight) / 2;
822              cropRect.right = inWidth - cropRect.left;
823              if (leftAligned) {
824                  cropRect.right -= cropRect.left;
825                  cropRect.left = 0;
826              }
827         } else {
828             cropRect.left = 0;
829             cropRect.right = inWidth;
830             cropRect.top = (inHeight - (outHeight / (float) outWidth) * inWidth) / 2;
831             cropRect.bottom = inHeight - cropRect.top;
832         }
833         return cropRect;
834     }
835 
836     protected static CompressFormat convertExtensionToCompressFormat(String extension) {
837         return extension.equals("png") ? CompressFormat.PNG : CompressFormat.JPEG;
838     }
839 
840     protected static String getFileExtension(String requestFormat) {
841         String outputFormat = (requestFormat == null)
842                 ? "jpg"
843                 : requestFormat;
844         outputFormat = outputFormat.toLowerCase();
845         return (outputFormat.equals("png") || outputFormat.equals("gif"))
846                 ? "png" // We don't support gif compression.
847                 : "jpg";
848     }
849 }
850