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