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