Home | History | Annotate | Download | only in launcher3
      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 
     17 package com.android.launcher3;
     18 
     19 import android.annotation.TargetApi;
     20 import android.app.ActionBar;
     21 import android.app.Activity;
     22 import android.app.WallpaperManager;
     23 import android.content.Context;
     24 import android.content.Intent;
     25 import android.content.SharedPreferences;
     26 import android.content.res.Configuration;
     27 import android.content.res.Resources;
     28 import android.graphics.Bitmap;
     29 import android.graphics.Matrix;
     30 import android.graphics.Point;
     31 import android.graphics.RectF;
     32 import android.net.Uri;
     33 import android.os.Build;
     34 import android.os.Bundle;
     35 import android.os.Handler;
     36 import android.os.HandlerThread;
     37 import android.os.Message;
     38 import android.util.Log;
     39 import android.view.Display;
     40 import android.view.View;
     41 import android.widget.Toast;
     42 
     43 import com.android.gallery3d.common.BitmapCropTask;
     44 import com.android.gallery3d.common.BitmapUtils;
     45 import com.android.gallery3d.common.Utils;
     46 import com.android.launcher3.base.BaseActivity;
     47 import com.android.launcher3.util.Thunk;
     48 import com.android.launcher3.util.WallpaperUtils;
     49 import com.android.photos.BitmapRegionTileSource;
     50 import com.android.photos.BitmapRegionTileSource.BitmapSource;
     51 import com.android.photos.BitmapRegionTileSource.BitmapSource.InBitmapProvider;
     52 import com.android.photos.views.TiledImageRenderer.TileSource;
     53 
     54 import java.util.Collections;
     55 import java.util.Set;
     56 import java.util.WeakHashMap;
     57 
     58 public class WallpaperCropActivity extends BaseActivity implements Handler.Callback {
     59     private static final String LOGTAG = "Launcher3.CropActivity";
     60 
     61     protected static final String WALLPAPER_WIDTH_KEY = WallpaperUtils.WALLPAPER_WIDTH_KEY;
     62     protected static final String WALLPAPER_HEIGHT_KEY = WallpaperUtils.WALLPAPER_HEIGHT_KEY;
     63 
     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     public static final float WALLPAPER_SCREENS_SPAN = WallpaperUtils.WALLPAPER_SCREENS_SPAN;
     73 
     74     private static final int MSG_LOAD_IMAGE = 1;
     75 
     76     protected CropView mCropView;
     77     protected View mProgressView;
     78     protected Uri mUri;
     79     protected View mSetWallpaperButton;
     80 
     81     private HandlerThread mLoaderThread;
     82     private Handler mLoaderHandler;
     83     @Thunk LoadRequest mCurrentLoadRequest;
     84     private byte[] mTempStorageForDecoding = new byte[16 * 1024];
     85     // A weak-set of reusable bitmaps
     86     @Thunk Set<Bitmap> mReusableBitmaps =
     87             Collections.newSetFromMap(new WeakHashMap<Bitmap, Boolean>());
     88 
     89     @Override
     90     public void onCreate(Bundle savedInstanceState) {
     91         super.onCreate(savedInstanceState);
     92 
     93         mLoaderThread = new HandlerThread("wallpaper_loader");
     94         mLoaderThread.start();
     95         mLoaderHandler = new Handler(mLoaderThread.getLooper(), this);
     96 
     97         init();
     98         if (!enableRotation()) {
     99             setRequestedOrientation(Configuration.ORIENTATION_PORTRAIT);
    100         }
    101     }
    102 
    103     protected void init() {
    104         setContentView(R.layout.wallpaper_cropper);
    105 
    106         mCropView = (CropView) findViewById(R.id.cropView);
    107         mProgressView = findViewById(R.id.loading);
    108 
    109         Intent cropIntent = getIntent();
    110         final Uri imageUri = cropIntent.getData();
    111 
    112         if (imageUri == null) {
    113             Log.e(LOGTAG, "No URI passed in intent, exiting WallpaperCropActivity");
    114             finish();
    115             return;
    116         }
    117 
    118         // Action bar
    119         // Show the custom action bar view
    120         final ActionBar actionBar = getActionBar();
    121         actionBar.setCustomView(R.layout.actionbar_set_wallpaper);
    122         actionBar.getCustomView().setOnClickListener(
    123                 new View.OnClickListener() {
    124                     @Override
    125                     public void onClick(View v) {
    126                         boolean finishActivityWhenDone = true;
    127                         cropImageAndSetWallpaper(imageUri, null, finishActivityWhenDone);
    128                     }
    129                 });
    130         mSetWallpaperButton = findViewById(R.id.set_wallpaper_button);
    131 
    132         // Load image in background
    133         final BitmapRegionTileSource.UriBitmapSource bitmapSource =
    134                 new BitmapRegionTileSource.UriBitmapSource(getContext(), imageUri);
    135         mSetWallpaperButton.setEnabled(false);
    136         Runnable onLoad = new Runnable() {
    137             public void run() {
    138                 if (bitmapSource.getLoadingState() != BitmapSource.State.LOADED) {
    139                     Toast.makeText(getContext(), R.string.wallpaper_load_fail,
    140                             Toast.LENGTH_LONG).show();
    141                     finish();
    142                 } else {
    143                     mSetWallpaperButton.setEnabled(true);
    144                 }
    145             }
    146         };
    147         setCropViewTileSource(bitmapSource, true, false, null, onLoad);
    148     }
    149 
    150     @Override
    151     public void onDestroy() {
    152         if (mCropView != null) {
    153             mCropView.destroy();
    154         }
    155         if (mLoaderThread != null) {
    156             mLoaderThread.quit();
    157         }
    158         super.onDestroy();
    159     }
    160 
    161     /**
    162      * This is called on {@link #mLoaderThread}
    163      */
    164     @Override
    165     public boolean handleMessage(Message msg) {
    166         if (msg.what == MSG_LOAD_IMAGE) {
    167             final LoadRequest req = (LoadRequest) msg.obj;
    168             try {
    169                 req.src.loadInBackground(new InBitmapProvider() {
    170 
    171                     @Override
    172                     public Bitmap forPixelCount(int count) {
    173                         Bitmap bitmapToReuse = null;
    174                         // Find the smallest bitmap that satisfies the pixel count limit
    175                         synchronized (mReusableBitmaps) {
    176                             int currentBitmapSize = Integer.MAX_VALUE;
    177                             for (Bitmap b : mReusableBitmaps) {
    178                                 int bitmapSize = b.getWidth() * b.getHeight();
    179                                 if ((bitmapSize >= count) && (bitmapSize < currentBitmapSize)) {
    180                                     bitmapToReuse = b;
    181                                     currentBitmapSize = bitmapSize;
    182                                 }
    183                             }
    184 
    185                             if (bitmapToReuse != null) {
    186                                 mReusableBitmaps.remove(bitmapToReuse);
    187                             }
    188                         }
    189                         return bitmapToReuse;
    190                     }
    191                 });
    192             } catch (SecurityException securityException) {
    193                 if (isActivityDestroyed()) {
    194                     // Temporarily granted permissions are revoked when the activity
    195                     // finishes, potentially resulting in a SecurityException here.
    196                     // Even though {@link #isDestroyed} might also return true in different
    197                     // situations where the configuration changes, we are fine with
    198                     // catching these cases here as well.
    199                     return true;
    200                 } else {
    201                     // otherwise it had a different cause and we throw it further
    202                     throw securityException;
    203                 }
    204             }
    205 
    206             req.result = new BitmapRegionTileSource(getContext(), req.src, mTempStorageForDecoding);
    207             runOnUiThread(new Runnable() {
    208 
    209                 @Override
    210                 public void run() {
    211                     if (req == mCurrentLoadRequest) {
    212                         onLoadRequestComplete(req,
    213                                 req.src.getLoadingState() == BitmapSource.State.LOADED);
    214                     } else {
    215                         addReusableBitmap(req.result);
    216                     }
    217                 }
    218             });
    219             return true;
    220         }
    221         return false;
    222     }
    223 
    224     @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
    225     protected boolean isActivityDestroyed() {
    226         return (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1)
    227                 && isDestroyed();
    228     }
    229 
    230     @Thunk void addReusableBitmap(TileSource src) {
    231         synchronized (mReusableBitmaps) {
    232             if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT
    233                     && src instanceof BitmapRegionTileSource) {
    234                 Bitmap preview = ((BitmapRegionTileSource) src).getBitmap();
    235                 if (preview != null && preview.isMutable()) {
    236                     mReusableBitmaps.add(preview);
    237                 }
    238             }
    239         }
    240     }
    241 
    242     protected void onLoadRequestComplete(LoadRequest req, boolean success) {
    243         mCurrentLoadRequest = null;
    244         if (success) {
    245             TileSource oldSrc = mCropView.getTileSource();
    246             mCropView.setTileSource(req.result, null);
    247             mCropView.setTouchEnabled(req.touchEnabled);
    248             if (req.moveToLeft) {
    249                 mCropView.moveToLeft();
    250             }
    251             if (req.scaleProvider != null) {
    252                 mCropView.setScale(req.scaleProvider.getScale(req.result));
    253             }
    254 
    255             // Free last image
    256             if (oldSrc != null) {
    257                 // Call yield instead of recycle, as we only want to free GL resource.
    258                 // We can still reuse the bitmap for decoding any other image.
    259                 oldSrc.getPreview().yield();
    260             }
    261             addReusableBitmap(oldSrc);
    262         }
    263         if (req.postExecute != null) {
    264             req.postExecute.run();
    265         }
    266         mProgressView.setVisibility(View.GONE);
    267     }
    268 
    269     public final void setCropViewTileSource(BitmapSource bitmapSource, boolean touchEnabled,
    270             boolean moveToLeft, CropViewScaleProvider scaleProvider, Runnable postExecute) {
    271         final LoadRequest req = new LoadRequest();
    272         req.moveToLeft = moveToLeft;
    273         req.src = bitmapSource;
    274         req.touchEnabled = touchEnabled;
    275         req.postExecute = postExecute;
    276         req.scaleProvider = scaleProvider;
    277         mCurrentLoadRequest = req;
    278 
    279         // Remove any pending requests
    280         mLoaderHandler.removeMessages(MSG_LOAD_IMAGE);
    281         Message.obtain(mLoaderHandler, MSG_LOAD_IMAGE, req).sendToTarget();
    282 
    283         // We don't want to show the spinner every time we load an image, because that would be
    284         // annoying; instead, only start showing the spinner if loading the image has taken
    285         // longer than 1 sec (ie 1000 ms)
    286         mProgressView.postDelayed(new Runnable() {
    287             public void run() {
    288                 if (mCurrentLoadRequest == req) {
    289                     mProgressView.setVisibility(View.VISIBLE);
    290                 }
    291             }
    292         }, 1000);
    293     }
    294 
    295 
    296     public boolean enableRotation() {
    297         return getResources().getBoolean(R.bool.allow_rotation);
    298     }
    299 
    300     protected void setWallpaper(Uri uri, final boolean finishActivityWhenDone) {
    301         int rotation = BitmapUtils.getRotationFromExif(getContext(), uri);
    302         BitmapCropTask cropTask = new BitmapCropTask(
    303                 getContext(), uri, null, rotation, 0, 0, true, false, null);
    304         final Point bounds = cropTask.getImageBounds();
    305         Runnable onEndCrop = new Runnable() {
    306             public void run() {
    307                 updateWallpaperDimensions(bounds.x, bounds.y);
    308                 if (finishActivityWhenDone) {
    309                     setResult(Activity.RESULT_OK);
    310                     finish();
    311                 }
    312             }
    313         };
    314         cropTask.setOnEndRunnable(onEndCrop);
    315         cropTask.setNoCrop(true);
    316         cropTask.execute();
    317     }
    318 
    319     protected void cropImageAndSetWallpaper(
    320             Resources res, int resId, final boolean finishActivityWhenDone) {
    321         // crop this image and scale it down to the default wallpaper size for
    322         // this device
    323         int rotation = BitmapUtils.getRotationFromExif(res, resId);
    324         Point inSize = mCropView.getSourceDimensions();
    325         Point outSize = WallpaperUtils.getDefaultWallpaperSize(getResources(),
    326                 getWindowManager());
    327         RectF crop = Utils.getMaxCropRect(
    328                 inSize.x, inSize.y, outSize.x, outSize.y, false);
    329         Runnable onEndCrop = new Runnable() {
    330             public void run() {
    331                 // Passing 0, 0 will cause launcher to revert to using the
    332                 // default wallpaper size
    333                 updateWallpaperDimensions(0, 0);
    334                 if (finishActivityWhenDone) {
    335                     setResult(Activity.RESULT_OK);
    336                     finish();
    337                 }
    338             }
    339         };
    340         BitmapCropTask cropTask = new BitmapCropTask(getContext(), res, resId,
    341                 crop, rotation, outSize.x, outSize.y, true, false, onEndCrop);
    342         cropTask.execute();
    343     }
    344 
    345     @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
    346     protected void cropImageAndSetWallpaper(Uri uri,
    347             BitmapCropTask.OnBitmapCroppedHandler onBitmapCroppedHandler, final boolean finishActivityWhenDone) {
    348         boolean centerCrop = getResources().getBoolean(R.bool.center_crop);
    349         // Get the crop
    350         boolean ltr = mCropView.getLayoutDirection() == View.LAYOUT_DIRECTION_LTR;
    351 
    352         Display d = getWindowManager().getDefaultDisplay();
    353 
    354         Point displaySize = new Point();
    355         d.getSize(displaySize);
    356         boolean isPortrait = displaySize.x < displaySize.y;
    357 
    358         Point defaultWallpaperSize = WallpaperUtils.getDefaultWallpaperSize(getResources(),
    359                 getWindowManager());
    360         // Get the crop
    361         RectF cropRect = mCropView.getCrop();
    362 
    363         Point inSize = mCropView.getSourceDimensions();
    364 
    365         int cropRotation = mCropView.getImageRotation();
    366         float cropScale = mCropView.getWidth() / (float) cropRect.width();
    367 
    368 
    369         Matrix rotateMatrix = new Matrix();
    370         rotateMatrix.setRotate(cropRotation);
    371         float[] rotatedInSize = new float[] { inSize.x, inSize.y };
    372         rotateMatrix.mapPoints(rotatedInSize);
    373         rotatedInSize[0] = Math.abs(rotatedInSize[0]);
    374         rotatedInSize[1] = Math.abs(rotatedInSize[1]);
    375 
    376 
    377         // due to rounding errors in the cropview renderer the edges can be slightly offset
    378         // therefore we ensure that the boundaries are sanely defined
    379         cropRect.left = Math.max(0, cropRect.left);
    380         cropRect.right = Math.min(rotatedInSize[0], cropRect.right);
    381         cropRect.top = Math.max(0, cropRect.top);
    382         cropRect.bottom = Math.min(rotatedInSize[1], cropRect.bottom);
    383 
    384         // ADJUST CROP WIDTH
    385         // Extend the crop all the way to the right, for parallax
    386         // (or all the way to the left, in RTL)
    387         float extraSpace;
    388         if (centerCrop) {
    389             extraSpace = 2f * Math.min(rotatedInSize[0] - cropRect.right, cropRect.left);
    390         } else {
    391             extraSpace = ltr ? rotatedInSize[0] - cropRect.right : cropRect.left;
    392         }
    393         // Cap the amount of extra width
    394         float maxExtraSpace = defaultWallpaperSize.x / cropScale - cropRect.width();
    395         extraSpace = Math.min(extraSpace, maxExtraSpace);
    396 
    397         if (centerCrop) {
    398             cropRect.left -= extraSpace / 2f;
    399             cropRect.right += extraSpace / 2f;
    400         } else {
    401             if (ltr) {
    402                 cropRect.right += extraSpace;
    403             } else {
    404                 cropRect.left -= extraSpace;
    405             }
    406         }
    407 
    408         // ADJUST CROP HEIGHT
    409         if (isPortrait) {
    410             cropRect.bottom = cropRect.top + defaultWallpaperSize.y / cropScale;
    411         } else { // LANDSCAPE
    412             float extraPortraitHeight =
    413                     defaultWallpaperSize.y / cropScale - cropRect.height();
    414             float expandHeight =
    415                     Math.min(Math.min(rotatedInSize[1] - cropRect.bottom, cropRect.top),
    416                             extraPortraitHeight / 2);
    417             cropRect.top -= expandHeight;
    418             cropRect.bottom += expandHeight;
    419         }
    420         final int outWidth = (int) Math.round(cropRect.width() * cropScale);
    421         final int outHeight = (int) Math.round(cropRect.height() * cropScale);
    422 
    423         Runnable onEndCrop = new Runnable() {
    424             public void run() {
    425                 updateWallpaperDimensions(outWidth, outHeight);
    426                 if (finishActivityWhenDone) {
    427                     setResult(Activity.RESULT_OK);
    428                     finish();
    429                 }
    430             }
    431         };
    432         BitmapCropTask cropTask = new BitmapCropTask(getContext(), uri,
    433                 cropRect, cropRotation, outWidth, outHeight, true, false, onEndCrop);
    434         if (onBitmapCroppedHandler != null) {
    435             cropTask.setOnBitmapCropped(onBitmapCroppedHandler);
    436         }
    437         cropTask.execute();
    438     }
    439 
    440     protected void updateWallpaperDimensions(int width, int height) {
    441         String spKey = LauncherFiles.WALLPAPER_CROP_PREFERENCES_KEY;
    442         SharedPreferences sp = getContext().getSharedPreferences(spKey, Context.MODE_MULTI_PROCESS);
    443         SharedPreferences.Editor editor = sp.edit();
    444         if (width != 0 && height != 0) {
    445             editor.putInt(WALLPAPER_WIDTH_KEY, width);
    446             editor.putInt(WALLPAPER_HEIGHT_KEY, height);
    447         } else {
    448             editor.remove(WALLPAPER_WIDTH_KEY);
    449             editor.remove(WALLPAPER_HEIGHT_KEY);
    450         }
    451         editor.commit();
    452         WallpaperUtils.suggestWallpaperDimension(getResources(),
    453                 sp, getWindowManager(), WallpaperManager.getInstance(getContext()), true);
    454     }
    455 
    456     static class LoadRequest {
    457         BitmapSource src;
    458         boolean touchEnabled;
    459         boolean moveToLeft;
    460         Runnable postExecute;
    461         CropViewScaleProvider scaleProvider;
    462 
    463         TileSource result;
    464     }
    465 
    466     interface CropViewScaleProvider {
    467         float getScale(TileSource src);
    468     }
    469 }
    470