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 { 50 public void onBitmapCropped(byte[] imageBytes, Rect cropHint); 51 } 52 53 public interface OnEndCropHandler { 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 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 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 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 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 114 public void setOnBitmapCropped(BitmapCropTask.OnBitmapCroppedHandler handler) { 115 mOnBitmapCroppedHandler = handler; 116 } 117 118 public void setNoCrop(boolean value) { 119 mNoCrop = value; 120 } 121 122 public void setOnEndRunnable(OnEndCropHandler onEndCropHandler) { 123 mOnEndCropHandler = onEndCropHandler; 124 } 125 126 // Helper to setup input stream 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 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 164 public void setCropBounds(RectF cropBounds) { 165 mCropBounds = cropBounds; 166 } 167 168 public Bitmap getCroppedBitmap() { 169 return mCroppedBitmap; 170 } 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 406 protected Boolean doInBackground(Integer... params) { 407 return cropBitmap(params.length == 0 ? WallpaperManager.FLAG_SYSTEM : params[0]); 408 } 409 410 @Override 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 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 }