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, mDecodeOpts.destH, mOpts.inSampleSize,
327                                 mDecodeOpts.horizontalCenter, 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             // Cancellations can't be guaranteed to be correct, so skip the cache
444             if (!isCancelled()) {
445                 // Put result in cache, regardless of null. The cache will handle null results.
446                 mCache.put(mKey, result);
447             }
448             if (result != null) {
449                 result.acquireReference();
450                 if (DEBUG) {
451                     Log.d(TAG, "placed result in cache: key=" + mKey + " bmp="
452                         + result + " cancelled=" + isCancelled());
453                 }
454             } else if (mInBitmap != null) {
455                 if (DEBUG) {
456                     Log.d(TAG, "placing failed/cancelled bitmap in pool: key="
457                         + mKey + " bmp=" + mInBitmap);
458                 }
459                 mCache.offer(mInBitmap);
460             }
461         }
462         return result;
463     }
464 
465     /**
466      * Return an input stream that can be read from the beginning using the most efficient way,
467      * given an input stream that may or may not support reset(), or given null.
468      *
469      * The returned input stream may or may not be the same stream.
470      */
reset(InputStream in)471     private InputStream reset(InputStream in) throws IOException {
472         Trace.beginSection("create stream");
473         if (in == null) {
474             in = mKey.createInputStream();
475         } else if (in.markSupported()) {
476             in.reset();
477         } else {
478             try {
479                 in.close();
480             } catch (IOException ignored) {
481             }
482             in = mKey.createInputStream();
483         }
484         Trace.endSection();
485         return in;
486     }
487 
decode(ParcelFileDescriptor fd, InputStream in)488     private Bitmap decode(ParcelFileDescriptor fd, InputStream in) {
489         final Bitmap result;
490         if (fd != null) {
491             result = BitmapFactory.decodeFileDescriptor(fd.getFileDescriptor(), null, mOpts);
492         } else {
493             result = BitmapFactory.decodeStream(in, null, mOpts);
494         }
495         return result;
496     }
497 
cancel()498     public void cancel() {
499         cancel(true);
500         mOpts.requestCancelDecode();
501     }
502 
503     @Override
onProgressUpdate(Void... values)504     protected void onProgressUpdate(Void... values) {
505         mDecodeCallback.onDecodeBegin(mKey);
506     }
507 
508     @Override
onPostExecute(ReusableBitmap result)509     public void onPostExecute(ReusableBitmap result) {
510         mDecodeCallback.onDecodeComplete(mKey, result);
511     }
512 
513     @Override
onCancelled(ReusableBitmap result)514     protected void onCancelled(ReusableBitmap result) {
515         mDecodeCallback.onDecodeCancel(mKey);
516         if (result == null) {
517             return;
518         }
519 
520         result.releaseReference();
521         if (mInBitmap == null) {
522             // not reusing bitmaps: can recycle immediately
523             result.bmp.recycle();
524         }
525     }
526 
527     /**
528      * Parameters to pass to the DecodeTask.
529      */
530     public static class DecodeOptions {
531 
532         /**
533          * Round sample size to the nearest power of 2. Depending on the source and destination
534          * dimensions, we will either truncate, in which case we decode from a bigger region and
535          * crop down, or we will round up, in which case we decode from a smaller region and scale
536          * up.
537          */
538         public static final int STRATEGY_ROUND_NEAREST = 0;
539         /**
540          * Always decode from a bigger region and crop down.
541          */
542         public static final int STRATEGY_TRUNCATE = 1;
543 
544         /**
545          * Always decode from a smaller region and scale up.
546          */
547         public static final int STRATEGY_ROUND_UP = 2;
548 
549         /**
550          * The destination width to decode to.
551          */
552         public int destW;
553         /**
554          * The destination height to decode to.
555          */
556         public int destH;
557         /**
558          * If the destination dimensions are smaller than the source image provided by the request
559          * key, this will determine where horizontally the destination rect will be cropped from.
560          * Value from 0f for left-most crop to 1f for right-most crop.
561          */
562         public float horizontalCenter;
563         /**
564          * If the destination dimensions are smaller than the source image provided by the request
565          * key, this will determine where vertically the destination rect will be cropped from.
566          * Value from 0f for top-most crop to 1f for bottom-most crop.
567          */
568         public float verticalCenter;
569         /**
570          * One of the STRATEGY constants.
571          */
572         public int sampleSizeStrategy;
573 
DecodeOptions(final int destW, final int destH)574         public DecodeOptions(final int destW, final int destH) {
575             this(destW, destH, 0.5f, 0.5f, STRATEGY_ROUND_NEAREST);
576         }
577 
578         /**
579          * Create new DecodeOptions with horizontally-centered cropping if applicable.
580          * @param destW The destination width to decode to.
581          * @param destH The destination height to decode to.
582          * @param verticalCenter If the destination dimensions are smaller than the source image
583          *                       provided by the request key, this will determine where vertically
584          *                       the destination rect will be cropped from.
585          * @param sampleSizeStrategy One of the STRATEGY constants.
586          */
DecodeOptions(final int destW, final int destH, final float verticalCenter, final int sampleSizeStrategy)587         public DecodeOptions(final int destW, final int destH,
588                 final float verticalCenter, final int sampleSizeStrategy) {
589             this(destW, destH, 0.5f, verticalCenter, sampleSizeStrategy);
590         }
591 
592         /**
593          * Create new DecodeOptions.
594          * @param destW The destination width to decode to.
595          * @param destH The destination height to decode to.
596          * @param horizontalCenter If the destination dimensions are smaller than the source image
597          *                         provided by the request key, this will determine where
598          *                         horizontally the destination rect will be cropped from.
599          * @param verticalCenter If the destination dimensions are smaller than the source image
600          *                       provided by the request key, this will determine where vertically
601          *                       the destination rect will be cropped from.
602          * @param sampleSizeStrategy One of the STRATEGY constants.
603          */
DecodeOptions(final int destW, final int destH, final float horizontalCenter, final float verticalCenter, final int sampleSizeStrategy)604         public DecodeOptions(final int destW, final int destH, final float horizontalCenter,
605                 final float verticalCenter, final int sampleSizeStrategy) {
606             this.destW = destW;
607             this.destH = destH;
608             this.horizontalCenter = horizontalCenter;
609             this.verticalCenter = verticalCenter;
610             this.sampleSizeStrategy = sampleSizeStrategy;
611         }
612     }
613 }
614