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