summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Leon Scroggins <scroggo@google.com> 2018-01-08 21:14:50 +0000
committer Android (Google) Code Review <android-gerrit@google.com> 2018-01-08 21:14:50 +0000
commit5dfd8194b29aa15c7e17906ef0933d6f6ac10acb (patch)
tree62d1d59b309bd56ba290ce86e5fa73b7871eedcb
parent7a085f9ea05126e959ab5de1ca810c1938072380 (diff)
parented074fd70011cf804a14b5cf53ffd4b529c5f367 (diff)
Merge "Support using ImageDecoder with ContentResolver + URI"
-rw-r--r--core/jni/android/graphics/CreateJavaOutputStreamAdaptor.cpp128
-rw-r--r--core/jni/android/graphics/CreateJavaOutputStreamAdaptor.h13
-rw-r--r--core/jni/android/graphics/ImageDecoder.cpp136
-rw-r--r--graphics/java/android/graphics/ImageDecoder.java334
4 files changed, 412 insertions, 199 deletions
diff --git a/core/jni/android/graphics/CreateJavaOutputStreamAdaptor.cpp b/core/jni/android/graphics/CreateJavaOutputStreamAdaptor.cpp
index de2b7c3db11d..4257c981be18 100644
--- a/core/jni/android/graphics/CreateJavaOutputStreamAdaptor.cpp
+++ b/core/jni/android/graphics/CreateJavaOutputStreamAdaptor.cpp
@@ -12,21 +12,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);
+ }
+
+ ~JavaInputStreamAdaptor() override {
+ auto* env = get_env_or_die(fJvm);
+ env->DeleteGlobalRef(fJavaInputStream);
+ env->DeleteGlobalRef(fJavaByteArray);
}
- virtual size_t read(void* buffer, size_t size) {
+ 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;
@@ -37,10 +78,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;
@@ -52,16 +93,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 {
@@ -76,13 +114,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.
@@ -92,14 +126,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);
@@ -111,14 +140,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;
}
@@ -129,20 +154,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;
diff --git a/core/jni/android/graphics/CreateJavaOutputStreamAdaptor.h b/core/jni/android/graphics/CreateJavaOutputStreamAdaptor.h
index 56cba51222a0..fccd4717c4b7 100644
--- a/core/jni/android/graphics/CreateJavaOutputStreamAdaptor.h
+++ b/core/jni/android/graphics/CreateJavaOutputStreamAdaptor.h
@@ -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_
diff --git a/core/jni/android/graphics/ImageDecoder.cpp b/core/jni/android/graphics/ImageDecoder.cpp
index bacab2a304cc..5bdad08e3e1e 100644
--- a/core/jni/android/graphics/ImageDecoder.cpp
+++ b/core/jni/android/graphics/ImageDecoder.cpp
@@ -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");
diff --git a/graphics/java/android/graphics/ImageDecoder.java b/graphics/java/android/graphics/ImageDecoder.java
index 60416a720231..97ce88606331 100644
--- a/graphics/java/android/graphics/ImageDecoder.java
+++ b/graphics/java/android/graphics/ImageDecoder.java
@@ -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);
+ 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 (is != null) {
- try {
- is.close();
- } catch (IOException e) {
- }
+ 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?
- 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() {
+ 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.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,14 +328,28 @@ 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.
* @param offset offset into data for where the decoder should begin
@@ -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;
- }
-
- if (listener != null) {
- ImageInfo info = new ImageInfo(decoder.mWidth, decoder.mHeight);
- listener.onHeaderDecoded(info, decoder);
- }
+ @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);
+ }
- 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;
- }
-
- if (listener != null) {
- ImageInfo info = new ImageInfo(decoder.mWidth, decoder.mHeight);
- listener.onHeaderDecoded(info, decoder);
- }
+ @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);
+ }
- 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);
}