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