Add tests for AImageDecoder

Bug: 135133301
Bug: 139124386
Test: This

Add AImageDecoderTest.java and its native counterpart for testing the
APIs of AImageDecoder.

Add Utils.java for sharing code between different tests.

Move BitmapFactoryTest#obtainPath into Utils.java

ImageDecoderTest:
- make various methods and classes package-private so they can be used
  by AImageDecoderTest.
- Add Record.isGray and (Asset)Record.hasAlpha and a grayscale Record to
  exercise AImageDecoder features.
- Move getAsResourceUri into Utils.

Change-Id: Ib84462ea5fa8a7779eaa44494775e182e52ecaca
diff --git a/tests/tests/graphics/jni/Android.bp b/tests/tests/graphics/jni/Android.bp
index 305ced3..0a302de 100644
--- a/tests/tests/graphics/jni/Android.bp
+++ b/tests/tests/graphics/jni/Android.bp
@@ -17,6 +17,7 @@
     gtest: false,
     srcs: [
         "CtsGraphicsJniOnLoad.cpp",
+        "android_graphics_cts_AImageDecoderTest.cpp",
         "android_graphics_cts_ANativeWindowTest.cpp",
         "android_graphics_cts_ASurfaceTextureTest.cpp",
         "android_graphics_cts_BasicVulkanGpuTest.cpp",
diff --git a/tests/tests/graphics/jni/CtsGraphicsJniOnLoad.cpp b/tests/tests/graphics/jni/CtsGraphicsJniOnLoad.cpp
index db7919d..1d7dd89 100644
--- a/tests/tests/graphics/jni/CtsGraphicsJniOnLoad.cpp
+++ b/tests/tests/graphics/jni/CtsGraphicsJniOnLoad.cpp
@@ -17,6 +17,7 @@
 #include <jni.h>
 #include <stdio.h>
 
+extern int register_android_graphics_cts_AImageDecoderTest(JNIEnv*);
 extern int register_android_graphics_cts_ANativeWindowTest(JNIEnv*);
 extern int register_android_graphics_cts_ASurfaceTextureTest(JNIEnv*);
 extern int register_android_graphics_cts_BasicVulkanGpuTest(JNIEnv*);
@@ -33,6 +34,8 @@
     JNIEnv* env = nullptr;
     if (vm->GetEnv((void**)&env, JNI_VERSION_1_4) != JNI_OK)
         return JNI_ERR;
+    if (register_android_graphics_cts_AImageDecoderTest(env))
+        return JNI_ERR;
     if (register_android_graphics_cts_ANativeWindowTest(env))
         return JNI_ERR;
     if (register_android_graphics_cts_ASurfaceTextureTest(env))
diff --git a/tests/tests/graphics/jni/android_graphics_cts_AImageDecoderTest.cpp b/tests/tests/graphics/jni/android_graphics_cts_AImageDecoderTest.cpp
new file mode 100644
index 0000000..01da233
--- /dev/null
+++ b/tests/tests/graphics/jni/android_graphics_cts_AImageDecoderTest.cpp
@@ -0,0 +1,1059 @@
+/*
+ * Copyright 2019 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.
+ *
+ */
+
+#define LOG_TAG "AImageDecoderTest"
+
+#include <jni.h>
+#include <android/asset_manager.h>
+#include <android/asset_manager_jni.h>
+#include <android/bitmap.h>
+#include <android/imagedecoder.h>
+#include <android/rect.h>
+
+#include "NativeTestHelpers.h"
+
+#include <cstdlib>
+#include <cstring>
+#include <initializer_list>
+#include <limits>
+#include <memory>
+#include <stdio.h>
+#include <unistd.h>
+
+#define ALOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
+
+using AssetCloser = std::unique_ptr<AAsset, decltype(&AAsset_close)>;
+using DecoderDeleter = std::unique_ptr<AImageDecoder, decltype(&AImageDecoder_delete)>;
+
+static void testEmptyCreate(JNIEnv* env, jclass) {
+    AImageDecoder* decoderPtr = nullptr;
+    for (AImageDecoder** outDecoder : { &decoderPtr, (AImageDecoder**) nullptr }) {
+        for (AAsset* asset : { nullptr }) {
+            int result = AImageDecoder_createFromAAsset(asset, outDecoder);
+            ASSERT_EQ(ANDROID_IMAGE_DECODER_BAD_PARAMETER, result);
+            if (outDecoder) {
+                ASSERT_EQ(nullptr, *outDecoder);
+            }
+        }
+
+        for (int fd : { 0, -1 }) {
+            int result = AImageDecoder_createFromFd(fd, outDecoder);
+            ASSERT_EQ(ANDROID_IMAGE_DECODER_BAD_PARAMETER, result);
+            if (outDecoder) {
+                ASSERT_EQ(nullptr, *outDecoder);
+            }
+        }
+
+        auto testEmptyBuffer = [env, outDecoder](void* buffer, size_t length) {
+            int result = AImageDecoder_createFromBuffer(buffer, length, outDecoder);
+            ASSERT_EQ(ANDROID_IMAGE_DECODER_BAD_PARAMETER, result);
+            if (outDecoder) {
+                ASSERT_EQ(nullptr, *outDecoder);
+            }
+        };
+        testEmptyBuffer(nullptr, 0);
+        char buf[4];
+        testEmptyBuffer(buf, 0);
+    }
+}
+
+static AAsset* openAsset(JNIEnv* env, jobject jAssets, jstring jFile, int mode) {
+    AAssetManager* nativeManager = AAssetManager_fromJava(env, jAssets);
+    const char* file = env->GetStringUTFChars(jFile, nullptr);
+    AAsset* asset = AAssetManager_open(nativeManager, file, mode);
+    if (!asset) {
+        ALOGE("Could not open %s", file);
+    } else {
+        ALOGD("Testing %s", file);
+    }
+    env->ReleaseStringUTFChars(jFile, file);
+    return asset;
+}
+
+static void testNullDecoder(JNIEnv* env, jclass, jobject jAssets, jstring jFile) {
+    AAsset* asset = openAsset(env, jAssets, jFile, AASSET_MODE_BUFFER);
+    ASSERT_NE(asset, nullptr);
+    AssetCloser assetCloser(asset, AAsset_close);
+
+    {
+        int result = AImageDecoder_createFromAAsset(asset, nullptr);
+        ASSERT_EQ(ANDROID_IMAGE_DECODER_BAD_PARAMETER, result);
+    }
+
+    {
+        const void* buffer = AAsset_getBuffer(asset);
+        ASSERT_NE(buffer, nullptr);
+
+        int result = AImageDecoder_createFromBuffer(buffer, AAsset_getLength(asset), nullptr);
+        ASSERT_EQ(ANDROID_IMAGE_DECODER_BAD_PARAMETER, result);
+    }
+
+    {
+        off_t start, length;
+        int fd = AAsset_openFileDescriptor(asset, &start, &length);
+        ASSERT_GT(fd, 0);
+
+        off_t offset = ::lseek(fd, start, SEEK_SET);
+        ASSERT_EQ(start, offset);
+
+        int result = AImageDecoder_createFromFd(fd, nullptr);
+        ASSERT_EQ(ANDROID_IMAGE_DECODER_BAD_PARAMETER, result);
+        close(fd);
+    }
+
+    {
+        auto stride = AImageDecoder_getMinimumStride(nullptr);
+        ASSERT_EQ(0, stride);
+    }
+
+    {
+        char buf[4];
+        int result = AImageDecoder_decodeImage(nullptr, buf, 4, 4);
+        ASSERT_EQ(ANDROID_IMAGE_DECODER_BAD_PARAMETER, result);
+    }
+
+    {
+        int result = AImageDecoder_setAndroidBitmapFormat(nullptr, ANDROID_BITMAP_FORMAT_RGBA_8888);
+        ASSERT_EQ(ANDROID_IMAGE_DECODER_BAD_PARAMETER, result);
+
+        auto format = AImageDecoderHeaderInfo_getAndroidBitmapFormat(nullptr);
+        ASSERT_EQ(ANDROID_BITMAP_FORMAT_NONE, format);
+    }
+
+    {
+        int result = AImageDecoder_setAlphaFlags(nullptr, ANDROID_BITMAP_FLAGS_ALPHA_PREMUL);
+        ASSERT_EQ(ANDROID_IMAGE_DECODER_BAD_PARAMETER, result);
+
+        int alpha = AImageDecoderHeaderInfo_getAlphaFlags(nullptr);
+        // FIXME: Better invalid value?
+        ASSERT_EQ(-1, alpha);
+    }
+
+    ASSERT_EQ(0, AImageDecoderHeaderInfo_getWidth(nullptr));
+    ASSERT_EQ(0, AImageDecoderHeaderInfo_getHeight(nullptr));
+    ASSERT_EQ(nullptr, AImageDecoderHeaderInfo_getMimeType(nullptr));
+    ASSERT_EQ(false, AImageDecoderHeaderInfo_isAnimated(nullptr));
+
+    {
+        int result = AImageDecoder_setTargetSize(nullptr, 1, 1);
+        ASSERT_EQ(ANDROID_IMAGE_DECODER_BAD_PARAMETER, result);
+    }
+    {
+        ARect rect {0, 0, 10, 10};
+        int result = AImageDecoder_setCrop(nullptr, rect);
+        ASSERT_EQ(ANDROID_IMAGE_DECODER_BAD_PARAMETER, result);
+    }
+}
+
+static void testInfo(JNIEnv* env, jclass, jlong imageDecoderPtr, jint width, jint height,
+                     jstring jMimeType, jboolean isAnimated, jboolean isF16) {
+    AImageDecoder* decoder = reinterpret_cast<AImageDecoder*>(imageDecoderPtr);
+    ASSERT_NE(decoder, nullptr);
+    DecoderDeleter decoderDeleter(decoder, AImageDecoder_delete);
+
+    const AImageDecoderHeaderInfo* info = AImageDecoder_getHeaderInfo(decoder);
+    ASSERT_NE(info, nullptr);
+    int32_t actualWidth = AImageDecoderHeaderInfo_getWidth(info);
+    ASSERT_EQ(width, actualWidth);
+    int32_t actualHeight = AImageDecoderHeaderInfo_getHeight(info);
+    ASSERT_EQ(height, actualHeight);
+
+    const char* mimeType = env->GetStringUTFChars(jMimeType, nullptr);
+    ASSERT_NE(mimeType, nullptr);
+    ASSERT_EQ(0, strcmp(mimeType, AImageDecoderHeaderInfo_getMimeType(info)));
+    env->ReleaseStringUTFChars(jMimeType, mimeType);
+    ASSERT_EQ(isAnimated, AImageDecoderHeaderInfo_isAnimated(info));
+    auto format = AImageDecoderHeaderInfo_getAndroidBitmapFormat(info);
+    if (isF16) {
+        ASSERT_EQ(ANDROID_BITMAP_FORMAT_RGBA_F16, format);
+    } else {
+        ASSERT_EQ(ANDROID_BITMAP_FORMAT_RGBA_8888, format);
+    }
+}
+
+static jlong openAssetNative(JNIEnv* env, jclass, jobject jAssets, jstring jFile) {
+    // FIXME: Test the other modes? Or more to the point, pass in the mode? It
+    // seems that when we want a buffer we should use AASSET_MODE_BUFFER.
+    AAsset* asset = openAsset(env, jAssets, jFile, AASSET_MODE_UNKNOWN);
+    if (!asset) {
+        fail(env, "Failed to open native asset!");
+    }
+    return reinterpret_cast<jlong>(asset);
+}
+
+static void closeAsset(JNIEnv*, jclass, jlong asset) {
+    AAsset_close(reinterpret_cast<AAsset*>(asset));
+}
+
+static jlong createFromAsset(JNIEnv* env, jclass, jlong asset) {
+    AImageDecoder* decoder = nullptr;
+    int result = AImageDecoder_createFromAAsset(reinterpret_cast<AAsset*>(asset), &decoder);
+    if (ANDROID_IMAGE_DECODER_SUCCESS != result || !decoder) {
+        fail(env, "Failed to create AImageDecoder!");
+    }
+    return reinterpret_cast<jlong>(decoder);
+}
+
+static jlong createFromFd(JNIEnv* env, jclass, int fd) {
+    AImageDecoder* decoder = nullptr;
+    int result = AImageDecoder_createFromFd(fd, &decoder);
+    if (ANDROID_IMAGE_DECODER_SUCCESS != result || !decoder) {
+        fail(env, "Failed to create AImageDecoder!");
+    }
+    return reinterpret_cast<jlong>(decoder);
+}
+
+static jlong createFromAssetFd(JNIEnv* env, jclass, jlong assetPtr) {
+    AAsset* asset = reinterpret_cast<AAsset*>(assetPtr);
+    off_t start, length;
+    int fd = AAsset_openFileDescriptor(asset, &start, &length);
+    if (fd <= 0) {
+        fail(env, "Failed to open file descriptor!");
+        return -1;
+    }
+
+    off_t offset = ::lseek(fd, start, SEEK_SET);
+    if (offset != start) {
+        fail(env, "Failed to seek file descriptor!");
+        return -1;
+    }
+
+    return createFromFd(env, nullptr, fd);
+}
+
+static jlong createFromAssetBuffer(JNIEnv* env, jclass, jlong assetPtr) {
+    AAsset* asset = reinterpret_cast<AAsset*>(assetPtr);
+    const void* buffer = AAsset_getBuffer(asset);
+    if (!buffer) {
+        fail(env, "AAsset_getBuffer failed!");
+        return -1;
+    }
+
+    AImageDecoder* decoder = nullptr;
+    int result = AImageDecoder_createFromBuffer(buffer, AAsset_getLength(asset), &decoder);
+    if (ANDROID_IMAGE_DECODER_SUCCESS != result || !decoder) {
+        fail(env, "AImageDecoder_createFromBuffer failed!");
+        return -1;
+    }
+    return reinterpret_cast<jlong>(decoder);
+}
+
+static void testCreateIncomplete(JNIEnv* env, jclass, jobject jAssets, jstring jFile,
+                                 jint truncatedLength) {
+    AAsset* asset = openAsset(env, jAssets, jFile, AASSET_MODE_UNKNOWN);
+    ASSERT_NE(asset, nullptr);
+    AssetCloser assetCloser(asset, AAsset_close);
+
+    const void* buffer = AAsset_getBuffer(asset);
+    ASSERT_NE(buffer, nullptr);
+
+    AImageDecoder* decoder;
+    int result = AImageDecoder_createFromBuffer(buffer, truncatedLength, &decoder);
+    ASSERT_EQ(ANDROID_IMAGE_DECODER_INCOMPLETE, result);
+    ASSERT_EQ(decoder, nullptr);
+}
+
+static void testCreateUnsupported(JNIEnv* env, jclass, jobject jAssets, jstring jFile) {
+    AAsset* asset = openAsset(env, jAssets, jFile, AASSET_MODE_UNKNOWN);
+    ASSERT_NE(asset, nullptr);
+    AssetCloser assetCloser(asset, AAsset_close);
+
+    AImageDecoder* decoder;
+    int result = AImageDecoder_createFromAAsset(asset, &decoder);
+    ASSERT_EQ(ANDROID_IMAGE_DECODER_UNSUPPORTED_FORMAT, result);
+    ASSERT_EQ(decoder, nullptr);
+}
+
+static void testSetFormat(JNIEnv* env, jclass, jlong imageDecoderPtr,
+                          jboolean isF16, jboolean isGray) {
+    AImageDecoder* decoder = reinterpret_cast<AImageDecoder*>(imageDecoderPtr);
+    DecoderDeleter decoderDeleter(decoder, AImageDecoder_delete);
+
+    const AImageDecoderHeaderInfo* info = AImageDecoder_getHeaderInfo(decoder);
+    ASSERT_NE(info, nullptr);
+
+    // Store the format so we can ensure that it doesn't change when we call
+    // AImageDecoder_setAndroidBitmapFormat.
+    const auto format = AImageDecoderHeaderInfo_getAndroidBitmapFormat(info);
+    if (isF16) {
+        ASSERT_EQ(ANDROID_BITMAP_FORMAT_RGBA_F16, format);
+    } else {
+        ASSERT_EQ(ANDROID_BITMAP_FORMAT_RGBA_8888, format);
+    }
+
+    int result = AImageDecoder_setAndroidBitmapFormat(decoder, ANDROID_BITMAP_FORMAT_A_8);
+    if (isGray) {
+        ASSERT_EQ(ANDROID_IMAGE_DECODER_SUCCESS, result);
+    } else {
+        ASSERT_EQ(ANDROID_IMAGE_DECODER_INVALID_CONVERSION, result);
+    }
+    ASSERT_EQ(format, AImageDecoderHeaderInfo_getAndroidBitmapFormat(info));
+
+    result = AImageDecoder_setAndroidBitmapFormat(decoder, ANDROID_BITMAP_FORMAT_RGB_565);
+    int alpha = AImageDecoderHeaderInfo_getAlphaFlags(info);
+    if (alpha == ANDROID_BITMAP_FLAGS_ALPHA_OPAQUE) {
+        ASSERT_EQ(ANDROID_IMAGE_DECODER_SUCCESS, result);
+    } else {
+        ASSERT_EQ(ANDROID_BITMAP_FLAGS_ALPHA_PREMUL, alpha);
+        ASSERT_EQ(ANDROID_IMAGE_DECODER_INVALID_CONVERSION, result);
+    }
+    ASSERT_EQ(format, AImageDecoderHeaderInfo_getAndroidBitmapFormat(info));
+
+    for (auto newFormat : { ANDROID_BITMAP_FORMAT_RGBA_4444, ANDROID_BITMAP_FORMAT_NONE }) {
+        result = AImageDecoder_setAndroidBitmapFormat(decoder, newFormat);
+        ASSERT_EQ(ANDROID_IMAGE_DECODER_INVALID_CONVERSION, result);
+        ASSERT_EQ(format, AImageDecoderHeaderInfo_getAndroidBitmapFormat(info));
+    }
+
+    for (auto newFormat : { ANDROID_BITMAP_FORMAT_RGBA_8888, ANDROID_BITMAP_FORMAT_RGBA_F16 }) {
+        result = AImageDecoder_setAndroidBitmapFormat(decoder, newFormat);
+        ASSERT_EQ(ANDROID_IMAGE_DECODER_SUCCESS, result);
+        ASSERT_EQ(format, AImageDecoderHeaderInfo_getAndroidBitmapFormat(info));
+    }
+
+    for (auto invalidFormat : { -1, 42, 67 }) {
+        result = AImageDecoder_setAndroidBitmapFormat(decoder, invalidFormat);
+        ASSERT_EQ(ANDROID_IMAGE_DECODER_BAD_PARAMETER, result);
+        ASSERT_EQ(format, AImageDecoderHeaderInfo_getAndroidBitmapFormat(info));
+    }
+}
+
+static void testSetAlpha(JNIEnv* env, jclass, jlong imageDecoderPtr, jboolean hasAlpha) {
+    AImageDecoder* decoder = reinterpret_cast<AImageDecoder*>(imageDecoderPtr);
+    DecoderDeleter decoderDeleter(decoder, AImageDecoder_delete);
+
+    const AImageDecoderHeaderInfo* info = AImageDecoder_getHeaderInfo(decoder);
+    ASSERT_NE(info, nullptr);
+
+    // Store the alpha so we can ensure that it doesn't change when we call
+    // AImageDecoder_setAlphaFlags.
+    const int alpha = AImageDecoderHeaderInfo_getAlphaFlags(info);
+    if (hasAlpha) {
+        ASSERT_EQ(ANDROID_BITMAP_FLAGS_ALPHA_PREMUL, alpha);
+    } else {
+        ASSERT_EQ(ANDROID_BITMAP_FLAGS_ALPHA_OPAQUE, alpha);
+    }
+
+    int result = AImageDecoder_setAlphaFlags(decoder, ANDROID_BITMAP_FLAGS_ALPHA_OPAQUE);
+    if (hasAlpha) {
+        ASSERT_EQ(ANDROID_IMAGE_DECODER_INVALID_CONVERSION, result);
+    } else {
+        ASSERT_EQ(ANDROID_IMAGE_DECODER_SUCCESS, result);
+    }
+    ASSERT_EQ(alpha, AImageDecoderHeaderInfo_getAlphaFlags(info));
+
+    for (int newAlpha : { ANDROID_BITMAP_FLAGS_ALPHA_UNPREMUL, ANDROID_BITMAP_FLAGS_ALPHA_PREMUL }){
+        result = AImageDecoder_setAlphaFlags(decoder, newAlpha);
+        ASSERT_EQ(ANDROID_IMAGE_DECODER_SUCCESS, result);
+        ASSERT_EQ(alpha, AImageDecoderHeaderInfo_getAlphaFlags(info));
+    }
+
+    for (int invalidAlpha : std::initializer_list<int>{
+            ANDROID_BITMAP_FLAGS_ALPHA_MASK, -1, 3, 5, 16 }) {
+        result = AImageDecoder_setAlphaFlags(decoder, invalidAlpha);
+        ASSERT_EQ(ANDROID_IMAGE_DECODER_BAD_PARAMETER, result);
+        ASSERT_EQ(alpha, AImageDecoderHeaderInfo_getAlphaFlags(info));
+    }
+}
+
+static int bytesPerPixel(AndroidBitmapFormat format) {
+    switch (format) {
+        case ANDROID_BITMAP_FORMAT_RGBA_8888:
+            return 4;
+        case ANDROID_BITMAP_FORMAT_RGB_565:
+            return 2;
+        case ANDROID_BITMAP_FORMAT_A_8:
+            return 1;
+        case ANDROID_BITMAP_FORMAT_RGBA_F16:
+            return 8;
+        case ANDROID_BITMAP_FORMAT_NONE:
+        case ANDROID_BITMAP_FORMAT_RGBA_4444:
+            return 0;
+    }
+}
+
+static void testGetMinimumStride(JNIEnv* env, jclass, jlong imageDecoderPtr,
+                                 jboolean isF16, jboolean isGray) {
+    AImageDecoder* decoder = reinterpret_cast<AImageDecoder*>(imageDecoderPtr);
+    DecoderDeleter decoderDeleter(decoder, AImageDecoder_delete);
+
+    const AImageDecoderHeaderInfo* info = AImageDecoder_getHeaderInfo(decoder);
+    ASSERT_NE(info, nullptr);
+
+    const int32_t width = AImageDecoderHeaderInfo_getWidth(info);
+    size_t stride = AImageDecoder_getMinimumStride(decoder);
+
+    if (isF16) {
+        ASSERT_EQ(bytesPerPixel(ANDROID_BITMAP_FORMAT_RGBA_F16) * width, stride);
+    } else {
+        ASSERT_EQ(bytesPerPixel(ANDROID_BITMAP_FORMAT_RGBA_8888) * width, stride);
+    }
+
+    auto setFormatAndCheckStride = [env, decoder, width, &stride](AndroidBitmapFormat format) {
+        int result = AImageDecoder_setAndroidBitmapFormat(decoder, format);
+        ASSERT_EQ(ANDROID_IMAGE_DECODER_SUCCESS, result);
+
+        stride = AImageDecoder_getMinimumStride(decoder);
+        ASSERT_EQ(bytesPerPixel(format) * width, stride);
+    };
+
+    int alpha = AImageDecoderHeaderInfo_getAlphaFlags(info);
+    if (alpha == ANDROID_BITMAP_FLAGS_ALPHA_OPAQUE) {
+        setFormatAndCheckStride(ANDROID_BITMAP_FORMAT_RGB_565);
+    }
+
+    if (isGray) {
+        setFormatAndCheckStride(ANDROID_BITMAP_FORMAT_A_8);
+    }
+
+    for (auto newFormat : { ANDROID_BITMAP_FORMAT_RGBA_8888, ANDROID_BITMAP_FORMAT_RGBA_F16 }) {
+        setFormatAndCheckStride(newFormat);
+    }
+
+    for (auto badFormat : { ANDROID_BITMAP_FORMAT_RGBA_4444, ANDROID_BITMAP_FORMAT_NONE }) {
+        int result = AImageDecoder_setAndroidBitmapFormat(decoder, badFormat);
+        ASSERT_EQ(ANDROID_IMAGE_DECODER_INVALID_CONVERSION, result);
+
+        // Stride is unchanged.
+        ASSERT_EQ(stride, AImageDecoder_getMinimumStride(decoder));
+    }
+}
+
+static bool bitmapsEqual(size_t minStride, int height,
+                         void* pixelsA, size_t strideA,
+                         void* pixelsB, size_t strideB) {
+    for (int y = 0; y < height; ++y) {
+        auto* rowA = reinterpret_cast<char*>(pixelsA) + strideA * y;
+        auto* rowB = reinterpret_cast<char*>(pixelsB) + strideB * y;
+        if (memcmp(rowA, rowB, minStride) != 0) {
+            ALOGE("Bitmap mismatch on line %i", y);
+            return false;
+        }
+    }
+    return true;
+}
+
+#define EXPECT_EQ(msg, a, b)    \
+    if ((a) != (b)) {           \
+        ALOGE(msg);             \
+        return false;           \
+    }
+
+#define EXPECT_GE(msg, a, b)    \
+    if ((a) < (b)) {            \
+        ALOGE(msg);             \
+        return false;           \
+    }
+
+static bool bitmapsEqual(JNIEnv* env, jobject jbitmap, AndroidBitmapFormat format,
+                         int width, int height, int alphaFlags, size_t minStride,
+                         void* pixelsA, size_t strideA) {
+    AndroidBitmapInfo jInfo;
+    int bitmapResult = AndroidBitmap_getInfo(env, jbitmap, &jInfo);
+    EXPECT_EQ("Failed to getInfo on Bitmap", ANDROID_BITMAP_RESULT_SUCCESS, bitmapResult);
+
+    EXPECT_EQ("Wrong format", jInfo.format, format);
+
+    // If the image is truly opaque, the Java Bitmap will report OPAQUE, even if
+    // the AImageDecoder requested PREMUL/UNPREMUL. In that case, it is okay for
+    // the two to disagree. We must ensure that we don't end up with one PREMUL
+    // and the other UNPREMUL, though.
+    auto jAlphaFlags = jInfo.flags & ANDROID_BITMAP_FLAGS_ALPHA_MASK;
+    if (jAlphaFlags != ANDROID_BITMAP_FLAGS_ALPHA_OPAQUE) {
+        EXPECT_EQ("Wrong alpha type", jAlphaFlags, alphaFlags);
+    }
+
+    EXPECT_EQ("Wrong width", jInfo.width, width);
+    EXPECT_EQ("Wrong height", jInfo.height, height);
+
+    EXPECT_GE("Stride too small", jInfo.stride, minStride);
+
+    void* jPixels;
+    bitmapResult = AndroidBitmap_lockPixels(env, jbitmap, &jPixels);
+    EXPECT_EQ("Failed to lockPixels", ANDROID_BITMAP_RESULT_SUCCESS, bitmapResult);
+
+    bool equal = bitmapsEqual(minStride, height, pixelsA, strideA, jPixels, jInfo.stride);
+
+    bitmapResult = AndroidBitmap_unlockPixels(env, jbitmap);
+    EXPECT_EQ("Failed to unlockPixels", ANDROID_BITMAP_RESULT_SUCCESS, bitmapResult);
+
+    return equal;
+}
+
+static void testDecode(JNIEnv* env, jclass, jlong imageDecoderPtr,
+                       jint androidBitmapFormat, jboolean unpremul, jobject jbitmap) {
+    AImageDecoder* decoder = reinterpret_cast<AImageDecoder*>(imageDecoderPtr);
+    DecoderDeleter decoderDeleter(decoder, AImageDecoder_delete);
+
+    const AImageDecoderHeaderInfo* info = AImageDecoder_getHeaderInfo(decoder);
+    ASSERT_NE(info, nullptr);
+
+    int result;
+    int alphaFlags = AImageDecoderHeaderInfo_getAlphaFlags(info);
+    if (androidBitmapFormat == ANDROID_BITMAP_FORMAT_NONE) {
+        androidBitmapFormat = AImageDecoderHeaderInfo_getAndroidBitmapFormat(info);
+    } else {
+        result = AImageDecoder_setAndroidBitmapFormat(decoder, androidBitmapFormat);
+        if (androidBitmapFormat == ANDROID_BITMAP_FORMAT_RGB_565) {
+            if (alphaFlags != ANDROID_BITMAP_FLAGS_ALPHA_OPAQUE) {
+                ASSERT_EQ(ANDROID_IMAGE_DECODER_INVALID_CONVERSION, result);
+
+                // The caller only passes down the Bitmap if it is opaque.
+                ASSERT_EQ(nullptr, jbitmap);
+                return;
+            }
+        }
+        ASSERT_EQ(ANDROID_IMAGE_DECODER_SUCCESS, result);
+    }
+
+    if (unpremul) {
+        result = AImageDecoder_setAlphaFlags(decoder, ANDROID_BITMAP_FLAGS_ALPHA_UNPREMUL);
+        ASSERT_EQ(ANDROID_IMAGE_DECODER_SUCCESS, result);
+        alphaFlags = ANDROID_BITMAP_FLAGS_ALPHA_UNPREMUL;
+    }
+
+    const int32_t width = AImageDecoderHeaderInfo_getWidth(info);
+    const int32_t height = AImageDecoderHeaderInfo_getHeight(info);
+    size_t minStride = AImageDecoder_getMinimumStride(decoder);
+
+    size_t size = minStride * height;
+    void* pixels = malloc(size);
+
+    {
+        // Try some invalid parameters.
+        result = AImageDecoder_decodeImage(decoder, nullptr, minStride, size);
+        ASSERT_EQ(ANDROID_IMAGE_DECODER_BAD_PARAMETER, result);
+
+        result = AImageDecoder_decodeImage(decoder, pixels, minStride - 1, size);
+        ASSERT_EQ(ANDROID_IMAGE_DECODER_BAD_PARAMETER, result);
+
+        result = AImageDecoder_decodeImage(decoder, pixels, minStride, size - minStride);
+        ASSERT_EQ(ANDROID_IMAGE_DECODER_BAD_PARAMETER, result);
+
+        result = AImageDecoder_decodeImage(decoder, pixels, 0, size);
+        ASSERT_EQ(ANDROID_IMAGE_DECODER_BAD_PARAMETER, result);
+    }
+
+    result = AImageDecoder_decodeImage(decoder, pixels, minStride, size);
+    ASSERT_EQ(ANDROID_IMAGE_DECODER_SUCCESS, result);
+
+    ASSERT_NE(jbitmap, nullptr);
+    ASSERT_TRUE(bitmapsEqual(env, jbitmap, (AndroidBitmapFormat) androidBitmapFormat,
+                             width, height, alphaFlags, minStride, pixels, minStride));
+
+    // Used for subsequent decodes, to ensure they are identical to the
+    // original. For opaque images, this verifies that using PREMUL or UNPREMUL
+    // look the same. For all images, this verifies that decodeImage can be
+    // called multiple times.
+    auto decodeAgain = [=](int alpha) {
+        int r = AImageDecoder_setAlphaFlags(decoder, alpha);
+        ASSERT_EQ(ANDROID_IMAGE_DECODER_SUCCESS, r);
+
+        void* otherPixels = malloc(size);
+        r = AImageDecoder_decodeImage(decoder, otherPixels, minStride, size);
+        ASSERT_EQ(ANDROID_IMAGE_DECODER_SUCCESS, r);
+
+        ASSERT_TRUE(bitmapsEqual(minStride, height, pixels, minStride, otherPixels, minStride));
+        free(otherPixels);
+    };
+    if (alphaFlags == ANDROID_BITMAP_FLAGS_ALPHA_OPAQUE) {
+        for (int otherAlpha : { ANDROID_BITMAP_FLAGS_ALPHA_PREMUL,
+                                ANDROID_BITMAP_FLAGS_ALPHA_UNPREMUL }) {
+            decodeAgain(otherAlpha);
+        }
+    } else {
+        decodeAgain(alphaFlags);
+    }
+
+    free(pixels);
+}
+
+static void testDecodeStride(JNIEnv* env, jclass, jlong imageDecoderPtr) {
+    AImageDecoder* decoder = reinterpret_cast<AImageDecoder*>(imageDecoderPtr);
+    DecoderDeleter decoderDeleter(decoder, AImageDecoder_delete);
+
+    const AImageDecoderHeaderInfo* info = AImageDecoder_getHeaderInfo(decoder);
+    ASSERT_NE(info, nullptr);
+
+    const int height = AImageDecoderHeaderInfo_getHeight(info);
+    size_t minStride = AImageDecoder_getMinimumStride(decoder);
+
+    void* pixels = nullptr;
+
+    // The code in this loop relies on minStride being used first.
+    for (size_t stride : { minStride, (size_t) (minStride * 1.5), minStride * 3 }) {
+        size_t size = stride * (height - 1) + minStride;
+        void* decodePixels = malloc(size);
+        int result = AImageDecoder_decodeImage(decoder, decodePixels, stride, size);
+        ASSERT_EQ(ANDROID_IMAGE_DECODER_SUCCESS, result);
+
+        if (pixels == nullptr) {
+            pixels = decodePixels;
+        } else {
+            ASSERT_TRUE(bitmapsEqual(minStride, height, pixels, minStride, decodePixels, stride));
+            free(decodePixels);
+        }
+    }
+
+    free(pixels);
+}
+
+static void testSetTargetSize(JNIEnv* env, jclass, jlong imageDecoderPtr) {
+    AImageDecoder* decoder = reinterpret_cast<AImageDecoder*>(imageDecoderPtr);
+    DecoderDeleter decoderDeleter(decoder, AImageDecoder_delete);
+
+    const size_t defaultStride = AImageDecoder_getMinimumStride(decoder);
+
+    for (int width : { -1, 0, -500 }) {
+        int result = AImageDecoder_setTargetSize(decoder, width, 100);
+        ASSERT_EQ(ANDROID_IMAGE_DECODER_INVALID_SCALE, result);
+        // stride is unchanged, as the target size did not change.
+        ASSERT_EQ(defaultStride, AImageDecoder_getMinimumStride(decoder));
+    }
+
+    for (int height : { -1, 0, -300 }) {
+        int result = AImageDecoder_setTargetSize(decoder, 100, height);
+        ASSERT_EQ(ANDROID_IMAGE_DECODER_INVALID_SCALE, result);
+        // stride is unchanged, as the target size did not change.
+        ASSERT_EQ(defaultStride, AImageDecoder_getMinimumStride(decoder));
+    }
+
+    const AImageDecoderHeaderInfo* info = AImageDecoder_getHeaderInfo(decoder);
+    ASSERT_NE(info, nullptr);
+    const int bpp = bytesPerPixel(AImageDecoderHeaderInfo_getAndroidBitmapFormat(info));
+
+    for (int width : { 7, 100, 275, 300 }) {
+        int result = AImageDecoder_setTargetSize(decoder, width, 100);
+        ASSERT_EQ(ANDROID_IMAGE_DECODER_SUCCESS, result);
+
+        size_t actualStride = AImageDecoder_getMinimumStride(decoder);
+        size_t expectedStride = bpp * width;
+        ASSERT_EQ(expectedStride, actualStride);
+    }
+
+    // Verify that setting a large enough width to overflow 31 bits fails.
+    constexpr auto kMaxInt32 = std::numeric_limits<int32_t>::max();
+    int32_t maxWidth = kMaxInt32 / bpp;
+    for (int32_t width : { maxWidth / 2, maxWidth - 1, maxWidth }) {
+        int result = AImageDecoder_setTargetSize(decoder, width, 1);
+        ASSERT_EQ(ANDROID_IMAGE_DECODER_SUCCESS, result);
+
+        size_t actualStride = AImageDecoder_getMinimumStride(decoder);
+        size_t expectedStride = bpp * width;
+        ASSERT_EQ(expectedStride, actualStride);
+    }
+
+    for (int32_t width : { maxWidth + 1, (int32_t) (maxWidth * 1.5) }) {
+        int result = AImageDecoder_setTargetSize(decoder, width, 1);
+        ASSERT_EQ(ANDROID_IMAGE_DECODER_INVALID_SCALE, result);
+    }
+
+    // A height that results in overflowing 31 bits also fails.
+    int32_t maxHeight = kMaxInt32 / defaultStride;
+    const int32_t width = AImageDecoderHeaderInfo_getWidth(info);
+    for (int32_t height : { maxHeight / 2, maxHeight - 1, maxHeight }) {
+        int result = AImageDecoder_setTargetSize(decoder, width, height);
+        ASSERT_EQ(ANDROID_IMAGE_DECODER_SUCCESS, result);
+
+        size_t actualStride = AImageDecoder_getMinimumStride(decoder);
+        size_t expectedStride = bpp * width;
+        ASSERT_EQ(expectedStride, actualStride);
+    }
+
+    for (int32_t height : { maxHeight + 1, (int32_t) (maxHeight * 1.5) }) {
+        int result = AImageDecoder_setTargetSize(decoder, width, height);
+        ASSERT_EQ(ANDROID_IMAGE_DECODER_INVALID_SCALE, result);
+    }
+}
+
+static void testDecodeScaled(JNIEnv* env, jclass, jlong imageDecoderPtr,
+                             jobject jbitmap) {
+    AImageDecoder* decoder = reinterpret_cast<AImageDecoder*>(imageDecoderPtr);
+    DecoderDeleter decoderDeleter(decoder, AImageDecoder_delete);
+
+    AndroidBitmapInfo jInfo;
+    int bitmapResult = AndroidBitmap_getInfo(env, jbitmap, &jInfo);
+    ASSERT_EQ(ANDROID_BITMAP_RESULT_SUCCESS, bitmapResult);
+
+    int result = AImageDecoder_setTargetSize(decoder, jInfo.width, jInfo.height);
+    ASSERT_EQ(ANDROID_IMAGE_DECODER_SUCCESS, result);
+
+    size_t minStride = AImageDecoder_getMinimumStride(decoder);
+    size_t size = minStride * jInfo.height;
+    void* pixels = malloc(size);
+
+    {
+        // Try some invalid parameters.
+        result = AImageDecoder_decodeImage(decoder, nullptr, minStride, size);
+        ASSERT_EQ(ANDROID_IMAGE_DECODER_BAD_PARAMETER, result);
+
+        result = AImageDecoder_decodeImage(decoder, pixels, minStride - 1, size);
+        ASSERT_EQ(ANDROID_IMAGE_DECODER_BAD_PARAMETER, result);
+
+        result = AImageDecoder_decodeImage(decoder, pixels, minStride, size - minStride);
+        ASSERT_EQ(ANDROID_IMAGE_DECODER_BAD_PARAMETER, result);
+    }
+
+    result = AImageDecoder_decodeImage(decoder, pixels, minStride, size);
+    ASSERT_EQ(ANDROID_IMAGE_DECODER_SUCCESS, result);
+
+    const AImageDecoderHeaderInfo* info = AImageDecoder_getHeaderInfo(decoder);
+    ASSERT_NE(info, nullptr);
+
+    ASSERT_NE(jbitmap, nullptr);
+    ASSERT_TRUE(bitmapsEqual(env, jbitmap, AImageDecoderHeaderInfo_getAndroidBitmapFormat(info),
+                             jInfo.width, jInfo.height, AImageDecoderHeaderInfo_getAlphaFlags(info),
+                             minStride, pixels, minStride));
+
+    // Verify that larger strides still behave as expected.
+    for (size_t stride : { (size_t) (minStride * 1.5), minStride * 3 }) {
+        size_t size = stride * (jInfo.height - 1) + minStride;
+        void* decodePixels = malloc(size);
+        result = AImageDecoder_decodeImage(decoder, decodePixels, stride, size);
+        ASSERT_EQ(ANDROID_IMAGE_DECODER_SUCCESS, result);
+
+        ASSERT_TRUE(bitmapsEqual(minStride, jInfo.height, pixels, minStride, decodePixels, stride));
+        free(decodePixels);
+    }
+
+    free(pixels);
+}
+
+static void testSetCrop(JNIEnv* env, jclass, jobject jAssets, jstring jFile) {
+    AAsset* asset = openAsset(env, jAssets, jFile, AASSET_MODE_UNKNOWN);
+    ASSERT_NE(asset, nullptr);
+    AssetCloser assetCloser(asset, AAsset_close);
+
+    AImageDecoder* decoder;
+    int result = AImageDecoder_createFromAAsset(asset, &decoder);
+    ASSERT_EQ(ANDROID_IMAGE_DECODER_SUCCESS, result);
+    ASSERT_NE(decoder, nullptr);
+    DecoderDeleter decoderDeleter(decoder, AImageDecoder_delete);
+
+    const AImageDecoderHeaderInfo* info = AImageDecoder_getHeaderInfo(decoder);
+    ASSERT_NE(info, nullptr);
+
+    const int32_t width = AImageDecoderHeaderInfo_getWidth(info);
+    const int32_t height = AImageDecoderHeaderInfo_getHeight(info);
+    const AndroidBitmapFormat format = AImageDecoderHeaderInfo_getAndroidBitmapFormat(info);
+    const size_t defaultStride = AImageDecoder_getMinimumStride(decoder);
+
+    if (width == 1 && height == 1) {
+        // The more general crop tests do not map well to this image. Test 1 x 1
+        // specifically.
+        for (ARect invalidCrop : std::initializer_list<ARect> {
+                { -1, 0, width, height },
+                { 0, -1, width, height },
+                { width, 0, 2 * width, height },
+                { 0, height, width, 2 * height },
+                { 1, 0, width + 1, height },
+                { 0, 1, width, height + 1 },
+                { 0, 0, 0, height },
+                { 0, 0, width, 0 },
+        }) {
+            int result = AImageDecoder_setCrop(decoder, invalidCrop);
+            ASSERT_EQ(ANDROID_IMAGE_DECODER_BAD_PARAMETER, result);
+            ASSERT_EQ(defaultStride, AImageDecoder_getMinimumStride(decoder));
+        }
+        return;
+    }
+
+    for (ARect invalidCrop : std::initializer_list<ARect> {
+            { -1, 0, width, height },
+            { 0, -1, width, height },
+            { width, 0, 2 * width, height },
+            { 0, height, width, 2 * height },
+            { 1, 0, width + 1, height },
+            { 0, 1, width, height + 1 },
+            { width - 1, 0, 1, height },
+            { 0, height - 1, width, 1 },
+            { 0, 0, 0, height },
+            { 0, 0, width, 0 },
+            { 1, 1, 1, 1 },
+            { width, height, 0, 0 },
+    }) {
+        int result = AImageDecoder_setCrop(decoder, invalidCrop);
+        ASSERT_EQ(ANDROID_IMAGE_DECODER_BAD_PARAMETER, result);
+        ASSERT_EQ(defaultStride, AImageDecoder_getMinimumStride(decoder));
+    }
+
+    for (ARect validCrop : std::initializer_list<ARect> {
+            { 0, 0, width, height },
+            { 0, 0, width / 2, height / 2},
+            { 0, 0, width / 3, height },
+            { 0, 0, width, height / 4},
+            { width / 2, 0, width, height / 2},
+            { 0, height / 2, width / 2, height },
+            { width / 2, height / 2, width, height },
+            { 1, 1, width - 1, height - 1 },
+    }) {
+        int result = AImageDecoder_setCrop(decoder, validCrop);
+        ASSERT_EQ(ANDROID_IMAGE_DECODER_SUCCESS, result);
+        size_t actualStride = AImageDecoder_getMinimumStride(decoder);
+        size_t expectedStride = bytesPerPixel(format) * (validCrop.right - validCrop.left);
+        ASSERT_EQ(expectedStride, actualStride);
+    }
+
+    // Reset the crop, so we can test setting a crop *after* changing the
+    // target size.
+    result = AImageDecoder_setCrop(decoder, { 0, 0, 0, 0 });
+    ASSERT_EQ(ANDROID_IMAGE_DECODER_SUCCESS, result);
+    ASSERT_EQ(defaultStride, AImageDecoder_getMinimumStride(decoder));
+
+    int newWidth = width / 2, newHeight = height / 2;
+    result = AImageDecoder_setTargetSize(decoder, newWidth, newHeight);
+    ASSERT_EQ(ANDROID_IMAGE_DECODER_SUCCESS, result);
+    const size_t halfStride = AImageDecoder_getMinimumStride(decoder);
+    {
+        size_t expectedStride = bytesPerPixel(format) * newWidth;
+        ASSERT_EQ(expectedStride, halfStride);
+    }
+
+    // At the smaller target size, crops that were previously valid no longer
+    // are.
+    for (ARect invalidCrop : std::initializer_list<ARect> {
+            { 0, 0, width / 3, height },
+            { 0, 0, width, height / 4},
+            { width / 2, 0, width, height / 2},
+            { 0, height / 2, width / 2, height },
+            { width / 2, height / 2, width, height },
+            { 1, 1, width - 1, height - 1 },
+    }) {
+        int result = AImageDecoder_setCrop(decoder, invalidCrop);
+        ASSERT_EQ(ANDROID_IMAGE_DECODER_BAD_PARAMETER, result);
+        ASSERT_EQ(halfStride, AImageDecoder_getMinimumStride(decoder));
+    }
+
+    for (ARect validCrop : std::initializer_list<ARect> {
+            { 0, 0, newWidth, newHeight },
+            { 0, 0, newWidth / 3, newHeight },
+            { 0, 0, newWidth, newHeight / 4},
+            { newWidth / 2, 0, newWidth, newHeight / 2},
+            { 0, newHeight / 2, newWidth / 2, newHeight },
+            { newWidth / 2, newHeight / 2, newWidth, newHeight },
+            { 1, 1, newWidth - 1, newHeight - 1 },
+    }) {
+        int result = AImageDecoder_setCrop(decoder, validCrop);
+        ASSERT_EQ(ANDROID_IMAGE_DECODER_SUCCESS, result);
+        size_t actualStride = AImageDecoder_getMinimumStride(decoder);
+        size_t expectedStride = bytesPerPixel(format) * (validCrop.right - validCrop.left);
+        ASSERT_EQ(expectedStride, actualStride);
+    }
+
+    newWidth = width * 2;
+    newHeight = height * 2;
+    result = AImageDecoder_setTargetSize(decoder, newWidth, newHeight);
+    ASSERT_EQ(ANDROID_IMAGE_DECODER_SUCCESS, result);
+
+    for (ARect validCrop : std::initializer_list<ARect> {
+            { width, 0, newWidth, height },
+            { 0, height * 3 / 4, width * 4 / 3, height }
+    }) {
+        int result = AImageDecoder_setCrop(decoder, validCrop);
+        ASSERT_EQ(ANDROID_IMAGE_DECODER_SUCCESS, result);
+        size_t actualStride = AImageDecoder_getMinimumStride(decoder);
+        size_t expectedStride = bytesPerPixel(format) * (validCrop.right - validCrop.left);
+        ASSERT_EQ(expectedStride, actualStride);
+    }
+
+    // Reset crop and target size, so that we can verify that setting a crop and
+    // then setting target size that will not support the crop fails.
+    result = AImageDecoder_setCrop(decoder, { 0, 0, 0, 0 });
+    ASSERT_EQ(ANDROID_IMAGE_DECODER_SUCCESS, result);
+    result = AImageDecoder_setTargetSize(decoder, width, height);
+    ASSERT_EQ(ANDROID_IMAGE_DECODER_SUCCESS, result);
+    ASSERT_EQ(defaultStride, AImageDecoder_getMinimumStride(decoder));
+
+    ARect crop{ width / 2, height / 2, width, height };
+    result = AImageDecoder_setCrop(decoder, crop);
+    ASSERT_EQ(ANDROID_IMAGE_DECODER_SUCCESS, result);
+    const size_t croppedStride = AImageDecoder_getMinimumStride(decoder);
+    {
+        size_t expectedStride = bytesPerPixel(format) * (crop.right - crop.left);
+        ASSERT_EQ(expectedStride, croppedStride);
+    }
+    result = AImageDecoder_setTargetSize(decoder, width / 2, height / 2);
+    ASSERT_EQ(ANDROID_IMAGE_DECODER_INVALID_SCALE, result);
+    ASSERT_EQ(croppedStride, AImageDecoder_getMinimumStride(decoder));
+}
+
+static void testDecodeCrop(JNIEnv* env, jclass, jlong imageDecoderPtr,
+                           jobject jbitmap, jint targetWidth, jint targetHeight,
+                           jint left, jint top, jint right, jint bottom) {
+    AImageDecoder* decoder = reinterpret_cast<AImageDecoder*>(imageDecoderPtr);
+    DecoderDeleter decoderDeleter(decoder, AImageDecoder_delete);
+
+    int result;
+    if (targetWidth != 0 && targetHeight != 0) {
+        result = AImageDecoder_setTargetSize(decoder, targetWidth, targetHeight);
+        ASSERT_EQ(ANDROID_IMAGE_DECODER_SUCCESS, result);
+    }
+
+    ARect rect { left, top, right, bottom };
+    result = AImageDecoder_setCrop(decoder, rect);
+    ASSERT_EQ(ANDROID_IMAGE_DECODER_SUCCESS, result);
+
+    const int32_t width = right - left;
+    const int32_t height = bottom - top;
+    size_t minStride = AImageDecoder_getMinimumStride(decoder);
+    size_t size = minStride * height;
+    void* pixels = malloc(size);
+
+    {
+        // Try some invalid parameters.
+        result = AImageDecoder_decodeImage(decoder, nullptr, minStride, size);
+        ASSERT_EQ(ANDROID_IMAGE_DECODER_BAD_PARAMETER, result);
+
+        result = AImageDecoder_decodeImage(decoder, pixels, minStride - 1, size);
+        ASSERT_EQ(ANDROID_IMAGE_DECODER_BAD_PARAMETER, result);
+
+        result = AImageDecoder_decodeImage(decoder, pixels, minStride, size - minStride);
+        ASSERT_EQ(ANDROID_IMAGE_DECODER_BAD_PARAMETER, result);
+    }
+
+    result = AImageDecoder_decodeImage(decoder, pixels, minStride, size);
+    ASSERT_EQ(ANDROID_IMAGE_DECODER_SUCCESS, result);
+
+    const AImageDecoderHeaderInfo* info = AImageDecoder_getHeaderInfo(decoder);
+    ASSERT_NE(info, nullptr);
+
+    ASSERT_NE(jbitmap, nullptr);
+    ASSERT_TRUE(bitmapsEqual(env, jbitmap, AImageDecoderHeaderInfo_getAndroidBitmapFormat(info),
+                             width, height, AImageDecoderHeaderInfo_getAlphaFlags(info),
+                             minStride, pixels, minStride));
+
+    // Verify that larger strides still behave as expected.
+    for (size_t stride : { (size_t) (minStride * 1.5), minStride * 3 }) {
+        size_t size = stride * (height - 1) + minStride;
+        void* decodePixels = malloc(size);
+        result = AImageDecoder_decodeImage(decoder, decodePixels, stride, size);
+        ASSERT_EQ(ANDROID_IMAGE_DECODER_SUCCESS, result);
+
+        ASSERT_TRUE(bitmapsEqual(minStride, height, pixels, minStride, decodePixels, stride));
+        free(decodePixels);
+    }
+
+    free(pixels);
+}
+
+static void testScalePlusUnpremul(JNIEnv* env, jclass, jlong imageDecoderPtr) {
+    AImageDecoder* decoder = reinterpret_cast<AImageDecoder*>(imageDecoderPtr);
+    DecoderDeleter decoderDeleter(decoder, AImageDecoder_delete);
+
+    const AImageDecoderHeaderInfo* info = AImageDecoder_getHeaderInfo(decoder);
+    ASSERT_NE(nullptr, info);
+
+    const int32_t width = AImageDecoderHeaderInfo_getWidth(info);
+    const int32_t height = AImageDecoderHeaderInfo_getHeight(info);
+    const int alpha = AImageDecoderHeaderInfo_getAlphaFlags(info);
+
+    if (alpha == ANDROID_BITMAP_FLAGS_ALPHA_OPAQUE) {
+        // Set alpha, then scale. This succeeds for an opaque image.
+        int result = AImageDecoder_setAlphaFlags(decoder, ANDROID_BITMAP_FLAGS_ALPHA_UNPREMUL);
+        ASSERT_EQ(ANDROID_IMAGE_DECODER_SUCCESS, result);
+
+        result = AImageDecoder_setTargetSize(decoder, width * 2, height * 2);
+        ASSERT_EQ(ANDROID_IMAGE_DECODER_SUCCESS, result);
+
+        result = AImageDecoder_setTargetSize(decoder, width * 2/3, height * 2/3);
+        if (width * 2/3 == 0 || height * 2/3 == 0) {
+            // The image that is 1x1 cannot be downscaled.
+            ASSERT_EQ(ANDROID_IMAGE_DECODER_INVALID_SCALE, result);
+        } else {
+            ASSERT_EQ(ANDROID_IMAGE_DECODER_SUCCESS, result);
+        }
+
+        // Reset to the original settings to test the other order.
+        result = AImageDecoder_setAlphaFlags(decoder, ANDROID_BITMAP_FLAGS_ALPHA_PREMUL);
+        ASSERT_EQ(ANDROID_IMAGE_DECODER_SUCCESS, result);
+
+        result = AImageDecoder_setTargetSize(decoder, width, height);
+        ASSERT_EQ(ANDROID_IMAGE_DECODER_SUCCESS, result);
+
+        // Specify scale and then unpremul.
+        if (width * 2/3 == 0 || height * 2/3 == 0) {
+            // The image that is 1x1 cannot be downscaled. Scale up instead.
+            result = AImageDecoder_setTargetSize(decoder, width * 2, height * 2);
+        } else {
+            result = AImageDecoder_setTargetSize(decoder, width * 2/3, height * 2/3);
+        }
+        ASSERT_EQ(ANDROID_IMAGE_DECODER_SUCCESS, result);
+
+        result = AImageDecoder_setAlphaFlags(decoder, ANDROID_BITMAP_FLAGS_ALPHA_UNPREMUL);
+        ASSERT_EQ(ANDROID_IMAGE_DECODER_SUCCESS, result);
+    } else {
+        // Use unpremul and then scale. Setting to unpremul is successful, but
+        // later calls to change the scale fail.
+        int result = AImageDecoder_setAlphaFlags(decoder, ANDROID_BITMAP_FLAGS_ALPHA_UNPREMUL);
+        ASSERT_EQ(ANDROID_IMAGE_DECODER_SUCCESS, result);
+
+        result = AImageDecoder_setTargetSize(decoder, width * 2, height * 2);
+        ASSERT_EQ(ANDROID_IMAGE_DECODER_INVALID_SCALE, result);
+
+        result = AImageDecoder_setTargetSize(decoder, width * 2/3, height * 2/3);
+        ASSERT_EQ(ANDROID_IMAGE_DECODER_INVALID_SCALE, result);
+
+        // Set back to premul to verify that the opposite order also fails.
+        result = AImageDecoder_setAlphaFlags(decoder, ANDROID_BITMAP_FLAGS_ALPHA_PREMUL);
+        ASSERT_EQ(ANDROID_IMAGE_DECODER_SUCCESS, result);
+
+        result = AImageDecoder_setTargetSize(decoder, width * 2, height * 2);
+        ASSERT_EQ(ANDROID_IMAGE_DECODER_SUCCESS, result);
+        result = AImageDecoder_setAlphaFlags(decoder, ANDROID_BITMAP_FLAGS_ALPHA_UNPREMUL);
+        ASSERT_EQ(ANDROID_IMAGE_DECODER_INVALID_CONVERSION, result);
+
+        result = AImageDecoder_setTargetSize(decoder, width * 2/3, height * 2/3);
+        ASSERT_EQ(ANDROID_IMAGE_DECODER_SUCCESS, result);
+        result = AImageDecoder_setAlphaFlags(decoder, ANDROID_BITMAP_FLAGS_ALPHA_UNPREMUL);
+        ASSERT_EQ(ANDROID_IMAGE_DECODER_INVALID_CONVERSION, result);
+    }
+}
+
+#define ASSET_MANAGER "Landroid/content/res/AssetManager;"
+#define STRING "Ljava/lang/String;"
+#define BITMAP "Landroid/graphics/Bitmap;"
+
+static JNINativeMethod gMethods[] = {
+    { "nTestEmptyCreate", "()V", (void*) testEmptyCreate },
+    { "nTestNullDecoder", "(" ASSET_MANAGER STRING ")V", (void*) testNullDecoder },
+    { "nTestInfo", "(JII" STRING "ZZ)V", (void*) testInfo },
+    { "nOpenAsset", "(" ASSET_MANAGER STRING ")J", (void*) openAssetNative },
+    { "nCloseAsset", "(J)V", (void*) closeAsset },
+    { "nCreateFromAsset", "(J)J", (void*) createFromAsset },
+    { "nCreateFromAssetFd", "(J)J", (void*) createFromAssetFd },
+    { "nCreateFromAssetBuffer", "(J)J", (void*) createFromAssetBuffer },
+    { "nCreateFromFd", "(I)J", (void*) createFromFd },
+    { "nTestCreateIncomplete", "(" ASSET_MANAGER STRING "I)V", (void*) testCreateIncomplete },
+    { "nTestCreateUnsupported", "(" ASSET_MANAGER STRING ")V", (void*) testCreateUnsupported },
+    { "nTestSetFormat", "(JZZ)V", (void*) testSetFormat },
+    { "nTestSetAlpha", "(JZ)V", (void*) testSetAlpha },
+    { "nTestGetMinimumStride", "(JZZ)V", (void*) testGetMinimumStride },
+    { "nTestDecode", "(JIZ" BITMAP ")V", (void*) testDecode },
+    { "nTestDecodeStride", "(J)V", (void*) testDecodeStride },
+    { "nTestSetTargetSize", "(J)V", (void*) testSetTargetSize },
+    { "nTestDecodeScaled", "(J" BITMAP ")V", (void*) testDecodeScaled },
+    { "nTestSetCrop", "(" ASSET_MANAGER STRING ")V", (void*) testSetCrop },
+    { "nTestDecodeCrop", "(J" BITMAP "IIIIII)V", (void*) testDecodeCrop },
+    { "nTestScalePlusUnpremul", "(J)V", (void*) testScalePlusUnpremul },
+};
+
+int register_android_graphics_cts_AImageDecoderTest(JNIEnv* env) {
+    jclass clazz = env->FindClass("android/graphics/cts/AImageDecoderTest");
+    return env->RegisterNatives(clazz, gMethods,
+            sizeof(gMethods) / sizeof(JNINativeMethod));
+}
+
diff --git a/tests/tests/graphics/src/android/graphics/cts/AImageDecoderTest.java b/tests/tests/graphics/src/android/graphics/cts/AImageDecoderTest.java
new file mode 100644
index 0000000..d4daa18
--- /dev/null
+++ b/tests/tests/graphics/src/android/graphics/cts/AImageDecoderTest.java
@@ -0,0 +1,888 @@
+/*
+ * Copyright (C) 2019 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 android.graphics.cts;
+
+import static android.system.OsConstants.SEEK_SET;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.fail;
+
+import android.content.ContentResolver;
+import android.content.res.AssetManager;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.ImageDecoder;
+import android.graphics.Rect;
+import android.net.Uri;
+import android.os.ParcelFileDescriptor;
+import android.system.ErrnoException;
+import android.system.Os;
+
+import androidx.test.InstrumentationRegistry;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.File;
+import java.io.FileDescriptor;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+
+import junitparams.JUnitParamsRunner;
+import junitparams.Parameters;
+
+@RunWith(JUnitParamsRunner.class)
+public class AImageDecoderTest {
+    static {
+        System.loadLibrary("ctsgraphics_jni");
+    }
+
+    private static AssetManager getAssetManager() {
+        return InstrumentationRegistry.getTargetContext().getAssets();
+    }
+
+    private static Resources getResources() {
+        return InstrumentationRegistry.getTargetContext().getResources();
+    }
+
+    private static ContentResolver getContentResolver() {
+        return InstrumentationRegistry.getTargetContext().getContentResolver();
+    }
+
+    // These match the formats in the NDK.
+    // ANDROID_BITMAP_FORMAT_NONE is used by nTestDecode to signal using the default.
+    private static final int ANDROID_BITMAP_FORMAT_NONE = 0;
+    private static final int ANDROID_BITMAP_FORMAT_RGB_565 = 4;
+    private static final int ANDROID_BITMAP_FORMAT_A_8 = 8;
+    private static final int ANDROID_BITMAP_FORMAT_RGBA_F16 = 9;
+
+    @Test
+    public void testEmptyCreate() {
+        nTestEmptyCreate();
+    }
+
+    private static Object[] getAssetRecords() {
+        return ImageDecoderTest.getAssetRecords();
+    }
+
+    private static Object[] getRecords() {
+        return ImageDecoderTest.getRecords();
+    }
+
+    // For testing all of the assets as premul and unpremul.
+    private static Object[] getAssetRecordsUnpremul() {
+        return ImageDecoderTest.crossProduct(getAssetRecords(), new Object[] { true, false });
+    }
+
+    private static Object[] getRecordsUnpremul() {
+        return ImageDecoderTest.crossProduct(getRecords(), new Object[] { true, false });
+    }
+
+    // For testing all of the assets at different sample sizes.
+    private static Object[] getAssetRecordsSample() {
+        return ImageDecoderTest.crossProduct(getAssetRecords(), new Object[] { 2, 3, 4, 8, 16 });
+    }
+
+    private static Object[] getRecordsSample() {
+        return ImageDecoderTest.crossProduct(getRecords(), new Object[] { 2, 3, 4, 8, 16 });
+    }
+
+    @Test
+    @Parameters(method = "getAssetRecords")
+    public void testNullDecoder(ImageDecoderTest.AssetRecord record) {
+        nTestNullDecoder(getAssetManager(), record.name);
+    }
+
+    @Test
+    @Parameters(method = "getAssetRecords")
+    public void testCreateBuffer(ImageDecoderTest.AssetRecord record) {
+        // Note: This uses an asset for simplicity, but in native it gets a
+        // buffer.
+        long asset = nOpenAsset(getAssetManager(), record.name);
+        long aimagedecoder = nCreateFromAssetBuffer(asset);
+
+        nTestInfo(aimagedecoder, record.width, record.height, "image/png",
+                false /* isAnimated */, record.isF16);
+        nCloseAsset(asset);
+    }
+
+    @Test
+    @Parameters(method = "getAssetRecords")
+    public void testCreateFd(ImageDecoderTest.AssetRecord record) {
+        // Note: This uses an asset for simplicity, but in native it gets a
+        // file descriptor.
+        long asset = nOpenAsset(getAssetManager(), record.name);
+        long aimagedecoder = nCreateFromAssetFd(asset);
+
+        nTestInfo(aimagedecoder, record.width, record.height, "image/png",
+                false /* isAnimated */, record.isF16);
+        nCloseAsset(asset);
+    }
+
+    @Test
+    @Parameters(method = "getAssetRecords")
+    public void testCreateAsset(ImageDecoderTest.AssetRecord record) {
+        long asset = nOpenAsset(getAssetManager(), record.name);
+        long aimagedecoder = nCreateFromAsset(asset);
+
+        nTestInfo(aimagedecoder, record.width, record.height, "image/png",
+                false /* isAnimated */, record.isF16);
+        nCloseAsset(asset);
+    }
+
+    private static ParcelFileDescriptor open(int resId, int offset) throws FileNotFoundException {
+        File file = Utils.obtainFile(resId, offset);
+        assertNotNull(file);
+
+        ParcelFileDescriptor pfd = ParcelFileDescriptor.open(file,
+                ParcelFileDescriptor.MODE_READ_ONLY);
+        assertNotNull(pfd);
+        return pfd;
+    }
+
+    private static ParcelFileDescriptor open(int resId) throws FileNotFoundException {
+        return open(resId, 0);
+    }
+
+    @Test
+    @Parameters(method = "getRecords")
+    public void testCreateFdResources(ImageDecoderTest.Record record) throws IOException {
+        try (ParcelFileDescriptor pfd = open(record.resId)) {
+            long aimagedecoder = nCreateFromFd(pfd.getFd());
+
+            nTestInfo(aimagedecoder, record.width, record.height, record.mimeType,
+                    false /* isAnimated */, false /*isF16*/);
+        } catch (FileNotFoundException e) {
+            fail("Could not open " + Utils.getAsResourceUri(record.resId));
+        }
+    }
+
+    @Test
+    @Parameters(method = "getRecords")
+    public void testCreateFdOffset(ImageDecoderTest.Record record) throws IOException {
+        // Use an arbitrary offset. This ensures that we rewind to the correct offset.
+        final int offset = 15;
+        try (ParcelFileDescriptor pfd = open(record.resId, offset)) {
+            FileDescriptor fd = pfd.getFileDescriptor();
+            Os.lseek(fd, offset, SEEK_SET);
+            long aimagedecoder = nCreateFromFd(pfd.getFd());
+
+            nTestInfo(aimagedecoder, record.width, record.height, record.mimeType,
+                    false /* isAnimated */, false /*isF16*/);
+        } catch (FileNotFoundException e) {
+            fail("Could not open " + Utils.getAsResourceUri(record.resId));
+        } catch (ErrnoException err) {
+            fail("Failed to seek " + Utils.getAsResourceUri(record.resId));
+        }
+    }
+
+    @Test
+    public void testCreateIncomplete() {
+        String file = "green-srgb.png";
+        // This truncates the file before the IDAT.
+        nTestCreateIncomplete(getAssetManager(), file, 823);
+    }
+
+    @Test
+    @Parameters({"shaders/tri.frag", "test_video.mp4"})
+    public void testUnsupportedFormat(String file) {
+        nTestCreateUnsupported(getAssetManager(), file);
+    }
+
+    @Test
+    @Parameters(method = "getAssetRecords")
+    public void testSetFormat(ImageDecoderTest.AssetRecord record) {
+        long asset = nOpenAsset(getAssetManager(), record.name);
+        long aimagedecoder = nCreateFromAsset(asset);
+
+        nTestSetFormat(aimagedecoder, record.isF16, record.isGray);
+        nCloseAsset(asset);
+    }
+
+    @Test
+    @Parameters(method = "getRecords")
+    public void testSetFormatResources(ImageDecoderTest.Record record) throws IOException {
+        try (ParcelFileDescriptor pfd = open(record.resId)) {
+            long aimagedecoder = nCreateFromFd(pfd.getFd());
+
+            nTestSetFormat(aimagedecoder, false /* isF16 */, record.isGray);
+        } catch (FileNotFoundException e) {
+            fail("Could not open " + Utils.getAsResourceUri(record.resId));
+        }
+    }
+
+    @Test
+    @Parameters(method = "getAssetRecords")
+    public void testSetAlpha(ImageDecoderTest.AssetRecord record) {
+        long asset = nOpenAsset(getAssetManager(), record.name);
+        long aimagedecoder = nCreateFromAsset(asset);
+
+        nTestSetAlpha(aimagedecoder, record.hasAlpha);
+        nCloseAsset(asset);
+    }
+
+    @Test
+    @Parameters(method = "getRecords")
+    public void testSetAlphaResources(ImageDecoderTest.Record record) throws IOException {
+        try (ParcelFileDescriptor pfd = open(record.resId)) {
+            long aimagedecoder = nCreateFromFd(pfd.getFd());
+
+            nTestSetAlpha(aimagedecoder, record.hasAlpha);
+        } catch (FileNotFoundException e) {
+            fail("Could not open " + Utils.getAsResourceUri(record.resId));
+        }
+    }
+
+    @Test
+    @Parameters(method = "getAssetRecords")
+    public void testGetMinimumStride(ImageDecoderTest.AssetRecord record) {
+        long asset = nOpenAsset(getAssetManager(), record.name);
+        long aimagedecoder = nCreateFromAsset(asset);
+
+        nTestGetMinimumStride(aimagedecoder, record.isF16, record.isGray);
+    }
+
+    @Test
+    @Parameters(method = "getRecords")
+    public void testGetMinimumStrideResources(ImageDecoderTest.Record record) throws IOException {
+        try (ParcelFileDescriptor pfd = open(record.resId)) {
+            long aimagedecoder = nCreateFromFd(pfd.getFd());
+
+            nTestGetMinimumStride(aimagedecoder, false /* isF16 */, record.isGray);
+        } catch (FileNotFoundException e) {
+            fail("Could not open " + Utils.getAsResourceUri(record.resId));
+        }
+    }
+
+    private static Bitmap decode(ImageDecoder.Source src, boolean unpremul) {
+        try {
+            return ImageDecoder.decodeBitmap(src, (decoder, info, source) -> {
+                // So we can compare pixels to the native decode.
+                decoder.setAllocator(ImageDecoder.ALLOCATOR_SOFTWARE);
+                decoder.setUnpremultipliedRequired(unpremul);
+            });
+        } catch (IOException e) {
+            fail("Failed to decode in Java with " + e);
+            return null;
+        }
+    }
+
+    @Test
+    @Parameters(method = "getAssetRecordsUnpremul")
+    public void testDecode(ImageDecoderTest.AssetRecord record, boolean unpremul) {
+        AssetManager assets = getAssetManager();
+        ImageDecoder.Source src = ImageDecoder.createSource(assets, record.name);
+        Bitmap bm = decode(src, unpremul);
+
+        long asset = nOpenAsset(assets, record.name);
+        long aimagedecoder = nCreateFromAsset(asset);
+
+        nTestDecode(aimagedecoder, ANDROID_BITMAP_FORMAT_NONE, unpremul, bm);
+        nCloseAsset(asset);
+    }
+
+    @Test
+    @Parameters(method = "getRecordsUnpremul")
+    public void testDecodeResources(ImageDecoderTest.Record record, boolean unpremul)
+            throws IOException {
+        ImageDecoder.Source src = ImageDecoder.createSource(getResources(),
+                record.resId);
+        Bitmap bm = decode(src, unpremul);
+        try (ParcelFileDescriptor pfd = open(record.resId)) {
+            long aimagedecoder = nCreateFromFd(pfd.getFd());
+
+            nTestDecode(aimagedecoder, ANDROID_BITMAP_FORMAT_NONE, unpremul, bm);
+        } catch (FileNotFoundException e) {
+            fail("Could not open " + Utils.getAsResourceUri(record.resId));
+        }
+    }
+
+    private static Bitmap decode(ImageDecoder.Source src, Bitmap.Config config) {
+        try {
+            return ImageDecoder.decodeBitmap(src, (decoder, info, source) -> {
+                // So we can compare pixels to the native decode.
+                decoder.setAllocator(ImageDecoder.ALLOCATOR_SOFTWARE);
+                switch (config) {
+                    case RGB_565:
+                        decoder.setMemorySizePolicy(ImageDecoder.MEMORY_POLICY_LOW_RAM);
+                        break;
+                    case ALPHA_8:
+                        decoder.setDecodeAsAlphaMaskEnabled(true);
+                        break;
+                    default:
+                        fail("Unexpected Config " + config);
+                        break;
+                }
+            });
+        } catch (IOException e) {
+            fail("Failed to decode in Java with " + e);
+            return null;
+        }
+    }
+
+    @Test
+    @Parameters(method = "getAssetRecords")
+    public void testDecode565(ImageDecoderTest.AssetRecord record) {
+        AssetManager assets = getAssetManager();
+        ImageDecoder.Source src = ImageDecoder.createSource(assets, record.name);
+        Bitmap bm = decode(src, Bitmap.Config.RGB_565);
+
+        if (bm.getConfig() != Bitmap.Config.RGB_565) {
+            bm = null;
+        }
+
+        long asset = nOpenAsset(assets, record.name);
+        long aimagedecoder = nCreateFromAsset(asset);
+
+        nTestDecode(aimagedecoder, ANDROID_BITMAP_FORMAT_RGB_565, false, bm);
+        nCloseAsset(asset);
+    }
+
+    @Test
+    @Parameters(method = "getRecords")
+    public void testDecode565Resources(ImageDecoderTest.Record record)
+            throws IOException {
+        ImageDecoder.Source src = ImageDecoder.createSource(getResources(),
+                record.resId);
+        Bitmap bm = decode(src, Bitmap.Config.RGB_565);
+
+        if (bm.getConfig() != Bitmap.Config.RGB_565) {
+            bm = null;
+        }
+
+        try (ParcelFileDescriptor pfd = open(record.resId)) {
+            long aimagedecoder = nCreateFromFd(pfd.getFd());
+
+            nTestDecode(aimagedecoder, ANDROID_BITMAP_FORMAT_RGB_565, false, bm);
+        } catch (FileNotFoundException e) {
+            fail("Could not open " + Utils.getAsResourceUri(record.resId));
+        }
+    }
+
+    @Test
+    @Parameters("grayscale-linearSrgb.png")
+    public void testDecodeA8(String name) {
+        AssetManager assets = getAssetManager();
+        ImageDecoder.Source src = ImageDecoder.createSource(assets, name);
+        Bitmap bm = decode(src, Bitmap.Config.ALPHA_8);
+
+        assertNotNull(bm);
+        assertNull(bm.getColorSpace());
+        assertEquals(Bitmap.Config.ALPHA_8, bm.getConfig());
+
+        long asset = nOpenAsset(assets, name);
+        long aimagedecoder = nCreateFromAsset(asset);
+
+        nTestDecode(aimagedecoder, ANDROID_BITMAP_FORMAT_A_8, false, bm);
+        nCloseAsset(asset);
+    }
+
+    @Test
+    public void testDecodeA8Resources()
+            throws IOException {
+        final int resId = R.drawable.grayscale_jpg;
+        ImageDecoder.Source src = ImageDecoder.createSource(getResources(),
+                resId);
+        Bitmap bm = decode(src, Bitmap.Config.ALPHA_8);
+
+        assertNotNull(bm);
+        assertNull(bm.getColorSpace());
+        assertEquals(Bitmap.Config.ALPHA_8, bm.getConfig());
+
+        try (ParcelFileDescriptor pfd = open(resId)) {
+            long aimagedecoder = nCreateFromFd(pfd.getFd());
+
+            nTestDecode(aimagedecoder, ANDROID_BITMAP_FORMAT_A_8, false, bm);
+        } catch (FileNotFoundException e) {
+            fail("Could not open " + Utils.getAsResourceUri(resId));
+        }
+    }
+
+    @Test
+    @Parameters(method = "getAssetRecordsUnpremul")
+    public void testDecodeF16(ImageDecoderTest.AssetRecord record, boolean unpremul) {
+        AssetManager assets = getAssetManager();
+
+        // ImageDecoder doesn't allow forcing a decode to F16, so use BitmapFactory.
+        BitmapFactory.Options options = new BitmapFactory.Options();
+        options.inPreferredConfig = Bitmap.Config.RGBA_F16;
+        options.inPremultiplied = !unpremul;
+
+        InputStream is = null;
+        try {
+            is = assets.open(record.name);
+        } catch (IOException e) {
+            fail("Failed to open " + record.name + " with " + e);
+        }
+        assertNotNull(is);
+
+        Bitmap bm = BitmapFactory.decodeStream(is, null, options);
+        assertNotNull(bm);
+
+        long asset = nOpenAsset(assets, record.name);
+        long aimagedecoder = nCreateFromAsset(asset);
+
+        nTestDecode(aimagedecoder, ANDROID_BITMAP_FORMAT_RGBA_F16, unpremul, bm);
+        nCloseAsset(asset);
+    }
+
+    @Test
+    @Parameters(method = "getRecordsUnpremul")
+    public void testDecodeF16Resources(ImageDecoderTest.Record record, boolean unpremul)
+            throws IOException {
+        // ImageDecoder doesn't allow forcing a decode to F16, so use BitmapFactory.
+        BitmapFactory.Options options = new BitmapFactory.Options();
+        options.inPreferredConfig = Bitmap.Config.RGBA_F16;
+        options.inPremultiplied = !unpremul;
+        options.inScaled = false;
+
+        Bitmap bm = BitmapFactory.decodeResource(getResources(),
+                record.resId, options);
+        assertNotNull(bm);
+
+        try (ParcelFileDescriptor pfd = open(record.resId)) {
+            long aimagedecoder = nCreateFromFd(pfd.getFd());
+
+            nTestDecode(aimagedecoder, ANDROID_BITMAP_FORMAT_RGBA_F16, unpremul, bm);
+        } catch (FileNotFoundException e) {
+            fail("Could not open " + Utils.getAsResourceUri(record.resId));
+        }
+    }
+
+    @Test
+    @Parameters(method = "getAssetRecords")
+    public void testDecodeStride(ImageDecoderTest.AssetRecord record) {
+        long asset = nOpenAsset(getAssetManager(), record.name);
+        long aimagedecoder = nCreateFromAsset(asset);
+        nTestDecodeStride(aimagedecoder);
+        nCloseAsset(asset);
+    }
+
+    @Test
+    @Parameters(method = "getRecords")
+    public void testDecodeStrideResources(ImageDecoderTest.Record record)
+            throws IOException {
+        try (ParcelFileDescriptor pfd = open(record.resId)) {
+            long aimagedecoder = nCreateFromFd(pfd.getFd());
+
+            nTestDecodeStride(aimagedecoder);
+        } catch (FileNotFoundException e) {
+            fail("Could not open " + Utils.getAsResourceUri(record.resId));
+        }
+    }
+
+    @Test
+    @Parameters(method = "getAssetRecords")
+    public void testSetTargetSize(ImageDecoderTest.AssetRecord record) {
+        long asset = nOpenAsset(getAssetManager(), record.name);
+        long aimagedecoder = nCreateFromAsset(asset);
+        nTestSetTargetSize(aimagedecoder);
+        nCloseAsset(asset);
+    }
+
+    @Test
+    @Parameters(method = "getRecords")
+    public void testSetTargetSizeResources(ImageDecoderTest.Record record)
+            throws IOException {
+        try (ParcelFileDescriptor pfd = open(record.resId)) {
+            long aimagedecoder = nCreateFromFd(pfd.getFd());
+
+            nTestSetTargetSize(aimagedecoder);
+        } catch (FileNotFoundException e) {
+            fail("Could not open " + Utils.getAsResourceUri(record.resId));
+        }
+    }
+
+    private Bitmap decodeSampled(String name, ImageDecoder.Source src, int sampleSize) {
+        try {
+            return ImageDecoder.decodeBitmap(src, (decoder, info, source) -> {
+                // So we can compare pixels to the native decode.
+                decoder.setAllocator(ImageDecoder.ALLOCATOR_SOFTWARE);
+                decoder.setTargetSampleSize(sampleSize);
+            });
+        } catch (IOException e) {
+            fail("Failed to decode " + name + " in Java (sampleSize: "
+                    + sampleSize + ") with " + e);
+            return null;
+        }
+    }
+
+    @Test
+    @Parameters(method = "getAssetRecordsSample")
+    public void testDecodeSampled(ImageDecoderTest.AssetRecord record, int sampleSize) {
+        AssetManager assets = getAssetManager();
+        ImageDecoder.Source src = ImageDecoder.createSource(assets, record.name);
+        Bitmap bm = decodeSampled(record.name, src, sampleSize);
+
+        long asset = nOpenAsset(assets, record.name);
+        long aimagedecoder = nCreateFromAsset(asset);
+
+        nTestDecodeScaled(aimagedecoder, bm);
+        nCloseAsset(asset);
+    }
+
+    @Test
+    @Parameters(method = "getRecordsSample")
+    public void testDecodeResourceSampled(ImageDecoderTest.Record record, int sampleSize)
+            throws IOException {
+        ImageDecoder.Source src = ImageDecoder.createSource(getResources(),
+                record.resId);
+        String name = Utils.getAsResourceUri(record.resId).toString();
+        Bitmap bm = decodeSampled(name, src, sampleSize);
+        assertNotNull(bm);
+
+        try (ParcelFileDescriptor pfd = open(record.resId)) {
+            long aimagedecoder = nCreateFromFd(pfd.getFd());
+
+            nTestDecodeScaled(aimagedecoder, bm);
+        } catch (FileNotFoundException e) {
+            fail("Could not open " + name + ": " + e);
+        }
+    }
+
+    private Bitmap decodeScaled(String name, ImageDecoder.Source src) {
+        try {
+            return ImageDecoder.decodeBitmap(src, (decoder, info, source) -> {
+                // So we can compare pixels to the native decode.
+                decoder.setAllocator(ImageDecoder.ALLOCATOR_SOFTWARE);
+
+                // Scale to an arbitrary width and height.
+                decoder.setTargetSize(300, 300);
+            });
+        } catch (IOException e) {
+            fail("Failed to decode " + name + " in Java (size: "
+                    + "300 x 300) with " + e);
+            return null;
+        }
+    }
+
+    @Test
+    @Parameters(method = "getAssetRecords")
+    public void testDecodeScaled(ImageDecoderTest.AssetRecord record) {
+        AssetManager assets = getAssetManager();
+        ImageDecoder.Source src = ImageDecoder.createSource(assets, record.name);
+        Bitmap bm = decodeScaled(record.name, src);
+
+        long asset = nOpenAsset(assets, record.name);
+        long aimagedecoder = nCreateFromAsset(asset);
+
+        nTestDecodeScaled(aimagedecoder, bm);
+        nCloseAsset(asset);
+    }
+
+    @Test
+    @Parameters(method = "getRecords")
+    public void testDecodeResourceScaled(ImageDecoderTest.Record record)
+            throws IOException {
+        ImageDecoder.Source src = ImageDecoder.createSource(getResources(),
+                record.resId);
+        String name = Utils.getAsResourceUri(record.resId).toString();
+        Bitmap bm = decodeScaled(name, src);
+        assertNotNull(bm);
+
+        try (ParcelFileDescriptor pfd = open(record.resId)) {
+            long aimagedecoder = nCreateFromFd(pfd.getFd());
+
+            nTestDecodeScaled(aimagedecoder, bm);
+        } catch (FileNotFoundException e) {
+            fail("Could not open " + name + ": " + e);
+        }
+    }
+
+    private Bitmap decodeScaleUp(String name, ImageDecoder.Source src) {
+        try {
+            return ImageDecoder.decodeBitmap(src, (decoder, info, source) -> {
+                // So we can compare pixels to the native decode.
+                decoder.setAllocator(ImageDecoder.ALLOCATOR_SOFTWARE);
+
+                decoder.setTargetSize(info.getSize().getWidth() * 2,
+                        info.getSize().getHeight() * 2);
+            });
+        } catch (IOException e) {
+            fail("Failed to decode " + name + " in Java (scaled up) with " + e);
+            return null;
+        }
+    }
+
+    @Test
+    @Parameters(method = "getAssetRecords")
+    public void testDecodeScaleUp(ImageDecoderTest.AssetRecord record) {
+        AssetManager assets = getAssetManager();
+        ImageDecoder.Source src = ImageDecoder.createSource(assets, record.name);
+        Bitmap bm = decodeScaleUp(record.name, src);
+
+        long asset = nOpenAsset(assets, record.name);
+        long aimagedecoder = nCreateFromAsset(asset);
+
+        nTestDecodeScaled(aimagedecoder, bm);
+        nCloseAsset(asset);
+    }
+
+    @Test
+    @Parameters(method = "getRecords")
+    public void testDecodeResourceScaleUp(ImageDecoderTest.Record record)
+            throws IOException {
+        ImageDecoder.Source src = ImageDecoder.createSource(getResources(),
+                record.resId);
+        String name = Utils.getAsResourceUri(record.resId).toString();
+        Bitmap bm = decodeScaleUp(name, src);
+        assertNotNull(bm);
+
+        try (ParcelFileDescriptor pfd = open(record.resId)) {
+            long aimagedecoder = nCreateFromFd(pfd.getFd());
+
+            nTestDecodeScaled(aimagedecoder, bm);
+        } catch (FileNotFoundException e) {
+            fail("Could not open " + name + ": " + e);
+        }
+    }
+
+    @Test
+    @Parameters(method = "getAssetRecords")
+    public void testSetCrop(ImageDecoderTest.AssetRecord record) {
+        nTestSetCrop(getAssetManager(), record.name);
+    }
+
+    private static class Cropper implements ImageDecoder.OnHeaderDecodedListener {
+        Cropper(boolean scale) {
+            mScale = scale;
+        }
+
+        public boolean withScale() {
+            return mScale;
+        }
+
+        public int getWidth() {
+            return mWidth;
+        }
+
+        public int getHeight() {
+            return mHeight;
+        }
+
+        public Rect getCropRect() {
+            return mCropRect;
+        }
+
+        @Override
+        public void onHeaderDecoded(ImageDecoder decoder, ImageDecoder.ImageInfo info,
+                ImageDecoder.Source source) {
+            mWidth = info.getSize().getWidth();
+            mHeight = info.getSize().getHeight();
+            if (mScale) {
+                mWidth = 40;
+                mHeight = 40;
+                decoder.setTargetSize(mWidth, mHeight);
+            }
+
+            mCropRect = new Rect(mWidth / 2, mHeight / 2, mWidth, mHeight);
+            decoder.setCrop(mCropRect);
+
+            // So we can compare pixels to the native decode.
+            decoder.setAllocator(ImageDecoder.ALLOCATOR_SOFTWARE);
+        }
+
+        private final boolean mScale;
+        private Rect mCropRect;
+        private int mWidth;
+        private int mHeight;
+    }
+
+    private static Bitmap decodeCropped(String name, Cropper cropper, ImageDecoder.Source src) {
+        try {
+            return ImageDecoder.decodeBitmap(src, cropper);
+        } catch (IOException e) {
+            fail("Failed to decode " + name + " in Java with "
+                    + (cropper.withScale() ? "scale and " : "a ") + "crop ("
+                    + cropper.getCropRect() + "): " + e);
+            return null;
+        }
+    }
+
+    @Test
+    @Parameters(method = "getAssetRecords")
+    public void testCrop(ImageDecoderTest.AssetRecord record) {
+        AssetManager assets = getAssetManager();
+        ImageDecoder.Source src = ImageDecoder.createSource(assets, record.name);
+        Cropper cropper = new Cropper(false /* scale */);
+        Bitmap bm = decodeCropped(record.name, cropper, src);
+
+        long asset = nOpenAsset(assets, record.name);
+        long aimagedecoder = nCreateFromAsset(asset);
+
+        Rect crop = cropper.getCropRect();
+        nTestDecodeCrop(aimagedecoder, bm, 0, 0, crop.left, crop.top, crop.right, crop.bottom);
+        nCloseAsset(asset);
+    }
+
+    @Test
+    @Parameters(method = "getRecords")
+    public void testCropResource(ImageDecoderTest.Record record)
+            throws IOException {
+        ImageDecoder.Source src = ImageDecoder.createSource(getResources(),
+                record.resId);
+        String name = Utils.getAsResourceUri(record.resId).toString();
+        Cropper cropper = new Cropper(false /* scale */);
+        Bitmap bm = decodeCropped(name, cropper, src);
+        assertNotNull(bm);
+
+        try (ParcelFileDescriptor pfd = open(record.resId)) {
+            long aimagedecoder = nCreateFromFd(pfd.getFd());
+
+            Rect crop = cropper.getCropRect();
+            nTestDecodeCrop(aimagedecoder, bm, 0, 0, crop.left, crop.top, crop.right, crop.bottom);
+        } catch (FileNotFoundException e) {
+            fail("Could not open " + name + ": " + e);
+        }
+    }
+
+    @Test
+    @Parameters(method = "getAssetRecords")
+    public void testCropAndScale(ImageDecoderTest.AssetRecord record) {
+        AssetManager assets = getAssetManager();
+        ImageDecoder.Source src = ImageDecoder.createSource(assets, record.name);
+        Cropper cropper = new Cropper(true /* scale */);
+        Bitmap bm = decodeCropped(record.name, cropper, src);
+
+        long asset = nOpenAsset(assets, record.name);
+        long aimagedecoder = nCreateFromAsset(asset);
+
+        Rect crop = cropper.getCropRect();
+        nTestDecodeCrop(aimagedecoder, bm, cropper.getWidth(), cropper.getHeight(),
+                crop.left, crop.top, crop.right, crop.bottom);
+        nCloseAsset(asset);
+    }
+
+    @Test
+    @Parameters(method = "getRecords")
+    public void testCropAndScaleResource(ImageDecoderTest.Record record)
+            throws IOException {
+        ImageDecoder.Source src = ImageDecoder.createSource(getResources(),
+                record.resId);
+        String name = Utils.getAsResourceUri(record.resId).toString();
+        Cropper cropper = new Cropper(true /* scale */);
+        Bitmap bm = decodeCropped(name, cropper, src);
+        assertNotNull(bm);
+
+        try (ParcelFileDescriptor pfd = open(record.resId)) {
+            long aimagedecoder = nCreateFromFd(pfd.getFd());
+
+            Rect crop = cropper.getCropRect();
+            nTestDecodeCrop(aimagedecoder, bm, cropper.getWidth(), cropper.getHeight(),
+                    crop.left, crop.top, crop.right, crop.bottom);
+        } catch (FileNotFoundException e) {
+            fail("Could not open " + name + ": " + e);
+        }
+    }
+
+    private static Object[] getExifImages() {
+        return new Object[] {
+            R.drawable.orientation_1,
+            R.drawable.orientation_2,
+            R.drawable.orientation_3,
+            R.drawable.orientation_4,
+            R.drawable.orientation_5,
+            R.drawable.orientation_6,
+            R.drawable.orientation_7,
+            R.drawable.orientation_8,
+            R.drawable.webp_orientation1,
+            R.drawable.webp_orientation2,
+            R.drawable.webp_orientation3,
+            R.drawable.webp_orientation4,
+            R.drawable.webp_orientation5,
+            R.drawable.webp_orientation6,
+            R.drawable.webp_orientation7,
+            R.drawable.webp_orientation8,
+        };
+    }
+
+    @Test
+    @Parameters(method = "getExifImages")
+    public void testRespectOrientation(int resId) throws IOException {
+        Uri uri = Utils.getAsResourceUri(resId);
+        ImageDecoder.Source src = ImageDecoder.createSource(getContentResolver(),
+                uri);
+        Bitmap bm = decode(src, false /* unpremul */);
+        assertNotNull(bm);
+        assertEquals(100, bm.getWidth());
+        assertEquals(80,  bm.getHeight());
+
+        try (ParcelFileDescriptor pfd = open(resId)) {
+            long aimagedecoder = nCreateFromFd(pfd.getFd());
+
+            nTestDecode(aimagedecoder, ANDROID_BITMAP_FORMAT_NONE, false /* unpremul */, bm);
+        } catch (FileNotFoundException e) {
+            e.printStackTrace();
+            fail("Could not open " + uri);
+        }
+        bm.recycle();
+    }
+
+    @Test
+    @Parameters(method = "getAssetRecords")
+    public void testScalePlusUnpremul(ImageDecoderTest.AssetRecord record) {
+        long asset = nOpenAsset(getAssetManager(), record.name);
+        long aimagedecoder = nCreateFromAsset(asset);
+
+        nTestScalePlusUnpremul(aimagedecoder);
+        nCloseAsset(asset);
+    }
+
+    // Return a pointer to the native AAsset named |file|. Must be closed with nCloseAsset.
+    // Throws an Exception on failure.
+    private static native long nOpenAsset(AssetManager assets, String file);
+    private static native void nCloseAsset(long asset);
+
+    // Methods for creating and returning a pointer to an AImageDecoder. All
+    // throw an Exception on failure.
+    private static native long nCreateFromFd(int fd);
+    private static native long nCreateFromAsset(long asset);
+    private static native long nCreateFromAssetFd(long asset);
+    private static native long nCreateFromAssetBuffer(long asset);
+
+    private static native void nTestEmptyCreate();
+    private static native void nTestNullDecoder(AssetManager assets, String file);
+    private static native void nTestCreateIncomplete(AssetManager assets,
+            String file, int truncatedLength);
+    private static native void nTestCreateUnsupported(AssetManager assets, String file);
+
+    // For convenience, all methods that take aimagedecoder as a parameter delete
+    // it.
+    private static native void nTestInfo(long aimagedecoder, int width, int height,
+            String mimeType, boolean isAnimated, boolean isF16);
+    private static native void nTestSetFormat(long aimagedecoder, boolean isF16, boolean isGray);
+    private static native void nTestSetAlpha(long aimagedecoder, boolean hasAlpha);
+    private static native void nTestGetMinimumStride(long aimagedecoder,
+            boolean isF16, boolean isGray);
+    private static native void nTestDecode(long aimagedecoder,
+            int requestedAndroidBitmapFormat, boolean unpremul, Bitmap bitmap);
+    private static native void nTestDecodeStride(long aimagedecoder);
+    private static native void nTestSetTargetSize(long aimagedecoder);
+    // Decode with the target width and height to match |bitmap|.
+    private static native void nTestDecodeScaled(long aimagedecoder, Bitmap bitmap);
+    private static native void nTestSetCrop(AssetManager assets, String file);
+    // Decode and compare to |bitmap|, where they both use the specified target
+    // size and crop rect. target size of 0 means to skip scaling.
+    private static native void nTestDecodeCrop(long aimagedecoder,
+            Bitmap bitmap, int targetWidth, int targetHeight,
+            int cropLeft, int cropTop, int cropRight, int cropBottom);
+    private static native void nTestScalePlusUnpremul(long aimagedecoder);
+}
diff --git a/tests/tests/graphics/src/android/graphics/cts/BitmapFactoryTest.java b/tests/tests/graphics/src/android/graphics/cts/BitmapFactoryTest.java
index 959aeef..6fcc5c0 100644
--- a/tests/tests/graphics/src/android/graphics/cts/BitmapFactoryTest.java
+++ b/tests/tests/graphics/src/android/graphics/cts/BitmapFactoryTest.java
@@ -354,7 +354,7 @@
                 opts.inInputShareable = purgeable;
 
                 long actualOffset = actual_offsets[j];
-                String path = obtainPath(testImage.id, actualOffset);
+                String path = Utils.obtainPath(testImage.id, actualOffset);
                 RandomAccessFile file = new RandomAccessFile(path, "r");
                 FileDescriptor fd = file.getFD();
                 assertTrue(fd.valid());
@@ -1015,38 +1015,6 @@
     }
 
     private String obtainPath() throws IOException {
-        return obtainPath(R.drawable.start, 0);
-    }
-
-    /*
-     * Create a new file and return a path to it.
-     * @param resId Original file. It will be copied into the new file.
-     * @param offset Number of zeroes to write to the new file before the
-     *               copied file. This allows testing decodeFileDescriptor
-     *               with an offset. Must be less than or equal to 1024
-     */
-    private String obtainPath(int resId, long offset) throws IOException {
-        File dir = InstrumentationRegistry.getTargetContext().getFilesDir();
-        dir.mkdirs();
-        // The suffix does not necessarily represent theactual file type.
-        File file = new File(dir, "test.jpg");
-        if (!file.createNewFile()) {
-            if (!file.exists()) {
-                fail("Failed to create new File!");
-            }
-        }
-        InputStream is = obtainInputStream(resId);
-        FileOutputStream fOutput = new FileOutputStream(file);
-        byte[] dataBuffer = new byte[1024];
-        // Write a bunch of zeroes before the image.
-        assertTrue(offset <= 1024);
-        fOutput.write(dataBuffer, 0, (int) offset);
-        int readLength = 0;
-        while ((readLength = is.read(dataBuffer)) != -1) {
-            fOutput.write(dataBuffer, 0, readLength);
-        }
-        is.close();
-        fOutput.close();
-        return (file.getPath());
+        return Utils.obtainPath(R.drawable.start, 0);
     }
 }
diff --git a/tests/tests/graphics/src/android/graphics/cts/ImageDecoderTest.java b/tests/tests/graphics/src/android/graphics/cts/ImageDecoderTest.java
index fe13ea4..f6c278a 100644
--- a/tests/tests/graphics/src/android/graphics/cts/ImageDecoderTest.java
+++ b/tests/tests/graphics/src/android/graphics/cts/ImageDecoderTest.java
@@ -75,35 +75,41 @@
 
 @RunWith(JUnitParamsRunner.class)
 public class ImageDecoderTest {
-    private static final class Record {
+    static final class Record {
         public final int resId;
         public final int width;
         public final int height;
+        public final boolean isGray;
+        public final boolean hasAlpha;
         public final String mimeType;
         public final ColorSpace colorSpace;
 
-        Record(int resId, int width, int height, String mimeType, ColorSpace colorSpace) {
+        Record(int resId, int width, int height, String mimeType, boolean isGray,
+                boolean hasAlpha, ColorSpace colorSpace) {
             this.resId    = resId;
             this.width    = width;
             this.height   = height;
             this.mimeType = mimeType;
+            this.isGray   = isGray;
+            this.hasAlpha = hasAlpha;
             this.colorSpace = colorSpace;
         }
     }
 
     private static final ColorSpace sSRGB = ColorSpace.get(ColorSpace.Named.SRGB);
 
-    private Object[] getRecords() {
+    static Object[] getRecords() {
         return new Record[] {
-            new Record(R.drawable.baseline_jpeg, 1280, 960, "image/jpeg", sSRGB),
-            new Record(R.drawable.png_test, 640, 480, "image/png", sSRGB),
-            new Record(R.drawable.gif_test, 320, 240, "image/gif", sSRGB),
-            new Record(R.drawable.bmp_test, 320, 240, "image/bmp", sSRGB),
-            new Record(R.drawable.webp_test, 640, 480, "image/webp", sSRGB),
-            new Record(R.drawable.google_chrome, 256, 256, "image/x-ico", sSRGB),
-            new Record(R.drawable.color_wheel, 128, 128, "image/x-ico", sSRGB),
-            new Record(R.raw.sample_1mp, 600, 338, "image/x-adobe-dng", sSRGB),
-            new Record(R.raw.heifwriter_input, 1920, 1080, "image/heif", sSRGB),
+            new Record(R.drawable.baseline_jpeg, 1280, 960, "image/jpeg", false, false, sSRGB),
+            new Record(R.drawable.grayscale_jpg, 128, 128, "image/jpeg", true, false, sSRGB),
+            new Record(R.drawable.png_test, 640, 480, "image/png", false, false, sSRGB),
+            new Record(R.drawable.gif_test, 320, 240, "image/gif", false, false, sSRGB),
+            new Record(R.drawable.bmp_test, 320, 240, "image/bmp", false, false, sSRGB),
+            new Record(R.drawable.webp_test, 640, 480, "image/webp", false, false, sSRGB),
+            new Record(R.drawable.google_chrome, 256, 256, "image/x-ico", false, true, sSRGB),
+            new Record(R.drawable.color_wheel, 128, 128, "image/x-ico", false, true, sSRGB),
+            new Record(R.raw.sample_1mp, 600, 338, "image/x-adobe-dng", false, false, sSRGB),
+            new Record(R.raw.heifwriter_input, 1920, 1080, "image/heif", false, false, sSRGB),
         };
     }
 
@@ -115,7 +121,7 @@
         return output.toByteArray();
     }
 
-    private static void writeToStream(OutputStream output, int resId, int offset, int extra) {
+    static void writeToStream(OutputStream output, int resId, int offset, int extra) {
         InputStream input = getResources().openRawResource(resId);
         byte[] buffer = new byte[4096];
         int bytesRead;
@@ -194,16 +200,6 @@
                 "android.graphics.cts.fileprovider", getAsFile(resId));
     }
 
-    private Uri getAsResourceUri(int resId) {
-        Resources res = getResources();
-        return new Uri.Builder()
-                .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE)
-                .authority(res.getResourcePackageName(resId))
-                .appendPath(res.getResourceTypeName(resId))
-                .appendPath(res.getResourceEntryName(resId))
-                .build();
-    }
-
     private Callable<AssetFileDescriptor> getAsCallable(int resId) {
         final Context context = InstrumentationRegistry.getTargetContext();
         final Uri uri = getAsContentUri(resId);
@@ -225,7 +221,7 @@
     private interface UriCreator extends IntFunction<Uri> {};
 
     private UriCreator[] mUriCreators = new UriCreator[] {
-            resId -> getAsResourceUri(resId),
+            resId -> Utils.getAsResourceUri(resId),
             resId -> getAsFileUri(resId),
             resId -> getAsContentUri(resId),
     };
@@ -260,7 +256,7 @@
         return InstrumentationRegistry.getTargetContext().getResources();
     }
 
-    private ContentResolver getContentResolver() {
+    private static ContentResolver getContentResolver() {
         return InstrumentationRegistry.getTargetContext().getContentResolver();
     }
 
@@ -278,7 +274,7 @@
                     assertSame(record.colorSpace, info.getColorSpace());
                 });
             } catch (IOException e) {
-                fail("Failed " + getAsResourceUri(record.resId) + " with exception " + e);
+                fail("Failed " + Utils.getAsResourceUri(record.resId) + " with exception " + e);
             }
         }
     }
@@ -357,7 +353,7 @@
                 }
             });
         } catch (IOException e) {
-            fail("Failed " + getAsResourceUri(resId) + " with exception " + e);
+            fail("Failed " + Utils.getAsResourceUri(resId) + " with exception " + e);
         }
     }
 
@@ -398,7 +394,7 @@
                     try {
                         bm = ImageDecoder.decodeBitmap(src, l);
                     } catch (IOException e) {
-                        fail("Failed " + getAsResourceUri(record.resId)
+                        fail("Failed " + Utils.getAsResourceUri(record.resId)
                                 + " with exception " + e);
                     }
                     assertNotNull(bm);
@@ -421,7 +417,7 @@
                             }
                             break;
                         default:
-                            String name = getAsResourceUri(record.resId).toString();
+                            String name = Utils.getAsResourceUri(record.resId).toString();
                             assertEquals("image " + name + "; allocator: " + allocator,
                                          Bitmap.Config.HARDWARE, bm.getConfig());
                             break;
@@ -446,7 +442,7 @@
                 assertFalse(decoder.isUnpremultipliedRequired());
             });
         } catch (IOException e) {
-            fail("Failed " + getAsResourceUri(resId) + " with exception " + e);
+            fail("Failed " + Utils.getAsResourceUri(resId) + " with exception " + e);
         }
     }
 
@@ -501,7 +497,7 @@
                 }
             });
         } catch (IOException e) {
-            fail("Failed " + getAsResourceUri(resId) + " with exception " + e);
+            fail("Failed " + Utils.getAsResourceUri(resId) + " with exception " + e);
         }
     }
 
@@ -728,7 +724,7 @@
                 assertEquals(pp.width,  drawable.getIntrinsicWidth());
                 assertEquals(pp.height, drawable.getIntrinsicHeight());
             } catch (IOException e) {
-                fail("Failed " + getAsResourceUri(record.resId) + " with exception " + e);
+                fail("Failed " + Utils.getAsResourceUri(record.resId) + " with exception " + e);
             }
         }
     }
@@ -751,7 +747,7 @@
     @Test
     @Parameters(method = "getRecords")
     public void testSampleSize(Record record) {
-        final String name = getAsResourceUri(record.resId).toString();
+        final String name = Utils.getAsResourceUri(record.resId).toString();
         for (int sampleSize : new int[] { 2, 3, 4, 8, 32 }) {
             ImageDecoder.Source src = mCreators[0].apply(record.resId);
             try {
@@ -785,7 +781,7 @@
                 });
                 assertEquals(1, dr.getIntrinsicWidth());
             } catch (Exception e) {
-                String file = getAsResourceUri(record.resId).toString();
+                String file = Utils.getAsResourceUri(record.resId).toString();
                 fail("Failed to decode " + file + " with exception " + e);
             }
         }
@@ -861,7 +857,7 @@
                 }
             });
         } catch (IOException e) {
-            fail("Failed " + getAsResourceUri(resId) + " with exception " + e);
+            fail("Failed " + Utils.getAsResourceUri(resId) + " with exception " + e);
         }
     }
 
@@ -970,6 +966,11 @@
             // FIXME (scroggo): Some codecs currently do not support incomplete images.
             return;
         }
+        if (record.resId == R.drawable.grayscale_jpg) {
+            // FIXME (scroggo): This is a progressive jpeg. If Skia switches to
+            // decoding jpegs progressively, this image can be partially decoded.
+            return;
+        }
         for (boolean abort : abortDecode) {
             ImageDecoder.Source src = ImageDecoder.createSource(
                     ByteBuffer.wrap(bytes, 0, truncatedLength));
@@ -1072,7 +1073,7 @@
                 assertFalse(decoder.isMutableRequired());
             });
         } catch (IOException e) {
-            fail("Failed " + getAsResourceUri(resId) + " with exception " + e);
+            fail("Failed " + Utils.getAsResourceUri(resId) + " with exception " + e);
         }
     }
 
@@ -1109,7 +1110,7 @@
                     assertTrue(bm.isMutable());
                     assertNotEquals(Bitmap.Config.HARDWARE, bm.getConfig());
                 } catch (Exception e) {
-                    String file = getAsResourceUri(record.resId).toString();
+                    String file = Utils.getAsResourceUri(record.resId).toString();
                     fail("Failed to decode " + file + " with exception " + e);
                 }
             }
@@ -1229,7 +1230,7 @@
                 assertEquals(l.width,  bm.getWidth());
                 assertEquals(l.height, bm.getHeight());
             } catch (IOException e) {
-                fail("Failed " + getAsResourceUri(record.resId) + " with exception " + e);
+                fail("Failed " + Utils.getAsResourceUri(record.resId) + " with exception " + e);
             }
         }
 
@@ -1336,7 +1337,7 @@
                 assertEquals(r, decoder.getCrop());
             });
         } catch (IOException e) {
-            fail("Failed " + getAsResourceUri(resId) + " with exception " + e);
+            fail("Failed " + Utils.getAsResourceUri(resId) + " with exception " + e);
         }
     }
 
@@ -1383,7 +1384,7 @@
                         assertEquals(l.cropRect.width(),  drawable.getIntrinsicWidth());
                         assertEquals(l.cropRect.height(), drawable.getIntrinsicHeight());
                     } catch (IOException e) {
-                        fail("Failed " + getAsResourceUri(record.resId)
+                        fail("Failed " + Utils.getAsResourceUri(record.resId)
                                 + " with exception " + e);
                     }
                 }
@@ -1740,7 +1741,7 @@
                 assertFalse(decoder.isDecodeAsAlphaMaskEnabled());
             });
         } catch (IOException e) {
-            fail("Failed " + getAsResourceUri(resId) + " with exception " + e);
+            fail("Failed " + Utils.getAsResourceUri(resId) + " with exception " + e);
         }
     }
 
@@ -1822,7 +1823,7 @@
                 assertEquals(ImageDecoder.MEMORY_POLICY_DEFAULT, decoder.getMemorySizePolicy());
             });
         } catch (IOException e) {
-            fail("Failed " + getAsResourceUri(resId) + " with exception " + e);
+            fail("Failed " + Utils.getAsResourceUri(resId) + " with exception " + e);
         }
     }
 
@@ -1972,7 +1973,7 @@
                 reference = null;
                 isWebp = true;
             }
-            Uri uri = getAsResourceUri(resId);
+            Uri uri = Utils.getAsResourceUri(resId);
             ImageDecoder.Source src = ImageDecoder.createSource(getContentResolver(), uri);
             try {
                 Bitmap bm = ImageDecoder.decodeBitmap(src, (decoder, info, s) -> {
@@ -2099,7 +2100,7 @@
             Drawable drawable = ImageDecoder.decodeDrawable(src);
             assertNotNull(drawable);
         } catch (IOException e) {
-            fail("Failed " + getAsResourceUri(record.resId) + " with " + e);
+            fail("Failed " + Utils.getAsResourceUri(record.resId) + " with " + e);
         }
     }
 
@@ -2111,7 +2112,7 @@
             assertTrue(drawable instanceof BitmapDrawable);
             return (BitmapDrawable) drawable;
         } catch (IOException e) {
-            fail("Failed " + getAsResourceUri(resId) + " with " + e);
+            fail("Failed " + Utils.getAsResourceUri(resId) + " with " + e);
             return null;
         }
     }
@@ -2168,21 +2169,23 @@
         }
     }
 
-    private static class AssetRecord {
+    static class AssetRecord {
         public final String name;
         public final int width;
         public final int height;
         public final boolean isF16;
         public final boolean isGray;
+        public final boolean hasAlpha;
         private final ColorSpace mColorSpace;
 
-        AssetRecord(String name, int width, int height, boolean isF16, boolean isGray,
-                ColorSpace colorSpace) {
+        AssetRecord(String name, int width, int height, boolean isF16,
+                boolean isGray, boolean hasAlpha, ColorSpace colorSpace) {
             this.name = name;
             this.width = width;
             this.height = height;
             this.isF16 = isF16;
             this.isGray = isGray;
+            this.hasAlpha = hasAlpha;
             mColorSpace = colorSpace;
         }
 
@@ -2207,24 +2210,24 @@
         }
     }
 
-    private Object [] getAssetRecords() {
+    static Object[] getAssetRecords() {
         return new Object [] {
             // A null ColorSpace means that the color space is "Unknown".
-            new AssetRecord("almost-red-adobe.png", 1, 1, false, false, null),
-            new AssetRecord("green-p3.png", 64, 64, false, false,
+            new AssetRecord("almost-red-adobe.png", 1, 1, false, false, false, null),
+            new AssetRecord("green-p3.png", 64, 64, false, false, false,
                     ColorSpace.get(ColorSpace.Named.DISPLAY_P3)),
-            new AssetRecord("green-srgb.png", 64, 64, false, false, sSRGB),
-            new AssetRecord("blue-16bit-prophoto.png", 100, 100, true, false,
+            new AssetRecord("green-srgb.png", 64, 64, false, false, false, sSRGB),
+            new AssetRecord("blue-16bit-prophoto.png", 100, 100, true, false, true,
                     ColorSpace.get(ColorSpace.Named.PRO_PHOTO_RGB)),
-            new AssetRecord("blue-16bit-srgb.png", 64, 64, true, false,
+            new AssetRecord("blue-16bit-srgb.png", 64, 64, true, false, false,
                     ColorSpace.get(ColorSpace.Named.EXTENDED_SRGB)),
-            new AssetRecord("purple-cmyk.png", 64, 64, false, false, sSRGB),
-            new AssetRecord("purple-displayprofile.png", 64, 64, false, false, null),
-            new AssetRecord("red-adobergb.png", 64, 64, false, false,
+            new AssetRecord("purple-cmyk.png", 64, 64, false, false, false, sSRGB),
+            new AssetRecord("purple-displayprofile.png", 64, 64, false, false, false, null),
+            new AssetRecord("red-adobergb.png", 64, 64, false, false, false,
                     ColorSpace.get(ColorSpace.Named.ADOBE_RGB)),
-            new AssetRecord("translucent-green-p3.png", 64, 64, false, false,
+            new AssetRecord("translucent-green-p3.png", 64, 64, false, false, true,
                     ColorSpace.get(ColorSpace.Named.DISPLAY_P3)),
-            new AssetRecord("grayscale-linearSrgb.png", 32, 32, false, true,
+            new AssetRecord("grayscale-linearSrgb.png", 32, 32, false, true, false,
                     ColorSpace.get(ColorSpace.Named.LINEAR_SRGB)),
         };
     }
@@ -2247,6 +2250,7 @@
             assertEquals(record.name, record.width, bm.getWidth());
             assertEquals(record.name, record.height, bm.getHeight());
             record.checkColorSpace(null, bm.getColorSpace());
+            assertEquals(record.hasAlpha, bm.hasAlpha());
         } catch (IOException e) {
             fail("Failed to decode asset " + record.name + " with " + e);
         }
@@ -2471,7 +2475,7 @@
     }
 
 
-    private Object[] crossProduct(Object[] a, Object[] b) {
+    static Object[] crossProduct(Object[] a, Object[] b) {
         final int length = a.length * b.length;
         Object[] ret = new Object[length];
         for (int i = 0; i < a.length; i++) {
@@ -2498,7 +2502,7 @@
             return;
         }
 
-        String name = getAsResourceUri(record.resId).toString();
+        String name = Utils.getAsResourceUri(record.resId).toString();
         ImageDecoder.Source src = f.apply(record.resId);
         testReuse(src, name);
     }
@@ -2511,7 +2515,7 @@
             return;
         }
 
-        String name = getAsResourceUri(record.resId).toString();
+        String name = Utils.getAsResourceUri(record.resId).toString();
         ImageDecoder.Source src = ImageDecoder.createSource(getResources(), record.resId);
         testReuse(src, name);
 
diff --git a/tests/tests/graphics/src/android/graphics/cts/Utils.java b/tests/tests/graphics/src/android/graphics/cts/Utils.java
new file mode 100644
index 0000000..f7229ed
--- /dev/null
+++ b/tests/tests/graphics/src/android/graphics/cts/Utils.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) 2019 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 android.graphics.cts;
+
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import android.content.ContentResolver;
+import android.content.res.Resources;
+import android.net.Uri;
+
+import androidx.test.InstrumentationRegistry;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+public class Utils {
+    private static Resources getResources() {
+        return InstrumentationRegistry.getTargetContext().getResources();
+    }
+
+    private static InputStream obtainInputStream(int resId) {
+        return getResources().openRawResource(resId);
+    }
+
+    static Uri getAsResourceUri(int resId) {
+        Resources res = getResources();
+        return new Uri.Builder()
+                .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE)
+                .authority(res.getResourcePackageName(resId))
+                .appendPath(res.getResourceTypeName(resId))
+                .appendPath(res.getResourceEntryName(resId))
+                .build();
+    }
+
+    /*
+     * Create a new file and return a path to it.
+     * @param resId Original file. It will be copied into the new file.
+     * @param offset Number of zeroes to write to the new file before the
+     *               copied file. This allows testing decodeFileDescriptor
+     *               with an offset. Must be less than or equal to 1024
+     */
+    static String obtainPath(int resId, long offset) {
+        return obtainFile(resId, offset).getPath();
+    }
+
+    /*
+     * Create and return a new file.
+     * @param resId Original file. It will be copied into the new file.
+     * @param offset Number of zeroes to write to the new file before the
+     *               copied file. This allows testing decodeFileDescriptor
+     *               with an offset. Must be less than or equal to 1024
+     */
+    static File obtainFile(int resId, long offset) {
+        assertTrue(offset >= 0);
+        File dir = InstrumentationRegistry.getTargetContext().getFilesDir();
+        dir.mkdirs();
+
+        String name = getResources().getResourceEntryName(resId).toString();
+        if (offset > 0) {
+            name = name + "_" + String.valueOf(offset);
+        }
+
+        File file = new File(dir, name);
+        if (file.exists()) {
+            return file;
+        }
+
+        try {
+            file.createNewFile();
+        } catch (IOException e) {
+            e.printStackTrace();
+            // If the file does not exist it will be handled below.
+        }
+        if (!file.exists()) {
+            fail("Failed to create new File for " + name + "!");
+        }
+
+        InputStream is = obtainInputStream(resId);
+
+        try {
+            FileOutputStream fOutput = new FileOutputStream(file);
+            byte[] dataBuffer = new byte[1024];
+            // Write a bunch of zeroes before the image.
+            assertTrue(offset <= 1024);
+            fOutput.write(dataBuffer, 0, (int) offset);
+            int readLength = 0;
+            while ((readLength = is.read(dataBuffer)) != -1) {
+                fOutput.write(dataBuffer, 0, readLength);
+            }
+            is.close();
+            fOutput.close();
+        } catch (IOException e) {
+            e.printStackTrace();
+            fail("Failed to create file \"" + name + "\" with exception " + e);
+        }
+        return file;
+    }
+}