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