DecodeTask.java revision 9c6ac19d4a3d39b7c2992060957920118ff56a65
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            mOpts.inSampleSize = calculateSampleSize(srcW, srcH, mDecodeOpts.destW,
226                    mDecodeOpts.destH, mDecodeOpts.sampleSizeStrategy);
227            mOpts.inJustDecodeBounds = false;
228            mOpts.inMutable = true;
229            if (isJellyBeanOrAbove && orientation == 0) {
230                if (mInBitmap == null) {
231                    if (DEBUG) {
232                        Log.e(TAG, "decode thread wants a bitmap. cache dump:\n"
233                                + mCache.toDebugString());
234                    }
235                    Trace.beginSection("create reusable bitmap");
236                    mInBitmap = new ReusableBitmap(
237                            Bitmap.createBitmap(mDecodeOpts.destW, mDecodeOpts.destH,
238                                    Bitmap.Config.ARGB_8888));
239                    Trace.endSection();
240
241                    if (isCancelled()) {
242                        return null;
243                    }
244
245                    if (DEBUG) {
246                        Log.e(TAG, "*** allocated new bitmap in decode thread: "
247                                + mInBitmap + " key=" + mKey);
248                    }
249                } else {
250                    if (DEBUG) {
251                        Log.e(TAG, "*** reusing existing bitmap in decode thread: "
252                                + mInBitmap + " key=" + mKey);
253                    }
254
255                }
256                mOpts.inBitmap = mInBitmap.bmp;
257            }
258
259            if (isCancelled()) {
260                return null;
261            }
262
263            if (fd == null) {
264                in = reset(in);
265                if (in == null) {
266                    return null;
267                }
268            }
269
270            Bitmap decodeResult = null;
271            final Rect srcRect = new Rect(); // Not orientation corrected. True coordinates.
272            if (CROP_DURING_DECODE) {
273                try {
274                    Trace.beginSection("decodeCropped" + mOpts.inSampleSize);
275                    decodeResult = decodeCropped(fd, in, orientation, srcRect);
276                } catch (IOException e) {
277                    // fall through to below and try again with the non-cropping decoder
278                    e.printStackTrace();
279                } finally {
280                    Trace.endSection();
281                }
282
283                if (isCancelled()) {
284                    return null;
285                }
286            }
287
288            //noinspection PointlessBooleanExpression
289            if (!CROP_DURING_DECODE || (decodeResult == null && !isCancelled())) {
290                try {
291                    Trace.beginSection("decode" + mOpts.inSampleSize);
292                    // disable inBitmap-- bitmap reuse doesn't work well below K
293                    if (mInBitmap != null) {
294                        mCache.offer(mInBitmap);
295                        mInBitmap = null;
296                        mOpts.inBitmap = null;
297                    }
298                    decodeResult = decode(fd, in);
299                } catch (IllegalArgumentException e) {
300                    Log.e(TAG, "decode failed: reason='" + e.getMessage() + "' ss="
301                            + mOpts.inSampleSize);
302
303                    if (mOpts.inSampleSize > 1) {
304                        // try again with ss=1
305                        mOpts.inSampleSize = 1;
306                        decodeResult = decode(fd, in);
307                    }
308                } finally {
309                    Trace.endSection();
310                }
311
312                if (isCancelled()) {
313                    return null;
314                }
315            }
316
317            if (decodeResult == null) {
318                return null;
319            }
320
321            if (mInBitmap != null) {
322                result = mInBitmap;
323                // srcRect is non-empty when using the cropping BitmapRegionDecoder codepath
324                if (!srcRect.isEmpty()) {
325                    result.setLogicalWidth((srcRect.right - srcRect.left) / mOpts.inSampleSize);
326                    result.setLogicalHeight(
327                            (srcRect.bottom - srcRect.top) / mOpts.inSampleSize);
328                } else {
329                    result.setLogicalWidth(mOpts.outWidth);
330                    result.setLogicalHeight(mOpts.outHeight);
331                }
332            } else {
333                // no mInBitmap means no pooling
334                result = new ReusableBitmap(decodeResult, false /* reusable */);
335                if (isNotRotatedOr180) {
336                    result.setLogicalWidth(decodeResult.getWidth());
337                    result.setLogicalHeight(decodeResult.getHeight());
338                } else {
339                    result.setLogicalWidth(decodeResult.getHeight());
340                    result.setLogicalHeight(decodeResult.getWidth());
341                }
342            }
343            result.setOrientation(orientation);
344        } catch (Exception e) {
345            e.printStackTrace();
346        } finally {
347            if (fd != null) {
348                try {
349                    fd.close();
350                } catch (IOException ignored) {
351                }
352            }
353            if (in != null) {
354                try {
355                    in.close();
356                } catch (IOException ignored) {
357                }
358            }
359            if (result != null) {
360                result.acquireReference();
361                mCache.put(mKey, result);
362                if (DEBUG) {
363                    Log.d(TAG, "placed result in cache: key=" + mKey + " bmp="
364                        + result + " cancelled=" + isCancelled());
365                }
366            } else if (mInBitmap != null) {
367                if (DEBUG) {
368                    Log.d(TAG, "placing failed/cancelled bitmap in pool: key="
369                        + mKey + " bmp=" + mInBitmap);
370                }
371                mCache.offer(mInBitmap);
372            }
373        }
374        return result;
375    }
376
377    private Bitmap decodeCropped(final ParcelFileDescriptor fd, final InputStream in,
378            final int orientation, final Rect outSrcRect) throws IOException {
379        final BitmapRegionDecoder brd;
380        if (fd != null) {
381            brd = BitmapRegionDecoder.newInstance(fd.getFileDescriptor(), true /* shareable */);
382        } else {
383            brd = BitmapRegionDecoder.newInstance(in, true /* shareable */);
384        }
385        if (isCancelled()) {
386            brd.recycle();
387            return null;
388        }
389
390        // We want to call calculateCroppedSrcRect() on the source rectangle "as if" the
391        // orientation has been corrected.
392        final int srcW, srcH; //Orientation corrected.
393        final boolean isNotRotatedOr180 = orientation == 0 || orientation == 180;
394        if (isNotRotatedOr180) {
395            srcW = mOpts.outWidth;
396            srcH = mOpts.outHeight;
397        } else {
398            srcW = mOpts.outHeight;
399            srcH = mOpts.outWidth;
400        }
401
402        // Coordinates are orientation corrected.
403        // Center the decode on the top 1/3.
404        BitmapUtils.calculateCroppedSrcRect(srcW, srcH, mDecodeOpts.destW, mDecodeOpts.destH,
405                mDecodeOpts.destH, mOpts.inSampleSize, mDecodeOpts.verticalCenter,
406                true /* absoluteFraction */,
407                1f, outSrcRect);
408        if (DEBUG) System.out.println("rect for this decode is: " + outSrcRect
409                + " srcW/H=" + srcW + "/" + srcH
410                + " dstW/H=" + mDecodeOpts.destW + "/" + mDecodeOpts.destH);
411
412        // calculateCroppedSrcRect() gave us the source rectangle "as if" the orientation has
413        // been corrected. We need to decode the uncorrected source rectangle. Calculate true
414        // coordinates.
415        RectUtils.rotateRectForOrientation(orientation, new Rect(0, 0, srcW, srcH), outSrcRect);
416
417        final Bitmap result = brd.decodeRegion(outSrcRect, mOpts);
418        brd.recycle();
419        return result;
420    }
421
422    /**
423     * Return an input stream that can be read from the beginning using the most efficient way,
424     * given an input stream that may or may not support reset(), or given null.
425     *
426     * The returned input stream may or may not be the same stream.
427     */
428    private InputStream reset(InputStream in) throws IOException {
429        Trace.beginSection("create stream");
430        if (in == null) {
431            in = mKey.createInputStream();
432        } else if (in.markSupported()) {
433            in.reset();
434        } else {
435            try {
436                in.close();
437            } catch (IOException ignored) {
438            }
439            in = mKey.createInputStream();
440        }
441        Trace.endSection();
442        return in;
443    }
444
445    private Bitmap decode(ParcelFileDescriptor fd, InputStream in) {
446        final Bitmap result;
447        if (fd != null) {
448            result = BitmapFactory.decodeFileDescriptor(fd.getFileDescriptor(), null, mOpts);
449        } else {
450            result = BitmapFactory.decodeStream(in, null, mOpts);
451        }
452        return result;
453    }
454
455    private static int calculateSampleSize(int srcW, int srcH, int destW, int destH, int strategy) {
456        int result;
457
458        final float sz = Math.min((float) srcW / destW, (float) srcH / destH);
459
460        switch (strategy) {
461            case DecodeOptions.STRATEGY_TRUNCATE:
462                result = (int) sz;
463                break;
464            case DecodeOptions.STRATEGY_ROUND_UP:
465                result = (int) Math.ceil(sz);
466                break;
467            case DecodeOptions.STRATEGY_ROUND_NEAREST:
468            default:
469                result = (int) Math.pow(2, (int) (0.5 + (Math.log(sz) / Math.log(2))));
470                break;
471        }
472        return Math.max(1, result);
473    }
474
475    public void cancel() {
476        cancel(true);
477        mOpts.requestCancelDecode();
478    }
479
480    @Override
481    protected void onProgressUpdate(Void... values) {
482        mDecodeCallback.onDecodeBegin(mKey);
483    }
484
485    @Override
486    public void onPostExecute(ReusableBitmap result) {
487        mDecodeCallback.onDecodeComplete(mKey, result);
488    }
489
490    @Override
491    protected void onCancelled(ReusableBitmap result) {
492        mDecodeCallback.onDecodeCancel(mKey);
493        if (result == null) {
494            return;
495        }
496
497        result.releaseReference();
498        if (mInBitmap == null) {
499            // not reusing bitmaps: can recycle immediately
500            result.bmp.recycle();
501        }
502    }
503
504    public static class DecodeOptions {
505
506        /**
507         * Round sample size to the nearest power of 2. Depending on the source and destination
508         * dimensions, we will either truncate, in which case we decode from a bigger region and
509         * crop down, or we will round up, in which case we decode from a smaller region and scale
510         * up.
511         */
512        public static final int STRATEGY_ROUND_NEAREST = 0;
513        /**
514         * Always decode from a bigger region and crop down.
515         */
516        public static final int STRATEGY_TRUNCATE = 1;
517
518        /**
519         * Always decode from a smaller region and scale up.
520         */
521        public static final int STRATEGY_ROUND_UP = 2;
522
523        /**
524         * The destination width to decode to.
525         */
526        public int destW;
527        /**
528         * The destination height to decode to.
529         */
530        public int destH;
531        /**
532         * If the destination dimensions are smaller than the source image provided by the request
533         * key, this will determine where vertically the destination rect will be cropped from.
534         * Value from 0f for top-most crop to 1f for bottom-most crop.
535         */
536        public float verticalCenter;
537        /**
538         * One of the STRATEGY constants.
539         */
540        public int sampleSizeStrategy;
541
542        public DecodeOptions(final int destW, final int destH) {
543            this(destW, destH, 0.5f, STRATEGY_ROUND_NEAREST);
544        }
545
546        /**
547         * Create new DecodeOptions.
548         * @param destW The destination width to decode to.
549         * @param destH The destination height to decode to.
550         * @param verticalCenter If the destination dimensions are smaller than the source image
551         *                       provided by the request key, this will determine where vertically
552         *                       the destination rect will be cropped from.
553         * @param sampleSizeStrategy One of the STRATEGY constants.
554         */
555        public DecodeOptions(final int destW, final int destH, final float verticalCenter,
556                final int sampleSizeStrategy) {
557            this.destW = destW;
558            this.destH = destH;
559            this.verticalCenter = verticalCenter;
560            this.sampleSizeStrategy = sampleSizeStrategy;
561        }
562    }
563}
564