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
onCreate(Bundle savedInstanceState)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 
init()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
onDestroy()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
handleMessage(Message msg)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)
isActivityDestroyed()244     protected boolean isActivityDestroyed() {
245         return Utilities.ATLEAST_JB_MR1 && isDestroyed();
246     }
247 
addReusableBitmap(TileSource src)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 
getOnDialogCancelListener()259     public DialogInterface.OnCancelListener getOnDialogCancelListener() {
260         return mOnDialogCancelListener;
261     }
262 
onLoadRequestComplete(LoadRequest req, boolean success)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 
setCropViewTileSource(BitmapSource bitmapSource, boolean touchEnabled, boolean moveToLeft, CropViewScaleAndOffsetProvider scaleProvider, Runnable postExecute)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 
enableRotation()317     public boolean enableRotation() {
318         return getResources().getBoolean(R.bool.allow_rotation);
319     }
320 
setWallpaper(Uri uri, final boolean finishActivityWhenDone, final boolean shouldFadeOutOnFinish)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 
cropImageAndSetWallpaper(Resources res, int resId, final boolean finishActivityWhenDone, final boolean shouldFadeOutOnFinish)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)
cropImageAndSetWallpaper(Uri uri, BitmapCropTask.OnBitmapCroppedHandler onBitmapCroppedHandler, final boolean finishActivityWhenDone, final boolean shouldFadeOutOnFinish)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