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.app.ActionBar; 20 import android.app.Activity; 21 import android.app.WallpaperManager; 22 import android.content.Context; 23 import android.content.Intent; 24 import android.content.SharedPreferences; 25 import android.content.res.Configuration; 26 import android.content.res.Resources; 27 import android.graphics.Bitmap; 28 import android.graphics.Bitmap.CompressFormat; 29 import android.graphics.BitmapFactory; 30 import android.graphics.BitmapRegionDecoder; 31 import android.graphics.Canvas; 32 import android.graphics.Matrix; 33 import android.graphics.Paint; 34 import android.graphics.Point; 35 import android.graphics.Rect; 36 import android.graphics.RectF; 37 import android.net.Uri; 38 import android.os.AsyncTask; 39 import android.os.Bundle; 40 import android.util.Log; 41 import android.view.Display; 42 import android.view.View; 43 import android.view.WindowManager; 44 import android.widget.Toast; 45 46 import com.android.gallery3d.common.Utils; 47 import com.android.gallery3d.exif.ExifInterface; 48 import com.android.photos.BitmapRegionTileSource; 49 import com.android.photos.BitmapRegionTileSource.BitmapSource; 50 51 import java.io.BufferedInputStream; 52 import java.io.ByteArrayInputStream; 53 import java.io.ByteArrayOutputStream; 54 import java.io.FileNotFoundException; 55 import java.io.IOException; 56 import java.io.InputStream; 57 58 public class WallpaperCropActivity extends Activity { 59 private static final String LOGTAG = "Launcher3.CropActivity"; 60 61 protected static final String WALLPAPER_WIDTH_KEY = "wallpaper.width"; 62 protected static final String WALLPAPER_HEIGHT_KEY = "wallpaper.height"; 63 private static final int DEFAULT_COMPRESS_QUALITY = 90; 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 private static final float WALLPAPER_SCREENS_SPAN = 2f; 73 74 protected static Point sDefaultWallpaperSize; 75 76 protected CropView mCropView; 77 protected Uri mUri; 78 protected View mSetWallpaperButton; 79 80 @Override 81 protected void onCreate(Bundle savedInstanceState) { 82 super.onCreate(savedInstanceState); 83 init(); 84 if (!enableRotation()) { 85 setRequestedOrientation(Configuration.ORIENTATION_PORTRAIT); 86 } 87 } 88 89 protected void init() { 90 setContentView(R.layout.wallpaper_cropper); 91 92 mCropView = (CropView) findViewById(R.id.cropView); 93 94 Intent cropIntent = getIntent(); 95 final Uri imageUri = cropIntent.getData(); 96 97 if (imageUri == null) { 98 Log.e(LOGTAG, "No URI passed in intent, exiting WallpaperCropActivity"); 99 finish(); 100 return; 101 } 102 103 // Action bar 104 // Show the custom action bar view 105 final ActionBar actionBar = getActionBar(); 106 actionBar.setCustomView(R.layout.actionbar_set_wallpaper); 107 actionBar.getCustomView().setOnClickListener( 108 new View.OnClickListener() { 109 @Override 110 public void onClick(View v) { 111 boolean finishActivityWhenDone = true; 112 cropImageAndSetWallpaper(imageUri, null, finishActivityWhenDone); 113 } 114 }); 115 mSetWallpaperButton = findViewById(R.id.set_wallpaper_button); 116 117 // Load image in background 118 final BitmapRegionTileSource.UriBitmapSource bitmapSource = 119 new BitmapRegionTileSource.UriBitmapSource(this, imageUri, 1024); 120 mSetWallpaperButton.setEnabled(false); 121 Runnable onLoad = new Runnable() { 122 public void run() { 123 if (bitmapSource.getLoadingState() != BitmapSource.State.LOADED) { 124 Toast.makeText(WallpaperCropActivity.this, 125 getString(R.string.wallpaper_load_fail), 126 Toast.LENGTH_LONG).show(); 127 finish(); 128 } else { 129 mSetWallpaperButton.setEnabled(true); 130 } 131 } 132 }; 133 setCropViewTileSource(bitmapSource, true, false, onLoad); 134 } 135 136 @Override 137 protected void onDestroy() { 138 if (mCropView != null) { 139 mCropView.destroy(); 140 } 141 super.onDestroy(); 142 } 143 144 public void setCropViewTileSource( 145 final BitmapRegionTileSource.BitmapSource bitmapSource, final boolean touchEnabled, 146 final boolean moveToLeft, final Runnable postExecute) { 147 final Context context = WallpaperCropActivity.this; 148 final View progressView = findViewById(R.id.loading); 149 final AsyncTask<Void, Void, Void> loadBitmapTask = new AsyncTask<Void, Void, Void>() { 150 protected Void doInBackground(Void...args) { 151 if (!isCancelled()) { 152 try { 153 bitmapSource.loadInBackground(); 154 } catch (SecurityException securityException) { 155 if (isDestroyed()) { 156 // Temporarily granted permissions are revoked when the activity 157 // finishes, potentially resulting in a SecurityException here. 158 // Even though {@link #isDestroyed} might also return true in different 159 // situations where the configuration changes, we are fine with 160 // catching these cases here as well. 161 cancel(false); 162 } else { 163 // otherwise it had a different cause and we throw it further 164 throw securityException; 165 } 166 } 167 } 168 return null; 169 } 170 protected void onPostExecute(Void arg) { 171 if (!isCancelled()) { 172 progressView.setVisibility(View.INVISIBLE); 173 if (bitmapSource.getLoadingState() == BitmapSource.State.LOADED) { 174 mCropView.setTileSource( 175 new BitmapRegionTileSource(context, bitmapSource), null); 176 mCropView.setTouchEnabled(touchEnabled); 177 if (moveToLeft) { 178 mCropView.moveToLeft(); 179 } 180 } 181 } 182 if (postExecute != null) { 183 postExecute.run(); 184 } 185 } 186 }; 187 // We don't want to show the spinner every time we load an image, because that would be 188 // annoying; instead, only start showing the spinner if loading the image has taken 189 // longer than 1 sec (ie 1000 ms) 190 progressView.postDelayed(new Runnable() { 191 public void run() { 192 if (loadBitmapTask.getStatus() != AsyncTask.Status.FINISHED) { 193 progressView.setVisibility(View.VISIBLE); 194 } 195 } 196 }, 1000); 197 loadBitmapTask.execute(); 198 } 199 200 public boolean enableRotation() { 201 return getResources().getBoolean(R.bool.allow_rotation); 202 } 203 204 public static String getSharedPreferencesKey() { 205 return WallpaperCropActivity.class.getName(); 206 } 207 208 // As a ratio of screen height, the total distance we want the parallax effect to span 209 // horizontally 210 private static float wallpaperTravelToScreenWidthRatio(int width, int height) { 211 float aspectRatio = width / (float) height; 212 213 // At an aspect ratio of 16/10, the wallpaper parallax effect should span 1.5 * screen width 214 // At an aspect ratio of 10/16, the wallpaper parallax effect should span 1.2 * screen width 215 // We will use these two data points to extrapolate how much the wallpaper parallax effect 216 // to span (ie travel) at any aspect ratio: 217 218 final float ASPECT_RATIO_LANDSCAPE = 16/10f; 219 final float ASPECT_RATIO_PORTRAIT = 10/16f; 220 final float WALLPAPER_WIDTH_TO_SCREEN_RATIO_LANDSCAPE = 1.5f; 221 final float WALLPAPER_WIDTH_TO_SCREEN_RATIO_PORTRAIT = 1.2f; 222 223 // To find out the desired width at different aspect ratios, we use the following two 224 // formulas, where the coefficient on x is the aspect ratio (width/height): 225 // (16/10)x + y = 1.5 226 // (10/16)x + y = 1.2 227 // We solve for x and y and end up with a final formula: 228 final float x = 229 (WALLPAPER_WIDTH_TO_SCREEN_RATIO_LANDSCAPE - WALLPAPER_WIDTH_TO_SCREEN_RATIO_PORTRAIT) / 230 (ASPECT_RATIO_LANDSCAPE - ASPECT_RATIO_PORTRAIT); 231 final float y = WALLPAPER_WIDTH_TO_SCREEN_RATIO_PORTRAIT - x * ASPECT_RATIO_PORTRAIT; 232 return x * aspectRatio + y; 233 } 234 235 static protected Point getDefaultWallpaperSize(Resources res, WindowManager windowManager) { 236 if (sDefaultWallpaperSize == null) { 237 Point minDims = new Point(); 238 Point maxDims = new Point(); 239 windowManager.getDefaultDisplay().getCurrentSizeRange(minDims, maxDims); 240 241 int maxDim = Math.max(maxDims.x, maxDims.y); 242 int minDim = Math.max(minDims.x, minDims.y); 243 244 if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN_MR1) { 245 Point realSize = new Point(); 246 windowManager.getDefaultDisplay().getRealSize(realSize); 247 maxDim = Math.max(realSize.x, realSize.y); 248 minDim = Math.min(realSize.x, realSize.y); 249 } 250 251 // We need to ensure that there is enough extra space in the wallpaper 252 // for the intended parallax effects 253 final int defaultWidth, defaultHeight; 254 if (isScreenLarge(res)) { 255 defaultWidth = (int) (maxDim * wallpaperTravelToScreenWidthRatio(maxDim, minDim)); 256 defaultHeight = maxDim; 257 } else { 258 defaultWidth = Math.max((int) (minDim * WALLPAPER_SCREENS_SPAN), maxDim); 259 defaultHeight = maxDim; 260 } 261 sDefaultWallpaperSize = new Point(defaultWidth, defaultHeight); 262 } 263 return sDefaultWallpaperSize; 264 } 265 266 public static int getRotationFromExif(String path) { 267 return getRotationFromExifHelper(path, null, 0, null, null); 268 } 269 270 public static int getRotationFromExif(Context context, Uri uri) { 271 return getRotationFromExifHelper(null, null, 0, context, uri); 272 } 273 274 public static int getRotationFromExif(Resources res, int resId) { 275 return getRotationFromExifHelper(null, res, resId, null, null); 276 } 277 278 private static int getRotationFromExifHelper( 279 String path, Resources res, int resId, Context context, Uri uri) { 280 ExifInterface ei = new ExifInterface(); 281 InputStream is = null; 282 BufferedInputStream bis = null; 283 try { 284 if (path != null) { 285 ei.readExif(path); 286 } else if (uri != null) { 287 is = context.getContentResolver().openInputStream(uri); 288 bis = new BufferedInputStream(is); 289 ei.readExif(bis); 290 } else { 291 is = res.openRawResource(resId); 292 bis = new BufferedInputStream(is); 293 ei.readExif(bis); 294 } 295 Integer ori = ei.getTagIntValue(ExifInterface.TAG_ORIENTATION); 296 if (ori != null) { 297 return ExifInterface.getRotationForOrientationValue(ori.shortValue()); 298 } 299 } catch (IOException e) { 300 Log.w(LOGTAG, "Getting exif data failed", e); 301 } catch (NullPointerException e) { 302 // Sometimes the ExifInterface has an internal NPE if Exif data isn't valid 303 Log.w(LOGTAG, "Getting exif data failed", e); 304 } finally { 305 Utils.closeSilently(bis); 306 Utils.closeSilently(is); 307 } 308 return 0; 309 } 310 311 protected void setWallpaper(Uri uri, final boolean finishActivityWhenDone) { 312 int rotation = getRotationFromExif(this, uri); 313 BitmapCropTask cropTask = new BitmapCropTask( 314 this, uri, null, rotation, 0, 0, true, false, null); 315 final Point bounds = cropTask.getImageBounds(); 316 Runnable onEndCrop = new Runnable() { 317 public void run() { 318 updateWallpaperDimensions(bounds.x, bounds.y); 319 if (finishActivityWhenDone) { 320 setResult(Activity.RESULT_OK); 321 finish(); 322 } 323 } 324 }; 325 cropTask.setOnEndRunnable(onEndCrop); 326 cropTask.setNoCrop(true); 327 cropTask.execute(); 328 } 329 330 protected void cropImageAndSetWallpaper( 331 Resources res, int resId, final boolean finishActivityWhenDone) { 332 // crop this image and scale it down to the default wallpaper size for 333 // this device 334 int rotation = getRotationFromExif(res, resId); 335 Point inSize = mCropView.getSourceDimensions(); 336 Point outSize = getDefaultWallpaperSize(getResources(), 337 getWindowManager()); 338 RectF crop = getMaxCropRect( 339 inSize.x, inSize.y, outSize.x, outSize.y, false); 340 Runnable onEndCrop = new Runnable() { 341 public void run() { 342 // Passing 0, 0 will cause launcher to revert to using the 343 // default wallpaper size 344 updateWallpaperDimensions(0, 0); 345 if (finishActivityWhenDone) { 346 setResult(Activity.RESULT_OK); 347 finish(); 348 } 349 } 350 }; 351 BitmapCropTask cropTask = new BitmapCropTask(this, res, resId, 352 crop, rotation, outSize.x, outSize.y, true, false, onEndCrop); 353 cropTask.execute(); 354 } 355 356 private static boolean isScreenLarge(Resources res) { 357 Configuration config = res.getConfiguration(); 358 return config.smallestScreenWidthDp >= 720; 359 } 360 361 protected void cropImageAndSetWallpaper(Uri uri, 362 OnBitmapCroppedHandler onBitmapCroppedHandler, final boolean finishActivityWhenDone) { 363 boolean centerCrop = getResources().getBoolean(R.bool.center_crop); 364 // Get the crop 365 boolean ltr = mCropView.getLayoutDirection() == View.LAYOUT_DIRECTION_LTR; 366 367 Display d = getWindowManager().getDefaultDisplay(); 368 369 Point displaySize = new Point(); 370 d.getSize(displaySize); 371 boolean isPortrait = displaySize.x < displaySize.y; 372 373 Point defaultWallpaperSize = getDefaultWallpaperSize(getResources(), 374 getWindowManager()); 375 // Get the crop 376 RectF cropRect = mCropView.getCrop(); 377 378 Point inSize = mCropView.getSourceDimensions(); 379 380 int cropRotation = mCropView.getImageRotation(); 381 float cropScale = mCropView.getWidth() / (float) cropRect.width(); 382 383 384 Matrix rotateMatrix = new Matrix(); 385 rotateMatrix.setRotate(cropRotation); 386 float[] rotatedInSize = new float[] { inSize.x, inSize.y }; 387 rotateMatrix.mapPoints(rotatedInSize); 388 rotatedInSize[0] = Math.abs(rotatedInSize[0]); 389 rotatedInSize[1] = Math.abs(rotatedInSize[1]); 390 391 392 // due to rounding errors in the cropview renderer the edges can be slightly offset 393 // therefore we ensure that the boundaries are sanely defined 394 cropRect.left = Math.max(0, cropRect.left); 395 cropRect.right = Math.min(rotatedInSize[0], cropRect.right); 396 cropRect.top = Math.max(0, cropRect.top); 397 cropRect.bottom = Math.min(rotatedInSize[1], cropRect.bottom); 398 399 // ADJUST CROP WIDTH 400 // Extend the crop all the way to the right, for parallax 401 // (or all the way to the left, in RTL) 402 float extraSpace; 403 if (centerCrop) { 404 extraSpace = 2f * Math.min(rotatedInSize[0] - cropRect.right, cropRect.left); 405 } else { 406 extraSpace = ltr ? rotatedInSize[0] - cropRect.right : cropRect.left; 407 } 408 // Cap the amount of extra width 409 float maxExtraSpace = defaultWallpaperSize.x / cropScale - cropRect.width(); 410 extraSpace = Math.min(extraSpace, maxExtraSpace); 411 412 if (centerCrop) { 413 cropRect.left -= extraSpace / 2f; 414 cropRect.right += extraSpace / 2f; 415 } else { 416 if (ltr) { 417 cropRect.right += extraSpace; 418 } else { 419 cropRect.left -= extraSpace; 420 } 421 } 422 423 // ADJUST CROP HEIGHT 424 if (isPortrait) { 425 cropRect.bottom = cropRect.top + defaultWallpaperSize.y / cropScale; 426 } else { // LANDSCAPE 427 float extraPortraitHeight = 428 defaultWallpaperSize.y / cropScale - cropRect.height(); 429 float expandHeight = 430 Math.min(Math.min(rotatedInSize[1] - cropRect.bottom, cropRect.top), 431 extraPortraitHeight / 2); 432 cropRect.top -= expandHeight; 433 cropRect.bottom += expandHeight; 434 } 435 final int outWidth = (int) Math.round(cropRect.width() * cropScale); 436 final int outHeight = (int) Math.round(cropRect.height() * cropScale); 437 438 Runnable onEndCrop = new Runnable() { 439 public void run() { 440 updateWallpaperDimensions(outWidth, outHeight); 441 if (finishActivityWhenDone) { 442 setResult(Activity.RESULT_OK); 443 finish(); 444 } 445 } 446 }; 447 BitmapCropTask cropTask = new BitmapCropTask(this, uri, 448 cropRect, cropRotation, outWidth, outHeight, true, false, onEndCrop); 449 if (onBitmapCroppedHandler != null) { 450 cropTask.setOnBitmapCropped(onBitmapCroppedHandler); 451 } 452 cropTask.execute(); 453 } 454 455 public interface OnBitmapCroppedHandler { 456 public void onBitmapCropped(byte[] imageBytes); 457 } 458 459 protected static class BitmapCropTask extends AsyncTask<Void, Void, Boolean> { 460 Uri mInUri = null; 461 Context mContext; 462 String mInFilePath; 463 byte[] mInImageBytes; 464 int mInResId = 0; 465 RectF mCropBounds = null; 466 int mOutWidth, mOutHeight; 467 int mRotation; 468 String mOutputFormat = "jpg"; // for now 469 boolean mSetWallpaper; 470 boolean mSaveCroppedBitmap; 471 Bitmap mCroppedBitmap; 472 Runnable mOnEndRunnable; 473 Resources mResources; 474 OnBitmapCroppedHandler mOnBitmapCroppedHandler; 475 boolean mNoCrop; 476 477 public BitmapCropTask(Context c, String filePath, 478 RectF cropBounds, int rotation, int outWidth, int outHeight, 479 boolean setWallpaper, boolean saveCroppedBitmap, Runnable onEndRunnable) { 480 mContext = c; 481 mInFilePath = filePath; 482 init(cropBounds, rotation, 483 outWidth, outHeight, setWallpaper, saveCroppedBitmap, onEndRunnable); 484 } 485 486 public BitmapCropTask(byte[] imageBytes, 487 RectF cropBounds, int rotation, int outWidth, int outHeight, 488 boolean setWallpaper, boolean saveCroppedBitmap, Runnable onEndRunnable) { 489 mInImageBytes = imageBytes; 490 init(cropBounds, rotation, 491 outWidth, outHeight, setWallpaper, saveCroppedBitmap, onEndRunnable); 492 } 493 494 public BitmapCropTask(Context c, Uri inUri, 495 RectF cropBounds, int rotation, int outWidth, int outHeight, 496 boolean setWallpaper, boolean saveCroppedBitmap, Runnable onEndRunnable) { 497 mContext = c; 498 mInUri = inUri; 499 init(cropBounds, rotation, 500 outWidth, outHeight, setWallpaper, saveCroppedBitmap, onEndRunnable); 501 } 502 503 public BitmapCropTask(Context c, Resources res, int inResId, 504 RectF cropBounds, int rotation, int outWidth, int outHeight, 505 boolean setWallpaper, boolean saveCroppedBitmap, Runnable onEndRunnable) { 506 mContext = c; 507 mInResId = inResId; 508 mResources = res; 509 init(cropBounds, rotation, 510 outWidth, outHeight, setWallpaper, saveCroppedBitmap, onEndRunnable); 511 } 512 513 private void init(RectF cropBounds, int rotation, int outWidth, int outHeight, 514 boolean setWallpaper, boolean saveCroppedBitmap, Runnable onEndRunnable) { 515 mCropBounds = cropBounds; 516 mRotation = rotation; 517 mOutWidth = outWidth; 518 mOutHeight = outHeight; 519 mSetWallpaper = setWallpaper; 520 mSaveCroppedBitmap = saveCroppedBitmap; 521 mOnEndRunnable = onEndRunnable; 522 } 523 524 public void setOnBitmapCropped(OnBitmapCroppedHandler handler) { 525 mOnBitmapCroppedHandler = handler; 526 } 527 528 public void setNoCrop(boolean value) { 529 mNoCrop = value; 530 } 531 532 public void setOnEndRunnable(Runnable onEndRunnable) { 533 mOnEndRunnable = onEndRunnable; 534 } 535 536 // Helper to setup input stream 537 private InputStream regenerateInputStream() { 538 if (mInUri == null && mInResId == 0 && mInFilePath == null && mInImageBytes == null) { 539 Log.w(LOGTAG, "cannot read original file, no input URI, resource ID, or " + 540 "image byte array given"); 541 } else { 542 try { 543 if (mInUri != null) { 544 return new BufferedInputStream( 545 mContext.getContentResolver().openInputStream(mInUri)); 546 } else if (mInFilePath != null) { 547 return mContext.openFileInput(mInFilePath); 548 } else if (mInImageBytes != null) { 549 return new BufferedInputStream(new ByteArrayInputStream(mInImageBytes)); 550 } else { 551 return new BufferedInputStream(mResources.openRawResource(mInResId)); 552 } 553 } catch (FileNotFoundException e) { 554 Log.w(LOGTAG, "cannot read file: " + mInUri.toString(), e); 555 } 556 } 557 return null; 558 } 559 560 public Point getImageBounds() { 561 InputStream is = regenerateInputStream(); 562 if (is != null) { 563 BitmapFactory.Options options = new BitmapFactory.Options(); 564 options.inJustDecodeBounds = true; 565 BitmapFactory.decodeStream(is, null, options); 566 Utils.closeSilently(is); 567 if (options.outWidth != 0 && options.outHeight != 0) { 568 return new Point(options.outWidth, options.outHeight); 569 } 570 } 571 return null; 572 } 573 574 public void setCropBounds(RectF cropBounds) { 575 mCropBounds = cropBounds; 576 } 577 578 public Bitmap getCroppedBitmap() { 579 return mCroppedBitmap; 580 } 581 public boolean cropBitmap() { 582 boolean failure = false; 583 584 585 WallpaperManager wallpaperManager = null; 586 if (mSetWallpaper) { 587 wallpaperManager = WallpaperManager.getInstance(mContext.getApplicationContext()); 588 } 589 590 591 if (mSetWallpaper && mNoCrop) { 592 try { 593 InputStream is = regenerateInputStream(); 594 if (is != null) { 595 wallpaperManager.setStream(is); 596 Utils.closeSilently(is); 597 } 598 } catch (IOException e) { 599 Log.w(LOGTAG, "cannot write stream to wallpaper", e); 600 failure = true; 601 } 602 return !failure; 603 } else { 604 // Find crop bounds (scaled to original image size) 605 Rect roundedTrueCrop = new Rect(); 606 Matrix rotateMatrix = new Matrix(); 607 Matrix inverseRotateMatrix = new Matrix(); 608 609 Point bounds = getImageBounds(); 610 if (mRotation > 0) { 611 rotateMatrix.setRotate(mRotation); 612 inverseRotateMatrix.setRotate(-mRotation); 613 614 mCropBounds.roundOut(roundedTrueCrop); 615 mCropBounds = new RectF(roundedTrueCrop); 616 617 if (bounds == null) { 618 Log.w(LOGTAG, "cannot get bounds for image"); 619 failure = true; 620 return false; 621 } 622 623 float[] rotatedBounds = new float[] { bounds.x, bounds.y }; 624 rotateMatrix.mapPoints(rotatedBounds); 625 rotatedBounds[0] = Math.abs(rotatedBounds[0]); 626 rotatedBounds[1] = Math.abs(rotatedBounds[1]); 627 628 mCropBounds.offset(-rotatedBounds[0]/2, -rotatedBounds[1]/2); 629 inverseRotateMatrix.mapRect(mCropBounds); 630 mCropBounds.offset(bounds.x/2, bounds.y/2); 631 632 } 633 634 mCropBounds.roundOut(roundedTrueCrop); 635 636 if (roundedTrueCrop.width() <= 0 || roundedTrueCrop.height() <= 0) { 637 Log.w(LOGTAG, "crop has bad values for full size image"); 638 failure = true; 639 return false; 640 } 641 642 // See how much we're reducing the size of the image 643 int scaleDownSampleSize = Math.max(1, Math.min(roundedTrueCrop.width() / mOutWidth, 644 roundedTrueCrop.height() / mOutHeight)); 645 // Attempt to open a region decoder 646 BitmapRegionDecoder decoder = null; 647 InputStream is = null; 648 try { 649 is = regenerateInputStream(); 650 if (is == null) { 651 Log.w(LOGTAG, "cannot get input stream for uri=" + mInUri.toString()); 652 failure = true; 653 return false; 654 } 655 decoder = BitmapRegionDecoder.newInstance(is, false); 656 Utils.closeSilently(is); 657 } catch (IOException e) { 658 Log.w(LOGTAG, "cannot open region decoder for file: " + mInUri.toString(), e); 659 } finally { 660 Utils.closeSilently(is); 661 is = null; 662 } 663 664 Bitmap crop = null; 665 if (decoder != null) { 666 // Do region decoding to get crop bitmap 667 BitmapFactory.Options options = new BitmapFactory.Options(); 668 if (scaleDownSampleSize > 1) { 669 options.inSampleSize = scaleDownSampleSize; 670 } 671 crop = decoder.decodeRegion(roundedTrueCrop, options); 672 decoder.recycle(); 673 } 674 675 if (crop == null) { 676 // BitmapRegionDecoder has failed, try to crop in-memory 677 is = regenerateInputStream(); 678 Bitmap fullSize = null; 679 if (is != null) { 680 BitmapFactory.Options options = new BitmapFactory.Options(); 681 if (scaleDownSampleSize > 1) { 682 options.inSampleSize = scaleDownSampleSize; 683 } 684 fullSize = BitmapFactory.decodeStream(is, null, options); 685 Utils.closeSilently(is); 686 } 687 if (fullSize != null) { 688 // Find out the true sample size that was used by the decoder 689 scaleDownSampleSize = bounds.x / fullSize.getWidth(); 690 mCropBounds.left /= scaleDownSampleSize; 691 mCropBounds.top /= scaleDownSampleSize; 692 mCropBounds.bottom /= scaleDownSampleSize; 693 mCropBounds.right /= scaleDownSampleSize; 694 mCropBounds.roundOut(roundedTrueCrop); 695 696 // Adjust values to account for issues related to rounding 697 if (roundedTrueCrop.width() > fullSize.getWidth()) { 698 // Adjust the width 699 roundedTrueCrop.right = roundedTrueCrop.left + fullSize.getWidth(); 700 } 701 if (roundedTrueCrop.right > fullSize.getWidth()) { 702 // Adjust the left value 703 int adjustment = roundedTrueCrop.left - 704 Math.max(0, roundedTrueCrop.right - roundedTrueCrop.width()); 705 roundedTrueCrop.left -= adjustment; 706 roundedTrueCrop.right -= adjustment; 707 } 708 if (roundedTrueCrop.height() > fullSize.getHeight()) { 709 // Adjust the height 710 roundedTrueCrop.bottom = roundedTrueCrop.top + fullSize.getHeight(); 711 } 712 if (roundedTrueCrop.bottom > fullSize.getHeight()) { 713 // Adjust the top value 714 int adjustment = roundedTrueCrop.top - 715 Math.max(0, roundedTrueCrop.bottom - roundedTrueCrop.height()); 716 roundedTrueCrop.top -= adjustment; 717 roundedTrueCrop.bottom -= adjustment; 718 } 719 720 crop = Bitmap.createBitmap(fullSize, roundedTrueCrop.left, 721 roundedTrueCrop.top, roundedTrueCrop.width(), 722 roundedTrueCrop.height()); 723 } 724 } 725 726 if (crop == null) { 727 Log.w(LOGTAG, "cannot decode file: " + mInUri.toString()); 728 failure = true; 729 return false; 730 } 731 if (mOutWidth > 0 && mOutHeight > 0 || mRotation > 0) { 732 float[] dimsAfter = new float[] { crop.getWidth(), crop.getHeight() }; 733 rotateMatrix.mapPoints(dimsAfter); 734 dimsAfter[0] = Math.abs(dimsAfter[0]); 735 dimsAfter[1] = Math.abs(dimsAfter[1]); 736 737 if (!(mOutWidth > 0 && mOutHeight > 0)) { 738 mOutWidth = Math.round(dimsAfter[0]); 739 mOutHeight = Math.round(dimsAfter[1]); 740 } 741 742 RectF cropRect = new RectF(0, 0, dimsAfter[0], dimsAfter[1]); 743 RectF returnRect = new RectF(0, 0, mOutWidth, mOutHeight); 744 745 Matrix m = new Matrix(); 746 if (mRotation == 0) { 747 m.setRectToRect(cropRect, returnRect, Matrix.ScaleToFit.FILL); 748 } else { 749 Matrix m1 = new Matrix(); 750 m1.setTranslate(-crop.getWidth() / 2f, -crop.getHeight() / 2f); 751 Matrix m2 = new Matrix(); 752 m2.setRotate(mRotation); 753 Matrix m3 = new Matrix(); 754 m3.setTranslate(dimsAfter[0] / 2f, dimsAfter[1] / 2f); 755 Matrix m4 = new Matrix(); 756 m4.setRectToRect(cropRect, returnRect, Matrix.ScaleToFit.FILL); 757 758 Matrix c1 = new Matrix(); 759 c1.setConcat(m2, m1); 760 Matrix c2 = new Matrix(); 761 c2.setConcat(m4, m3); 762 m.setConcat(c2, c1); 763 } 764 765 Bitmap tmp = Bitmap.createBitmap((int) returnRect.width(), 766 (int) returnRect.height(), Bitmap.Config.ARGB_8888); 767 if (tmp != null) { 768 Canvas c = new Canvas(tmp); 769 Paint p = new Paint(); 770 p.setFilterBitmap(true); 771 c.drawBitmap(crop, m, p); 772 crop = tmp; 773 } 774 } 775 776 if (mSaveCroppedBitmap) { 777 mCroppedBitmap = crop; 778 } 779 780 // Get output compression format 781 CompressFormat cf = 782 convertExtensionToCompressFormat(getFileExtension(mOutputFormat)); 783 784 // Compress to byte array 785 ByteArrayOutputStream tmpOut = new ByteArrayOutputStream(2048); 786 if (crop.compress(cf, DEFAULT_COMPRESS_QUALITY, tmpOut)) { 787 // If we need to set to the wallpaper, set it 788 if (mSetWallpaper && wallpaperManager != null) { 789 try { 790 byte[] outByteArray = tmpOut.toByteArray(); 791 wallpaperManager.setStream(new ByteArrayInputStream(outByteArray)); 792 if (mOnBitmapCroppedHandler != null) { 793 mOnBitmapCroppedHandler.onBitmapCropped(outByteArray); 794 } 795 } catch (IOException e) { 796 Log.w(LOGTAG, "cannot write stream to wallpaper", e); 797 failure = true; 798 } 799 } 800 } else { 801 Log.w(LOGTAG, "cannot compress bitmap"); 802 failure = true; 803 } 804 } 805 return !failure; // True if any of the operations failed 806 } 807 808 @Override 809 protected Boolean doInBackground(Void... params) { 810 return cropBitmap(); 811 } 812 813 @Override 814 protected void onPostExecute(Boolean result) { 815 if (mOnEndRunnable != null) { 816 mOnEndRunnable.run(); 817 } 818 } 819 } 820 821 protected void updateWallpaperDimensions(int width, int height) { 822 String spKey = getSharedPreferencesKey(); 823 SharedPreferences sp = getSharedPreferences(spKey, Context.MODE_MULTI_PROCESS); 824 SharedPreferences.Editor editor = sp.edit(); 825 if (width != 0 && height != 0) { 826 editor.putInt(WALLPAPER_WIDTH_KEY, width); 827 editor.putInt(WALLPAPER_HEIGHT_KEY, height); 828 } else { 829 editor.remove(WALLPAPER_WIDTH_KEY); 830 editor.remove(WALLPAPER_HEIGHT_KEY); 831 } 832 editor.commit(); 833 834 suggestWallpaperDimension(getResources(), 835 sp, getWindowManager(), WallpaperManager.getInstance(this), true); 836 } 837 838 static public void suggestWallpaperDimension(Resources res, 839 final SharedPreferences sharedPrefs, 840 WindowManager windowManager, 841 final WallpaperManager wallpaperManager, boolean fallBackToDefaults) { 842 final Point defaultWallpaperSize = getDefaultWallpaperSize(res, windowManager); 843 // If we have saved a wallpaper width/height, use that instead 844 845 int savedWidth = sharedPrefs.getInt(WALLPAPER_WIDTH_KEY, -1); 846 int savedHeight = sharedPrefs.getInt(WALLPAPER_HEIGHT_KEY, -1); 847 848 if (savedWidth == -1 || savedHeight == -1) { 849 if (!fallBackToDefaults) { 850 return; 851 } else { 852 savedWidth = defaultWallpaperSize.x; 853 savedHeight = defaultWallpaperSize.y; 854 } 855 } 856 857 if (savedWidth != wallpaperManager.getDesiredMinimumWidth() || 858 savedHeight != wallpaperManager.getDesiredMinimumHeight()) { 859 wallpaperManager.suggestDesiredDimensions(savedWidth, savedHeight); 860 } 861 } 862 863 protected static RectF getMaxCropRect( 864 int inWidth, int inHeight, int outWidth, int outHeight, boolean leftAligned) { 865 RectF cropRect = new RectF(); 866 // Get a crop rect that will fit this 867 if (inWidth / (float) inHeight > outWidth / (float) outHeight) { 868 cropRect.top = 0; 869 cropRect.bottom = inHeight; 870 cropRect.left = (inWidth - (outWidth / (float) outHeight) * inHeight) / 2; 871 cropRect.right = inWidth - cropRect.left; 872 if (leftAligned) { 873 cropRect.right -= cropRect.left; 874 cropRect.left = 0; 875 } 876 } else { 877 cropRect.left = 0; 878 cropRect.right = inWidth; 879 cropRect.top = (inHeight - (outHeight / (float) outWidth) * inWidth) / 2; 880 cropRect.bottom = inHeight - cropRect.top; 881 } 882 return cropRect; 883 } 884 885 protected static CompressFormat convertExtensionToCompressFormat(String extension) { 886 return extension.equals("png") ? CompressFormat.PNG : CompressFormat.JPEG; 887 } 888 889 protected static String getFileExtension(String requestFormat) { 890 String outputFormat = (requestFormat == null) 891 ? "jpg" 892 : requestFormat; 893 outputFormat = outputFormat.toLowerCase(); 894 return (outputFormat.equals("png") || outputFormat.equals("gif")) 895 ? "png" // We don't support gif compression. 896 : "jpg"; 897 } 898 } 899