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