DecodeTask.java revision 4309c1f708f469a5ab3ac52b6b22cc6ede1d50ff
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
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, mDecodeOpts.destH, mOpts.inSampleSize,
327                                mDecodeOpts.horizontalCenter, 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            // Cancellations can't be guaranteed to be correct, so skip the cache
444            if (!isCancelled()) {
445                // Put result in cache, regardless of null. The cache will handle null results.
446                mCache.put(mKey, result);
447            }
448            if (result != null) {
449                result.acquireReference();
450                if (DEBUG) {
451                    Log.d(TAG, "placed result in cache: key=" + mKey + " bmp="
452                        + result + " cancelled=" + isCancelled());
453                }
454            } else if (mInBitmap != null) {
455                if (DEBUG) {
456                    Log.d(TAG, "placing failed/cancelled bitmap in pool: key="
457                        + mKey + " bmp=" + mInBitmap);
458                }
459                mCache.offer(mInBitmap);
460            }
461        }
462        return result;
463    }
464
465    /**
466     * Return an input stream that can be read from the beginning using the most efficient way,
467     * given an input stream that may or may not support reset(), or given null.
468     *
469     * The returned input stream may or may not be the same stream.
470     */
471    private InputStream reset(InputStream in) throws IOException {
472        Trace.beginSection("create stream");
473        if (in == null) {
474            in = mKey.createInputStream();
475        } else if (in.markSupported()) {
476            in.reset();
477        } else {
478            try {
479                in.close();
480            } catch (IOException ignored) {
481            }
482            in = mKey.createInputStream();
483        }
484        Trace.endSection();
485        return in;
486    }
487
488    private Bitmap decode(ParcelFileDescriptor fd, InputStream in) {
489        final Bitmap result;
490        if (fd != null) {
491            result = BitmapFactory.decodeFileDescriptor(fd.getFileDescriptor(), null, mOpts);
492        } else {
493            result = BitmapFactory.decodeStream(in, null, mOpts);
494        }
495        return result;
496    }
497
498    public void cancel() {
499        cancel(true);
500        mOpts.requestCancelDecode();
501    }
502
503    @Override
504    protected void onProgressUpdate(Void... values) {
505        mDecodeCallback.onDecodeBegin(mKey);
506    }
507
508    @Override
509    public void onPostExecute(ReusableBitmap result) {
510        mDecodeCallback.onDecodeComplete(mKey, result);
511    }
512
513    @Override
514    protected void onCancelled(ReusableBitmap result) {
515        mDecodeCallback.onDecodeCancel(mKey);
516        if (result == null) {
517            return;
518        }
519
520        result.releaseReference();
521        if (mInBitmap == null) {
522            // not reusing bitmaps: can recycle immediately
523            result.bmp.recycle();
524        }
525    }
526
527    /**
528     * Parameters to pass to the DecodeTask.
529     */
530    public static class DecodeOptions {
531
532        /**
533         * Round sample size to the nearest power of 2. Depending on the source and destination
534         * dimensions, we will either truncate, in which case we decode from a bigger region and
535         * crop down, or we will round up, in which case we decode from a smaller region and scale
536         * up.
537         */
538        public static final int STRATEGY_ROUND_NEAREST = 0;
539        /**
540         * Always decode from a bigger region and crop down.
541         */
542        public static final int STRATEGY_TRUNCATE = 1;
543
544        /**
545         * Always decode from a smaller region and scale up.
546         */
547        public static final int STRATEGY_ROUND_UP = 2;
548
549        /**
550         * The destination width to decode to.
551         */
552        public int destW;
553        /**
554         * The destination height to decode to.
555         */
556        public int destH;
557        /**
558         * If the destination dimensions are smaller than the source image provided by the request
559         * key, this will determine where horizontally the destination rect will be cropped from.
560         * Value from 0f for left-most crop to 1f for right-most crop.
561         */
562        public float horizontalCenter;
563        /**
564         * If the destination dimensions are smaller than the source image provided by the request
565         * key, this will determine where vertically the destination rect will be cropped from.
566         * Value from 0f for top-most crop to 1f for bottom-most crop.
567         */
568        public float verticalCenter;
569        /**
570         * One of the STRATEGY constants.
571         */
572        public int sampleSizeStrategy;
573
574        public DecodeOptions(final int destW, final int destH) {
575            this(destW, destH, 0.5f, 0.5f, STRATEGY_ROUND_NEAREST);
576        }
577
578        /**
579         * Create new DecodeOptions with horizontally-centered cropping if applicable.
580         * @param destW The destination width to decode to.
581         * @param destH The destination height to decode to.
582         * @param verticalCenter If the destination dimensions are smaller than the source image
583         *                       provided by the request key, this will determine where vertically
584         *                       the destination rect will be cropped from.
585         * @param sampleSizeStrategy One of the STRATEGY constants.
586         */
587        public DecodeOptions(final int destW, final int destH,
588                final float verticalCenter, final int sampleSizeStrategy) {
589            this(destW, destH, 0.5f, verticalCenter, sampleSizeStrategy);
590        }
591
592        /**
593         * Create new DecodeOptions.
594         * @param destW The destination width to decode to.
595         * @param destH The destination height to decode to.
596         * @param horizontalCenter If the destination dimensions are smaller than the source image
597         *                         provided by the request key, this will determine where
598         *                         horizontally the destination rect will be cropped from.
599         * @param verticalCenter If the destination dimensions are smaller than the source image
600         *                       provided by the request key, this will determine where vertically
601         *                       the destination rect will be cropped from.
602         * @param sampleSizeStrategy One of the STRATEGY constants.
603         */
604        public DecodeOptions(final int destW, final int destH, final float horizontalCenter,
605                final float verticalCenter, final int sampleSizeStrategy) {
606            this.destW = destW;
607            this.destH = destH;
608            this.horizontalCenter = horizontalCenter;
609            this.verticalCenter = verticalCenter;
610            this.sampleSizeStrategy = sampleSizeStrategy;
611        }
612    }
613}
614