Home | History | Annotate | Download | only in bitmap
      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          */
     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          */
     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          */
     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    */
     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
    108     protected ReusableBitmap doInBackground(Void... params) {
    109         // enqueue the 'onDecodeBegin' signal on the main thread
    110         publishProgress();
    111 
    112         return decode();
    113     }
    114 
    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      */
    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 
    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 
    498     public void cancel() {
    499         cancel(true);
    500         mOpts.requestCancelDecode();
    501     }
    502 
    503     @Override
    504     protected void onProgressUpdate(Void... values) {
    505         mDecodeCallback.onDecodeBegin(mKey);
    506     }
    507 
    508     @Override
    509     public void onPostExecute(ReusableBitmap result) {
    510         mDecodeCallback.onDecodeComplete(mKey, result);
    511     }
    512 
    513     @Override
    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 
    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          */
    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          */
    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