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, 327 mDecodeOpts.destH, mOpts.inSampleSize, 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 // Put result in cache, regardless of null. The cache will handle null results. 444 mCache.put(mKey, result); 445 if (result != null) { 446 result.acquireReference(); 447 if (DEBUG) { 448 Log.d(TAG, "placed result in cache: key=" + mKey + " bmp=" 449 + result + " cancelled=" + isCancelled()); 450 } 451 } else if (mInBitmap != null) { 452 if (DEBUG) { 453 Log.d(TAG, "placing failed/cancelled bitmap in pool: key=" 454 + mKey + " bmp=" + mInBitmap); 455 } 456 mCache.offer(mInBitmap); 457 } 458 } 459 return result; 460 } 461 462 /** 463 * Return an input stream that can be read from the beginning using the most efficient way, 464 * given an input stream that may or may not support reset(), or given null. 465 * 466 * The returned input stream may or may not be the same stream. 467 */ reset(InputStream in)468 private InputStream reset(InputStream in) throws IOException { 469 Trace.beginSection("create stream"); 470 if (in == null) { 471 in = mKey.createInputStream(); 472 } else if (in.markSupported()) { 473 in.reset(); 474 } else { 475 try { 476 in.close(); 477 } catch (IOException ignored) { 478 } 479 in = mKey.createInputStream(); 480 } 481 Trace.endSection(); 482 return in; 483 } 484 decode(ParcelFileDescriptor fd, InputStream in)485 private Bitmap decode(ParcelFileDescriptor fd, InputStream in) { 486 final Bitmap result; 487 if (fd != null) { 488 result = BitmapFactory.decodeFileDescriptor(fd.getFileDescriptor(), null, mOpts); 489 } else { 490 result = BitmapFactory.decodeStream(in, null, mOpts); 491 } 492 return result; 493 } 494 cancel()495 public void cancel() { 496 cancel(true); 497 mOpts.requestCancelDecode(); 498 } 499 500 @Override onProgressUpdate(Void... values)501 protected void onProgressUpdate(Void... values) { 502 mDecodeCallback.onDecodeBegin(mKey); 503 } 504 505 @Override onPostExecute(ReusableBitmap result)506 public void onPostExecute(ReusableBitmap result) { 507 mDecodeCallback.onDecodeComplete(mKey, result); 508 } 509 510 @Override onCancelled(ReusableBitmap result)511 protected void onCancelled(ReusableBitmap result) { 512 mDecodeCallback.onDecodeCancel(mKey); 513 if (result == null) { 514 return; 515 } 516 517 result.releaseReference(); 518 if (mInBitmap == null) { 519 // not reusing bitmaps: can recycle immediately 520 result.bmp.recycle(); 521 } 522 } 523 524 /** 525 * Parameters to pass to the DecodeTask. 526 */ 527 public static class DecodeOptions { 528 529 /** 530 * Round sample size to the nearest power of 2. Depending on the source and destination 531 * dimensions, we will either truncate, in which case we decode from a bigger region and 532 * crop down, or we will round up, in which case we decode from a smaller region and scale 533 * up. 534 */ 535 public static final int STRATEGY_ROUND_NEAREST = 0; 536 /** 537 * Always decode from a bigger region and crop down. 538 */ 539 public static final int STRATEGY_TRUNCATE = 1; 540 541 /** 542 * Always decode from a smaller region and scale up. 543 */ 544 public static final int STRATEGY_ROUND_UP = 2; 545 546 /** 547 * The destination width to decode to. 548 */ 549 public int destW; 550 /** 551 * The destination height to decode to. 552 */ 553 public int destH; 554 /** 555 * If the destination dimensions are smaller than the source image provided by the request 556 * key, this will determine where vertically the destination rect will be cropped from. 557 * Value from 0f for top-most crop to 1f for bottom-most crop. 558 */ 559 public float verticalCenter; 560 /** 561 * One of the STRATEGY constants. 562 */ 563 public int sampleSizeStrategy; 564 DecodeOptions(final int destW, final int destH)565 public DecodeOptions(final int destW, final int destH) { 566 this(destW, destH, 0.5f, STRATEGY_ROUND_NEAREST); 567 } 568 569 /** 570 * Create new DecodeOptions. 571 * @param destW The destination width to decode to. 572 * @param destH The destination height to decode to. 573 * @param verticalCenter If the destination dimensions are smaller than the source image 574 * provided by the request key, this will determine where vertically 575 * the destination rect will be cropped from. 576 * @param sampleSizeStrategy One of the STRATEGY constants. 577 */ DecodeOptions(final int destW, final int destH, final float verticalCenter, final int sampleSizeStrategy)578 public DecodeOptions(final int destW, final int destH, final float verticalCenter, 579 final int sampleSizeStrategy) { 580 this.destW = destW; 581 this.destH = destH; 582 this.verticalCenter = verticalCenter; 583 this.sampleSizeStrategy = sampleSizeStrategy; 584 } 585 } 586 } 587