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 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 = (CropView) 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, WindowManager windowManager)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 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(), 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 isScreenLarge(Resources res)351 private static boolean isScreenLarge(Resources res) { 352 Configuration config = res.getConfiguration(); 353 return config.smallestScreenWidthDp >= 720; 354 } 355 cropImageAndSetWallpaper(Uri uri, OnBitmapCroppedHandler onBitmapCroppedHandler, final boolean finishActivityWhenDone)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 doInBackground(Void... params)801 protected Boolean doInBackground(Void... params) { 802 return cropBitmap(); 803 } 804 805 @Override onPostExecute(Boolean result)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