Support using ImageDecoder with ContentResolver + URI

Bug: 63909536
Test: CTS: I0f36ce34c968fd7fae4d8edebabea3a421859615

Add ImageDecoder.createSource(ContentResolver, URI), allowing a client
to decode images from files, content, resources, etc.

Prefer using a file descriptor to using an InputStream so the input can
be cheaply seeked and rewound if necessary.

Make ImageDecoder implement AutoCloseable to handle closing the input.

Make decodeDrawable/decodeBitmap always return an object or throw an
IOException. Avoid checking for a file in the Source constructor.

Fix a bug where inner Exception classes were not static.

Update JavaInputStreamAdaptor to be usable by ImageDecoder:
- previously it always swallowed exceptions. Allow them to propagate
  (optionally) so that they can be reported back to the client.
- Add refs to the InputStream and byte[]. ImageDecoder returns from
  native and then uses the JavaInputStreamAdaptor again, making the
  local refs go out of scope.
- Hold on to the JavaVM and convert to the JNIEnv when necessary. Pass
  local env pointers to avoid looking it up multiple times in one call.
- If an exception is thrown inside the doRead() loop, return the number
  of bytes successfully read.

Change-Id: I869dad55521cf942efd010c06baf3f44c1c08374
This commit is contained in:
Leon Scroggins III
2017-12-11 13:47:23 -05:00
parent 5c72a77bca
commit ed074fd700
4 changed files with 413 additions and 200 deletions

View File

@@ -11,21 +11,62 @@
static jmethodID gInputStream_readMethodID;
static jmethodID gInputStream_skipMethodID;
// FIXME: Share with ByteBufferStreamAdaptor.cpp?
static JNIEnv* get_env_or_die(JavaVM* jvm) {
JNIEnv* env;
if (jvm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) {
LOG_ALWAYS_FATAL("Failed to get JNIEnv for JavaVM: %p", jvm);
}
return env;
}
/**
* Wrapper for a Java InputStream.
*/
class JavaInputStreamAdaptor : public SkStream {
JavaInputStreamAdaptor(JavaVM* jvm, jobject js, jbyteArray ar, jint capacity,
bool swallowExceptions)
: fJvm(jvm)
, fJavaInputStream(js)
, fJavaByteArray(ar)
, fCapacity(capacity)
, fBytesRead(0)
, fIsAtEnd(false)
, fSwallowExceptions(swallowExceptions) {}
public:
JavaInputStreamAdaptor(JNIEnv* env, jobject js, jbyteArray ar)
: fEnv(env), fJavaInputStream(js), fJavaByteArray(ar) {
SkASSERT(ar);
fCapacity = env->GetArrayLength(ar);
SkASSERT(fCapacity > 0);
fBytesRead = 0;
fIsAtEnd = false;
static JavaInputStreamAdaptor* Create(JNIEnv* env, jobject js, jbyteArray ar,
bool swallowExceptions) {
JavaVM* jvm;
LOG_ALWAYS_FATAL_IF(env->GetJavaVM(&jvm) != JNI_OK);
js = env->NewGlobalRef(js);
if (!js) {
return nullptr;
}
ar = (jbyteArray) env->NewGlobalRef(ar);
if (!ar) {
env->DeleteGlobalRef(js);
return nullptr;
}
jint capacity = env->GetArrayLength(ar);
return new JavaInputStreamAdaptor(jvm, js, ar, capacity, swallowExceptions);
}
virtual size_t read(void* buffer, size_t size) {
~JavaInputStreamAdaptor() override {
auto* env = get_env_or_die(fJvm);
env->DeleteGlobalRef(fJavaInputStream);
env->DeleteGlobalRef(fJavaByteArray);
}
size_t read(void* buffer, size_t size) override {
auto* env = get_env_or_die(fJvm);
if (!fSwallowExceptions && checkException(env)) {
// Just in case the caller did not clear from a previous exception.
return 0;
}
if (NULL == buffer) {
if (0 == size) {
return 0;
@@ -36,10 +77,10 @@ public:
*/
size_t amountSkipped = 0;
do {
size_t amount = this->doSkip(size - amountSkipped);
size_t amount = this->doSkip(size - amountSkipped, env);
if (0 == amount) {
char tmp;
amount = this->doRead(&tmp, 1);
amount = this->doRead(&tmp, 1, env);
if (0 == amount) {
// if read returned 0, we're at EOF
fIsAtEnd = true;
@@ -51,16 +92,13 @@ public:
return amountSkipped;
}
}
return this->doRead(buffer, size);
return this->doRead(buffer, size, env);
}
virtual bool isAtEnd() const {
return fIsAtEnd;
}
bool isAtEnd() const override { return fIsAtEnd; }
private:
size_t doRead(void* buffer, size_t size) {
JNIEnv* env = fEnv;
size_t doRead(void* buffer, size_t size, JNIEnv* env) {
size_t bytesRead = 0;
// read the bytes
do {
@@ -75,13 +113,9 @@ private:
jint n = env->CallIntMethod(fJavaInputStream,
gInputStream_readMethodID, fJavaByteArray, 0, requested);
if (env->ExceptionCheck()) {
env->ExceptionDescribe();
env->ExceptionClear();
if (checkException(env)) {
SkDebugf("---- read threw an exception\n");
// Consider the stream to be at the end, since there was an error.
fIsAtEnd = true;
return 0;
return bytesRead;
}
if (n < 0) { // n == 0 should not be possible, see InputStream read() specifications.
@@ -91,14 +125,9 @@ private:
env->GetByteArrayRegion(fJavaByteArray, 0, n,
reinterpret_cast<jbyte*>(buffer));
if (env->ExceptionCheck()) {
env->ExceptionDescribe();
env->ExceptionClear();
if (checkException(env)) {
SkDebugf("---- read:GetByteArrayRegion threw an exception\n");
// The error was not with the stream itself, but consider it to be at the
// end, since we do not have a way to recover.
fIsAtEnd = true;
return 0;
return bytesRead;
}
buffer = (void*)((char*)buffer + n);
@@ -110,14 +139,10 @@ private:
return bytesRead;
}
size_t doSkip(size_t size) {
JNIEnv* env = fEnv;
size_t doSkip(size_t size, JNIEnv* env) {
jlong skipped = env->CallLongMethod(fJavaInputStream,
gInputStream_skipMethodID, (jlong)size);
if (env->ExceptionCheck()) {
env->ExceptionDescribe();
env->ExceptionClear();
if (checkException(env)) {
SkDebugf("------- skip threw an exception\n");
return 0;
}
@@ -128,20 +153,37 @@ private:
return (size_t)skipped;
}
JNIEnv* fEnv;
jobject fJavaInputStream; // the caller owns this object
jbyteArray fJavaByteArray; // the caller owns this object
jint fCapacity;
bool checkException(JNIEnv* env) {
if (!env->ExceptionCheck()) {
return false;
}
env->ExceptionDescribe();
if (fSwallowExceptions) {
env->ExceptionClear();
}
// There is no way to recover from the error, so consider the stream
// to be at the end.
fIsAtEnd = true;
return true;
}
JavaVM* fJvm;
jobject fJavaInputStream;
jbyteArray fJavaByteArray;
const jint fCapacity;
size_t fBytesRead;
bool fIsAtEnd;
const bool fSwallowExceptions;
};
SkStream* CreateJavaInputStreamAdaptor(JNIEnv* env, jobject stream,
jbyteArray storage) {
return new JavaInputStreamAdaptor(env, stream, storage);
SkStream* CreateJavaInputStreamAdaptor(JNIEnv* env, jobject stream, jbyteArray storage,
bool swallowExceptions) {
return JavaInputStreamAdaptor::Create(env, stream, storage, swallowExceptions);
}
static SkMemoryStream* adaptor_to_mem_stream(SkStream* stream) {
SkASSERT(stream != NULL);
size_t bufferSize = 4096;

View File

@@ -16,13 +16,16 @@ class SkWStream;
* @param stream Pointer to Java InputStream.
* @param storage Java byte array for retrieving data from the
* Java InputStream.
* @param swallowExceptions Whether to call ExceptionClear() after
* an Exception is thrown. If false, it is up to the client to
* clear or propagate the exception.
* @return SkStream Simple subclass of SkStream which supports its
* basic methods like reading. Only valid until the calling
* function returns, since the Java InputStream is not managed
* by the SkStream.
*/
SkStream* CreateJavaInputStreamAdaptor(JNIEnv* env, jobject stream,
jbyteArray storage);
SkStream* CreateJavaInputStreamAdaptor(JNIEnv* env, jobject stream, jbyteArray storage,
bool swallowExceptions = true);
/**
* Copy a Java InputStream. The result will be rewindable.
@@ -33,10 +36,8 @@ SkStream* CreateJavaInputStreamAdaptor(JNIEnv* env, jobject stream,
* @return SkStreamRewindable The data in stream will be copied
* to a new SkStreamRewindable.
*/
SkStreamRewindable* CopyJavaInputStream(JNIEnv* env, jobject stream,
jbyteArray storage);
SkStreamRewindable* CopyJavaInputStream(JNIEnv* env, jobject stream, jbyteArray storage);
SkWStream* CreateJavaOutputStreamAdaptor(JNIEnv* env, jobject stream,
jbyteArray storage);
SkWStream* CreateJavaOutputStreamAdaptor(JNIEnv* env, jobject stream, jbyteArray storage);
#endif // _ANDROID_GRAPHICS_CREATE_JAVA_OUTPUT_STREAM_ADAPTOR_H_

View File

@@ -16,6 +16,7 @@
#include "Bitmap.h"
#include "ByteBufferStreamAdaptor.h"
#include "CreateJavaOutputStreamAdaptor.h"
#include "GraphicsJNI.h"
#include "NinePatchPeeker.h"
#include "Utils.h"
@@ -26,10 +27,12 @@
#include <SkAndroidCodec.h>
#include <SkEncodedImageFormat.h>
#include <SkFrontBufferedStream.h>
#include <SkStream.h>
#include <androidfw/Asset.h>
#include <jni.h>
#include <sys/stat.h>
using namespace android;
@@ -69,15 +72,15 @@ struct ImageDecoder {
static jobject native_create(JNIEnv* env, std::unique_ptr<SkStream> stream) {
if (!stream.get()) {
return nullObjectReturn("Failed to create a stream");
doThrowIOE(env, "Failed to create a stream");
return nullptr;
}
std::unique_ptr<ImageDecoder> decoder(new ImageDecoder);
decoder->mCodec = SkAndroidCodec::MakeFromStream(std::move(stream), &decoder->mPeeker);
if (!decoder->mCodec.get()) {
// FIXME: Add an error code to SkAndroidCodec::MakeFromStream, like
// SkCodec? Then this can print a more informative error message.
// (Or we can print one from within SkCodec.)
ALOGE("Failed to create an SkCodec");
// FIXME: (b/71578461) Use the error message from
// SkCodec::MakeFromStream to report a more informative error message.
doThrowIOE(env, "Failed to create an SkCodec");
return nullptr;
}
@@ -88,7 +91,52 @@ static jobject native_create(JNIEnv* env, std::unique_ptr<SkStream> stream) {
reinterpret_cast<jlong>(decoder.release()), width, height);
}
static jobject ImageDecoder_nCreate(JNIEnv* env, jobject /*clazz*/, jlong assetPtr) {
static jobject ImageDecoder_nCreateFd(JNIEnv* env, jobject /*clazz*/,
jobject fileDescriptor) {
int descriptor = jniGetFDFromFileDescriptor(env, fileDescriptor);
struct stat fdStat;
if (fstat(descriptor, &fdStat) == -1) {
doThrowIOE(env, "broken file descriptor; fstat returned -1");
return nullptr;
}
int dupDescriptor = dup(descriptor);
FILE* file = fdopen(dupDescriptor, "r");
if (file == NULL) {
close(dupDescriptor);
doThrowIOE(env, "Could not open file");
return nullptr;
}
std::unique_ptr<SkFILEStream> fileStream(new SkFILEStream(file));
if (::lseek(descriptor, 0, SEEK_CUR) == 0) {
return native_create(env, std::move(fileStream));
}
// FIXME: This allows us to pretend the current location is the beginning,
// but it would be better if SkFILEStream allowed treating its starting
// point as the beginning.
std::unique_ptr<SkStream> stream(SkFrontBufferedStream::Make(std::move(fileStream),
SkCodec::MinBufferedBytesNeeded()));
return native_create(env, std::move(stream));
}
static jobject ImageDecoder_nCreateInputStream(JNIEnv* env, jobject /*clazz*/,
jobject is, jbyteArray storage) {
std::unique_ptr<SkStream> stream(CreateJavaInputStreamAdaptor(env, is, storage, false));
if (!stream.get()) {
doThrowIOE(env, "Failed to create stream!");
return nullptr;
}
std::unique_ptr<SkStream> bufferedStream(
SkFrontBufferedStream::Make(std::move(stream),
SkCodec::MinBufferedBytesNeeded()));
return native_create(env, std::move(bufferedStream));
}
static jobject ImageDecoder_nCreateAsset(JNIEnv* env, jobject /*clazz*/, jlong assetPtr) {
Asset* asset = reinterpret_cast<Asset*>(assetPtr);
std::unique_ptr<SkStream> stream(new AssetStreamAdaptor(asset));
return native_create(env, std::move(stream));
@@ -99,6 +147,7 @@ static jobject ImageDecoder_nCreateByteBuffer(JNIEnv* env, jobject /*clazz*/, jo
std::unique_ptr<SkStream> stream = CreateByteBufferStreamAdaptor(env, jbyteBuffer,
initialPosition, limit);
if (!stream) {
doThrowIOE(env, "Failed to read ByteBuffer");
return nullptr;
}
return native_create(env, std::move(stream));
@@ -114,6 +163,7 @@ static bool supports_any_down_scale(const SkAndroidCodec* codec) {
return codec->getEncodedFormat() == SkEncodedImageFormat::kWEBP;
}
// This method should never return null. Instead, it should throw an exception.
static jobject ImageDecoder_nDecodeBitmap(JNIEnv* env, jobject /*clazz*/, jlong nativePtr,
jobject jcallback, jobject jpostProcess,
jint desiredWidth, jint desiredHeight, jobject jsubset,
@@ -165,7 +215,8 @@ static jobject ImageDecoder_nDecodeBitmap(JNIEnv* env, jobject /*clazz*/, jlong
case kOpaque_SkAlphaType:
break;
case kUnknown_SkAlphaType:
return nullObjectReturn("Unknown alpha type");
doThrowIOE(env, "Unknown alpha type");
return nullptr;
}
SkColorType colorType = kN32_SkColorType;
@@ -200,7 +251,8 @@ static jobject ImageDecoder_nDecodeBitmap(JNIEnv* env, jobject /*clazz*/, jlong
bitmapInfo = bitmapInfo.makeColorType(kAlpha_8_SkColorType);
}
if (!bm.setInfo(bitmapInfo)) {
return nullObjectReturn("Failed to setInfo properly");
doThrowIOE(env, "Failed to setInfo properly");
return nullptr;
}
sk_sp<Bitmap> nativeBitmap;
@@ -213,35 +265,44 @@ static jobject ImageDecoder_nDecodeBitmap(JNIEnv* env, jobject /*clazz*/, jlong
nativeBitmap = Bitmap::allocateHeapBitmap(&bm);
}
if (!nativeBitmap) {
ALOGE("OOM allocating Bitmap with dimensions %i x %i",
decodeInfo.width(), decodeInfo.height());
doThrowOOME(env);
SkString msg;
msg.printf("OOM allocating Bitmap with dimensions %i x %i",
decodeInfo.width(), decodeInfo.height());
doThrowOOME(env, msg.c_str());
return nullptr;
}
jobject jexception = nullptr;
SkAndroidCodec::AndroidOptions options;
options.fSampleSize = sampleSize;
auto result = codec->getAndroidPixels(decodeInfo, bm.getPixels(), bm.rowBytes(), &options);
jobject jexception = env->ExceptionOccurred();
if (jexception) {
env->ExceptionClear();
}
switch (result) {
case SkCodec::kSuccess:
// Ignore the exception, since the decode was successful anyway.
jexception = nullptr;
break;
case SkCodec::kIncompleteInput:
if (jcallback) {
if (jcallback && !jexception) {
jexception = env->NewObject(gIncomplete_class, gIncomplete_constructorMethodID);
}
break;
case SkCodec::kErrorInInput:
if (jcallback) {
if (jcallback && !jexception) {
jexception = env->NewObject(gCorrupt_class, gCorrupt_constructorMethodID);
}
break;
default:
ALOGE("getPixels failed with error %i", result);
SkString msg;
msg.printf("getPixels failed with error %i", result);
doThrowIOE(env, msg.c_str());
return nullptr;
}
if (jexception) {
// FIXME: Do not provide a way for the client to force the method to return null.
if (!env->CallBooleanMethod(jcallback, gCallback_onExceptionMethodID, jexception) ||
env->ExceptionCheck()) {
return nullptr;
@@ -268,7 +329,8 @@ static jobject ImageDecoder_nDecodeBitmap(JNIEnv* env, jobject /*clazz*/, jlong
size_t ninePatchArraySize = decoder->mPeeker.mPatch->serializedSize();
ninePatchChunk = env->NewByteArray(ninePatchArraySize);
if (ninePatchChunk == nullptr) {
return nullObjectReturn("ninePatchChunk == null");
doThrowOOME(env, "Failed to allocate nine patch chunk.");
return nullptr;
}
env->SetByteArrayRegion(ninePatchChunk, 0, decoder->mPeeker.mPatchSize,
@@ -278,7 +340,8 @@ static jobject ImageDecoder_nDecodeBitmap(JNIEnv* env, jobject /*clazz*/, jlong
if (decoder->mPeeker.mHasInsets) {
ninePatchInsets = decoder->mPeeker.createNinePatchInsets(env, 1.0f);
if (ninePatchInsets == nullptr) {
return nullObjectReturn("nine patch insets == null");
doThrowOOME(env, "Failed to allocate nine patch insets.");
return nullptr;
}
}
}
@@ -303,7 +366,8 @@ static jobject ImageDecoder_nDecodeBitmap(JNIEnv* env, jobject /*clazz*/, jlong
SkImageInfo scaledInfo = bitmapInfo.makeWH(desiredWidth, desiredHeight);
SkBitmap scaledBm;
if (!scaledBm.setInfo(scaledInfo)) {
nullObjectReturn("Failed scaled setInfo");
doThrowIOE(env, "Failed scaled setInfo");
return nullptr;
}
sk_sp<Bitmap> scaledPixelRef;
@@ -313,9 +377,10 @@ static jobject ImageDecoder_nDecodeBitmap(JNIEnv* env, jobject /*clazz*/, jlong
scaledPixelRef = Bitmap::allocateHeapBitmap(&scaledBm);
}
if (!scaledPixelRef) {
ALOGE("OOM allocating scaled Bitmap with dimensions %i x %i",
desiredWidth, desiredHeight);
doThrowOOME(env);
SkString msg;
msg.printf("OOM allocating scaled Bitmap with dimensions %i x %i",
desiredWidth, desiredHeight);
doThrowOOME(env, msg.c_str());
return nullptr;
}
@@ -334,13 +399,11 @@ static jobject ImageDecoder_nDecodeBitmap(JNIEnv* env, jobject /*clazz*/, jlong
if (jpostProcess) {
std::unique_ptr<Canvas> canvas(Canvas::create_canvas(bm));
if (!canvas) {
return nullObjectReturn("Failed to create Canvas for PostProcess!");
}
jobject jcanvas = env->NewObject(gCanvas_class, gCanvas_constructorMethodID,
reinterpret_cast<jlong>(canvas.get()));
if (!jcanvas) {
return nullObjectReturn("Failed to create Java Canvas for PostProcess!");
doThrowOOME(env, "Failed to create Java Canvas for PostProcess!");
return nullptr;
}
// jcanvas will now own canvas.
canvas.release();
@@ -368,15 +431,17 @@ static jobject ImageDecoder_nDecodeBitmap(JNIEnv* env, jobject /*clazz*/, jlong
newAlphaType = kOpaque_SkAlphaType;
break;
default:
ALOGE("invalid return from postProcess: %i", pixelFormat);
doThrowIAE(env);
SkString msg;
msg.printf("invalid return from postProcess: %i", pixelFormat);
doThrowIAE(env, msg.c_str());
return nullptr;
}
if (newAlphaType != bm.alphaType()) {
if (!bm.setAlphaType(newAlphaType)) {
ALOGE("incompatible return from postProcess: %i", pixelFormat);
doThrowIAE(env);
SkString msg;
msg.printf("incompatible return from postProcess: %i", pixelFormat);
doThrowIAE(env, msg.c_str());
return nullptr;
}
nativeBitmap->setAlphaType(newAlphaType);
@@ -405,7 +470,8 @@ static jobject ImageDecoder_nDecodeBitmap(JNIEnv* env, jobject /*clazz*/, jlong
ninePatchChunk, ninePatchInsets);
}
if (allocator == ImageDecoder::kHardware_Allocator) {
return nullObjectReturn("failed to allocate hardware Bitmap!");
doThrowOOME(env, "failed to allocate hardware Bitmap!");
return nullptr;
}
// If we failed to create a hardware bitmap, go ahead and create a
// software one.
@@ -430,19 +496,21 @@ static void ImageDecoder_nGetPadding(JNIEnv* env, jobject /*clazz*/, jlong nativ
decoder->mPeeker.getPadding(env, outPadding);
}
static void ImageDecoder_nRecycle(JNIEnv* /*env*/, jobject /*clazz*/, jlong nativePtr) {
static void ImageDecoder_nClose(JNIEnv* /*env*/, jobject /*clazz*/, jlong nativePtr) {
delete reinterpret_cast<ImageDecoder*>(nativePtr);
}
static const JNINativeMethod gImageDecoderMethods[] = {
{ "nCreate", "(J)Landroid/graphics/ImageDecoder;", (void*) ImageDecoder_nCreate },
{ "nCreate", "(J)Landroid/graphics/ImageDecoder;", (void*) ImageDecoder_nCreateAsset },
{ "nCreate", "(Ljava/nio/ByteBuffer;II)Landroid/graphics/ImageDecoder;", (void*) ImageDecoder_nCreateByteBuffer },
{ "nCreate", "([BII)Landroid/graphics/ImageDecoder;", (void*) ImageDecoder_nCreateByteArray },
{ "nCreate", "(Ljava/io/InputStream;[B)Landroid/graphics/ImageDecoder;", (void*) ImageDecoder_nCreateInputStream },
{ "nCreate", "(Ljava/io/FileDescriptor;)Landroid/graphics/ImageDecoder;", (void*) ImageDecoder_nCreateFd },
{ "nDecodeBitmap", "(JLandroid/graphics/ImageDecoder$OnExceptionListener;Landroid/graphics/PostProcess;IILandroid/graphics/Rect;ZIZZZ)Landroid/graphics/Bitmap;",
(void*) ImageDecoder_nDecodeBitmap },
{ "nGetSampledSize","(JI)Landroid/graphics/Point;", (void*) ImageDecoder_nGetSampledSize },
{ "nGetPadding", "(JLandroid/graphics/Rect;)V", (void*) ImageDecoder_nGetPadding },
{ "nRecycle", "(J)V", (void*) ImageDecoder_nRecycle},
{ "nClose", "(J)V", (void*) ImageDecoder_nClose},
};
int register_android_graphics_ImageDecoder(JNIEnv* env) {
@@ -459,7 +527,7 @@ int register_android_graphics_ImageDecoder(JNIEnv* env) {
gCorrupt_constructorMethodID = GetMethodIDOrDie(env, gCorrupt_class, "<init>", "()V");
jclass callback_class = FindClassOrDie(env, "android/graphics/ImageDecoder$OnExceptionListener");
gCallback_onExceptionMethodID = GetMethodIDOrDie(env, callback_class, "onException", "(Ljava/lang/Exception;)Z");
gCallback_onExceptionMethodID = GetMethodIDOrDie(env, callback_class, "onException", "(Ljava/io/IOException;)Z");
jclass postProcess_class = FindClassOrDie(env, "android/graphics/PostProcess");
gPostProcess_postProcessMethodID = GetMethodIDOrDie(env, postProcess_class, "postProcess", "(Landroid/graphics/Canvas;II)I");

View File

@@ -16,29 +16,44 @@
package android.graphics;
import static android.system.OsConstants.SEEK_SET;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.RawRes;
import android.content.ContentResolver;
import android.content.res.AssetFileDescriptor;
import android.content.res.AssetManager;
import android.content.res.Resources;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.NinePatchDrawable;
import android.net.Uri;
import android.system.ErrnoException;
import android.system.Os;
import libcore.io.IoUtils;
import dalvik.system.CloseGuard;
import java.nio.ByteBuffer;
import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.lang.ArrayIndexOutOfBoundsException;
import java.lang.AutoCloseable;
import java.lang.NullPointerException;
import java.lang.RuntimeException;
import java.lang.annotation.Retention;
import static java.lang.annotation.RetentionPolicy.SOURCE;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* Class for decoding images as {@link Bitmap}s or {@link Drawable}s.
* @hide
*/
public final class ImageDecoder {
public final class ImageDecoder implements AutoCloseable {
/**
* Source of the encoded image data.
*/
@@ -47,10 +62,7 @@ public final class ImageDecoder {
Resources getResources() { return null; }
/* @hide */
void close() {}
/* @hide */
abstract ImageDecoder createImageDecoder();
abstract ImageDecoder createImageDecoder() throws IOException;
};
private static class ByteArraySource extends Source {
@@ -64,7 +76,7 @@ public final class ImageDecoder {
private final int mLength;
@Override
public ImageDecoder createImageDecoder() {
public ImageDecoder createImageDecoder() throws IOException {
return nCreate(mData, mOffset, mLength);
}
}
@@ -76,7 +88,7 @@ public final class ImageDecoder {
private final ByteBuffer mBuffer;
@Override
public ImageDecoder createImageDecoder() {
public ImageDecoder createImageDecoder() throws IOException {
if (!mBuffer.isDirect() && mBuffer.hasArray()) {
int offset = mBuffer.arrayOffset() + mBuffer.position();
int length = mBuffer.limit() - mBuffer.position();
@@ -86,61 +98,110 @@ public final class ImageDecoder {
}
}
private static class ResourceSource extends Source {
ResourceSource(Resources res, int resId)
throws Resources.NotFoundException {
// Test that the resource can be found.
InputStream is = null;
private static class ContentResolverSource extends Source {
ContentResolverSource(ContentResolver resolver, Uri uri) {
mResolver = resolver;
mUri = uri;
}
private final ContentResolver mResolver;
private final Uri mUri;
@Override
public ImageDecoder createImageDecoder() throws IOException {
AssetFileDescriptor assetFd = null;
try {
is = res.openRawResource(resId);
} finally {
if (is != null) {
try {
is.close();
} catch (IOException e) {
}
if (mUri.getScheme() == ContentResolver.SCHEME_CONTENT) {
assetFd = mResolver.openTypedAssetFileDescriptor(mUri,
"image/*", null);
} else {
assetFd = mResolver.openAssetFileDescriptor(mUri, "r");
}
} catch (FileNotFoundException e) {
// Some images cannot be opened as AssetFileDescriptors (e.g.
// bmp, ico). Open them as InputStreams.
InputStream is = mResolver.openInputStream(mUri);
if (is == null) {
throw new FileNotFoundException(mUri.toString());
}
return createFromStream(is);
}
final FileDescriptor fd = assetFd.getFileDescriptor();
final long offset = assetFd.getStartOffset();
ImageDecoder decoder = null;
try {
try {
Os.lseek(fd, offset, SEEK_SET);
decoder = nCreate(fd);
} catch (ErrnoException e) {
decoder = createFromStream(new FileInputStream(fd));
}
} finally {
if (decoder == null) {
IoUtils.closeQuietly(assetFd);
} else {
decoder.mAssetFd = assetFd;
}
}
return decoder;
}
}
private static ImageDecoder createFromStream(InputStream is) throws IOException {
// Arbitrary size matches BitmapFactory.
byte[] storage = new byte[16 * 1024];
ImageDecoder decoder = null;
try {
decoder = nCreate(is, storage);
} finally {
if (decoder == null) {
IoUtils.closeQuietly(is);
} else {
decoder.mInputStream = is;
decoder.mTempStorage = storage;
}
}
return decoder;
}
private static class ResourceSource extends Source {
ResourceSource(Resources res, int resId) {
mResources = res;
mResId = resId;
}
final Resources mResources;
final int mResId;
// This is just stored here in order to keep the underlying Asset
// alive. FIXME: Can I access the Asset (and keep it alive) without
// this object?
InputStream mInputStream;
@Override
public Resources getResources() { return mResources; }
@Override
public ImageDecoder createImageDecoder() {
// FIXME: Can I bypass creating the stream?
public ImageDecoder createImageDecoder() throws IOException {
// This is just used in order to access the underlying Asset and
// keep it alive. FIXME: Can we skip creating this object?
InputStream is = null;
ImageDecoder decoder = null;
try {
mInputStream = mResources.openRawResource(mResId);
} catch (Resources.NotFoundException e) {
// This should never happen, since we already tested in the
// constructor.
}
if (!(mInputStream instanceof AssetManager.AssetInputStream)) {
// This should never happen.
throw new RuntimeException("Resource is not an asset?");
}
long asset = ((AssetManager.AssetInputStream) mInputStream).getNativeAsset();
return nCreate(asset);
}
@Override
public void close() {
try {
mInputStream.close();
} catch (IOException e) {
is = mResources.openRawResource(mResId);
if (!(is instanceof AssetManager.AssetInputStream)) {
// This should never happen.
throw new RuntimeException("Resource is not an asset?");
}
long asset = ((AssetManager.AssetInputStream) is).getNativeAsset();
decoder = nCreate(asset);
} finally {
mInputStream = null;
if (decoder == null) {
IoUtils.closeQuietly(is);
} else {
decoder.mInputStream = is;
}
}
return decoder;
}
}
@@ -159,18 +220,23 @@ public final class ImageDecoder {
};
/**
* Used if the provided data is incomplete.
* Supplied to onException if the provided data is incomplete.
*
* Will never be thrown by ImageDecoder.
*
* There may be a partial image to display.
*/
public class IncompleteException extends Exception {};
public static class IncompleteException extends IOException {};
/**
* Used if the provided data is corrupt.
*
* There may be a partial image to display.
* May be thrown if there is nothing to display.
*
* If supplied to onException, there may be a correct partial image to
* display.
*/
public class CorruptException extends Exception {};
public static class CorruptException extends IOException {};
/**
* Optional listener supplied to {@link #decodeDrawable} or
@@ -193,15 +259,14 @@ public final class ImageDecoder {
public static interface OnExceptionListener {
/**
* Called when there is a problem in the stream or in the data.
* FIXME: Or do not allow streams?
* FIXME: Report how much of the image has been decoded?
*
* @param e Exception containing information about the error.
* @param e IOException containing information about the error.
* @return True to create and return a {@link Drawable}/
* {@link Bitmap} with partial data. False to return
* {@code null}. True is the default.
*/
public boolean onException(Exception e);
public boolean onException(IOException e);
};
// Fields
@@ -221,9 +286,15 @@ public final class ImageDecoder {
private PostProcess mPostProcess;
private OnExceptionListener mOnExceptionListener;
// Objects for interacting with the input.
private InputStream mInputStream;
private byte[] mTempStorage;
private AssetFileDescriptor mAssetFd;
private final AtomicBoolean mClosed = new AtomicBoolean();
private final CloseGuard mCloseGuard = CloseGuard.get();
/**
* Private constructor called by JNI. {@link #recycle} must be
* Private constructor called by JNI. {@link #close} must be
* called after decoding to delete native resources.
*/
@SuppressWarnings("unused")
@@ -233,6 +304,20 @@ public final class ImageDecoder {
mHeight = height;
mDesiredWidth = width;
mDesiredHeight = height;
mCloseGuard.open("close");
}
@Override
protected void finalize() throws Throwable {
try {
if (mCloseGuard != null) {
mCloseGuard.warnIfOpen();
}
close();
} finally {
super.finalize();
}
}
/**
@@ -243,13 +328,27 @@ public final class ImageDecoder {
* // FIXME: Can be an @DrawableRes?
* @return a new Source object, which can be passed to
* {@link #decodeDrawable} or {@link #decodeBitmap}.
* @throws Resources.NotFoundException if the asset does not exist.
*/
@NonNull
public static Source createSource(@NonNull Resources res, @RawRes int resId)
throws Resources.NotFoundException {
{
return new ResourceSource(res, resId);
}
/**
* Create a new {@link Source} from a {@link android.net.Uri}.
*
* @param cr to retrieve from.
* @param uri of the image file.
* @return a new Source object, which can be passed to
* {@link #decodeDrawable} or {@link #decodeBitmap}.
*/
@NonNull
public static Source createSource(@NonNull ContentResolver cr,
@NonNull Uri uri) {
return new ContentResolverSource(cr, uri);
}
/**
* Create a new {@link Source} from a byte array.
* @param data byte array of compressed image data.
@@ -307,7 +406,7 @@ public final class ImageDecoder {
+ "provided " + sampleSize);
}
if (mNativePtr == 0) {
throw new IllegalStateException("ImageDecoder is recycled!");
throw new IllegalStateException("ImageDecoder is closed!");
}
return nGetSampledSize(mNativePtr, sampleSize);
@@ -500,24 +599,26 @@ public final class ImageDecoder {
mAsAlphaMask = true;
}
/**
* Clean up resources.
*
* ImageDecoder has a private constructor, and will always be recycled
* by decodeDrawable or decodeBitmap which creates it, so there is no
* need for a finalizer.
*/
private void recycle() {
if (mNativePtr == 0) {
@Override
public void close() {
mCloseGuard.close();
if (!mClosed.compareAndSet(false, true)) {
return;
}
nRecycle(mNativePtr);
nClose(mNativePtr);
mNativePtr = 0;
IoUtils.closeQuietly(mInputStream);
IoUtils.closeQuietly(mAssetFd);
mInputStream = null;
mAssetFd = null;
mTempStorage = null;
}
private void checkState() {
if (mNativePtr == 0) {
throw new IllegalStateException("Cannot reuse ImageDecoder.Source!");
throw new IllegalStateException("Cannot use closed ImageDecoder!");
}
checkSubset(mDesiredWidth, mDesiredHeight, mCropRect);
@@ -548,44 +649,47 @@ public final class ImageDecoder {
/**
* Create a {@link Drawable}.
* @throws IOException if {@code src} is not found, is an unsupported
* format, or cannot be decoded for any reason.
*/
public static Drawable decodeDrawable(Source src, OnHeaderDecodedListener listener) {
ImageDecoder decoder = src.createImageDecoder();
if (decoder == null) {
return null;
}
@NonNull
public static Drawable decodeDrawable(Source src, OnHeaderDecodedListener listener)
throws IOException {
try (ImageDecoder decoder = src.createImageDecoder()) {
if (listener != null) {
ImageInfo info = new ImageInfo(decoder.mWidth, decoder.mHeight);
listener.onHeaderDecoded(info, decoder);
}
if (listener != null) {
ImageInfo info = new ImageInfo(decoder.mWidth, decoder.mHeight);
listener.onHeaderDecoded(info, decoder);
}
decoder.checkState();
decoder.checkState();
if (decoder.mRequireUnpremultiplied) {
// Though this could be supported (ignored) for opaque images,
// it seems better to always report this error.
throw new IllegalStateException("Cannot decode a Drawable " +
"with unpremultiplied pixels!");
}
if (decoder.mRequireUnpremultiplied) {
// Though this could be supported (ignored) for opaque images, it
// seems better to always report this error.
throw new IllegalStateException("Cannot decode a Drawable with" +
" unpremultiplied pixels!");
}
if (decoder.mMutable) {
throw new IllegalStateException("Cannot decode a mutable " +
"Drawable!");
}
if (decoder.mMutable) {
throw new IllegalStateException("Cannot decode a mutable Drawable!");
}
try {
Bitmap bm = nDecodeBitmap(decoder.mNativePtr,
decoder.mOnExceptionListener,
decoder.mPostProcess,
decoder.mDesiredWidth, decoder.mDesiredHeight,
decoder.mDesiredWidth,
decoder.mDesiredHeight,
decoder.mCropRect,
false, // decoder.mMutable
false, // mMutable
decoder.mAllocator,
false, // decoder.mRequireUnpremultiplied
false, // mRequireUnpremultiplied
decoder.mPreferRamOverQuality,
decoder.mAsAlphaMask
);
decoder.mAsAlphaMask);
if (bm == null) {
// FIXME: bm should never be null. Currently a return value
// of false from onException will result in bm being null. What
// is the right API to choose to discard partial Bitmaps?
return null;
}
@@ -606,60 +710,58 @@ public final class ImageDecoder {
// TODO: Handle animation.
return new BitmapDrawable(res, bm);
} finally {
decoder.recycle();
src.close();
}
}
/**
* Create a {@link Bitmap}.
* Create a {@link Bitmap}.
* @throws IOException if {@code src} is not found, is an unsupported
* format, or cannot be decoded for any reason.
*/
public static Bitmap decodeBitmap(Source src, OnHeaderDecodedListener listener) {
ImageDecoder decoder = src.createImageDecoder();
if (decoder == null) {
return null;
}
@NonNull
public static Bitmap decodeBitmap(Source src, OnHeaderDecodedListener listener)
throws IOException {
try (ImageDecoder decoder = src.createImageDecoder()) {
if (listener != null) {
ImageInfo info = new ImageInfo(decoder.mWidth, decoder.mHeight);
listener.onHeaderDecoded(info, decoder);
}
if (listener != null) {
ImageInfo info = new ImageInfo(decoder.mWidth, decoder.mHeight);
listener.onHeaderDecoded(info, decoder);
}
decoder.checkState();
decoder.checkState();
try {
return nDecodeBitmap(decoder.mNativePtr,
decoder.mOnExceptionListener,
decoder.mPostProcess,
decoder.mDesiredWidth, decoder.mDesiredHeight,
decoder.mDesiredWidth,
decoder.mDesiredHeight,
decoder.mCropRect,
decoder.mMutable,
decoder.mAllocator,
decoder.mRequireUnpremultiplied,
decoder.mPreferRamOverQuality,
decoder.mAsAlphaMask);
} finally {
decoder.recycle();
src.close();
}
}
private static native ImageDecoder nCreate(long asset);
private static native ImageDecoder nCreate(long asset) throws IOException;
private static native ImageDecoder nCreate(ByteBuffer buffer,
int position,
int limit);
int limit) throws IOException;
private static native ImageDecoder nCreate(byte[] data, int offset,
int length);
int length) throws IOException;
private static native ImageDecoder nCreate(InputStream is, byte[] storage);
private static native ImageDecoder nCreate(FileDescriptor fd) throws IOException;
@NonNull
private static native Bitmap nDecodeBitmap(long nativePtr,
OnExceptionListener listener,
PostProcess postProcess,
int width, int height,
Rect cropRect, boolean mutable,
int allocator, boolean requireUnpremul,
boolean preferRamOverQuality, boolean asAlphaMask);
boolean preferRamOverQuality, boolean asAlphaMask)
throws IOException;
private static native Point nGetSampledSize(long nativePtr,
int sampleSize);
private static native void nGetPadding(long nativePtr, Rect outRect);
private static native void nRecycle(long nativePtr);
private static native void nClose(long nativePtr);
}