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