summaryrefslogtreecommitdiff
path: root/src/com/android/bitmap/DecodeTask.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/com/android/bitmap/DecodeTask.java')
-rw-r--r--src/com/android/bitmap/DecodeTask.java498
1 files changed, 498 insertions, 0 deletions
diff --git a/src/com/android/bitmap/DecodeTask.java b/src/com/android/bitmap/DecodeTask.java
new file mode 100644
index 0000000..ab2a994
--- /dev/null
+++ b/src/com/android/bitmap/DecodeTask.java
@@ -0,0 +1,498 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.bitmap;
+
+import android.content.res.AssetFileDescriptor;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.BitmapRegionDecoder;
+import android.graphics.Rect;
+import android.os.AsyncTask;
+import android.util.Log;
+
+import com.android.bitmap.util.BitmapUtils;
+import com.android.bitmap.util.Exif;
+import com.android.bitmap.util.RectUtils;
+import com.android.bitmap.util.Trace;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * Decodes an image from either a file descriptor or input stream on a worker thread. After the
+ * decode is complete, even if the task is cancelled, the result is placed in the given cache.
+ * A {@link DecodeCallback} client may be notified on decode begin and completion.
+ * <p>
+ * This class uses {@link BitmapRegionDecoder} when possible to minimize unnecessary decoding
+ * and allow bitmap reuse on Jellybean 4.1 and later.
+ * <p>
+ * GIFs are supported, but their decode does not reuse bitmaps at all. The resulting
+ * {@link ReusableBitmap} will be marked as not reusable
+ * ({@link ReusableBitmap#isEligibleForPooling()} will return false).
+ */
+public class DecodeTask extends AsyncTask<Void, Void, ReusableBitmap> {
+
+ private final Request mKey;
+ private final int mDestW;
+ private final int mDestH;
+ private final DecodeCallback mDecodeCallback;
+ private final BitmapCache mCache;
+ private final BitmapFactory.Options mOpts = new BitmapFactory.Options();
+
+ private ReusableBitmap mInBitmap = null;
+
+ private static final boolean CROP_DURING_DECODE = true;
+
+ private static final String TAG = DecodeTask.class.getSimpleName();
+ private static final boolean DEBUG = false;
+
+ /**
+ * The decode task uses this class to get input to decode. You must implement at least one of
+ * {@link #createFd()} or {@link #createInputStream()}. {@link DecodeTask} will prioritize
+ * {@link #createFd()} before falling back to {@link #createInputStream()}.
+ * <p>
+ * When {@link DecodeTask} is used in conjunction with a {@link BitmapCache}, objects of this
+ * type will also serve as cache keys to fetch cached data.
+ */
+ public interface Request {
+ AssetFileDescriptor createFd() throws IOException;
+ InputStream createInputStream() throws IOException;
+ boolean hasOrientationExif() throws IOException;
+ }
+
+ /**
+ * Callback interface for clients to be notified of decode state changes and completion.
+ */
+ public interface DecodeCallback {
+ /**
+ * Notifies that the async task's work is about to begin. Up until this point, the task
+ * may have been preempted by the scheduler or queued up by a bottlenecked executor.
+ * <p>
+ * N.B. this method runs on the UI thread.
+ */
+ void onDecodeBegin(Request key);
+ /**
+ * The task is now complete and the ReusableBitmap is available for use. Clients should
+ * double check that the request matches what the client is expecting.
+ */
+ void onDecodeComplete(Request key, ReusableBitmap result);
+ /**
+ * The task has been canceled, and {@link #onDecodeComplete(Request, ReusableBitmap)} will
+ * not be called.
+ */
+ void onDecodeCancel(Request key);
+ }
+
+ public DecodeTask(Request key, int w, int h, DecodeCallback view,
+ BitmapCache cache) {
+ mKey = key;
+ mDestW = w;
+ mDestH = h;
+ mDecodeCallback = view;
+ mCache = cache;
+ }
+
+ @Override
+ protected ReusableBitmap doInBackground(Void... params) {
+ // enqueue the 'onDecodeBegin' signal on the main thread
+ publishProgress();
+
+ return decode();
+ }
+
+ public ReusableBitmap decode() {
+ if (isCancelled()) {
+ return null;
+ }
+
+ ReusableBitmap result = null;
+ AssetFileDescriptor fd = null;
+ InputStream in = null;
+ try {
+ final boolean isJellyBeanOrAbove = android.os.Build.VERSION.SDK_INT
+ >= android.os.Build.VERSION_CODES.JELLY_BEAN;
+ // This blocks during fling when the pool is empty. We block early to avoid jank.
+ if (isJellyBeanOrAbove) {
+ Trace.beginSection("poll for reusable bitmap");
+ mInBitmap = mCache.poll();
+ Trace.endSection();
+
+ if (isCancelled()) {
+ return null;
+ }
+ }
+
+ Trace.beginSection("create fd and stream");
+ fd = mKey.createFd();
+ Trace.endSection();
+ if (fd == null) {
+ in = reset(in);
+ if (in == null) {
+ return null;
+ }
+ }
+
+ Trace.beginSection("get bytesize");
+ final long byteSize;
+ if (fd != null) {
+ byteSize = fd.getLength();
+ } else {
+ byteSize = -1;
+ }
+ Trace.endSection();
+
+ Trace.beginSection("get orientation");
+ final int orientation;
+ if (mKey.hasOrientationExif()) {
+ if (fd != null) {
+ // Creating an input stream from the file descriptor makes it useless
+ // afterwards.
+ Trace.beginSection("create fd and stream");
+ final AssetFileDescriptor orientationFd = mKey.createFd();
+ in = orientationFd.createInputStream();
+ Trace.endSection();
+ }
+ orientation = Exif.getOrientation(in, byteSize);
+ if (fd != null) {
+ try {
+ // Close the temporary file descriptor.
+ in.close();
+ } catch (IOException ignored) {
+ }
+ }
+ } else {
+ orientation = 0;
+ }
+ final boolean isNotRotatedOr180 = orientation == 0 || orientation == 180;
+ Trace.endSection();
+
+ if (orientation != 0) {
+ // disable inBitmap-- bitmap reuse doesn't work with different decode regions due
+ // to orientation
+ if (mInBitmap != null) {
+ mCache.offer(mInBitmap);
+ mInBitmap = null;
+ mOpts.inBitmap = null;
+ }
+ }
+
+ if (isCancelled()) {
+ return null;
+ }
+
+ if (fd == null) {
+ in = reset(in);
+ if (in == null) {
+ return null;
+ }
+ }
+
+ Trace.beginSection("decodeBounds");
+ mOpts.inJustDecodeBounds = true;
+ if (fd != null) {
+ BitmapFactory.decodeFileDescriptor(fd.getFileDescriptor(), null, mOpts);
+ } else {
+ BitmapFactory.decodeStream(in, null, mOpts);
+ }
+ Trace.endSection();
+
+ if (isCancelled()) {
+ return null;
+ }
+
+ // We want to calculate the sample size "as if" the orientation has been corrected.
+ final int srcW, srcH; // Orientation corrected.
+ if (isNotRotatedOr180) {
+ srcW = mOpts.outWidth;
+ srcH = mOpts.outHeight;
+ } else {
+ srcW = mOpts.outHeight;
+ srcH = mOpts.outWidth;
+ }
+ mOpts.inSampleSize = calculateSampleSize(srcW, srcH, mDestW, mDestH);
+ mOpts.inJustDecodeBounds = false;
+ mOpts.inMutable = true;
+ if (isJellyBeanOrAbove && orientation == 0) {
+ if (mInBitmap == null) {
+ if (DEBUG) {
+ Log.e(TAG, "decode thread wants a bitmap. cache dump:\n"
+ + mCache.toDebugString());
+ }
+ Trace.beginSection("create reusable bitmap");
+ mInBitmap = new ReusableBitmap(Bitmap.createBitmap(mDestW, mDestH,
+ Bitmap.Config.ARGB_8888));
+ Trace.endSection();
+
+ if (isCancelled()) {
+ return null;
+ }
+
+ if (DEBUG) {
+ Log.e(TAG, "*** allocated new bitmap in decode thread: "
+ + mInBitmap + " key=" + mKey);
+ }
+ } else {
+ if (DEBUG) {
+ Log.e(TAG, "*** reusing existing bitmap in decode thread: "
+ + mInBitmap + " key=" + mKey);
+ }
+
+ }
+ mOpts.inBitmap = mInBitmap.bmp;
+ }
+
+ if (isCancelled()) {
+ return null;
+ }
+
+ if (fd == null) {
+ in = reset(in);
+ if (in == null) {
+ return null;
+ }
+ }
+
+ Bitmap decodeResult = null;
+ final Rect srcRect = new Rect(); // Not orientation corrected. True coordinates.
+ if (CROP_DURING_DECODE) {
+ try {
+ Trace.beginSection("decodeCropped" + mOpts.inSampleSize);
+ decodeResult = decodeCropped(fd, in, orientation, srcRect);
+ } catch (IOException e) {
+ // fall through to below and try again with the non-cropping decoder
+ e.printStackTrace();
+ } finally {
+ Trace.endSection();
+ }
+
+ if (isCancelled()) {
+ return null;
+ }
+ }
+
+ //noinspection PointlessBooleanExpression
+ if (!CROP_DURING_DECODE || (decodeResult == null && !isCancelled())) {
+ try {
+ Trace.beginSection("decode" + mOpts.inSampleSize);
+ // disable inBitmap-- bitmap reuse doesn't work well below K
+ if (mInBitmap != null) {
+ mCache.offer(mInBitmap);
+ mInBitmap = null;
+ mOpts.inBitmap = null;
+ }
+ decodeResult = decode(fd, in);
+ } catch (IllegalArgumentException e) {
+ Log.e(TAG, "decode failed: reason='" + e.getMessage() + "' ss="
+ + mOpts.inSampleSize);
+
+ if (mOpts.inSampleSize > 1) {
+ // try again with ss=1
+ mOpts.inSampleSize = 1;
+ decodeResult = decode(fd, in);
+ }
+ } finally {
+ Trace.endSection();
+ }
+
+ if (isCancelled()) {
+ return null;
+ }
+ }
+
+ if (decodeResult == null) {
+ return null;
+ }
+
+ if (mInBitmap != null) {
+ result = mInBitmap;
+ // srcRect is non-empty when using the cropping BitmapRegionDecoder codepath
+ if (!srcRect.isEmpty()) {
+ result.setLogicalWidth((srcRect.right - srcRect.left) / mOpts.inSampleSize);
+ result.setLogicalHeight(
+ (srcRect.bottom - srcRect.top) / mOpts.inSampleSize);
+ } else {
+ result.setLogicalWidth(mOpts.outWidth);
+ result.setLogicalHeight(mOpts.outHeight);
+ }
+ } else {
+ // no mInBitmap means no pooling
+ result = new ReusableBitmap(decodeResult, false /* reusable */);
+ if (isNotRotatedOr180) {
+ result.setLogicalWidth(decodeResult.getWidth());
+ result.setLogicalHeight(decodeResult.getHeight());
+ } else {
+ result.setLogicalWidth(decodeResult.getHeight());
+ result.setLogicalHeight(decodeResult.getWidth());
+ }
+ }
+ result.setOrientation(orientation);
+ } catch (Exception e) {
+ e.printStackTrace();
+ } finally {
+ if (fd != null) {
+ try {
+ fd.close();
+ } catch (IOException ignored) {
+ }
+ }
+ if (in != null) {
+ try {
+ in.close();
+ } catch (IOException ignored) {
+ }
+ }
+ if (result != null) {
+ result.acquireReference();
+ mCache.put(mKey, result);
+ if (DEBUG) {
+ Log.d(TAG, "placed result in cache: key=" + mKey + " bmp="
+ + result + " cancelled=" + isCancelled());
+ }
+ } else if (mInBitmap != null) {
+ if (DEBUG) {
+ Log.d(TAG, "placing failed/cancelled bitmap in pool: key="
+ + mKey + " bmp=" + mInBitmap);
+ }
+ mCache.offer(mInBitmap);
+ }
+ }
+ return result;
+ }
+
+ private Bitmap decodeCropped(final AssetFileDescriptor fd, final InputStream in,
+ final int orientation, final Rect outSrcRect) throws IOException {
+ final BitmapRegionDecoder brd;
+ if (fd != null) {
+ brd = BitmapRegionDecoder.newInstance(fd.getFileDescriptor(), true /* shareable */);
+ } else {
+ brd = BitmapRegionDecoder.newInstance(in, true /* shareable */);
+ }
+ if (isCancelled()) {
+ brd.recycle();
+ return null;
+ }
+
+ // We want to call calculateCroppedSrcRect() on the source rectangle "as if" the
+ // orientation has been corrected.
+ final int srcW, srcH; //Orientation corrected.
+ final boolean isNotRotatedOr180 = orientation == 0 || orientation == 180;
+ if (isNotRotatedOr180) {
+ srcW = mOpts.outWidth;
+ srcH = mOpts.outHeight;
+ } else {
+ srcW = mOpts.outHeight;
+ srcH = mOpts.outWidth;
+ }
+
+ // Coordinates are orientation corrected.
+ // Center the decode on the top 1/3.
+ BitmapUtils.calculateCroppedSrcRect(srcW, srcH, mDestW, mDestH, mDestH, mOpts.inSampleSize,
+ 1f / 3, true /* absoluteFraction */, 1f, outSrcRect);
+ if (DEBUG) System.out.println("rect for this decode is: " + outSrcRect
+ + " srcW/H=" + srcW + "/" + srcH
+ + " dstW/H=" + mDestW + "/" + mDestH);
+
+ // calculateCroppedSrcRect() gave us the source rectangle "as if" the orientation has
+ // been corrected. We need to decode the uncorrected source rectangle. Calculate true
+ // coordinates.
+ RectUtils.rotateRectForOrientation(orientation, new Rect(0, 0, srcW, srcH), outSrcRect);
+
+ final Bitmap result = brd.decodeRegion(outSrcRect, mOpts);
+ brd.recycle();
+ return result;
+ }
+
+ /**
+ * Return an input stream that can be read from the beginning using the most efficient way,
+ * given an input stream that may or may not support reset(), or given null.
+ *
+ * The returned input stream may or may not be the same stream.
+ */
+ private InputStream reset(InputStream in) throws IOException {
+ Trace.beginSection("create stream");
+ if (in == null) {
+ in = mKey.createInputStream();
+ } else if (in.markSupported()) {
+ in.reset();
+ } else {
+ try {
+ in.close();
+ } catch (IOException ignored) {
+ }
+ in = mKey.createInputStream();
+ }
+ Trace.endSection();
+ return in;
+ }
+
+ private Bitmap decode(AssetFileDescriptor fd, InputStream in) {
+ final Bitmap result;
+ if (fd != null) {
+ result = BitmapFactory.decodeFileDescriptor(fd.getFileDescriptor(), null, mOpts);
+ } else {
+ result = BitmapFactory.decodeStream(in, null, mOpts);
+ }
+ return result;
+ }
+
+ private static int calculateSampleSize(int srcW, int srcH, int destW, int destH) {
+ int result;
+
+ final float sz = Math.min((float) srcW / destW, (float) srcH / destH);
+
+ // round to the nearest power of two, or just truncate
+ final boolean stricter = true;
+
+ //noinspection ConstantConditions
+ if (stricter) {
+ result = (int) Math.pow(2, (int) (0.5 + (Math.log(sz) / Math.log(2))));
+ } else {
+ result = (int) sz;
+ }
+ return Math.max(1, result);
+ }
+
+ public void cancel() {
+ cancel(true);
+ mOpts.requestCancelDecode();
+ }
+
+ @Override
+ protected void onProgressUpdate(Void... values) {
+ mDecodeCallback.onDecodeBegin(mKey);
+ }
+
+ @Override
+ public void onPostExecute(ReusableBitmap result) {
+ mDecodeCallback.onDecodeComplete(mKey, result);
+ }
+
+ @Override
+ protected void onCancelled(ReusableBitmap result) {
+ mDecodeCallback.onDecodeCancel(mKey);
+ if (result == null) {
+ return;
+ }
+
+ result.releaseReference();
+ if (mInBitmap == null) {
+ // not reusing bitmaps: can recycle immediately
+ result.bmp.recycle();
+ }
+ }
+
+}