1 /** 2 * Copyright (C) 2015 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 package com.android.gallery3d.common; 17 18 import android.app.WallpaperManager; 19 import android.content.Context; 20 import android.content.res.Resources; 21 import android.graphics.Bitmap; 22 import android.graphics.Bitmap.CompressFormat; 23 import android.graphics.BitmapFactory; 24 import android.graphics.BitmapRegionDecoder; 25 import android.graphics.Canvas; 26 import android.graphics.Matrix; 27 import android.graphics.Paint; 28 import android.graphics.Point; 29 import android.graphics.Rect; 30 import android.graphics.RectF; 31 import android.net.Uri; 32 import android.os.AsyncTask; 33 import android.util.Log; 34 import android.widget.Toast; 35 36 import com.android.launcher3.NycWallpaperUtils; 37 import com.android.launcher3.R; 38 import com.android.launcher3.Utilities; 39 40 import java.io.BufferedInputStream; 41 import java.io.ByteArrayInputStream; 42 import java.io.ByteArrayOutputStream; 43 import java.io.FileNotFoundException; 44 import java.io.IOException; 45 import java.io.InputStream; 46 47 public class BitmapCropTask extends AsyncTask<Integer, Void, Boolean> { 48 49 public interface OnBitmapCroppedHandler { onBitmapCropped(byte[] imageBytes, Rect cropHint)50 public void onBitmapCropped(byte[] imageBytes, Rect cropHint); 51 } 52 53 public interface OnEndCropHandler { run(boolean cropSucceeded)54 public void run(boolean cropSucceeded); 55 } 56 57 private static final int DEFAULT_COMPRESS_QUALITY = 90; 58 private static final String LOGTAG = "BitmapCropTask"; 59 60 Uri mInUri = null; 61 Context mContext; 62 String mInFilePath; 63 byte[] mInImageBytes; 64 int mInResId = 0; 65 RectF mCropBounds = null; 66 int mOutWidth, mOutHeight; 67 int mRotation; 68 boolean mSetWallpaper; 69 boolean mSaveCroppedBitmap; 70 Bitmap mCroppedBitmap; 71 BitmapCropTask.OnEndCropHandler mOnEndCropHandler; 72 Resources mResources; 73 BitmapCropTask.OnBitmapCroppedHandler mOnBitmapCroppedHandler; 74 boolean mNoCrop; 75 BitmapCropTask(byte[] imageBytes, RectF cropBounds, int rotation, int outWidth, int outHeight, boolean setWallpaper, boolean saveCroppedBitmap, OnEndCropHandler onEndCropHandler)76 public BitmapCropTask(byte[] imageBytes, 77 RectF cropBounds, int rotation, int outWidth, int outHeight, 78 boolean setWallpaper, boolean saveCroppedBitmap, OnEndCropHandler onEndCropHandler) { 79 mInImageBytes = imageBytes; 80 init(cropBounds, rotation, 81 outWidth, outHeight, setWallpaper, saveCroppedBitmap, onEndCropHandler); 82 } 83 BitmapCropTask(Context c, Uri inUri, RectF cropBounds, int rotation, int outWidth, int outHeight, boolean setWallpaper, boolean saveCroppedBitmap, OnEndCropHandler onEndCropHandler)84 public BitmapCropTask(Context c, Uri inUri, 85 RectF cropBounds, int rotation, int outWidth, int outHeight, 86 boolean setWallpaper, boolean saveCroppedBitmap, OnEndCropHandler onEndCropHandler) { 87 mContext = c; 88 mInUri = inUri; 89 init(cropBounds, rotation, 90 outWidth, outHeight, setWallpaper, saveCroppedBitmap, onEndCropHandler); 91 } 92 BitmapCropTask(Context c, Resources res, int inResId, RectF cropBounds, int rotation, int outWidth, int outHeight, boolean setWallpaper, boolean saveCroppedBitmap, OnEndCropHandler onEndCropHandler)93 public BitmapCropTask(Context c, Resources res, int inResId, 94 RectF cropBounds, int rotation, int outWidth, int outHeight, 95 boolean setWallpaper, boolean saveCroppedBitmap, OnEndCropHandler onEndCropHandler) { 96 mContext = c; 97 mInResId = inResId; 98 mResources = res; 99 init(cropBounds, rotation, 100 outWidth, outHeight, setWallpaper, saveCroppedBitmap, onEndCropHandler); 101 } 102 init(RectF cropBounds, int rotation, int outWidth, int outHeight, boolean setWallpaper, boolean saveCroppedBitmap, OnEndCropHandler onEndCropHandler)103 private void init(RectF cropBounds, int rotation, int outWidth, int outHeight, 104 boolean setWallpaper, boolean saveCroppedBitmap, OnEndCropHandler onEndCropHandler) { 105 mCropBounds = cropBounds; 106 mRotation = rotation; 107 mOutWidth = outWidth; 108 mOutHeight = outHeight; 109 mSetWallpaper = setWallpaper; 110 mSaveCroppedBitmap = saveCroppedBitmap; 111 mOnEndCropHandler = onEndCropHandler; 112 } 113 setOnBitmapCropped(BitmapCropTask.OnBitmapCroppedHandler handler)114 public void setOnBitmapCropped(BitmapCropTask.OnBitmapCroppedHandler handler) { 115 mOnBitmapCroppedHandler = handler; 116 } 117 setNoCrop(boolean value)118 public void setNoCrop(boolean value) { 119 mNoCrop = value; 120 } 121 setOnEndRunnable(OnEndCropHandler onEndCropHandler)122 public void setOnEndRunnable(OnEndCropHandler onEndCropHandler) { 123 mOnEndCropHandler = onEndCropHandler; 124 } 125 126 // Helper to setup input stream regenerateInputStream()127 private InputStream regenerateInputStream() { 128 if (mInUri == null && mInResId == 0 && mInFilePath == null && mInImageBytes == null) { 129 Log.w(LOGTAG, "cannot read original file, no input URI, resource ID, or " + 130 "image byte array given"); 131 } else { 132 try { 133 if (mInUri != null) { 134 return new BufferedInputStream( 135 mContext.getContentResolver().openInputStream(mInUri)); 136 } else if (mInFilePath != null) { 137 return mContext.openFileInput(mInFilePath); 138 } else if (mInImageBytes != null) { 139 return new BufferedInputStream(new ByteArrayInputStream(mInImageBytes)); 140 } else { 141 return new BufferedInputStream(mResources.openRawResource(mInResId)); 142 } 143 } catch (FileNotFoundException e) { 144 Log.w(LOGTAG, "cannot read file: " + mInUri.toString(), e); 145 } 146 } 147 return null; 148 } 149 getImageBounds()150 public Point getImageBounds() { 151 InputStream is = regenerateInputStream(); 152 if (is != null) { 153 BitmapFactory.Options options = new BitmapFactory.Options(); 154 options.inJustDecodeBounds = true; 155 BitmapFactory.decodeStream(is, null, options); 156 Utils.closeSilently(is); 157 if (options.outWidth != 0 && options.outHeight != 0) { 158 return new Point(options.outWidth, options.outHeight); 159 } 160 } 161 return null; 162 } 163 setCropBounds(RectF cropBounds)164 public void setCropBounds(RectF cropBounds) { 165 mCropBounds = cropBounds; 166 } 167 getCroppedBitmap()168 public Bitmap getCroppedBitmap() { 169 return mCroppedBitmap; 170 } cropBitmap(int whichWallpaper)171 public boolean cropBitmap(int whichWallpaper) { 172 boolean failure = false; 173 174 if (mSetWallpaper && mNoCrop) { 175 try { 176 InputStream is = regenerateInputStream(); 177 setWallpaper(is, null, whichWallpaper); 178 Utils.closeSilently(is); 179 } catch (IOException e) { 180 Log.w(LOGTAG, "cannot write stream to wallpaper", e); 181 failure = true; 182 } 183 return !failure; 184 } else if (mSetWallpaper && Utilities.ATLEAST_N 185 && mRotation == 0 && mOutWidth > 0 && mOutHeight > 0) { 186 Rect hint = new Rect(); 187 mCropBounds.roundOut(hint); 188 189 InputStream is = null; 190 try { 191 is = regenerateInputStream(); 192 if (is == null) { 193 Log.w(LOGTAG, "cannot get input stream for uri=" + mInUri.toString()); 194 failure = true; 195 return false; 196 } 197 WallpaperManager.getInstance(mContext).suggestDesiredDimensions(mOutWidth, mOutHeight); 198 setWallpaper(is, hint, whichWallpaper); 199 200 if (mOnBitmapCroppedHandler != null) { 201 mOnBitmapCroppedHandler.onBitmapCropped(null, hint); 202 } 203 204 failure = false; 205 } catch (IOException e) { 206 Log.w(LOGTAG, "cannot open region decoder for file: " + mInUri.toString(), e); 207 } finally { 208 Utils.closeSilently(is); 209 } 210 } else { 211 // Find crop bounds (scaled to original image size) 212 Rect roundedTrueCrop = new Rect(); 213 Matrix rotateMatrix = new Matrix(); 214 Matrix inverseRotateMatrix = new Matrix(); 215 216 Point bounds = getImageBounds(); 217 if (mRotation > 0) { 218 rotateMatrix.setRotate(mRotation); 219 inverseRotateMatrix.setRotate(-mRotation); 220 221 mCropBounds.roundOut(roundedTrueCrop); 222 mCropBounds = new RectF(roundedTrueCrop); 223 224 if (bounds == null) { 225 Log.w(LOGTAG, "cannot get bounds for image"); 226 failure = true; 227 return false; 228 } 229 230 float[] rotatedBounds = new float[] { bounds.x, bounds.y }; 231 rotateMatrix.mapPoints(rotatedBounds); 232 rotatedBounds[0] = Math.abs(rotatedBounds[0]); 233 rotatedBounds[1] = Math.abs(rotatedBounds[1]); 234 235 mCropBounds.offset(-rotatedBounds[0]/2, -rotatedBounds[1]/2); 236 inverseRotateMatrix.mapRect(mCropBounds); 237 mCropBounds.offset(bounds.x/2, bounds.y/2); 238 } 239 240 mCropBounds.roundOut(roundedTrueCrop); 241 242 if (roundedTrueCrop.width() <= 0 || roundedTrueCrop.height() <= 0) { 243 Log.w(LOGTAG, "crop has bad values for full size image"); 244 failure = true; 245 return false; 246 } 247 248 // See how much we're reducing the size of the image 249 int scaleDownSampleSize = Math.max(1, Math.min(roundedTrueCrop.width() / mOutWidth, 250 roundedTrueCrop.height() / mOutHeight)); 251 // Attempt to open a region decoder 252 BitmapRegionDecoder decoder = null; 253 InputStream is = null; 254 try { 255 is = regenerateInputStream(); 256 if (is == null) { 257 Log.w(LOGTAG, "cannot get input stream for uri=" + mInUri.toString()); 258 failure = true; 259 return false; 260 } 261 decoder = BitmapRegionDecoder.newInstance(is, false); 262 Utils.closeSilently(is); 263 } catch (IOException e) { 264 Log.w(LOGTAG, "cannot open region decoder for file: " + mInUri.toString(), e); 265 } finally { 266 Utils.closeSilently(is); 267 is = null; 268 } 269 270 Bitmap crop = null; 271 if (decoder != null) { 272 // Do region decoding to get crop bitmap 273 BitmapFactory.Options options = new BitmapFactory.Options(); 274 if (scaleDownSampleSize > 1) { 275 options.inSampleSize = scaleDownSampleSize; 276 } 277 crop = decoder.decodeRegion(roundedTrueCrop, options); 278 decoder.recycle(); 279 } 280 281 if (crop == null) { 282 // BitmapRegionDecoder has failed, try to crop in-memory 283 is = regenerateInputStream(); 284 Bitmap fullSize = null; 285 if (is != null) { 286 BitmapFactory.Options options = new BitmapFactory.Options(); 287 if (scaleDownSampleSize > 1) { 288 options.inSampleSize = scaleDownSampleSize; 289 } 290 fullSize = BitmapFactory.decodeStream(is, null, options); 291 Utils.closeSilently(is); 292 } 293 if (fullSize != null) { 294 // Find out the true sample size that was used by the decoder 295 scaleDownSampleSize = bounds.x / fullSize.getWidth(); 296 mCropBounds.left /= scaleDownSampleSize; 297 mCropBounds.top /= scaleDownSampleSize; 298 mCropBounds.bottom /= scaleDownSampleSize; 299 mCropBounds.right /= scaleDownSampleSize; 300 mCropBounds.roundOut(roundedTrueCrop); 301 302 // Adjust values to account for issues related to rounding 303 if (roundedTrueCrop.width() > fullSize.getWidth()) { 304 // Adjust the width 305 roundedTrueCrop.right = roundedTrueCrop.left + fullSize.getWidth(); 306 } 307 if (roundedTrueCrop.right > fullSize.getWidth()) { 308 // Adjust the left and right values. 309 roundedTrueCrop.offset(-(roundedTrueCrop.right - fullSize.getWidth()), 0); 310 } 311 if (roundedTrueCrop.height() > fullSize.getHeight()) { 312 // Adjust the height 313 roundedTrueCrop.bottom = roundedTrueCrop.top + fullSize.getHeight(); 314 } 315 if (roundedTrueCrop.bottom > fullSize.getHeight()) { 316 // Adjust the top and bottom values. 317 roundedTrueCrop.offset(0, -(roundedTrueCrop.bottom - fullSize.getHeight())); 318 } 319 320 crop = Bitmap.createBitmap(fullSize, roundedTrueCrop.left, 321 roundedTrueCrop.top, roundedTrueCrop.width(), 322 roundedTrueCrop.height()); 323 } 324 } 325 326 if (crop == null) { 327 Log.w(LOGTAG, "cannot decode file: " + mInUri.toString()); 328 failure = true; 329 return false; 330 } 331 if (mOutWidth > 0 && mOutHeight > 0 || mRotation > 0) { 332 float[] dimsAfter = new float[] { crop.getWidth(), crop.getHeight() }; 333 rotateMatrix.mapPoints(dimsAfter); 334 dimsAfter[0] = Math.abs(dimsAfter[0]); 335 dimsAfter[1] = Math.abs(dimsAfter[1]); 336 337 if (!(mOutWidth > 0 && mOutHeight > 0)) { 338 mOutWidth = Math.round(dimsAfter[0]); 339 mOutHeight = Math.round(dimsAfter[1]); 340 } 341 342 RectF cropRect = new RectF(0, 0, dimsAfter[0], dimsAfter[1]); 343 RectF returnRect = new RectF(0, 0, mOutWidth, mOutHeight); 344 345 Matrix m = new Matrix(); 346 if (mRotation == 0) { 347 m.setRectToRect(cropRect, returnRect, Matrix.ScaleToFit.FILL); 348 } else { 349 Matrix m1 = new Matrix(); 350 m1.setTranslate(-crop.getWidth() / 2f, -crop.getHeight() / 2f); 351 Matrix m2 = new Matrix(); 352 m2.setRotate(mRotation); 353 Matrix m3 = new Matrix(); 354 m3.setTranslate(dimsAfter[0] / 2f, dimsAfter[1] / 2f); 355 Matrix m4 = new Matrix(); 356 m4.setRectToRect(cropRect, returnRect, Matrix.ScaleToFit.FILL); 357 358 Matrix c1 = new Matrix(); 359 c1.setConcat(m2, m1); 360 Matrix c2 = new Matrix(); 361 c2.setConcat(m4, m3); 362 m.setConcat(c2, c1); 363 } 364 365 Bitmap tmp = Bitmap.createBitmap((int) returnRect.width(), 366 (int) returnRect.height(), Bitmap.Config.ARGB_8888); 367 if (tmp != null) { 368 Canvas c = new Canvas(tmp); 369 Paint p = new Paint(); 370 p.setFilterBitmap(true); 371 c.drawBitmap(crop, m, p); 372 crop = tmp; 373 } 374 } 375 376 if (mSaveCroppedBitmap) { 377 mCroppedBitmap = crop; 378 } 379 380 // Compress to byte array 381 ByteArrayOutputStream tmpOut = new ByteArrayOutputStream(2048); 382 if (crop.compress(CompressFormat.JPEG, DEFAULT_COMPRESS_QUALITY, tmpOut)) { 383 // If we need to set to the wallpaper, set it 384 if (mSetWallpaper) { 385 try { 386 byte[] outByteArray = tmpOut.toByteArray(); 387 setWallpaper(new ByteArrayInputStream(outByteArray), null, whichWallpaper); 388 if (mOnBitmapCroppedHandler != null) { 389 mOnBitmapCroppedHandler.onBitmapCropped(outByteArray, 390 new Rect(0, 0, crop.getWidth(), crop.getHeight())); 391 } 392 } catch (IOException e) { 393 Log.w(LOGTAG, "cannot write stream to wallpaper", e); 394 failure = true; 395 } 396 } 397 } else { 398 Log.w(LOGTAG, "cannot compress bitmap"); 399 failure = true; 400 } 401 } 402 return !failure; // True if any of the operations failed 403 } 404 405 @Override doInBackground(Integer... params)406 protected Boolean doInBackground(Integer... params) { 407 return cropBitmap(params.length == 0 ? WallpaperManager.FLAG_SYSTEM : params[0]); 408 } 409 410 @Override onPostExecute(Boolean cropSucceeded)411 protected void onPostExecute(Boolean cropSucceeded) { 412 if (!cropSucceeded) { 413 Toast.makeText(mContext, R.string.wallpaper_set_fail, Toast.LENGTH_SHORT).show(); 414 } 415 if (mOnEndCropHandler != null) { 416 mOnEndCropHandler.run(cropSucceeded); 417 } 418 } 419 setWallpaper(InputStream in, Rect crop, int whichWallpaper)420 private void setWallpaper(InputStream in, Rect crop, int whichWallpaper) throws IOException { 421 if (!Utilities.ATLEAST_N) { 422 WallpaperManager.getInstance(mContext.getApplicationContext()).setStream(in); 423 } else { 424 NycWallpaperUtils.setStream(mContext, in, crop, true, whichWallpaper); 425 } 426 } 427 }