DecodeTask.java revision 93a35b93dc582e38ff8ee5979754a16b4bf4da0c
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.content.res.AssetFileDescriptor; 20import android.graphics.Bitmap; 21import android.graphics.BitmapFactory; 22import android.graphics.BitmapRegionDecoder; 23import android.graphics.Rect; 24import android.os.AsyncTask; 25import android.util.Log; 26 27import com.android.bitmap.util.BitmapUtils; 28import com.android.bitmap.util.Exif; 29import com.android.bitmap.util.RectUtils; 30import com.android.bitmap.util.Trace; 31 32import java.io.IOException; 33import java.io.InputStream; 34 35/** 36 * Decodes an image from either a file descriptor or input stream on a worker thread. After the 37 * decode is complete, even if the task is cancelled, the result is placed in the given cache. 38 * A {@link DecodeCallback} client may be notified on decode begin and completion. 39 * <p> 40 * This class uses {@link BitmapRegionDecoder} when possible to minimize unnecessary decoding 41 * and allow bitmap reuse on Jellybean 4.1 and later. 42 * <p> 43 * GIFs are supported, but their decode does not reuse bitmaps at all. The resulting 44 * {@link ReusableBitmap} will be marked as not reusable 45 * ({@link ReusableBitmap#isEligibleForPooling()} will return false). 46 */ 47public class DecodeTask extends AsyncTask<Void, Void, ReusableBitmap> { 48 49 private final Request mKey; 50 private final int mDestW; 51 private final int mDestH; 52 private final DecodeCallback mDecodeCallback; 53 private final BitmapCache mCache; 54 private final BitmapFactory.Options mOpts = new BitmapFactory.Options(); 55 56 private ReusableBitmap mInBitmap = null; 57 58 private static final boolean CROP_DURING_DECODE = true; 59 60 private static final String TAG = DecodeTask.class.getSimpleName(); 61 private static final boolean DEBUG = false; 62 63 /** 64 * The decode task uses this class to get input to decode. You must implement at least one of 65 * {@link #createFd()} or {@link #createInputStream()}. {@link DecodeTask} will prioritize 66 * {@link #createFd()} before falling back to {@link #createInputStream()}. 67 * <p> 68 * When {@link DecodeTask} is used in conjunction with a {@link BitmapCache}, objects of this 69 * type will also serve as cache keys to fetch cached data. 70 */ 71 public interface Request { 72 AssetFileDescriptor createFd() throws IOException; 73 InputStream createInputStream() throws IOException; 74 boolean hasOrientationExif() throws IOException; 75 } 76 77 /** 78 * Callback interface for clients to be notified of decode state changes and completion. 79 */ 80 public interface DecodeCallback { 81 /** 82 * Notifies that the async task's work is about to begin. Up until this point, the task 83 * may have been preempted by the scheduler or queued up by a bottlenecked executor. 84 * <p> 85 * N.B. this method runs on the UI thread. 86 */ 87 void onDecodeBegin(Request key); 88 /** 89 * The task is now complete and the ReusableBitmap is available for use. Clients should 90 * double check that the request matches what the client is expecting. 91 */ 92 void onDecodeComplete(Request key, ReusableBitmap result); 93 /** 94 * The task has been canceled, and {@link #onDecodeComplete(Request, ReusableBitmap)} will 95 * not be called. 96 */ 97 void onDecodeCancel(Request key); 98 } 99 100 public DecodeTask(Request key, int w, int h, DecodeCallback view, 101 BitmapCache cache) { 102 mKey = key; 103 mDestW = w; 104 mDestH = h; 105 mDecodeCallback = view; 106 mCache = cache; 107 } 108 109 @Override 110 protected ReusableBitmap doInBackground(Void... params) { 111 // enqueue the 'onDecodeBegin' signal on the main thread 112 publishProgress(); 113 114 return decode(); 115 } 116 117 public ReusableBitmap decode() { 118 if (isCancelled()) { 119 return null; 120 } 121 122 ReusableBitmap result = null; 123 AssetFileDescriptor fd = null; 124 InputStream in = null; 125 try { 126 final boolean isJellyBeanOrAbove = android.os.Build.VERSION.SDK_INT 127 >= android.os.Build.VERSION_CODES.JELLY_BEAN; 128 // This blocks during fling when the pool is empty. We block early to avoid jank. 129 if (isJellyBeanOrAbove) { 130 Trace.beginSection("poll for reusable bitmap"); 131 mInBitmap = mCache.poll(); 132 Trace.endSection(); 133 134 if (isCancelled()) { 135 return null; 136 } 137 } 138 139 Trace.beginSection("create fd and stream"); 140 fd = mKey.createFd(); 141 Trace.endSection(); 142 if (fd == null) { 143 in = reset(in); 144 if (in == null) { 145 return null; 146 } 147 } 148 149 Trace.beginSection("get bytesize"); 150 final long byteSize; 151 if (fd != null) { 152 byteSize = fd.getLength(); 153 } else { 154 byteSize = -1; 155 } 156 Trace.endSection(); 157 158 Trace.beginSection("get orientation"); 159 final int orientation; 160 if (mKey.hasOrientationExif()) { 161 if (fd != null) { 162 // Creating an input stream from the file descriptor makes it useless 163 // afterwards. 164 Trace.beginSection("create fd and stream"); 165 final AssetFileDescriptor orientationFd = mKey.createFd(); 166 in = orientationFd.createInputStream(); 167 Trace.endSection(); 168 } 169 orientation = Exif.getOrientation(in, byteSize); 170 if (fd != null) { 171 try { 172 // Close the temporary file descriptor. 173 in.close(); 174 } catch (IOException ignored) { 175 } 176 } 177 } else { 178 orientation = 0; 179 } 180 final boolean isNotRotatedOr180 = orientation == 0 || orientation == 180; 181 Trace.endSection(); 182 183 if (orientation != 0) { 184 // disable inBitmap-- bitmap reuse doesn't work with different decode regions due 185 // to orientation 186 if (mInBitmap != null) { 187 mCache.offer(mInBitmap); 188 mInBitmap = null; 189 mOpts.inBitmap = null; 190 } 191 } 192 193 if (isCancelled()) { 194 return null; 195 } 196 197 if (fd == null) { 198 in = reset(in); 199 if (in == null) { 200 return null; 201 } 202 } 203 204 Trace.beginSection("decodeBounds"); 205 mOpts.inJustDecodeBounds = true; 206 if (fd != null) { 207 BitmapFactory.decodeFileDescriptor(fd.getFileDescriptor(), null, mOpts); 208 } else { 209 BitmapFactory.decodeStream(in, null, mOpts); 210 } 211 Trace.endSection(); 212 213 if (isCancelled()) { 214 return null; 215 } 216 217 // We want to calculate the sample size "as if" the orientation has been corrected. 218 final int srcW, srcH; // Orientation corrected. 219 if (isNotRotatedOr180) { 220 srcW = mOpts.outWidth; 221 srcH = mOpts.outHeight; 222 } else { 223 srcW = mOpts.outHeight; 224 srcH = mOpts.outWidth; 225 } 226 mOpts.inSampleSize = calculateSampleSize(srcW, srcH, mDestW, mDestH); 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(Bitmap.createBitmap(mDestW, mDestH, 237 Bitmap.Config.ARGB_8888)); 238 Trace.endSection(); 239 240 if (isCancelled()) { 241 return null; 242 } 243 244 if (DEBUG) { 245 Log.e(TAG, "*** allocated new bitmap in decode thread: " 246 + mInBitmap + " key=" + mKey); 247 } 248 } else { 249 if (DEBUG) { 250 Log.e(TAG, "*** reusing existing bitmap in decode thread: " 251 + mInBitmap + " key=" + mKey); 252 } 253 254 } 255 mOpts.inBitmap = mInBitmap.bmp; 256 } 257 258 if (isCancelled()) { 259 return null; 260 } 261 262 if (fd == null) { 263 in = reset(in); 264 if (in == null) { 265 return null; 266 } 267 } 268 269 Bitmap decodeResult = null; 270 final Rect srcRect = new Rect(); // Not orientation corrected. True coordinates. 271 if (CROP_DURING_DECODE) { 272 try { 273 Trace.beginSection("decodeCropped" + mOpts.inSampleSize); 274 decodeResult = decodeCropped(fd, in, orientation, srcRect); 275 } catch (IOException e) { 276 // fall through to below and try again with the non-cropping decoder 277 e.printStackTrace(); 278 } finally { 279 Trace.endSection(); 280 } 281 282 if (isCancelled()) { 283 return null; 284 } 285 } 286 287 //noinspection PointlessBooleanExpression 288 if (!CROP_DURING_DECODE || (decodeResult == null && !isCancelled())) { 289 try { 290 Trace.beginSection("decode" + mOpts.inSampleSize); 291 // disable inBitmap-- bitmap reuse doesn't work well below K 292 if (mInBitmap != null) { 293 mCache.offer(mInBitmap); 294 mInBitmap = null; 295 mOpts.inBitmap = null; 296 } 297 decodeResult = decode(fd, in); 298 } catch (IllegalArgumentException e) { 299 Log.e(TAG, "decode failed: reason='" + e.getMessage() + "' ss=" 300 + mOpts.inSampleSize); 301 302 if (mOpts.inSampleSize > 1) { 303 // try again with ss=1 304 mOpts.inSampleSize = 1; 305 decodeResult = decode(fd, in); 306 } 307 } finally { 308 Trace.endSection(); 309 } 310 311 if (isCancelled()) { 312 return null; 313 } 314 } 315 316 if (decodeResult == null) { 317 return null; 318 } 319 320 if (mInBitmap != null) { 321 result = mInBitmap; 322 // srcRect is non-empty when using the cropping BitmapRegionDecoder codepath 323 if (!srcRect.isEmpty()) { 324 result.setLogicalWidth((srcRect.right - srcRect.left) / mOpts.inSampleSize); 325 result.setLogicalHeight( 326 (srcRect.bottom - srcRect.top) / mOpts.inSampleSize); 327 } else { 328 result.setLogicalWidth(mOpts.outWidth); 329 result.setLogicalHeight(mOpts.outHeight); 330 } 331 } else { 332 // no mInBitmap means no pooling 333 result = new ReusableBitmap(decodeResult, false /* reusable */); 334 if (isNotRotatedOr180) { 335 result.setLogicalWidth(decodeResult.getWidth()); 336 result.setLogicalHeight(decodeResult.getHeight()); 337 } else { 338 result.setLogicalWidth(decodeResult.getHeight()); 339 result.setLogicalHeight(decodeResult.getWidth()); 340 } 341 } 342 result.setOrientation(orientation); 343 } catch (Exception e) { 344 e.printStackTrace(); 345 } finally { 346 if (fd != null) { 347 try { 348 fd.close(); 349 } catch (IOException ignored) { 350 } 351 } 352 if (in != null) { 353 try { 354 in.close(); 355 } catch (IOException ignored) { 356 } 357 } 358 if (result != null) { 359 result.acquireReference(); 360 mCache.put(mKey, result); 361 if (DEBUG) { 362 Log.d(TAG, "placed result in cache: key=" + mKey + " bmp=" 363 + result + " cancelled=" + isCancelled()); 364 } 365 } else if (mInBitmap != null) { 366 if (DEBUG) { 367 Log.d(TAG, "placing failed/cancelled bitmap in pool: key=" 368 + mKey + " bmp=" + mInBitmap); 369 } 370 mCache.offer(mInBitmap); 371 } 372 } 373 return result; 374 } 375 376 private Bitmap decodeCropped(final AssetFileDescriptor fd, final InputStream in, 377 final int orientation, final Rect outSrcRect) throws IOException { 378 final BitmapRegionDecoder brd; 379 if (fd != null) { 380 brd = BitmapRegionDecoder.newInstance(fd.getFileDescriptor(), true /* shareable */); 381 } else { 382 brd = BitmapRegionDecoder.newInstance(in, true /* shareable */); 383 } 384 if (isCancelled()) { 385 brd.recycle(); 386 return null; 387 } 388 389 // We want to call calculateCroppedSrcRect() on the source rectangle "as if" the 390 // orientation has been corrected. 391 final int srcW, srcH; //Orientation corrected. 392 final boolean isNotRotatedOr180 = orientation == 0 || orientation == 180; 393 if (isNotRotatedOr180) { 394 srcW = mOpts.outWidth; 395 srcH = mOpts.outHeight; 396 } else { 397 srcW = mOpts.outHeight; 398 srcH = mOpts.outWidth; 399 } 400 401 // Coordinates are orientation corrected. 402 // Center the decode on the top 1/3. 403 BitmapUtils.calculateCroppedSrcRect(srcW, srcH, mDestW, mDestH, mDestH, mOpts.inSampleSize, 404 1f / 3, true /* absoluteFraction */, 1f, outSrcRect); 405 if (DEBUG) System.out.println("rect for this decode is: " + outSrcRect 406 + " srcW/H=" + srcW + "/" + srcH 407 + " dstW/H=" + mDestW + "/" + mDestH); 408 409 // calculateCroppedSrcRect() gave us the source rectangle "as if" the orientation has 410 // been corrected. We need to decode the uncorrected source rectangle. Calculate true 411 // coordinates. 412 RectUtils.rotateRectForOrientation(orientation, new Rect(0, 0, srcW, srcH), outSrcRect); 413 414 final Bitmap result = brd.decodeRegion(outSrcRect, mOpts); 415 brd.recycle(); 416 return result; 417 } 418 419 /** 420 * Return an input stream that can be read from the beginning using the most efficient way, 421 * given an input stream that may or may not support reset(), or given null. 422 * 423 * The returned input stream may or may not be the same stream. 424 */ 425 private InputStream reset(InputStream in) throws IOException { 426 Trace.beginSection("create stream"); 427 if (in == null) { 428 in = mKey.createInputStream(); 429 } else if (in.markSupported()) { 430 in.reset(); 431 } else { 432 try { 433 in.close(); 434 } catch (IOException ignored) { 435 } 436 in = mKey.createInputStream(); 437 } 438 Trace.endSection(); 439 return in; 440 } 441 442 private Bitmap decode(AssetFileDescriptor fd, InputStream in) { 443 final Bitmap result; 444 if (fd != null) { 445 result = BitmapFactory.decodeFileDescriptor(fd.getFileDescriptor(), null, mOpts); 446 } else { 447 result = BitmapFactory.decodeStream(in, null, mOpts); 448 } 449 return result; 450 } 451 452 private static int calculateSampleSize(int srcW, int srcH, int destW, int destH) { 453 int result; 454 455 final float sz = Math.min((float) srcW / destW, (float) srcH / destH); 456 457 // round to the nearest power of two, or just truncate 458 final boolean stricter = true; 459 460 //noinspection ConstantConditions 461 if (stricter) { 462 result = (int) Math.pow(2, (int) (0.5 + (Math.log(sz) / Math.log(2)))); 463 } else { 464 result = (int) sz; 465 } 466 return Math.max(1, result); 467 } 468 469 public void cancel() { 470 cancel(true); 471 mOpts.requestCancelDecode(); 472 } 473 474 @Override 475 protected void onProgressUpdate(Void... values) { 476 mDecodeCallback.onDecodeBegin(mKey); 477 } 478 479 @Override 480 public void onPostExecute(ReusableBitmap result) { 481 mDecodeCallback.onDecodeComplete(mKey, result); 482 } 483 484 @Override 485 protected void onCancelled(ReusableBitmap result) { 486 mDecodeCallback.onDecodeCancel(mKey); 487 if (result == null) { 488 return; 489 } 490 491 result.releaseReference(); 492 if (mInBitmap == null) { 493 // not reusing bitmaps: can recycle immediately 494 result.bmp.recycle(); 495 } 496 } 497 498} 499