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