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