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 17 package com.android.bitmap; 18 19 import android.graphics.Bitmap; 20 import android.graphics.BitmapFactory; 21 import android.graphics.BitmapRegionDecoder; 22 import android.graphics.Rect; 23 import android.os.AsyncTask; 24 import android.os.ParcelFileDescriptor; 25 import android.os.ParcelFileDescriptor.AutoCloseInputStream; 26 import android.util.Log; 27 28 import com.android.bitmap.RequestKey.FileDescriptorFactory; 29 import com.android.bitmap.util.BitmapUtils; 30 import com.android.bitmap.util.Exif; 31 import com.android.bitmap.util.RectUtils; 32 import com.android.bitmap.util.Trace; 33 34 import java.io.IOException; 35 import java.io.InputStream; 36 37 /** 38 * Decodes an image from either a file descriptor or input stream on a worker thread. After the 39 * decode is complete, even if the task is cancelled, the result is placed in the given cache. 40 * A {@link DecodeCallback} client may be notified on decode begin and completion. 41 * <p> 42 * This class uses {@link BitmapRegionDecoder} when possible to minimize unnecessary decoding 43 * and allow bitmap reuse on Jellybean 4.1 and later. 44 * <p> 45 * GIFs are supported, but their decode does not reuse bitmaps at all. The resulting 46 * {@link ReusableBitmap} will be marked as not reusable 47 * ({@link ReusableBitmap#isEligibleForPooling()} will return false). 48 */ 49 public class DecodeTask extends AsyncTask<Void, Void, ReusableBitmap> { 50 51 private final RequestKey mKey; 52 private final DecodeOptions mDecodeOpts; 53 private final FileDescriptorFactory mFactory; 54 private final DecodeCallback mDecodeCallback; 55 private final BitmapCache mCache; 56 private final BitmapFactory.Options mOpts = new BitmapFactory.Options(); 57 58 private ReusableBitmap mInBitmap = null; 59 60 private static final boolean CROP_DURING_DECODE = true; 61 62 private static final String TAG = DecodeTask.class.getSimpleName(); 63 public static final boolean DEBUG = false; 64 65 /** 66 * Callback interface for clients to be notified of decode state changes and completion. 67 */ 68 public interface DecodeCallback { 69 /** 70 * Notifies that the async task's work is about to begin. Up until this point, the task 71 * may have been preempted by the scheduler or queued up by a bottlenecked executor. 72 * <p> 73 * N.B. this method runs on the UI thread. 74 */ onDecodeBegin(RequestKey key)75 void onDecodeBegin(RequestKey key); 76 /** 77 * The task is now complete and the ReusableBitmap is available for use. Clients should 78 * double check that the request matches what the client is expecting. 79 */ onDecodeComplete(RequestKey key, ReusableBitmap result)80 void onDecodeComplete(RequestKey key, ReusableBitmap result); 81 /** 82 * The task has been canceled, and {@link #onDecodeComplete(RequestKey, ReusableBitmap)} 83 * will not be called. 84 */ onDecodeCancel(RequestKey key)85 void onDecodeCancel(RequestKey key); 86 } 87 88 /** 89 * Create new DecodeTask. 90 * 91 * @param requestKey The request to decode, also the key to use for the cache. 92 * @param decodeOpts The decode options. 93 * @param factory The factory to obtain file descriptors to decode from. If this factory is 94 * null, then we will decode from requestKey.createInputStream(). 95 * @param callback The callback to notify of decode state changes. 96 * @param cache The cache and pool. 97 */ DecodeTask(RequestKey requestKey, DecodeOptions decodeOpts, FileDescriptorFactory factory, DecodeCallback callback, BitmapCache cache)98 public DecodeTask(RequestKey requestKey, DecodeOptions decodeOpts, 99 FileDescriptorFactory factory, DecodeCallback callback, BitmapCache cache) { 100 mKey = requestKey; 101 mDecodeOpts = decodeOpts; 102 mFactory = factory; 103 mDecodeCallback = callback; 104 mCache = cache; 105 } 106 107 @Override doInBackground(Void... params)108 protected ReusableBitmap doInBackground(Void... params) { 109 // enqueue the 'onDecodeBegin' signal on the main thread 110 publishProgress(); 111 112 return decode(); 113 } 114 decode()115 public ReusableBitmap decode() { 116 if (isCancelled()) { 117 return null; 118 } 119 120 ReusableBitmap result = null; 121 ParcelFileDescriptor fd = null; 122 InputStream in = null; 123 124 try { 125 if (mFactory != null) { 126 Trace.beginSection("create fd"); 127 fd = mFactory.createFileDescriptor(); 128 Trace.endSection(); 129 } else { 130 in = reset(in); 131 if (in == null) { 132 return null; 133 } 134 if (isCancelled()) { 135 return null; 136 } 137 } 138 139 final boolean isJellyBeanOrAbove = android.os.Build.VERSION.SDK_INT 140 >= android.os.Build.VERSION_CODES.JELLY_BEAN; 141 // This blocks during fling when the pool is empty. We block early to avoid jank. 142 if (isJellyBeanOrAbove) { 143 Trace.beginSection("poll for reusable bitmap"); 144 mInBitmap = mCache.poll(); 145 Trace.endSection(); 146 } 147 148 if (isCancelled()) { 149 return null; 150 } 151 152 Trace.beginSection("get bytesize"); 153 final long byteSize; 154 if (fd != null) { 155 byteSize = fd.getStatSize(); 156 } else { 157 byteSize = -1; 158 } 159 Trace.endSection(); 160 161 Trace.beginSection("get orientation"); 162 final int orientation; 163 if (mKey.hasOrientationExif()) { 164 if (fd != null) { 165 // Creating an input stream from the file descriptor makes it useless 166 // afterwards. 167 Trace.beginSection("create orientation fd and stream"); 168 final ParcelFileDescriptor orientationFd = mFactory.createFileDescriptor(); 169 in = new AutoCloseInputStream(orientationFd); 170 Trace.endSection(); 171 } 172 orientation = Exif.getOrientation(in, byteSize); 173 if (fd != null) { 174 try { 175 // Close the temporary file descriptor. 176 in.close(); 177 } catch (IOException ignored) { 178 } 179 } 180 } else { 181 orientation = 0; 182 } 183 final boolean isNotRotatedOr180 = orientation == 0 || orientation == 180; 184 Trace.endSection(); 185 186 if (orientation != 0) { 187 // disable inBitmap-- bitmap reuse doesn't work with different decode regions due 188 // to orientation 189 if (mInBitmap != null) { 190 mCache.offer(mInBitmap); 191 mInBitmap = null; 192 mOpts.inBitmap = null; 193 } 194 } 195 196 if (isCancelled()) { 197 return null; 198 } 199 200 if (fd == null) { 201 in = reset(in); 202 if (in == null) { 203 return null; 204 } 205 if (isCancelled()) { 206 return null; 207 } 208 } 209 210 Trace.beginSection("decodeBounds"); 211 mOpts.inJustDecodeBounds = true; 212 if (fd != null) { 213 BitmapFactory.decodeFileDescriptor(fd.getFileDescriptor(), null, mOpts); 214 } else { 215 BitmapFactory.decodeStream(in, null, mOpts); 216 } 217 Trace.endSection(); 218 219 if (isCancelled()) { 220 return null; 221 } 222 223 // We want to calculate the sample size "as if" the orientation has been corrected. 224 final int srcW, srcH; // Orientation corrected. 225 if (isNotRotatedOr180) { 226 srcW = mOpts.outWidth; 227 srcH = mOpts.outHeight; 228 } else { 229 srcW = mOpts.outHeight; 230 srcH = mOpts.outWidth; 231 } 232 233 // BEGIN MANUAL-INLINE calculateSampleSize() 234 235 final float sz = Math 236 .min((float) srcW / mDecodeOpts.destW, (float) srcH / mDecodeOpts.destH); 237 238 final int sampleSize; 239 switch (mDecodeOpts.sampleSizeStrategy) { 240 case DecodeOptions.STRATEGY_TRUNCATE: 241 sampleSize = (int) sz; 242 break; 243 case DecodeOptions.STRATEGY_ROUND_UP: 244 sampleSize = (int) Math.ceil(sz); 245 break; 246 case DecodeOptions.STRATEGY_ROUND_NEAREST: 247 default: 248 sampleSize = (int) Math.pow(2, (int) (0.5 + (Math.log(sz) / Math.log(2)))); 249 break; 250 } 251 mOpts.inSampleSize = Math.max(1, sampleSize); 252 253 // END MANUAL-INLINE calculateSampleSize() 254 255 mOpts.inJustDecodeBounds = false; 256 mOpts.inMutable = true; 257 if (isJellyBeanOrAbove && orientation == 0) { 258 if (mInBitmap == null) { 259 if (DEBUG) { 260 Log.e(TAG, "decode thread wants a bitmap. cache dump:\n" 261 + mCache.toDebugString()); 262 } 263 Trace.beginSection("create reusable bitmap"); 264 mInBitmap = new ReusableBitmap( 265 Bitmap.createBitmap(mDecodeOpts.destW, mDecodeOpts.destH, 266 Bitmap.Config.ARGB_8888)); 267 Trace.endSection(); 268 269 if (isCancelled()) { 270 return null; 271 } 272 273 if (DEBUG) { 274 Log.e(TAG, "*** allocated new bitmap in decode thread: " 275 + mInBitmap + " key=" + mKey); 276 } 277 } else { 278 if (DEBUG) { 279 Log.e(TAG, "*** reusing existing bitmap in decode thread: " 280 + mInBitmap + " key=" + mKey); 281 } 282 283 } 284 mOpts.inBitmap = mInBitmap.bmp; 285 } 286 287 if (isCancelled()) { 288 return null; 289 } 290 291 if (fd == null) { 292 in = reset(in); 293 if (in == null) { 294 return null; 295 } 296 if (isCancelled()) { 297 return null; 298 } 299 } 300 301 302 Bitmap decodeResult = null; 303 final Rect srcRect = new Rect(); // Not orientation corrected. True coordinates. 304 if (CROP_DURING_DECODE) { 305 try { 306 Trace.beginSection("decodeCropped" + mOpts.inSampleSize); 307 308 // BEGIN MANUAL INLINE decodeCropped() 309 310 final BitmapRegionDecoder brd; 311 if (fd != null) { 312 brd = BitmapRegionDecoder 313 .newInstance(fd.getFileDescriptor(), true /* shareable */); 314 } else { 315 brd = BitmapRegionDecoder.newInstance(in, true /* shareable */); 316 } 317 318 final Bitmap bitmap; 319 if (isCancelled()) { 320 bitmap = null; 321 } else { 322 // We want to call calculateCroppedSrcRect() on the source rectangle "as 323 // if" the orientation has been corrected. 324 // Center the decode on the top 1/3. 325 BitmapUtils.calculateCroppedSrcRect(srcW, srcH, mDecodeOpts.destW, 326 mDecodeOpts.destH, mDecodeOpts.destH, mOpts.inSampleSize, 327 mDecodeOpts.horizontalCenter, mDecodeOpts.verticalCenter, 328 true /* absoluteFraction */, 329 1f, srcRect); 330 if (DEBUG) { 331 System.out.println("rect for this decode is: " + srcRect 332 + " srcW/H=" + srcW + "/" + srcH 333 + " dstW/H=" + mDecodeOpts.destW + "/" + mDecodeOpts.destH); 334 } 335 336 // calculateCroppedSrcRect() gave us the source rectangle "as if" the 337 // orientation has been corrected. We need to decode the uncorrected 338 // source rectangle. Calculate true coordinates. 339 RectUtils.rotateRectForOrientation(orientation, new Rect(0, 0, srcW, srcH), 340 srcRect); 341 342 bitmap = brd.decodeRegion(srcRect, mOpts); 343 } 344 brd.recycle(); 345 346 // END MANUAL INLINE decodeCropped() 347 348 decodeResult = bitmap; 349 } catch (IOException e) { 350 // fall through to below and try again with the non-cropping decoder 351 if (fd == null) { 352 in = reset(in); 353 if (in == null) { 354 return null; 355 } 356 if (isCancelled()) { 357 return null; 358 } 359 } 360 361 e.printStackTrace(); 362 } finally { 363 Trace.endSection(); 364 } 365 366 if (isCancelled()) { 367 return null; 368 } 369 } 370 371 //noinspection PointlessBooleanExpression 372 if (!CROP_DURING_DECODE || (decodeResult == null && !isCancelled())) { 373 try { 374 Trace.beginSection("decode" + mOpts.inSampleSize); 375 // disable inBitmap-- bitmap reuse doesn't work well below K 376 if (mInBitmap != null) { 377 mCache.offer(mInBitmap); 378 mInBitmap = null; 379 mOpts.inBitmap = null; 380 } 381 decodeResult = decode(fd, in); 382 } catch (IllegalArgumentException e) { 383 Log.e(TAG, "decode failed: reason='" + e.getMessage() + "' ss=" 384 + mOpts.inSampleSize); 385 386 if (mOpts.inSampleSize > 1) { 387 // try again with ss=1 388 mOpts.inSampleSize = 1; 389 decodeResult = decode(fd, in); 390 } 391 } finally { 392 Trace.endSection(); 393 } 394 395 if (isCancelled()) { 396 return null; 397 } 398 } 399 400 if (decodeResult == null) { 401 return null; 402 } 403 404 if (mInBitmap != null) { 405 result = mInBitmap; 406 // srcRect is non-empty when using the cropping BitmapRegionDecoder codepath 407 if (!srcRect.isEmpty()) { 408 result.setLogicalWidth((srcRect.right - srcRect.left) / mOpts.inSampleSize); 409 result.setLogicalHeight( 410 (srcRect.bottom - srcRect.top) / mOpts.inSampleSize); 411 } else { 412 result.setLogicalWidth(mOpts.outWidth); 413 result.setLogicalHeight(mOpts.outHeight); 414 } 415 } else { 416 // no mInBitmap means no pooling 417 result = new ReusableBitmap(decodeResult, false /* reusable */); 418 if (isNotRotatedOr180) { 419 result.setLogicalWidth(decodeResult.getWidth()); 420 result.setLogicalHeight(decodeResult.getHeight()); 421 } else { 422 result.setLogicalWidth(decodeResult.getHeight()); 423 result.setLogicalHeight(decodeResult.getWidth()); 424 } 425 } 426 result.setOrientation(orientation); 427 } catch (Exception e) { 428 e.printStackTrace(); 429 } finally { 430 if (fd != null) { 431 try { 432 fd.close(); 433 } catch (IOException ignored) { 434 } 435 } 436 if (in != null) { 437 try { 438 in.close(); 439 } catch (IOException ignored) { 440 } 441 } 442 443 // Cancellations can't be guaranteed to be correct, so skip the cache 444 if (!isCancelled()) { 445 // Put result in cache, regardless of null. The cache will handle null results. 446 mCache.put(mKey, result); 447 } 448 if (result != null) { 449 result.acquireReference(); 450 if (DEBUG) { 451 Log.d(TAG, "placed result in cache: key=" + mKey + " bmp=" 452 + result + " cancelled=" + isCancelled()); 453 } 454 } else if (mInBitmap != null) { 455 if (DEBUG) { 456 Log.d(TAG, "placing failed/cancelled bitmap in pool: key=" 457 + mKey + " bmp=" + mInBitmap); 458 } 459 mCache.offer(mInBitmap); 460 } 461 } 462 return result; 463 } 464 465 /** 466 * Return an input stream that can be read from the beginning using the most efficient way, 467 * given an input stream that may or may not support reset(), or given null. 468 * 469 * The returned input stream may or may not be the same stream. 470 */ reset(InputStream in)471 private InputStream reset(InputStream in) throws IOException { 472 Trace.beginSection("create stream"); 473 if (in == null) { 474 in = mKey.createInputStream(); 475 } else if (in.markSupported()) { 476 in.reset(); 477 } else { 478 try { 479 in.close(); 480 } catch (IOException ignored) { 481 } 482 in = mKey.createInputStream(); 483 } 484 Trace.endSection(); 485 return in; 486 } 487 decode(ParcelFileDescriptor fd, InputStream in)488 private Bitmap decode(ParcelFileDescriptor fd, InputStream in) { 489 final Bitmap result; 490 if (fd != null) { 491 result = BitmapFactory.decodeFileDescriptor(fd.getFileDescriptor(), null, mOpts); 492 } else { 493 result = BitmapFactory.decodeStream(in, null, mOpts); 494 } 495 return result; 496 } 497 cancel()498 public void cancel() { 499 cancel(true); 500 mOpts.requestCancelDecode(); 501 } 502 503 @Override onProgressUpdate(Void... values)504 protected void onProgressUpdate(Void... values) { 505 mDecodeCallback.onDecodeBegin(mKey); 506 } 507 508 @Override onPostExecute(ReusableBitmap result)509 public void onPostExecute(ReusableBitmap result) { 510 mDecodeCallback.onDecodeComplete(mKey, result); 511 } 512 513 @Override onCancelled(ReusableBitmap result)514 protected void onCancelled(ReusableBitmap result) { 515 mDecodeCallback.onDecodeCancel(mKey); 516 if (result == null) { 517 return; 518 } 519 520 result.releaseReference(); 521 if (mInBitmap == null) { 522 // not reusing bitmaps: can recycle immediately 523 result.bmp.recycle(); 524 } 525 } 526 527 /** 528 * Parameters to pass to the DecodeTask. 529 */ 530 public static class DecodeOptions { 531 532 /** 533 * Round sample size to the nearest power of 2. Depending on the source and destination 534 * dimensions, we will either truncate, in which case we decode from a bigger region and 535 * crop down, or we will round up, in which case we decode from a smaller region and scale 536 * up. 537 */ 538 public static final int STRATEGY_ROUND_NEAREST = 0; 539 /** 540 * Always decode from a bigger region and crop down. 541 */ 542 public static final int STRATEGY_TRUNCATE = 1; 543 544 /** 545 * Always decode from a smaller region and scale up. 546 */ 547 public static final int STRATEGY_ROUND_UP = 2; 548 549 /** 550 * The destination width to decode to. 551 */ 552 public int destW; 553 /** 554 * The destination height to decode to. 555 */ 556 public int destH; 557 /** 558 * If the destination dimensions are smaller than the source image provided by the request 559 * key, this will determine where horizontally the destination rect will be cropped from. 560 * Value from 0f for left-most crop to 1f for right-most crop. 561 */ 562 public float horizontalCenter; 563 /** 564 * If the destination dimensions are smaller than the source image provided by the request 565 * key, this will determine where vertically the destination rect will be cropped from. 566 * Value from 0f for top-most crop to 1f for bottom-most crop. 567 */ 568 public float verticalCenter; 569 /** 570 * One of the STRATEGY constants. 571 */ 572 public int sampleSizeStrategy; 573 DecodeOptions(final int destW, final int destH)574 public DecodeOptions(final int destW, final int destH) { 575 this(destW, destH, 0.5f, 0.5f, STRATEGY_ROUND_NEAREST); 576 } 577 578 /** 579 * Create new DecodeOptions with horizontally-centered cropping if applicable. 580 * @param destW The destination width to decode to. 581 * @param destH The destination height to decode to. 582 * @param verticalCenter If the destination dimensions are smaller than the source image 583 * provided by the request key, this will determine where vertically 584 * the destination rect will be cropped from. 585 * @param sampleSizeStrategy One of the STRATEGY constants. 586 */ DecodeOptions(final int destW, final int destH, final float verticalCenter, final int sampleSizeStrategy)587 public DecodeOptions(final int destW, final int destH, 588 final float verticalCenter, final int sampleSizeStrategy) { 589 this(destW, destH, 0.5f, verticalCenter, sampleSizeStrategy); 590 } 591 592 /** 593 * Create new DecodeOptions. 594 * @param destW The destination width to decode to. 595 * @param destH The destination height to decode to. 596 * @param horizontalCenter If the destination dimensions are smaller than the source image 597 * provided by the request key, this will determine where 598 * horizontally the destination rect will be cropped from. 599 * @param verticalCenter If the destination dimensions are smaller than the source image 600 * provided by the request key, this will determine where vertically 601 * the destination rect will be cropped from. 602 * @param sampleSizeStrategy One of the STRATEGY constants. 603 */ DecodeOptions(final int destW, final int destH, final float horizontalCenter, final float verticalCenter, final int sampleSizeStrategy)604 public DecodeOptions(final int destW, final int destH, final float horizontalCenter, 605 final float verticalCenter, final int sampleSizeStrategy) { 606 this.destW = destW; 607 this.destH = destH; 608 this.horizontalCenter = horizontalCenter; 609 this.verticalCenter = verticalCenter; 610 this.sampleSizeStrategy = sampleSizeStrategy; 611 } 612 } 613 } 614