DecodeTask.java revision a07e0af0f1997ce3d40df6a8a9f44cb0b2e4c07f
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            if (mFactory != null) {
138                Trace.beginSection("create fd");
139                fd = mFactory.createFileDescriptor();
140                Trace.endSection();
141            } else {
142                in = reset(in);
143                if (in == null) {
144                    return null;
145                }
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