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 /* Copied from Launcher3 */ 17 package com.android.wallpapercropper; 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 private 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.setVisibility(View.INVISIBLE); 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.setVisibility(View.VISIBLE); 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(String filePath, final boolean finishActivityWhenDone) { 312 int rotation = getRotationFromExif(filePath); 313 BitmapCropTask cropTask = new BitmapCropTask( 314 this, filePath, null, rotation, 0, 0, true, false, null); 315 final Point bounds = cropTask.getImageBounds(); 316 Runnable onEndCrop = new Runnable() { 317 public void run() { 318 if (finishActivityWhenDone) { 319 setResult(Activity.RESULT_OK); 320 finish(); 321 } 322 } 323 }; 324 cropTask.setOnEndRunnable(onEndCrop); 325 cropTask.setNoCrop(true); 326 cropTask.execute(); 327 } 328 329 protected void cropImageAndSetWallpaper( 330 Resources res, int resId, final boolean finishActivityWhenDone) { 331 // crop this image and scale it down to the default wallpaper size for 332 // this device 333 int rotation = getRotationFromExif(res, resId); 334 Point inSize = mCropView.getSourceDimensions(); 335 Point outSize = getDefaultWallpaperSize(getResources(), 336 getWindowManager()); 337 RectF crop = getMaxCropRect( 338 inSize.x, inSize.y, outSize.x, outSize.y, false); 339 Runnable onEndCrop = new Runnable() { 340 public void run() { 341 if (finishActivityWhenDone) { 342 setResult(Activity.RESULT_OK); 343 finish(); 344 } 345 } 346 }; 347 BitmapCropTask cropTask = new BitmapCropTask(this, res, resId, 348 crop, rotation, outSize.x, outSize.y, true, false, onEndCrop); 349 cropTask.execute(); 350 } 351 352 private static boolean isScreenLarge(Resources res) { 353 Configuration config = res.getConfiguration(); 354 return config.smallestScreenWidthDp >= 720; 355 } 356 357 protected void cropImageAndSetWallpaper(Uri uri, 358 OnBitmapCroppedHandler onBitmapCroppedHandler, final boolean finishActivityWhenDone) { 359 boolean centerCrop = getResources().getBoolean(R.bool.center_crop); 360 // Get the crop 361 boolean ltr = mCropView.getLayoutDirection() == View.LAYOUT_DIRECTION_LTR; 362 363 Display d = getWindowManager().getDefaultDisplay(); 364 365 Point displaySize = new Point(); 366 d.getSize(displaySize); 367 boolean isPortrait = displaySize.x < displaySize.y; 368 369 Point defaultWallpaperSize = getDefaultWallpaperSize(getResources(), 370 getWindowManager()); 371 // Get the crop 372 RectF cropRect = mCropView.getCrop(); 373 374 Point inSize = mCropView.getSourceDimensions(); 375 376 int cropRotation = mCropView.getImageRotation(); 377 float cropScale = mCropView.getWidth() / (float) cropRect.width(); 378 379 Matrix rotateMatrix = new Matrix(); 380 rotateMatrix.setRotate(cropRotation); 381 float[] rotatedInSize = new float[] { inSize.x, inSize.y }; 382 rotateMatrix.mapPoints(rotatedInSize); 383 rotatedInSize[0] = Math.abs(rotatedInSize[0]); 384 rotatedInSize[1] = Math.abs(rotatedInSize[1]); 385 386 // Due to rounding errors in the cropview renderer the edges can be slightly offset 387 // therefore we ensure that the boundaries are sanely defined 388 cropRect.left = Math.max(0, cropRect.left); 389 cropRect.right = Math.min(rotatedInSize[0], cropRect.right); 390 cropRect.top = Math.max(0, cropRect.top); 391 cropRect.bottom = Math.min(rotatedInSize[1], cropRect.bottom); 392 393 // ADJUST CROP WIDTH 394 // Extend the crop all the way to the right, for parallax 395 // (or all the way to the left, in RTL) 396 float extraSpace; 397 if (centerCrop) { 398 extraSpace = 2f * Math.min(rotatedInSize[0] - cropRect.right, cropRect.left); 399 } else { 400 extraSpace = ltr ? rotatedInSize[0] - cropRect.right : cropRect.left; 401 } 402 // Cap the amount of extra width 403 float maxExtraSpace = defaultWallpaperSize.x / cropScale - cropRect.width(); 404 extraSpace = Math.min(extraSpace, maxExtraSpace); 405 406 if (centerCrop) { 407 cropRect.left -= extraSpace / 2f; 408 cropRect.right += extraSpace / 2f; 409 } else { 410 if (ltr) { 411 cropRect.right += extraSpace; 412 } else { 413 cropRect.left -= extraSpace; 414 } 415 } 416 417 // ADJUST CROP HEIGHT 418 if (isPortrait) { 419 cropRect.bottom = cropRect.top + defaultWallpaperSize.y / cropScale; 420 } else { // LANDSCAPE 421 float extraPortraitHeight = 422 defaultWallpaperSize.y / cropScale - cropRect.height(); 423 float expandHeight = 424 Math.min(Math.min(rotatedInSize[1] - cropRect.bottom, cropRect.top), 425 extraPortraitHeight / 2); 426 cropRect.top -= expandHeight; 427 cropRect.bottom += expandHeight; 428 } 429 final int outWidth = (int) Math.round(cropRect.width() * cropScale); 430 final int outHeight = (int) Math.round(cropRect.height() * cropScale); 431 432 Runnable onEndCrop = new Runnable() { 433 public void run() { 434 if (finishActivityWhenDone) { 435 setResult(Activity.RESULT_OK); 436 finish(); 437 } 438 } 439 }; 440 BitmapCropTask cropTask = new BitmapCropTask(this, uri, 441 cropRect, cropRotation, outWidth, outHeight, true, false, onEndCrop); 442 if (onBitmapCroppedHandler != null) { 443 cropTask.setOnBitmapCropped(onBitmapCroppedHandler); 444 } 445 cropTask.execute(); 446 } 447 448 public interface OnBitmapCroppedHandler { 449 public void onBitmapCropped(byte[] imageBytes); 450 } 451 452 protected static class BitmapCropTask extends AsyncTask<Void, Void, Boolean> { 453 Uri mInUri = null; 454 Context mContext; 455 String mInFilePath; 456 byte[] mInImageBytes; 457 int mInResId = 0; 458 RectF mCropBounds = null; 459 int mOutWidth, mOutHeight; 460 int mRotation; 461 String mOutputFormat = "jpg"; // for now 462 boolean mSetWallpaper; 463 boolean mSaveCroppedBitmap; 464 Bitmap mCroppedBitmap; 465 Runnable mOnEndRunnable; 466 Resources mResources; 467 OnBitmapCroppedHandler mOnBitmapCroppedHandler; 468 boolean mNoCrop; 469 470 public BitmapCropTask(Context c, String filePath, 471 RectF cropBounds, int rotation, int outWidth, int outHeight, 472 boolean setWallpaper, boolean saveCroppedBitmap, Runnable onEndRunnable) { 473 mContext = c; 474 mInFilePath = filePath; 475 init(cropBounds, rotation, 476 outWidth, outHeight, setWallpaper, saveCroppedBitmap, onEndRunnable); 477 } 478 479 public BitmapCropTask(byte[] imageBytes, 480 RectF cropBounds, int rotation, int outWidth, int outHeight, 481 boolean setWallpaper, boolean saveCroppedBitmap, Runnable onEndRunnable) { 482 mInImageBytes = imageBytes; 483 init(cropBounds, rotation, 484 outWidth, outHeight, setWallpaper, saveCroppedBitmap, onEndRunnable); 485 } 486 487 public BitmapCropTask(Context c, Uri inUri, 488 RectF cropBounds, int rotation, int outWidth, int outHeight, 489 boolean setWallpaper, boolean saveCroppedBitmap, Runnable onEndRunnable) { 490 mContext = c; 491 mInUri = inUri; 492 init(cropBounds, rotation, 493 outWidth, outHeight, setWallpaper, saveCroppedBitmap, onEndRunnable); 494 } 495 496 public BitmapCropTask(Context c, Resources res, int inResId, 497 RectF cropBounds, int rotation, int outWidth, int outHeight, 498 boolean setWallpaper, boolean saveCroppedBitmap, Runnable onEndRunnable) { 499 mContext = c; 500 mInResId = inResId; 501 mResources = res; 502 init(cropBounds, rotation, 503 outWidth, outHeight, setWallpaper, saveCroppedBitmap, onEndRunnable); 504 } 505 506 private void init(RectF cropBounds, int rotation, int outWidth, int outHeight, 507 boolean setWallpaper, boolean saveCroppedBitmap, Runnable onEndRunnable) { 508 mCropBounds = cropBounds; 509 mRotation = rotation; 510 mOutWidth = outWidth; 511 mOutHeight = outHeight; 512 mSetWallpaper = setWallpaper; 513 mSaveCroppedBitmap = saveCroppedBitmap; 514 mOnEndRunnable = onEndRunnable; 515 } 516 517 public void setOnBitmapCropped(OnBitmapCroppedHandler handler) { 518 mOnBitmapCroppedHandler = handler; 519 } 520 521 public void setNoCrop(boolean value) { 522 mNoCrop = value; 523 } 524 525 public void setOnEndRunnable(Runnable onEndRunnable) { 526 mOnEndRunnable = onEndRunnable; 527 } 528 529 // Helper to setup input stream 530 private InputStream regenerateInputStream() { 531 if (mInUri == null && mInResId == 0 && mInFilePath == null && mInImageBytes == null) { 532 Log.w(LOGTAG, "cannot read original file, no input URI, resource ID, or " + 533 "image byte array given"); 534 } else { 535 try { 536 if (mInUri != null) { 537 return new BufferedInputStream( 538 mContext.getContentResolver().openInputStream(mInUri)); 539 } else if (mInFilePath != null) { 540 return mContext.openFileInput(mInFilePath); 541 } else if (mInImageBytes != null) { 542 return new BufferedInputStream(new ByteArrayInputStream(mInImageBytes)); 543 } else { 544 return new BufferedInputStream(mResources.openRawResource(mInResId)); 545 } 546 } catch (FileNotFoundException e) { 547 Log.w(LOGTAG, "cannot read file: " + mInUri.toString(), e); 548 } 549 } 550 return null; 551 } 552 553 public Point getImageBounds() { 554 InputStream is = regenerateInputStream(); 555 if (is != null) { 556 BitmapFactory.Options options = new BitmapFactory.Options(); 557 options.inJustDecodeBounds = true; 558 BitmapFactory.decodeStream(is, null, options); 559 Utils.closeSilently(is); 560 if (options.outWidth != 0 && options.outHeight != 0) { 561 return new Point(options.outWidth, options.outHeight); 562 } 563 } 564 return null; 565 } 566 567 public void setCropBounds(RectF cropBounds) { 568 mCropBounds = cropBounds; 569 } 570 571 public Bitmap getCroppedBitmap() { 572 return mCroppedBitmap; 573 } 574 public boolean cropBitmap() { 575 boolean failure = false; 576 577 578 WallpaperManager wallpaperManager = null; 579 if (mSetWallpaper) { 580 wallpaperManager = WallpaperManager.getInstance(mContext.getApplicationContext()); 581 } 582 583 584 if (mSetWallpaper && mNoCrop) { 585 try { 586 InputStream is = regenerateInputStream(); 587 if (is != null) { 588 wallpaperManager.setStream(is); 589 Utils.closeSilently(is); 590 } 591 } catch (IOException e) { 592 Log.w(LOGTAG, "cannot write stream to wallpaper", e); 593 failure = true; 594 } 595 return !failure; 596 } else { 597 // Find crop bounds (scaled to original image size) 598 Rect roundedTrueCrop = new Rect(); 599 Matrix rotateMatrix = new Matrix(); 600 Matrix inverseRotateMatrix = new Matrix(); 601 602 Point bounds = getImageBounds(); 603 if (mRotation > 0) { 604 rotateMatrix.setRotate(mRotation); 605 inverseRotateMatrix.setRotate(-mRotation); 606 607 mCropBounds.roundOut(roundedTrueCrop); 608 mCropBounds = new RectF(roundedTrueCrop); 609 610 if (bounds == null) { 611 Log.w(LOGTAG, "cannot get bounds for image"); 612 failure = true; 613 return false; 614 } 615 616 float[] rotatedBounds = new float[] { bounds.x, bounds.y }; 617 rotateMatrix.mapPoints(rotatedBounds); 618 rotatedBounds[0] = Math.abs(rotatedBounds[0]); 619 rotatedBounds[1] = Math.abs(rotatedBounds[1]); 620 621 mCropBounds.offset(-rotatedBounds[0]/2, -rotatedBounds[1]/2); 622 inverseRotateMatrix.mapRect(mCropBounds); 623 mCropBounds.offset(bounds.x/2, bounds.y/2); 624 625 } 626 627 mCropBounds.roundOut(roundedTrueCrop); 628 629 if (roundedTrueCrop.width() <= 0 || roundedTrueCrop.height() <= 0) { 630 Log.w(LOGTAG, "crop has bad values for full size image"); 631 failure = true; 632 return false; 633 } 634 635 // See how much we're reducing the size of the image 636 int scaleDownSampleSize = Math.max(1, Math.min(roundedTrueCrop.width() / mOutWidth, 637 roundedTrueCrop.height() / mOutHeight)); 638 // Attempt to open a region decoder 639 BitmapRegionDecoder decoder = null; 640 InputStream is = null; 641 try { 642 is = regenerateInputStream(); 643 if (is == null) { 644 Log.w(LOGTAG, "cannot get input stream for uri=" + mInUri.toString()); 645 failure = true; 646 return false; 647 } 648 decoder = BitmapRegionDecoder.newInstance(is, false); 649 Utils.closeSilently(is); 650 } catch (IOException e) { 651 Log.w(LOGTAG, "cannot open region decoder for file: " + mInUri.toString(), e); 652 } finally { 653 Utils.closeSilently(is); 654 is = null; 655 } 656 657 Bitmap crop = null; 658 if (decoder != null) { 659 // Do region decoding to get crop bitmap 660 BitmapFactory.Options options = new BitmapFactory.Options(); 661 if (scaleDownSampleSize > 1) { 662 options.inSampleSize = scaleDownSampleSize; 663 } 664 crop = decoder.decodeRegion(roundedTrueCrop, options); 665 decoder.recycle(); 666 } 667 668 if (crop == null) { 669 // BitmapRegionDecoder has failed, try to crop in-memory 670 is = regenerateInputStream(); 671 Bitmap fullSize = null; 672 if (is != null) { 673 BitmapFactory.Options options = new BitmapFactory.Options(); 674 if (scaleDownSampleSize > 1) { 675 options.inSampleSize = scaleDownSampleSize; 676 } 677 fullSize = BitmapFactory.decodeStream(is, null, options); 678 Utils.closeSilently(is); 679 } 680 if (fullSize != null) { 681 // Find out the true sample size that was used by the decoder 682 scaleDownSampleSize = bounds.x / fullSize.getWidth(); 683 mCropBounds.left /= scaleDownSampleSize; 684 mCropBounds.top /= scaleDownSampleSize; 685 mCropBounds.bottom /= scaleDownSampleSize; 686 mCropBounds.right /= scaleDownSampleSize; 687 mCropBounds.roundOut(roundedTrueCrop); 688 689 // Adjust values to account for issues related to rounding 690 if (roundedTrueCrop.width() > fullSize.getWidth()) { 691 // Adjust the width 692 roundedTrueCrop.right = roundedTrueCrop.left + fullSize.getWidth(); 693 } 694 if (roundedTrueCrop.right > fullSize.getWidth()) { 695 // Adjust the left value 696 int adjustment = roundedTrueCrop.left - 697 Math.max(0, roundedTrueCrop.right - roundedTrueCrop.width()); 698 roundedTrueCrop.left -= adjustment; 699 roundedTrueCrop.right -= adjustment; 700 } 701 if (roundedTrueCrop.height() > fullSize.getHeight()) { 702 // Adjust the height 703 roundedTrueCrop.bottom = roundedTrueCrop.top + fullSize.getHeight(); 704 } 705 if (roundedTrueCrop.bottom > fullSize.getHeight()) { 706 // Adjust the top value 707 int adjustment = roundedTrueCrop.top - 708 Math.max(0, roundedTrueCrop.bottom - roundedTrueCrop.height()); 709 roundedTrueCrop.top -= adjustment; 710 roundedTrueCrop.bottom -= adjustment; 711 } 712 713 crop = Bitmap.createBitmap(fullSize, roundedTrueCrop.left, 714 roundedTrueCrop.top, roundedTrueCrop.width(), 715 roundedTrueCrop.height()); 716 } 717 } 718 719 if (crop == null) { 720 Log.w(LOGTAG, "cannot decode file: " + mInUri.toString()); 721 failure = true; 722 return false; 723 } 724 if (mOutWidth > 0 && mOutHeight > 0 || mRotation > 0) { 725 float[] dimsAfter = new float[] { crop.getWidth(), crop.getHeight() }; 726 rotateMatrix.mapPoints(dimsAfter); 727 dimsAfter[0] = Math.abs(dimsAfter[0]); 728 dimsAfter[1] = Math.abs(dimsAfter[1]); 729 730 if (!(mOutWidth > 0 && mOutHeight > 0)) { 731 mOutWidth = Math.round(dimsAfter[0]); 732 mOutHeight = Math.round(dimsAfter[1]); 733 } 734 735 RectF cropRect = new RectF(0, 0, dimsAfter[0], dimsAfter[1]); 736 RectF returnRect = new RectF(0, 0, mOutWidth, mOutHeight); 737 738 Matrix m = new Matrix(); 739 if (mRotation == 0) { 740 m.setRectToRect(cropRect, returnRect, Matrix.ScaleToFit.FILL); 741 } else { 742 Matrix m1 = new Matrix(); 743 m1.setTranslate(-crop.getWidth() / 2f, -crop.getHeight() / 2f); 744 Matrix m2 = new Matrix(); 745 m2.setRotate(mRotation); 746 Matrix m3 = new Matrix(); 747 m3.setTranslate(dimsAfter[0] / 2f, dimsAfter[1] / 2f); 748 Matrix m4 = new Matrix(); 749 m4.setRectToRect(cropRect, returnRect, Matrix.ScaleToFit.FILL); 750 751 Matrix c1 = new Matrix(); 752 c1.setConcat(m2, m1); 753 Matrix c2 = new Matrix(); 754 c2.setConcat(m4, m3); 755 m.setConcat(c2, c1); 756 } 757 758 Bitmap tmp = Bitmap.createBitmap((int) returnRect.width(), 759 (int) returnRect.height(), Bitmap.Config.ARGB_8888); 760 if (tmp != null) { 761 Canvas c = new Canvas(tmp); 762 Paint p = new Paint(); 763 p.setFilterBitmap(true); 764 c.drawBitmap(crop, m, p); 765 crop = tmp; 766 } 767 } 768 769 if (mSaveCroppedBitmap) { 770 mCroppedBitmap = crop; 771 } 772 773 // Get output compression format 774 CompressFormat cf = 775 convertExtensionToCompressFormat(getFileExtension(mOutputFormat)); 776 777 // Compress to byte array 778 ByteArrayOutputStream tmpOut = new ByteArrayOutputStream(2048); 779 if (crop.compress(cf, DEFAULT_COMPRESS_QUALITY, tmpOut)) { 780 // If we need to set to the wallpaper, set it 781 if (mSetWallpaper && wallpaperManager != null) { 782 try { 783 byte[] outByteArray = tmpOut.toByteArray(); 784 wallpaperManager.setStream(new ByteArrayInputStream(outByteArray)); 785 if (mOnBitmapCroppedHandler != null) { 786 mOnBitmapCroppedHandler.onBitmapCropped(outByteArray); 787 } 788 } catch (IOException e) { 789 Log.w(LOGTAG, "cannot write stream to wallpaper", e); 790 failure = true; 791 } 792 } 793 } else { 794 Log.w(LOGTAG, "cannot compress bitmap"); 795 failure = true; 796 } 797 } 798 return !failure; // True if any of the operations failed 799 } 800 801 @Override 802 protected Boolean doInBackground(Void... params) { 803 return cropBitmap(); 804 } 805 806 @Override 807 protected void onPostExecute(Boolean result) { 808 if (mOnEndRunnable != null) { 809 mOnEndRunnable.run(); 810 } 811 } 812 } 813 814 protected static RectF getMaxCropRect( 815 int inWidth, int inHeight, int outWidth, int outHeight, boolean leftAligned) { 816 RectF cropRect = new RectF(); 817 // Get a crop rect that will fit this 818 if (inWidth / (float) inHeight > outWidth / (float) outHeight) { 819 cropRect.top = 0; 820 cropRect.bottom = inHeight; 821 cropRect.left = (inWidth - (outWidth / (float) outHeight) * inHeight) / 2; 822 cropRect.right = inWidth - cropRect.left; 823 if (leftAligned) { 824 cropRect.right -= cropRect.left; 825 cropRect.left = 0; 826 } 827 } else { 828 cropRect.left = 0; 829 cropRect.right = inWidth; 830 cropRect.top = (inHeight - (outHeight / (float) outWidth) * inWidth) / 2; 831 cropRect.bottom = inHeight - cropRect.top; 832 } 833 return cropRect; 834 } 835 836 protected static CompressFormat convertExtensionToCompressFormat(String extension) { 837 return extension.equals("png") ? CompressFormat.PNG : CompressFormat.JPEG; 838 } 839 840 protected static String getFileExtension(String requestFormat) { 841 String outputFormat = (requestFormat == null) 842 ? "jpg" 843 : requestFormat; 844 outputFormat = outputFormat.toLowerCase(); 845 return (outputFormat.equals("png") || outputFormat.equals("gif")) 846 ? "png" // We don't support gif compression. 847 : "jpg"; 848 } 849 } 850