/* * 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.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.BitmapRegionDecoder; import android.graphics.Rect; import android.os.AsyncTask; import android.os.ParcelFileDescriptor; import android.os.ParcelFileDescriptor.AutoCloseInputStream; import android.util.Log; import com.android.bitmap.RequestKey.FileDescriptorFactory; 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. *
* This class uses {@link BitmapRegionDecoder} when possible to minimize unnecessary decoding * and allow bitmap reuse on Jellybean 4.1 and later. *
* 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
* N.B. this method runs on the UI thread.
*/
void onDecodeBegin(RequestKey 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(RequestKey key, ReusableBitmap result);
/**
* The task has been canceled, and {@link #onDecodeComplete(RequestKey, ReusableBitmap)}
* will not be called.
*/
void onDecodeCancel(RequestKey key);
}
/**
* Create new DecodeTask.
*
* @param requestKey The request to decode, also the key to use for the cache.
* @param decodeOpts The decode options.
* @param factory The factory to obtain file descriptors to decode from. If this factory is
* null, then we will decode from requestKey.createInputStream().
* @param callback The callback to notify of decode state changes.
* @param cache The cache and pool.
*/
public DecodeTask(RequestKey requestKey, DecodeOptions decodeOpts,
FileDescriptorFactory factory, DecodeCallback callback, BitmapCache cache) {
mKey = requestKey;
mDecodeOpts = decodeOpts;
mFactory = factory;
mDecodeCallback = callback;
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;
ParcelFileDescriptor fd = null;
InputStream in = null;
try {
if (mFactory != null) {
Trace.beginSection("create fd");
fd = mFactory.createFileDescriptor();
Trace.endSection();
} else {
in = reset(in);
if (in == null) {
return null;
}
if (isCancelled()) {
return null;
}
}
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("get bytesize");
final long byteSize;
if (fd != null) {
byteSize = fd.getStatSize();
} 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 orientation fd and stream");
final ParcelFileDescriptor orientationFd = mFactory.createFileDescriptor();
in = new AutoCloseInputStream(orientationFd);
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;
}
if (isCancelled()) {
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;
}
// BEGIN MANUAL-INLINE calculateSampleSize()
final float sz = Math
.min((float) srcW / mDecodeOpts.destW, (float) srcH / mDecodeOpts.destH);
final int sampleSize;
switch (mDecodeOpts.sampleSizeStrategy) {
case DecodeOptions.STRATEGY_TRUNCATE:
sampleSize = (int) sz;
break;
case DecodeOptions.STRATEGY_ROUND_UP:
sampleSize = (int) Math.ceil(sz);
break;
case DecodeOptions.STRATEGY_ROUND_NEAREST:
default:
sampleSize = (int) Math.pow(2, (int) (0.5 + (Math.log(sz) / Math.log(2))));
break;
}
mOpts.inSampleSize = Math.max(1, sampleSize);
// END MANUAL-INLINE calculateSampleSize()
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(mDecodeOpts.destW, mDecodeOpts.destH,
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;
}
if (isCancelled()) {
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);
// BEGIN MANUAL INLINE decodeCropped()
final BitmapRegionDecoder brd;
if (fd != null) {
brd = BitmapRegionDecoder
.newInstance(fd.getFileDescriptor(), true /* shareable */);
} else {
brd = BitmapRegionDecoder.newInstance(in, true /* shareable */);
}
final Bitmap bitmap;
if (isCancelled()) {
bitmap = null;
} else {
// We want to call calculateCroppedSrcRect() on the source rectangle "as
// if" the orientation has been corrected.
// Center the decode on the top 1/3.
BitmapUtils.calculateCroppedSrcRect(srcW, srcH, mDecodeOpts.destW,
mDecodeOpts.destH,
mDecodeOpts.destH, mOpts.inSampleSize, mDecodeOpts.verticalCenter,
true /* absoluteFraction */,
1f, srcRect);
if (DEBUG) {
System.out.println("rect for this decode is: " + srcRect
+ " srcW/H=" + srcW + "/" + srcH
+ " dstW/H=" + mDecodeOpts.destW + "/" + mDecodeOpts.destH);
}
// 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),
srcRect);
bitmap = brd.decodeRegion(srcRect, mOpts);
}
brd.recycle();
// END MANUAL INLINE decodeCropped()
decodeResult = bitmap;
} catch (IOException e) {
// fall through to below and try again with the non-cropping decoder
if (fd == null) {
in = reset(in);
if (in == null) {
return null;
}
if (isCancelled()) {
return null;
}
}
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) {
}
}
// Put result in cache, regardless of null. The cache will handle null results.
mCache.put(mKey, result);
if (result != null) {
result.acquireReference();
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;
}
/**
* 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(ParcelFileDescriptor 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;
}
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();
}
}
/**
* Parameters to pass to the DecodeTask.
*/
public static class DecodeOptions {
/**
* Round sample size to the nearest power of 2. Depending on the source and destination
* dimensions, we will either truncate, in which case we decode from a bigger region and
* crop down, or we will round up, in which case we decode from a smaller region and scale
* up.
*/
public static final int STRATEGY_ROUND_NEAREST = 0;
/**
* Always decode from a bigger region and crop down.
*/
public static final int STRATEGY_TRUNCATE = 1;
/**
* Always decode from a smaller region and scale up.
*/
public static final int STRATEGY_ROUND_UP = 2;
/**
* The destination width to decode to.
*/
public int destW;
/**
* The destination height to decode to.
*/
public int destH;
/**
* If the destination dimensions are smaller than the source image provided by the request
* key, this will determine where vertically the destination rect will be cropped from.
* Value from 0f for top-most crop to 1f for bottom-most crop.
*/
public float verticalCenter;
/**
* One of the STRATEGY constants.
*/
public int sampleSizeStrategy;
public DecodeOptions(final int destW, final int destH) {
this(destW, destH, 0.5f, STRATEGY_ROUND_NEAREST);
}
/**
* Create new DecodeOptions.
* @param destW The destination width to decode to.
* @param destH The destination height to decode to.
* @param verticalCenter If the destination dimensions are smaller than the source image
* provided by the request key, this will determine where vertically
* the destination rect will be cropped from.
* @param sampleSizeStrategy One of the STRATEGY constants.
*/
public DecodeOptions(final int destW, final int destH, final float verticalCenter,
final int sampleSizeStrategy) {
this.destW = destW;
this.destH = destH;
this.verticalCenter = verticalCenter;
this.sampleSizeStrategy = sampleSizeStrategy;
}
}
}