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