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