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