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