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