WallpaperCropActivity.java revision 929e75e8d0c258ca134093413ed0d8ffb3ead1d4
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        InputStream is = null;
251        BufferedInputStream bis = null;
252        try {
253            if (path != null) {
254                ei.readExif(path);
255            } else if (uri != null) {
256                is = context.getContentResolver().openInputStream(uri);
257                bis = new BufferedInputStream(is);
258                ei.readExif(bis);
259            } else {
260                is = res.openRawResource(resId);
261                bis = new BufferedInputStream(is);
262                ei.readExif(bis);
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        } finally {
271            Utils.closeSilently(bis);
272            Utils.closeSilently(is);
273        }
274        return 0;
275    }
276
277    protected void setWallpaper(String filePath, final boolean finishActivityWhenDone) {
278        int rotation = getRotationFromExif(filePath);
279        BitmapCropTask cropTask = new BitmapCropTask(
280                this, filePath, null, rotation, 0, 0, true, false, null);
281        final Point bounds = cropTask.getImageBounds();
282        Runnable onEndCrop = new Runnable() {
283            public void run() {
284                updateWallpaperDimensions(bounds.x, bounds.y);
285                if (finishActivityWhenDone) {
286                    setResult(Activity.RESULT_OK);
287                    finish();
288                }
289            }
290        };
291        cropTask.setOnEndRunnable(onEndCrop);
292        cropTask.setNoCrop(true);
293        cropTask.execute();
294    }
295
296    protected void cropImageAndSetWallpaper(
297            Resources res, int resId, final boolean finishActivityWhenDone) {
298        // crop this image and scale it down to the default wallpaper size for
299        // this device
300        int rotation = getRotationFromExif(res, resId);
301        Point inSize = mCropView.getSourceDimensions();
302        Point outSize = getDefaultWallpaperSize(getResources(),
303                getWindowManager());
304        RectF crop = getMaxCropRect(
305                inSize.x, inSize.y, outSize.x, outSize.y, false);
306        Runnable onEndCrop = new Runnable() {
307            public void run() {
308                // Passing 0, 0 will cause launcher to revert to using the
309                // default wallpaper size
310                updateWallpaperDimensions(0, 0);
311                if (finishActivityWhenDone) {
312                    setResult(Activity.RESULT_OK);
313                    finish();
314                }
315            }
316        };
317        BitmapCropTask cropTask = new BitmapCropTask(this, res, resId,
318                crop, rotation, outSize.x, outSize.y, true, false, onEndCrop);
319        cropTask.execute();
320    }
321
322    private static boolean isScreenLarge(Resources res) {
323        Configuration config = res.getConfiguration();
324        return config.smallestScreenWidthDp >= 720;
325    }
326
327    protected void cropImageAndSetWallpaper(Uri uri,
328            OnBitmapCroppedHandler onBitmapCroppedHandler, final boolean finishActivityWhenDone) {
329        boolean centerCrop = getResources().getBoolean(R.bool.center_crop);
330        // Get the crop
331        boolean ltr = mCropView.getLayoutDirection() == View.LAYOUT_DIRECTION_LTR;
332
333        Display d = getWindowManager().getDefaultDisplay();
334
335        Point displaySize = new Point();
336        d.getSize(displaySize);
337        boolean isPortrait = displaySize.x < displaySize.y;
338
339        Point defaultWallpaperSize = getDefaultWallpaperSize(getResources(),
340                getWindowManager());
341        // Get the crop
342        RectF cropRect = mCropView.getCrop();
343
344        // Due to rounding errors in the cropview renderer the edges can be slightly offset
345        // therefore we ensure that the boundaries are sanely defined
346        cropRect.left = Math.max(0, cropRect.left);
347        cropRect.right = Math.min(mCropView.getWidth(), cropRect.right);
348        cropRect.top = Math.max(0, cropRect.top);
349        cropRect.bottom = Math.min(mCropView.getHeight(), cropRect.bottom);
350        int cropRotation = mCropView.getImageRotation();
351        float cropScale = mCropView.getWidth() / (float) cropRect.width();
352
353        Point inSize = mCropView.getSourceDimensions();
354        Matrix rotateMatrix = new Matrix();
355        rotateMatrix.setRotate(cropRotation);
356        float[] rotatedInSize = new float[] { inSize.x, inSize.y };
357        rotateMatrix.mapPoints(rotatedInSize);
358        rotatedInSize[0] = Math.abs(rotatedInSize[0]);
359        rotatedInSize[1] = Math.abs(rotatedInSize[1]);
360
361        // ADJUST CROP WIDTH
362        // Extend the crop all the way to the right, for parallax
363        // (or all the way to the left, in RTL)
364        float extraSpace;
365        if (centerCrop) {
366            extraSpace = 2f * Math.min(rotatedInSize[0] - cropRect.right, cropRect.left);
367        } else {
368            extraSpace = ltr ? rotatedInSize[0] - cropRect.right : cropRect.left;
369        }
370        // Cap the amount of extra width
371        float maxExtraSpace = defaultWallpaperSize.x / cropScale - cropRect.width();
372        extraSpace = Math.min(extraSpace, maxExtraSpace);
373
374        if (centerCrop) {
375            cropRect.left -= extraSpace / 2f;
376            cropRect.right += extraSpace / 2f;
377        } else {
378            if (ltr) {
379                cropRect.right += extraSpace;
380            } else {
381                cropRect.left -= extraSpace;
382            }
383        }
384
385        // ADJUST CROP HEIGHT
386        if (isPortrait) {
387            cropRect.bottom = cropRect.top + defaultWallpaperSize.y / cropScale;
388        } else { // LANDSCAPE
389            float extraPortraitHeight =
390                    defaultWallpaperSize.y / cropScale - cropRect.height();
391            float expandHeight =
392                    Math.min(Math.min(rotatedInSize[1] - cropRect.bottom, cropRect.top),
393                            extraPortraitHeight / 2);
394            cropRect.top -= expandHeight;
395            cropRect.bottom += expandHeight;
396        }
397        final int outWidth = (int) Math.round(cropRect.width() * cropScale);
398        final int outHeight = (int) Math.round(cropRect.height() * cropScale);
399
400        Runnable onEndCrop = new Runnable() {
401            public void run() {
402                updateWallpaperDimensions(outWidth, outHeight);
403                if (finishActivityWhenDone) {
404                    setResult(Activity.RESULT_OK);
405                    finish();
406                }
407            }
408        };
409        BitmapCropTask cropTask = new BitmapCropTask(this, uri,
410                cropRect, cropRotation, outWidth, outHeight, true, false, onEndCrop);
411        if (onBitmapCroppedHandler != null) {
412            cropTask.setOnBitmapCropped(onBitmapCroppedHandler);
413        }
414        cropTask.execute();
415    }
416
417    public interface OnBitmapCroppedHandler {
418        public void onBitmapCropped(byte[] imageBytes);
419    }
420
421    protected static class BitmapCropTask extends AsyncTask<Void, Void, Boolean> {
422        Uri mInUri = null;
423        Context mContext;
424        String mInFilePath;
425        byte[] mInImageBytes;
426        int mInResId = 0;
427        RectF mCropBounds = null;
428        int mOutWidth, mOutHeight;
429        int mRotation;
430        String mOutputFormat = "jpg"; // for now
431        boolean mSetWallpaper;
432        boolean mSaveCroppedBitmap;
433        Bitmap mCroppedBitmap;
434        Runnable mOnEndRunnable;
435        Resources mResources;
436        OnBitmapCroppedHandler mOnBitmapCroppedHandler;
437        boolean mNoCrop;
438
439        public BitmapCropTask(Context c, String filePath,
440                RectF cropBounds, int rotation, int outWidth, int outHeight,
441                boolean setWallpaper, boolean saveCroppedBitmap, Runnable onEndRunnable) {
442            mContext = c;
443            mInFilePath = filePath;
444            init(cropBounds, rotation,
445                    outWidth, outHeight, setWallpaper, saveCroppedBitmap, onEndRunnable);
446        }
447
448        public BitmapCropTask(byte[] imageBytes,
449                RectF cropBounds, int rotation, int outWidth, int outHeight,
450                boolean setWallpaper, boolean saveCroppedBitmap, Runnable onEndRunnable) {
451            mInImageBytes = imageBytes;
452            init(cropBounds, rotation,
453                    outWidth, outHeight, setWallpaper, saveCroppedBitmap, onEndRunnable);
454        }
455
456        public BitmapCropTask(Context c, Uri inUri,
457                RectF cropBounds, int rotation, int outWidth, int outHeight,
458                boolean setWallpaper, boolean saveCroppedBitmap, Runnable onEndRunnable) {
459            mContext = c;
460            mInUri = inUri;
461            init(cropBounds, rotation,
462                    outWidth, outHeight, setWallpaper, saveCroppedBitmap, onEndRunnable);
463        }
464
465        public BitmapCropTask(Context c, Resources res, int inResId,
466                RectF cropBounds, int rotation, int outWidth, int outHeight,
467                boolean setWallpaper, boolean saveCroppedBitmap, Runnable onEndRunnable) {
468            mContext = c;
469            mInResId = inResId;
470            mResources = res;
471            init(cropBounds, rotation,
472                    outWidth, outHeight, setWallpaper, saveCroppedBitmap, onEndRunnable);
473        }
474
475        private void init(RectF cropBounds, int rotation, int outWidth, int outHeight,
476                boolean setWallpaper, boolean saveCroppedBitmap, Runnable onEndRunnable) {
477            mCropBounds = cropBounds;
478            mRotation = rotation;
479            mOutWidth = outWidth;
480            mOutHeight = outHeight;
481            mSetWallpaper = setWallpaper;
482            mSaveCroppedBitmap = saveCroppedBitmap;
483            mOnEndRunnable = onEndRunnable;
484        }
485
486        public void setOnBitmapCropped(OnBitmapCroppedHandler handler) {
487            mOnBitmapCroppedHandler = handler;
488        }
489
490        public void setNoCrop(boolean value) {
491            mNoCrop = value;
492        }
493
494        public void setOnEndRunnable(Runnable onEndRunnable) {
495            mOnEndRunnable = onEndRunnable;
496        }
497
498        // Helper to setup input stream
499        private InputStream regenerateInputStream() {
500            if (mInUri == null && mInResId == 0 && mInFilePath == null && mInImageBytes == null) {
501                Log.w(LOGTAG, "cannot read original file, no input URI, resource ID, or " +
502                        "image byte array given");
503            } else {
504                try {
505                    if (mInUri != null) {
506                        return new BufferedInputStream(
507                                mContext.getContentResolver().openInputStream(mInUri));
508                    } else if (mInFilePath != null) {
509                        return mContext.openFileInput(mInFilePath);
510                    } else if (mInImageBytes != null) {
511                        return new BufferedInputStream(new ByteArrayInputStream(mInImageBytes));
512                    } else {
513                        return new BufferedInputStream(mResources.openRawResource(mInResId));
514                    }
515                } catch (FileNotFoundException e) {
516                    Log.w(LOGTAG, "cannot read file: " + mInUri.toString(), e);
517                }
518            }
519            return null;
520        }
521
522        public Point getImageBounds() {
523            InputStream is = regenerateInputStream();
524            if (is != null) {
525                BitmapFactory.Options options = new BitmapFactory.Options();
526                options.inJustDecodeBounds = true;
527                BitmapFactory.decodeStream(is, null, options);
528                Utils.closeSilently(is);
529                if (options.outWidth != 0 && options.outHeight != 0) {
530                    return new Point(options.outWidth, options.outHeight);
531                }
532            }
533            return null;
534        }
535
536        public void setCropBounds(RectF cropBounds) {
537            mCropBounds = cropBounds;
538        }
539
540        public Bitmap getCroppedBitmap() {
541            return mCroppedBitmap;
542        }
543        public boolean cropBitmap() {
544            boolean failure = false;
545
546
547            WallpaperManager wallpaperManager = null;
548            if (mSetWallpaper) {
549                wallpaperManager = WallpaperManager.getInstance(mContext.getApplicationContext());
550            }
551
552
553            if (mSetWallpaper && mNoCrop) {
554                try {
555                    InputStream is = regenerateInputStream();
556                    if (is != null) {
557                        wallpaperManager.setStream(is);
558                        Utils.closeSilently(is);
559                    }
560                } catch (IOException e) {
561                    Log.w(LOGTAG, "cannot write stream to wallpaper", e);
562                    failure = true;
563                }
564                return !failure;
565            } else {
566                // Find crop bounds (scaled to original image size)
567                Rect roundedTrueCrop = new Rect();
568                Matrix rotateMatrix = new Matrix();
569                Matrix inverseRotateMatrix = new Matrix();
570                if (mRotation > 0) {
571                    rotateMatrix.setRotate(mRotation);
572                    inverseRotateMatrix.setRotate(-mRotation);
573
574                    mCropBounds.roundOut(roundedTrueCrop);
575                    mCropBounds = new RectF(roundedTrueCrop);
576
577                    Point bounds = getImageBounds();
578                    if (bounds == null) {
579                        Log.w(LOGTAG, "cannot get bounds for image");
580                        failure = true;
581                        return false;
582                    }
583
584                    float[] rotatedBounds = new float[] { bounds.x, bounds.y };
585                    rotateMatrix.mapPoints(rotatedBounds);
586                    rotatedBounds[0] = Math.abs(rotatedBounds[0]);
587                    rotatedBounds[1] = Math.abs(rotatedBounds[1]);
588
589                    mCropBounds.offset(-rotatedBounds[0]/2, -rotatedBounds[1]/2);
590                    inverseRotateMatrix.mapRect(mCropBounds);
591                    mCropBounds.offset(bounds.x/2, bounds.y/2);
592
593                }
594
595                mCropBounds.roundOut(roundedTrueCrop);
596
597                if (roundedTrueCrop.width() <= 0 || roundedTrueCrop.height() <= 0) {
598                    Log.w(LOGTAG, "crop has bad values for full size image");
599                    failure = true;
600                    return false;
601                }
602
603                // See how much we're reducing the size of the image
604                int scaleDownSampleSize = Math.max(1, Math.min(roundedTrueCrop.width() / mOutWidth,
605                        roundedTrueCrop.height() / mOutHeight));
606                // Attempt to open a region decoder
607                BitmapRegionDecoder decoder = null;
608                InputStream is = null;
609                try {
610                    is = regenerateInputStream();
611                    if (is == null) {
612                        Log.w(LOGTAG, "cannot get input stream for uri=" + mInUri.toString());
613                        failure = true;
614                        return false;
615                    }
616                    decoder = BitmapRegionDecoder.newInstance(is, false);
617                    Utils.closeSilently(is);
618                } catch (IOException e) {
619                    Log.w(LOGTAG, "cannot open region decoder for file: " + mInUri.toString(), e);
620                } finally {
621                   Utils.closeSilently(is);
622                   is = null;
623                }
624
625                Bitmap crop = null;
626                if (decoder != null) {
627                    // Do region decoding to get crop bitmap
628                    BitmapFactory.Options options = new BitmapFactory.Options();
629                    if (scaleDownSampleSize > 1) {
630                        options.inSampleSize = scaleDownSampleSize;
631                    }
632                    crop = decoder.decodeRegion(roundedTrueCrop, options);
633                    decoder.recycle();
634                }
635
636                if (crop == null) {
637                    // BitmapRegionDecoder has failed, try to crop in-memory
638                    is = regenerateInputStream();
639                    Bitmap fullSize = null;
640                    if (is != null) {
641                        BitmapFactory.Options options = new BitmapFactory.Options();
642                        if (scaleDownSampleSize > 1) {
643                            options.inSampleSize = scaleDownSampleSize;
644                        }
645                        fullSize = BitmapFactory.decodeStream(is, null, options);
646                        Utils.closeSilently(is);
647                    }
648                    if (fullSize != null) {
649                        mCropBounds.left /= scaleDownSampleSize;
650                        mCropBounds.top /= scaleDownSampleSize;
651                        mCropBounds.bottom /= scaleDownSampleSize;
652                        mCropBounds.right /= scaleDownSampleSize;
653                        mCropBounds.roundOut(roundedTrueCrop);
654
655                        crop = Bitmap.createBitmap(fullSize, roundedTrueCrop.left,
656                                roundedTrueCrop.top, roundedTrueCrop.width(),
657                                roundedTrueCrop.height());
658                    }
659                }
660
661                if (crop == null) {
662                    Log.w(LOGTAG, "cannot decode file: " + mInUri.toString());
663                    failure = true;
664                    return false;
665                }
666                if (mOutWidth > 0 && mOutHeight > 0 || mRotation > 0) {
667                    float[] dimsAfter = new float[] { crop.getWidth(), crop.getHeight() };
668                    rotateMatrix.mapPoints(dimsAfter);
669                    dimsAfter[0] = Math.abs(dimsAfter[0]);
670                    dimsAfter[1] = Math.abs(dimsAfter[1]);
671
672                    if (!(mOutWidth > 0 && mOutHeight > 0)) {
673                        mOutWidth = Math.round(dimsAfter[0]);
674                        mOutHeight = Math.round(dimsAfter[1]);
675                    }
676
677                    RectF cropRect = new RectF(0, 0, dimsAfter[0], dimsAfter[1]);
678                    RectF returnRect = new RectF(0, 0, mOutWidth, mOutHeight);
679
680                    Matrix m = new Matrix();
681                    if (mRotation == 0) {
682                        m.setRectToRect(cropRect, returnRect, Matrix.ScaleToFit.FILL);
683                    } else {
684                        Matrix m1 = new Matrix();
685                        m1.setTranslate(-crop.getWidth() / 2f, -crop.getHeight() / 2f);
686                        Matrix m2 = new Matrix();
687                        m2.setRotate(mRotation);
688                        Matrix m3 = new Matrix();
689                        m3.setTranslate(dimsAfter[0] / 2f, dimsAfter[1] / 2f);
690                        Matrix m4 = new Matrix();
691                        m4.setRectToRect(cropRect, returnRect, Matrix.ScaleToFit.FILL);
692
693                        Matrix c1 = new Matrix();
694                        c1.setConcat(m2, m1);
695                        Matrix c2 = new Matrix();
696                        c2.setConcat(m4, m3);
697                        m.setConcat(c2, c1);
698                    }
699
700                    Bitmap tmp = Bitmap.createBitmap((int) returnRect.width(),
701                            (int) returnRect.height(), Bitmap.Config.ARGB_8888);
702                    if (tmp != null) {
703                        Canvas c = new Canvas(tmp);
704                        Paint p = new Paint();
705                        p.setFilterBitmap(true);
706                        c.drawBitmap(crop, m, p);
707                        crop = tmp;
708                    }
709                }
710
711                if (mSaveCroppedBitmap) {
712                    mCroppedBitmap = crop;
713                }
714
715                // Get output compression format
716                CompressFormat cf =
717                        convertExtensionToCompressFormat(getFileExtension(mOutputFormat));
718
719                // Compress to byte array
720                ByteArrayOutputStream tmpOut = new ByteArrayOutputStream(2048);
721                if (crop.compress(cf, DEFAULT_COMPRESS_QUALITY, tmpOut)) {
722                    // If we need to set to the wallpaper, set it
723                    if (mSetWallpaper && wallpaperManager != null) {
724                        try {
725                            byte[] outByteArray = tmpOut.toByteArray();
726                            wallpaperManager.setStream(new ByteArrayInputStream(outByteArray));
727                            if (mOnBitmapCroppedHandler != null) {
728                                mOnBitmapCroppedHandler.onBitmapCropped(outByteArray);
729                            }
730                        } catch (IOException e) {
731                            Log.w(LOGTAG, "cannot write stream to wallpaper", e);
732                            failure = true;
733                        }
734                    }
735                } else {
736                    Log.w(LOGTAG, "cannot compress bitmap");
737                    failure = true;
738                }
739            }
740            return !failure; // True if any of the operations failed
741        }
742
743        @Override
744        protected Boolean doInBackground(Void... params) {
745            return cropBitmap();
746        }
747
748        @Override
749        protected void onPostExecute(Boolean result) {
750            if (mOnEndRunnable != null) {
751                mOnEndRunnable.run();
752            }
753        }
754    }
755
756    protected void updateWallpaperDimensions(int width, int height) {
757        String spKey = getSharedPreferencesKey();
758        SharedPreferences sp = getSharedPreferences(spKey, Context.MODE_MULTI_PROCESS);
759        SharedPreferences.Editor editor = sp.edit();
760        if (width != 0 && height != 0) {
761            editor.putInt(WALLPAPER_WIDTH_KEY, width);
762            editor.putInt(WALLPAPER_HEIGHT_KEY, height);
763        } else {
764            editor.remove(WALLPAPER_WIDTH_KEY);
765            editor.remove(WALLPAPER_HEIGHT_KEY);
766        }
767        editor.commit();
768
769        suggestWallpaperDimension(getResources(),
770                sp, getWindowManager(), WallpaperManager.getInstance(this));
771    }
772
773    static public void suggestWallpaperDimension(Resources res,
774            final SharedPreferences sharedPrefs,
775            WindowManager windowManager,
776            final WallpaperManager wallpaperManager) {
777        final Point defaultWallpaperSize = getDefaultWallpaperSize(res, windowManager);
778
779        new Thread("suggestWallpaperDimension") {
780            public void run() {
781                // If we have saved a wallpaper width/height, use that instead
782                int savedWidth = sharedPrefs.getInt(WALLPAPER_WIDTH_KEY, defaultWallpaperSize.x);
783                int savedHeight = sharedPrefs.getInt(WALLPAPER_HEIGHT_KEY, defaultWallpaperSize.y);
784                wallpaperManager.suggestDesiredDimensions(savedWidth, savedHeight);
785            }
786        }.start();
787    }
788
789    protected static RectF getMaxCropRect(
790            int inWidth, int inHeight, int outWidth, int outHeight, boolean leftAligned) {
791        RectF cropRect = new RectF();
792        // Get a crop rect that will fit this
793        if (inWidth / (float) inHeight > outWidth / (float) outHeight) {
794             cropRect.top = 0;
795             cropRect.bottom = inHeight;
796             cropRect.left = (inWidth - (outWidth / (float) outHeight) * inHeight) / 2;
797             cropRect.right = inWidth - cropRect.left;
798             if (leftAligned) {
799                 cropRect.right -= cropRect.left;
800                 cropRect.left = 0;
801             }
802        } else {
803            cropRect.left = 0;
804            cropRect.right = inWidth;
805            cropRect.top = (inHeight - (outHeight / (float) outWidth) * inWidth) / 2;
806            cropRect.bottom = inHeight - cropRect.top;
807        }
808        return cropRect;
809    }
810
811    protected static CompressFormat convertExtensionToCompressFormat(String extension) {
812        return extension.equals("png") ? CompressFormat.PNG : CompressFormat.JPEG;
813    }
814
815    protected static String getFileExtension(String requestFormat) {
816        String outputFormat = (requestFormat == null)
817                ? "jpg"
818                : requestFormat;
819        outputFormat = outputFormat.toLowerCase();
820        return (outputFormat.equals("png") || outputFormat.equals("gif"))
821                ? "png" // We don't support gif compression.
822                : "jpg";
823    }
824}
825