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