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
onCreate(Bundle savedInstanceState)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 
init()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
onDestroy()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
handleMessage(Message msg)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)
isActivityDestroyed()239     public boolean isActivityDestroyed() {
240         return Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1 && isDestroyed();
241     }
242 
addReusableBitmap(TileSource src)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 
getOnDialogCancelListener()255     public DialogInterface.OnCancelListener getOnDialogCancelListener() {
256         return mOnDialogCancelListener;
257     }
258 
showActionBarAndTiles()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 
onLoadRequestComplete(LoadRequest req, boolean success)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)
setCropViewTileSource(BitmapSource bitmapSource, boolean touchEnabled, boolean moveToLeft, CropViewScaleAndOffsetProvider scaleAndOffsetProvider, Runnable postExecute)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 
enableRotation()329     public boolean enableRotation() {
330         return true;
331     }
332 
cropImageAndSetWallpaper(Resources res, int resId, boolean shouldFadeOutOnFinish)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)
cropImageAndSetWallpaper(Uri uri, CropAndSetWallpaperTask.OnBitmapCroppedHandler onBitmapCroppedHandler, boolean shouldFadeOutOnFinish)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