DecodeTask.java revision 2e4d0863dba53435372ec96538f2ef3e1c3675bf
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 17package com.android.bitmap; 18 19import android.graphics.Bitmap; 20import android.graphics.BitmapFactory; 21import android.graphics.BitmapRegionDecoder; 22import android.graphics.Rect; 23import android.os.AsyncTask; 24import android.os.ParcelFileDescriptor; 25import android.os.ParcelFileDescriptor.AutoCloseInputStream; 26import android.util.Log; 27 28import com.android.bitmap.RequestKey.FileDescriptorFactory; 29import com.android.bitmap.util.BitmapUtils; 30import com.android.bitmap.util.Exif; 31import com.android.bitmap.util.RectUtils; 32import com.android.bitmap.util.Trace; 33 34import java.io.IOException; 35import 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 */ 49public 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 */ 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 */ 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 */ 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 */ 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 108 protected ReusableBitmap doInBackground(Void... params) { 109 // enqueue the 'onDecodeBegin' signal on the main thread 110 publishProgress(); 111 112 return decode(); 113 } 114 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 try { 124 final boolean isJellyBeanOrAbove = android.os.Build.VERSION.SDK_INT 125 >= android.os.Build.VERSION_CODES.JELLY_BEAN; 126 // This blocks during fling when the pool is empty. We block early to avoid jank. 127 if (isJellyBeanOrAbove) { 128 Trace.beginSection("poll for reusable bitmap"); 129 mInBitmap = mCache.poll(); 130 Trace.endSection(); 131 132 if (isCancelled()) { 133 return null; 134 } 135 } 136 137 if (mFactory != null) { 138 Trace.beginSection("create fd"); 139 fd = mFactory.createFileDescriptor(); 140 Trace.endSection(); 141 } else { 142 in = reset(in); 143 if (in == null) { 144 return null; 145 } 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 e.printStackTrace(); 352 } finally { 353 Trace.endSection(); 354 } 355 356 if (isCancelled()) { 357 return null; 358 } 359 } 360 361 //noinspection PointlessBooleanExpression 362 if (!CROP_DURING_DECODE || (decodeResult == null && !isCancelled())) { 363 try { 364 Trace.beginSection("decode" + mOpts.inSampleSize); 365 // disable inBitmap-- bitmap reuse doesn't work well below K 366 if (mInBitmap != null) { 367 mCache.offer(mInBitmap); 368 mInBitmap = null; 369 mOpts.inBitmap = null; 370 } 371 decodeResult = decode(fd, in); 372 } catch (IllegalArgumentException e) { 373 Log.e(TAG, "decode failed: reason='" + e.getMessage() + "' ss=" 374 + mOpts.inSampleSize); 375 376 if (mOpts.inSampleSize > 1) { 377 // try again with ss=1 378 mOpts.inSampleSize = 1; 379 decodeResult = decode(fd, in); 380 } 381 } finally { 382 Trace.endSection(); 383 } 384 385 if (isCancelled()) { 386 return null; 387 } 388 } 389 390 if (decodeResult == null) { 391 return null; 392 } 393 394 if (mInBitmap != null) { 395 result = mInBitmap; 396 // srcRect is non-empty when using the cropping BitmapRegionDecoder codepath 397 if (!srcRect.isEmpty()) { 398 result.setLogicalWidth((srcRect.right - srcRect.left) / mOpts.inSampleSize); 399 result.setLogicalHeight( 400 (srcRect.bottom - srcRect.top) / mOpts.inSampleSize); 401 } else { 402 result.setLogicalWidth(mOpts.outWidth); 403 result.setLogicalHeight(mOpts.outHeight); 404 } 405 } else { 406 // no mInBitmap means no pooling 407 result = new ReusableBitmap(decodeResult, false /* reusable */); 408 if (isNotRotatedOr180) { 409 result.setLogicalWidth(decodeResult.getWidth()); 410 result.setLogicalHeight(decodeResult.getHeight()); 411 } else { 412 result.setLogicalWidth(decodeResult.getHeight()); 413 result.setLogicalHeight(decodeResult.getWidth()); 414 } 415 } 416 result.setOrientation(orientation); 417 } catch (Exception e) { 418 e.printStackTrace(); 419 } finally { 420 if (fd != null) { 421 try { 422 fd.close(); 423 } catch (IOException ignored) { 424 } 425 } 426 if (in != null) { 427 try { 428 in.close(); 429 } catch (IOException ignored) { 430 } 431 } 432 if (result != null) { 433 result.acquireReference(); 434 mCache.put(mKey, result); 435 if (DEBUG) { 436 Log.d(TAG, "placed result in cache: key=" + mKey + " bmp=" 437 + result + " cancelled=" + isCancelled()); 438 } 439 } else if (mInBitmap != null) { 440 if (DEBUG) { 441 Log.d(TAG, "placing failed/cancelled bitmap in pool: key=" 442 + mKey + " bmp=" + mInBitmap); 443 } 444 mCache.offer(mInBitmap); 445 } 446 } 447 return result; 448 } 449 450 /** 451 * Return an input stream that can be read from the beginning using the most efficient way, 452 * given an input stream that may or may not support reset(), or given null. 453 * 454 * The returned input stream may or may not be the same stream. 455 */ 456 private InputStream reset(InputStream in) throws IOException { 457 Trace.beginSection("create stream"); 458 if (in == null) { 459 in = mKey.createInputStream(); 460 } else if (in.markSupported()) { 461 in.reset(); 462 } else { 463 try { 464 in.close(); 465 } catch (IOException ignored) { 466 } 467 in = mKey.createInputStream(); 468 } 469 Trace.endSection(); 470 return in; 471 } 472 473 private Bitmap decode(ParcelFileDescriptor fd, InputStream in) { 474 final Bitmap result; 475 if (fd != null) { 476 result = BitmapFactory.decodeFileDescriptor(fd.getFileDescriptor(), null, mOpts); 477 } else { 478 result = BitmapFactory.decodeStream(in, null, mOpts); 479 } 480 return result; 481 } 482 483 public void cancel() { 484 cancel(true); 485 mOpts.requestCancelDecode(); 486 } 487 488 @Override 489 protected void onProgressUpdate(Void... values) { 490 mDecodeCallback.onDecodeBegin(mKey); 491 } 492 493 @Override 494 public void onPostExecute(ReusableBitmap result) { 495 mDecodeCallback.onDecodeComplete(mKey, result); 496 } 497 498 @Override 499 protected void onCancelled(ReusableBitmap result) { 500 mDecodeCallback.onDecodeCancel(mKey); 501 if (result == null) { 502 return; 503 } 504 505 result.releaseReference(); 506 if (mInBitmap == null) { 507 // not reusing bitmaps: can recycle immediately 508 result.bmp.recycle(); 509 } 510 } 511 512 public static class DecodeOptions { 513 514 /** 515 * Round sample size to the nearest power of 2. Depending on the source and destination 516 * dimensions, we will either truncate, in which case we decode from a bigger region and 517 * crop down, or we will round up, in which case we decode from a smaller region and scale 518 * up. 519 */ 520 public static final int STRATEGY_ROUND_NEAREST = 0; 521 /** 522 * Always decode from a bigger region and crop down. 523 */ 524 public static final int STRATEGY_TRUNCATE = 1; 525 526 /** 527 * Always decode from a smaller region and scale up. 528 */ 529 public static final int STRATEGY_ROUND_UP = 2; 530 531 /** 532 * The destination width to decode to. 533 */ 534 public int destW; 535 /** 536 * The destination height to decode to. 537 */ 538 public int destH; 539 /** 540 * If the destination dimensions are smaller than the source image provided by the request 541 * key, this will determine where vertically the destination rect will be cropped from. 542 * Value from 0f for top-most crop to 1f for bottom-most crop. 543 */ 544 public float verticalCenter; 545 /** 546 * One of the STRATEGY constants. 547 */ 548 public int sampleSizeStrategy; 549 550 public DecodeOptions(final int destW, final int destH) { 551 this(destW, destH, 0.5f, STRATEGY_ROUND_NEAREST); 552 } 553 554 /** 555 * Create new DecodeOptions. 556 * @param destW The destination width to decode to. 557 * @param destH The destination height to decode to. 558 * @param verticalCenter If the destination dimensions are smaller than the source image 559 * provided by the request key, this will determine where vertically 560 * the destination rect will be cropped from. 561 * @param sampleSizeStrategy One of the STRATEGY constants. 562 */ 563 public DecodeOptions(final int destW, final int destH, final float verticalCenter, 564 final int sampleSizeStrategy) { 565 this.destW = destW; 566 this.destH = destH; 567 this.verticalCenter = verticalCenter; 568 this.sampleSizeStrategy = sampleSizeStrategy; 569 } 570 } 571} 572