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