DecodeTask.java revision 22955165fab693684cc3614c84ee81883ae933c8
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 Trace.beginSection("create fd and stream"); 138 if (mFactory != null) { 139 fd = mFactory.createFileDescriptor(); 140 } else { 141 in = reset(in); 142 if (in == null) { 143 return null; 144 } 145 } 146 Trace.endSection(); 147 148 Trace.beginSection("get bytesize"); 149 final long byteSize; 150 if (fd != null) { 151 byteSize = fd.getStatSize(); 152 } else { 153 byteSize = -1; 154 } 155 Trace.endSection(); 156 157 Trace.beginSection("get orientation"); 158 final int orientation; 159 if (mKey.hasOrientationExif()) { 160 if (fd != null) { 161 // Creating an input stream from the file descriptor makes it useless 162 // afterwards. 163 Trace.beginSection("create orientation fd and stream"); 164 final ParcelFileDescriptor orientationFd = mFactory.createFileDescriptor(); 165 in = new AutoCloseInputStream(orientationFd); 166 Trace.endSection(); 167 } 168 orientation = Exif.getOrientation(in, byteSize); 169 if (fd != null) { 170 try { 171 // Close the temporary file descriptor. 172 in.close(); 173 } catch (IOException ignored) { 174 } 175 } 176 } else { 177 orientation = 0; 178 } 179 final boolean isNotRotatedOr180 = orientation == 0 || orientation == 180; 180 Trace.endSection(); 181 182 if (orientation != 0) { 183 // disable inBitmap-- bitmap reuse doesn't work with different decode regions due 184 // to orientation 185 if (mInBitmap != null) { 186 mCache.offer(mInBitmap); 187 mInBitmap = null; 188 mOpts.inBitmap = null; 189 } 190 } 191 192 if (isCancelled()) { 193 return null; 194 } 195 196 if (fd == null) { 197 in = reset(in); 198 if (in == null) { 199 return null; 200 } 201 } 202 203 Trace.beginSection("decodeBounds"); 204 mOpts.inJustDecodeBounds = true; 205 if (fd != null) { 206 BitmapFactory.decodeFileDescriptor(fd.getFileDescriptor(), null, mOpts); 207 } else { 208 BitmapFactory.decodeStream(in, null, mOpts); 209 } 210 Trace.endSection(); 211 212 if (isCancelled()) { 213 return null; 214 } 215 216 // We want to calculate the sample size "as if" the orientation has been corrected. 217 final int srcW, srcH; // Orientation corrected. 218 if (isNotRotatedOr180) { 219 srcW = mOpts.outWidth; 220 srcH = mOpts.outHeight; 221 } else { 222 srcW = mOpts.outHeight; 223 srcH = mOpts.outWidth; 224 } 225 226 // BEGIN MANUAL-INLINE calculateSampleSize() 227 228 final float sz = Math 229 .min((float) srcW / mDecodeOpts.destW, (float) srcH / mDecodeOpts.destH); 230 231 final int sampleSize; 232 switch (mDecodeOpts.sampleSizeStrategy) { 233 case DecodeOptions.STRATEGY_TRUNCATE: 234 sampleSize = (int) sz; 235 break; 236 case DecodeOptions.STRATEGY_ROUND_UP: 237 sampleSize = (int) Math.ceil(sz); 238 break; 239 case DecodeOptions.STRATEGY_ROUND_NEAREST: 240 default: 241 sampleSize = (int) Math.pow(2, (int) (0.5 + (Math.log(sz) / Math.log(2)))); 242 break; 243 } 244 mOpts.inSampleSize = Math.max(1, sampleSize); 245 246 // END MANUAL-INLINE calculateSampleSize() 247 248 mOpts.inJustDecodeBounds = false; 249 mOpts.inMutable = true; 250 if (isJellyBeanOrAbove && orientation == 0) { 251 if (mInBitmap == null) { 252 if (DEBUG) { 253 Log.e(TAG, "decode thread wants a bitmap. cache dump:\n" 254 + mCache.toDebugString()); 255 } 256 Trace.beginSection("create reusable bitmap"); 257 mInBitmap = new ReusableBitmap( 258 Bitmap.createBitmap(mDecodeOpts.destW, mDecodeOpts.destH, 259 Bitmap.Config.ARGB_8888)); 260 Trace.endSection(); 261 262 if (isCancelled()) { 263 return null; 264 } 265 266 if (DEBUG) { 267 Log.e(TAG, "*** allocated new bitmap in decode thread: " 268 + mInBitmap + " key=" + mKey); 269 } 270 } else { 271 if (DEBUG) { 272 Log.e(TAG, "*** reusing existing bitmap in decode thread: " 273 + mInBitmap + " key=" + mKey); 274 } 275 276 } 277 mOpts.inBitmap = mInBitmap.bmp; 278 } 279 280 if (isCancelled()) { 281 return null; 282 } 283 284 if (fd == null) { 285 in = reset(in); 286 if (in == null) { 287 return null; 288 } 289 } 290 291 Bitmap decodeResult = null; 292 final Rect srcRect = new Rect(); // Not orientation corrected. True coordinates. 293 if (CROP_DURING_DECODE) { 294 try { 295 Trace.beginSection("decodeCropped" + mOpts.inSampleSize); 296 297 // BEGIN MANUAL INLINE decodeCropped() 298 299 final BitmapRegionDecoder brd; 300 if (fd != null) { 301 brd = BitmapRegionDecoder 302 .newInstance(fd.getFileDescriptor(), true /* shareable */); 303 } else { 304 brd = BitmapRegionDecoder.newInstance(in, true /* shareable */); 305 } 306 307 final Bitmap bitmap; 308 if (isCancelled()) { 309 bitmap = null; 310 } else { 311 // We want to call calculateCroppedSrcRect() on the source rectangle "as 312 // if" the orientation has been corrected. 313 // Center the decode on the top 1/3. 314 BitmapUtils.calculateCroppedSrcRect(srcW, srcH, mDecodeOpts.destW, 315 mDecodeOpts.destH, 316 mDecodeOpts.destH, mOpts.inSampleSize, mDecodeOpts.verticalCenter, 317 true /* absoluteFraction */, 318 1f, srcRect); 319 if (DEBUG) { 320 System.out.println("rect for this decode is: " + srcRect 321 + " srcW/H=" + srcW + "/" + srcH 322 + " dstW/H=" + mDecodeOpts.destW + "/" + mDecodeOpts.destH); 323 } 324 325 // calculateCroppedSrcRect() gave us the source rectangle "as if" the 326 // orientation has been corrected. We need to decode the uncorrected 327 // source rectangle. Calculate true coordinates. 328 RectUtils.rotateRectForOrientation(orientation, new Rect(0, 0, srcW, srcH), 329 srcRect); 330 331 bitmap = brd.decodeRegion(srcRect, mOpts); 332 } 333 brd.recycle(); 334 335 // END MANUAL INLINE decodeCropped() 336 337 decodeResult = bitmap; 338 } catch (IOException e) { 339 // fall through to below and try again with the non-cropping decoder 340 e.printStackTrace(); 341 } finally { 342 Trace.endSection(); 343 } 344 345 if (isCancelled()) { 346 return null; 347 } 348 } 349 350 //noinspection PointlessBooleanExpression 351 if (!CROP_DURING_DECODE || (decodeResult == null && !isCancelled())) { 352 try { 353 Trace.beginSection("decode" + mOpts.inSampleSize); 354 // disable inBitmap-- bitmap reuse doesn't work well below K 355 if (mInBitmap != null) { 356 mCache.offer(mInBitmap); 357 mInBitmap = null; 358 mOpts.inBitmap = null; 359 } 360 decodeResult = decode(fd, in); 361 } catch (IllegalArgumentException e) { 362 Log.e(TAG, "decode failed: reason='" + e.getMessage() + "' ss=" 363 + mOpts.inSampleSize); 364 365 if (mOpts.inSampleSize > 1) { 366 // try again with ss=1 367 mOpts.inSampleSize = 1; 368 decodeResult = decode(fd, in); 369 } 370 } finally { 371 Trace.endSection(); 372 } 373 374 if (isCancelled()) { 375 return null; 376 } 377 } 378 379 if (decodeResult == null) { 380 return null; 381 } 382 383 if (mInBitmap != null) { 384 result = mInBitmap; 385 // srcRect is non-empty when using the cropping BitmapRegionDecoder codepath 386 if (!srcRect.isEmpty()) { 387 result.setLogicalWidth((srcRect.right - srcRect.left) / mOpts.inSampleSize); 388 result.setLogicalHeight( 389 (srcRect.bottom - srcRect.top) / mOpts.inSampleSize); 390 } else { 391 result.setLogicalWidth(mOpts.outWidth); 392 result.setLogicalHeight(mOpts.outHeight); 393 } 394 } else { 395 // no mInBitmap means no pooling 396 result = new ReusableBitmap(decodeResult, false /* reusable */); 397 if (isNotRotatedOr180) { 398 result.setLogicalWidth(decodeResult.getWidth()); 399 result.setLogicalHeight(decodeResult.getHeight()); 400 } else { 401 result.setLogicalWidth(decodeResult.getHeight()); 402 result.setLogicalHeight(decodeResult.getWidth()); 403 } 404 } 405 result.setOrientation(orientation); 406 } catch (Exception e) { 407 e.printStackTrace(); 408 } finally { 409 if (fd != null) { 410 try { 411 fd.close(); 412 } catch (IOException ignored) { 413 } 414 } 415 if (in != null) { 416 try { 417 in.close(); 418 } catch (IOException ignored) { 419 } 420 } 421 if (result != null) { 422 result.acquireReference(); 423 mCache.put(mKey, result); 424 if (DEBUG) { 425 Log.d(TAG, "placed result in cache: key=" + mKey + " bmp=" 426 + result + " cancelled=" + isCancelled()); 427 } 428 } else if (mInBitmap != null) { 429 if (DEBUG) { 430 Log.d(TAG, "placing failed/cancelled bitmap in pool: key=" 431 + mKey + " bmp=" + mInBitmap); 432 } 433 mCache.offer(mInBitmap); 434 } 435 } 436 return result; 437 } 438 439 /** 440 * Return an input stream that can be read from the beginning using the most efficient way, 441 * given an input stream that may or may not support reset(), or given null. 442 * 443 * The returned input stream may or may not be the same stream. 444 */ 445 private InputStream reset(InputStream in) throws IOException { 446 Trace.beginSection("create stream"); 447 if (in == null) { 448 in = mKey.createInputStream(); 449 } else if (in.markSupported()) { 450 in.reset(); 451 } else { 452 try { 453 in.close(); 454 } catch (IOException ignored) { 455 } 456 in = mKey.createInputStream(); 457 } 458 Trace.endSection(); 459 return in; 460 } 461 462 private Bitmap decode(ParcelFileDescriptor fd, InputStream in) { 463 final Bitmap result; 464 if (fd != null) { 465 result = BitmapFactory.decodeFileDescriptor(fd.getFileDescriptor(), null, mOpts); 466 } else { 467 result = BitmapFactory.decodeStream(in, null, mOpts); 468 } 469 return result; 470 } 471 472 public void cancel() { 473 cancel(true); 474 mOpts.requestCancelDecode(); 475 } 476 477 @Override 478 protected void onProgressUpdate(Void... values) { 479 mDecodeCallback.onDecodeBegin(mKey); 480 } 481 482 @Override 483 public void onPostExecute(ReusableBitmap result) { 484 mDecodeCallback.onDecodeComplete(mKey, result); 485 } 486 487 @Override 488 protected void onCancelled(ReusableBitmap result) { 489 mDecodeCallback.onDecodeCancel(mKey); 490 if (result == null) { 491 return; 492 } 493 494 result.releaseReference(); 495 if (mInBitmap == null) { 496 // not reusing bitmaps: can recycle immediately 497 result.bmp.recycle(); 498 } 499 } 500 501 public static class DecodeOptions { 502 503 /** 504 * Round sample size to the nearest power of 2. Depending on the source and destination 505 * dimensions, we will either truncate, in which case we decode from a bigger region and 506 * crop down, or we will round up, in which case we decode from a smaller region and scale 507 * up. 508 */ 509 public static final int STRATEGY_ROUND_NEAREST = 0; 510 /** 511 * Always decode from a bigger region and crop down. 512 */ 513 public static final int STRATEGY_TRUNCATE = 1; 514 515 /** 516 * Always decode from a smaller region and scale up. 517 */ 518 public static final int STRATEGY_ROUND_UP = 2; 519 520 /** 521 * The destination width to decode to. 522 */ 523 public int destW; 524 /** 525 * The destination height to decode to. 526 */ 527 public int destH; 528 /** 529 * If the destination dimensions are smaller than the source image provided by the request 530 * key, this will determine where vertically the destination rect will be cropped from. 531 * Value from 0f for top-most crop to 1f for bottom-most crop. 532 */ 533 public float verticalCenter; 534 /** 535 * One of the STRATEGY constants. 536 */ 537 public int sampleSizeStrategy; 538 539 public DecodeOptions(final int destW, final int destH) { 540 this(destW, destH, 0.5f, STRATEGY_ROUND_NEAREST); 541 } 542 543 /** 544 * Create new DecodeOptions. 545 * @param destW The destination width to decode to. 546 * @param destH The destination height to decode to. 547 * @param verticalCenter If the destination dimensions are smaller than the source image 548 * provided by the request key, this will determine where vertically 549 * the destination rect will be cropped from. 550 * @param sampleSizeStrategy One of the STRATEGY constants. 551 */ 552 public DecodeOptions(final int destW, final int destH, final float verticalCenter, 553 final int sampleSizeStrategy) { 554 this.destW = destW; 555 this.destH = destH; 556 this.verticalCenter = verticalCenter; 557 this.sampleSizeStrategy = sampleSizeStrategy; 558 } 559 } 560} 561