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