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