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 */
17package com.android.wallpapercropper;
18
19import android.app.ActionBar;
20import android.app.Activity;
21import android.app.WallpaperManager;
22import android.content.Context;
23import android.content.Intent;
24import android.content.SharedPreferences;
25import android.content.res.Configuration;
26import android.content.res.Resources;
27import android.graphics.Bitmap;
28import android.graphics.Bitmap.CompressFormat;
29import android.graphics.BitmapFactory;
30import android.graphics.BitmapRegionDecoder;
31import android.graphics.Canvas;
32import android.graphics.Matrix;
33import android.graphics.Paint;
34import android.graphics.Point;
35import android.graphics.Rect;
36import android.graphics.RectF;
37import android.net.Uri;
38import android.os.AsyncTask;
39import android.os.Bundle;
40import android.util.Log;
41import android.view.Display;
42import android.view.View;
43import android.view.WindowManager;
44import android.widget.Toast;
45
46import com.android.gallery3d.common.Utils;
47import com.android.gallery3d.exif.ExifInterface;
48import com.android.photos.BitmapRegionTileSource;
49import com.android.photos.BitmapRegionTileSource.BitmapSource;
50
51import java.io.BufferedInputStream;
52import java.io.ByteArrayInputStream;
53import java.io.ByteArrayOutputStream;
54import java.io.FileNotFoundException;
55import java.io.IOException;
56import java.io.InputStream;
57
58public 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
81    protected void onCreate(Bundle savedInstanceState) {
82        super.onCreate(savedInstanceState);
83        init();
84        if (!enableRotation()) {
85            setRequestedOrientation(Configuration.ORIENTATION_PORTRAIT);
86        }
87    }
88
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
137    protected void onDestroy() {
138        if (mCropView != null) {
139            mCropView.destroy();
140        }
141        super.onDestroy();
142    }
143
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
200    public boolean enableRotation() {
201        return getResources().getBoolean(R.bool.allow_rotation);
202    }
203
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
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
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
266    public static int getRotationFromExif(String path) {
267        return getRotationFromExifHelper(path, null, 0, null, null);
268    }
269
270    public static int getRotationFromExif(Context context, Uri uri) {
271        return getRotationFromExifHelper(null, null, 0, context, uri);
272    }
273
274    public static int getRotationFromExif(Resources res, int resId) {
275        return getRotationFromExifHelper(null, res, resId, null, null);
276    }
277
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
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
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
352    private static boolean isScreenLarge(Resources res) {
353        Configuration config = res.getConfiguration();
354        return config.smallestScreenWidthDp >= 720;
355    }
356
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
802        protected Boolean doInBackground(Void... params) {
803            return cropBitmap();
804        }
805
806        @Override
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