blob: d7a3cabf8c0ff5ec90a42a8cf41ebbc90ec1a9d9 [file] [log] [blame]
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 com.android.ex.photo.util.Exif;
import com.android.mail.utils.RectUtils;
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 BitmapView} 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 int mDestBufferW;
private final int mDestBufferH;
private final BitmapView mView;
private final BitmapCache mCache;
private final BitmapFactory.Options mOpts = new BitmapFactory.Options();
private ReusableBitmap mInBitmap = null;
private static final boolean CROP_DURING_DECODE = true;
public 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 BitmapView {
/**
* 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);
void onDecodeComplete(Request key, ReusableBitmap result);
void onDecodeCancel(Request key);
}
public DecodeTask(Request key, int w, int h, int bufferW, int bufferH, BitmapView view,
BitmapCache cache) {
mKey = key;
mDestW = w;
mDestH = h;
mDestBufferW = bufferW;
mDestBufferH = bufferH;
mView = 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) System.err.println(
"decode thread wants a bitmap. cache dump:\n" + mCache.toDebugString());
Trace.beginSection("create reusable bitmap");
mInBitmap = new ReusableBitmap(Bitmap.createBitmap(mDestBufferW, mDestBufferH,
Bitmap.Config.ARGB_8888));
Trace.endSection();
if (isCancelled()) {
return null;
}
if (DEBUG) System.err.println("*** allocated new bitmap in decode thread: "
+ mInBitmap + " key=" + mKey);
} else {
if (DEBUG) System.out.println("*** 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) {
System.err.println("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) System.out.println("placed result in cache: key=" + mKey + " bmp="
+ result + " cancelled=" + isCancelled());
} else if (mInBitmap != null) {
if (DEBUG) System.out.println("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) {
mView.onDecodeBegin(mKey);
}
@Override
public void onPostExecute(ReusableBitmap result) {
mView.onDecodeComplete(mKey, result);
}
@Override
protected void onCancelled(ReusableBitmap result) {
mView.onDecodeCancel(mKey);
if (result == null) {
return;
}
result.releaseReference();
if (mInBitmap == null) {
// not reusing bitmaps: can recycle immediately
result.bmp.recycle();
}
}
}