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