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