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 onCreate(Bundle savedInstanceState)81 protected void onCreate(Bundle savedInstanceState) { 82 super.onCreate(savedInstanceState); 83 init(); 84 if (!enableRotation()) { 85 setRequestedOrientation(Configuration.ORIENTATION_PORTRAIT); 86 } 87 } 88 init()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 onDestroy()137 protected void onDestroy() { 138 if (mCropView != null) { 139 mCropView.destroy(); 140 } 141 super.onDestroy(); 142 } 143 setCropViewTileSource( final BitmapRegionTileSource.BitmapSource bitmapSource, final boolean touchEnabled, final boolean moveToLeft, final Runnable postExecute)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 enableRotation()200 public boolean enableRotation() { 201 return getResources().getBoolean(R.bool.allow_rotation); 202 } 203 getSharedPreferencesKey()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 wallpaperTravelToScreenWidthRatio(int width, int height)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 getDefaultWallpaperSize(Resources res, WindowManager windowManager)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 getRotationFromExif(String path)266 public static int getRotationFromExif(String path) { 267 return getRotationFromExifHelper(path, null, 0, null, null); 268 } 269 getRotationFromExif(Context context, Uri uri)270 public static int getRotationFromExif(Context context, Uri uri) { 271 return getRotationFromExifHelper(null, null, 0, context, uri); 272 } 273 getRotationFromExif(Resources res, int resId)274 public static int getRotationFromExif(Resources res, int resId) { 275 return getRotationFromExifHelper(null, res, resId, null, null); 276 } 277 getRotationFromExifHelper( String path, Resources res, int resId, Context context, Uri uri)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 setWallpaper(String filePath, final boolean finishActivityWhenDone)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 cropImageAndSetWallpaper( Resources res, int resId, final boolean finishActivityWhenDone)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 isScreenLarge(Resources res)352 private static boolean isScreenLarge(Resources res) { 353 Configuration config = res.getConfiguration(); 354 return config.smallestScreenWidthDp >= 720; 355 } 356 cropImageAndSetWallpaper(Uri uri, OnBitmapCroppedHandler onBitmapCroppedHandler, final boolean finishActivityWhenDone)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 doInBackground(Void... params)802 protected Boolean doInBackground(Void... params) { 803 return cropBitmap(); 804 } 805 806 @Override onPostExecute(Boolean result)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