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