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,
    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      */
    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 
    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 
    495     public void cancel() {
    496         cancel(true);
    497         mOpts.requestCancelDecode();
    498     }
    499 
    500     @Override
    501     protected void onProgressUpdate(Void... values) {
    502         mDecodeCallback.onDecodeBegin(mKey);
    503     }
    504 
    505     @Override
    506     public void onPostExecute(ReusableBitmap result) {
    507         mDecodeCallback.onDecodeComplete(mKey, result);
    508     }
    509 
    510     @Override
    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 
    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          */
    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