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