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 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 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 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 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) 225 protected boolean isActivityDestroyed() { 226 return (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) 227 && isDestroyed(); 228 } 229 230 @Thunk void addReusableBitmap(TileSource src) { 231 synchronized (mReusableBitmaps) { 232 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT 233 && src instanceof BitmapRegionTileSource) { 234 Bitmap preview = ((BitmapRegionTileSource) src).getBitmap(); 235 if (preview != null && preview.isMutable()) { 236 mReusableBitmaps.add(preview); 237 } 238 } 239 } 240 } 241 242 protected void onLoadRequestComplete(LoadRequest req, boolean success) { 243 mCurrentLoadRequest = null; 244 if (success) { 245 TileSource oldSrc = mCropView.getTileSource(); 246 mCropView.setTileSource(req.result, null); 247 mCropView.setTouchEnabled(req.touchEnabled); 248 if (req.moveToLeft) { 249 mCropView.moveToLeft(); 250 } 251 if (req.scaleProvider != null) { 252 mCropView.setScale(req.scaleProvider.getScale(req.result)); 253 } 254 255 // Free last image 256 if (oldSrc != null) { 257 // Call yield instead of recycle, as we only want to free GL resource. 258 // We can still reuse the bitmap for decoding any other image. 259 oldSrc.getPreview().yield(); 260 } 261 addReusableBitmap(oldSrc); 262 } 263 if (req.postExecute != null) { 264 req.postExecute.run(); 265 } 266 mProgressView.setVisibility(View.GONE); 267 } 268 269 public final void setCropViewTileSource(BitmapSource bitmapSource, boolean touchEnabled, 270 boolean moveToLeft, CropViewScaleProvider scaleProvider, Runnable postExecute) { 271 final LoadRequest req = new LoadRequest(); 272 req.moveToLeft = moveToLeft; 273 req.src = bitmapSource; 274 req.touchEnabled = touchEnabled; 275 req.postExecute = postExecute; 276 req.scaleProvider = scaleProvider; 277 mCurrentLoadRequest = req; 278 279 // Remove any pending requests 280 mLoaderHandler.removeMessages(MSG_LOAD_IMAGE); 281 Message.obtain(mLoaderHandler, MSG_LOAD_IMAGE, req).sendToTarget(); 282 283 // We don't want to show the spinner every time we load an image, because that would be 284 // annoying; instead, only start showing the spinner if loading the image has taken 285 // longer than 1 sec (ie 1000 ms) 286 mProgressView.postDelayed(new Runnable() { 287 public void run() { 288 if (mCurrentLoadRequest == req) { 289 mProgressView.setVisibility(View.VISIBLE); 290 } 291 } 292 }, 1000); 293 } 294 295 296 public boolean enableRotation() { 297 return getResources().getBoolean(R.bool.allow_rotation); 298 } 299 300 protected void setWallpaper(Uri uri, final boolean finishActivityWhenDone) { 301 int rotation = BitmapUtils.getRotationFromExif(getContext(), uri); 302 BitmapCropTask cropTask = new BitmapCropTask( 303 getContext(), uri, null, rotation, 0, 0, true, false, null); 304 final Point bounds = cropTask.getImageBounds(); 305 Runnable onEndCrop = new Runnable() { 306 public void run() { 307 updateWallpaperDimensions(bounds.x, bounds.y); 308 if (finishActivityWhenDone) { 309 setResult(Activity.RESULT_OK); 310 finish(); 311 } 312 } 313 }; 314 cropTask.setOnEndRunnable(onEndCrop); 315 cropTask.setNoCrop(true); 316 cropTask.execute(); 317 } 318 319 protected void cropImageAndSetWallpaper( 320 Resources res, int resId, final boolean finishActivityWhenDone) { 321 // crop this image and scale it down to the default wallpaper size for 322 // this device 323 int rotation = BitmapUtils.getRotationFromExif(res, resId); 324 Point inSize = mCropView.getSourceDimensions(); 325 Point outSize = WallpaperUtils.getDefaultWallpaperSize(getResources(), 326 getWindowManager()); 327 RectF crop = Utils.getMaxCropRect( 328 inSize.x, inSize.y, outSize.x, outSize.y, false); 329 Runnable onEndCrop = new Runnable() { 330 public void run() { 331 // Passing 0, 0 will cause launcher to revert to using the 332 // default wallpaper size 333 updateWallpaperDimensions(0, 0); 334 if (finishActivityWhenDone) { 335 setResult(Activity.RESULT_OK); 336 finish(); 337 } 338 } 339 }; 340 BitmapCropTask cropTask = new BitmapCropTask(getContext(), res, resId, 341 crop, rotation, outSize.x, outSize.y, true, false, onEndCrop); 342 cropTask.execute(); 343 } 344 345 @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) 346 protected void cropImageAndSetWallpaper(Uri uri, 347 BitmapCropTask.OnBitmapCroppedHandler onBitmapCroppedHandler, final boolean finishActivityWhenDone) { 348 boolean centerCrop = getResources().getBoolean(R.bool.center_crop); 349 // Get the crop 350 boolean ltr = mCropView.getLayoutDirection() == View.LAYOUT_DIRECTION_LTR; 351 352 Display d = getWindowManager().getDefaultDisplay(); 353 354 Point displaySize = new Point(); 355 d.getSize(displaySize); 356 boolean isPortrait = displaySize.x < displaySize.y; 357 358 Point defaultWallpaperSize = WallpaperUtils.getDefaultWallpaperSize(getResources(), 359 getWindowManager()); 360 // Get the crop 361 RectF cropRect = mCropView.getCrop(); 362 363 Point inSize = mCropView.getSourceDimensions(); 364 365 int cropRotation = mCropView.getImageRotation(); 366 float cropScale = mCropView.getWidth() / (float) cropRect.width(); 367 368 369 Matrix rotateMatrix = new Matrix(); 370 rotateMatrix.setRotate(cropRotation); 371 float[] rotatedInSize = new float[] { inSize.x, inSize.y }; 372 rotateMatrix.mapPoints(rotatedInSize); 373 rotatedInSize[0] = Math.abs(rotatedInSize[0]); 374 rotatedInSize[1] = Math.abs(rotatedInSize[1]); 375 376 377 // due to rounding errors in the cropview renderer the edges can be slightly offset 378 // therefore we ensure that the boundaries are sanely defined 379 cropRect.left = Math.max(0, cropRect.left); 380 cropRect.right = Math.min(rotatedInSize[0], cropRect.right); 381 cropRect.top = Math.max(0, cropRect.top); 382 cropRect.bottom = Math.min(rotatedInSize[1], cropRect.bottom); 383 384 // ADJUST CROP WIDTH 385 // Extend the crop all the way to the right, for parallax 386 // (or all the way to the left, in RTL) 387 float extraSpace; 388 if (centerCrop) { 389 extraSpace = 2f * Math.min(rotatedInSize[0] - cropRect.right, cropRect.left); 390 } else { 391 extraSpace = ltr ? rotatedInSize[0] - cropRect.right : cropRect.left; 392 } 393 // Cap the amount of extra width 394 float maxExtraSpace = defaultWallpaperSize.x / cropScale - cropRect.width(); 395 extraSpace = Math.min(extraSpace, maxExtraSpace); 396 397 if (centerCrop) { 398 cropRect.left -= extraSpace / 2f; 399 cropRect.right += extraSpace / 2f; 400 } else { 401 if (ltr) { 402 cropRect.right += extraSpace; 403 } else { 404 cropRect.left -= extraSpace; 405 } 406 } 407 408 // ADJUST CROP HEIGHT 409 if (isPortrait) { 410 cropRect.bottom = cropRect.top + defaultWallpaperSize.y / cropScale; 411 } else { // LANDSCAPE 412 float extraPortraitHeight = 413 defaultWallpaperSize.y / cropScale - cropRect.height(); 414 float expandHeight = 415 Math.min(Math.min(rotatedInSize[1] - cropRect.bottom, cropRect.top), 416 extraPortraitHeight / 2); 417 cropRect.top -= expandHeight; 418 cropRect.bottom += expandHeight; 419 } 420 final int outWidth = (int) Math.round(cropRect.width() * cropScale); 421 final int outHeight = (int) Math.round(cropRect.height() * cropScale); 422 423 Runnable onEndCrop = new Runnable() { 424 public void run() { 425 updateWallpaperDimensions(outWidth, outHeight); 426 if (finishActivityWhenDone) { 427 setResult(Activity.RESULT_OK); 428 finish(); 429 } 430 } 431 }; 432 BitmapCropTask cropTask = new BitmapCropTask(getContext(), uri, 433 cropRect, cropRotation, outWidth, outHeight, true, false, onEndCrop); 434 if (onBitmapCroppedHandler != null) { 435 cropTask.setOnBitmapCropped(onBitmapCroppedHandler); 436 } 437 cropTask.execute(); 438 } 439 440 protected void updateWallpaperDimensions(int width, int height) { 441 String spKey = LauncherFiles.WALLPAPER_CROP_PREFERENCES_KEY; 442 SharedPreferences sp = getContext().getSharedPreferences(spKey, Context.MODE_MULTI_PROCESS); 443 SharedPreferences.Editor editor = sp.edit(); 444 if (width != 0 && height != 0) { 445 editor.putInt(WALLPAPER_WIDTH_KEY, width); 446 editor.putInt(WALLPAPER_HEIGHT_KEY, height); 447 } else { 448 editor.remove(WALLPAPER_WIDTH_KEY); 449 editor.remove(WALLPAPER_HEIGHT_KEY); 450 } 451 editor.commit(); 452 WallpaperUtils.suggestWallpaperDimension(getResources(), 453 sp, getWindowManager(), WallpaperManager.getInstance(getContext()), true); 454 } 455 456 static class LoadRequest { 457 BitmapSource src; 458 boolean touchEnabled; 459 boolean moveToLeft; 460 Runnable postExecute; 461 CropViewScaleProvider scaleProvider; 462 463 TileSource result; 464 } 465 466 interface CropViewScaleProvider { 467 float getScale(TileSource src); 468 } 469 } 470