Merge "ImageReader: Fix rogue RuntimeException in #detachImage"
diff --git a/media/java/android/media/ImageReader.java b/media/java/android/media/ImageReader.java
index 472586b..fde7afd 100644
--- a/media/java/android/media/ImageReader.java
+++ b/media/java/android/media/ImageReader.java
@@ -18,7 +18,11 @@
 
 import android.annotation.IntRange;
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.annotation.SuppressLint;
+import android.app.compat.CompatChanges;
+import android.compat.annotation.ChangeId;
+import android.compat.annotation.EnabledAfter;
 import android.graphics.GraphicBuffer;
 import android.graphics.ImageFormat;
 import android.graphics.ImageFormat.Format;
@@ -29,6 +33,7 @@
 import android.hardware.HardwareBuffer.Usage;
 import android.hardware.SyncFence;
 import android.hardware.camera2.MultiResolutionImageReader;
+import android.os.Build;
 import android.os.Handler;
 import android.os.Looper;
 import android.os.ParcelFileDescriptor;
@@ -87,6 +92,38 @@
 
     /**
      * <p>
+     * Flag to gate correct exception thrown by {@code #detachImage}.
+     * </p>
+     * <p>
+     * {@code #detachImage} is documented as throwing {@link java.lang.IllegalStateException} in
+     * the event of an error; a native helper method to this threw
+     * {@link java.lang.RuntimeException} if the surface was abandoned while detaching the
+     * {@code Image}.
+     * <p>
+     * This previously undocumented exception behavior continues through Android T.
+     * </p>
+     * <p>
+     * After Android T, the native helper method only throws {@code IllegalStateExceptions} in
+     * accordance with the documentation.
+     * </p>
+     * <p>
+     * {@code #detachImage} will now throw only ISEs if it runs into errors while detaching
+     * the image. Behavior on apps targeting API levels <= T remains unchanged.
+     * </p>
+     */
+    @ChangeId
+    @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.TIRAMISU)
+    private static final long DETACH_THROWS_ISE_ONLY = 236825255L;
+
+    /**
+     * Cached value of {@link #DETACH_THROWS_ISE_ONLY} flag to prevent repeated calls when
+     * detaching image.
+     */
+    private final boolean mDetachThrowsIseOnly =
+            CompatChanges.isChangeEnabled(DETACH_THROWS_ISE_ONLY);
+
+    /**
+     * <p>
      * Create a new reader for images of the desired size and format.
      * </p>
      * <p>
@@ -825,10 +862,10 @@
      * </p>
      * <p>
      * After this call, the ImageReader no longer owns this image, and the image
-     * ownership can be transfered to another entity like {@link ImageWriter}
+     * ownership can be transferred to another entity like {@link ImageWriter}
      * via {@link ImageWriter#queueInputImage}. It's up to the new owner to
      * release the resources held by this image. For example, if the ownership
-     * of this image is transfered to an {@link ImageWriter}, the image will be
+     * of this image is transferred to an {@link ImageWriter}, the image will be
      * freed by the ImageWriter after the image data consumption is done.
      * </p>
      * <p>
@@ -849,16 +886,22 @@
      * @throws IllegalStateException If the ImageReader or image have been
      *             closed, or the has been detached, or has not yet been
      *             acquired.
+     * @throws RuntimeException If there is an error detaching {@code Image} from {@code Surface}.
+     *              {@code RuntimeException} is only thrown for applications targeting SDK <=
+     *              {@link android.os.Build.VERSION_CODES#TIRAMISU}.
+     *              For applications targeting SDK >
+     *              {@link android.os.Build.VERSION_CODES#TIRAMISU},
+     *              this method only throws {@code IllegalStateException}.
      * @hide
      */
-     public void detachImage(Image image) {
-       if (image == null) {
+    public void detachImage(@Nullable Image image) {
+        if (image == null) {
            throw new IllegalArgumentException("input image must not be null");
-       }
-       if (!isImageOwnedbyMe(image)) {
+        }
+        if (!isImageOwnedbyMe(image)) {
            throw new IllegalArgumentException("Trying to detach an image that is not owned by"
                    + " this ImageReader");
-       }
+        }
 
         SurfaceImage si = (SurfaceImage) image;
         si.throwISEIfImageIsInvalid();
@@ -867,7 +910,7 @@
             throw new IllegalStateException("Image was already detached from this ImageReader");
         }
 
-        nativeDetachImage(image);
+        nativeDetachImage(image, mDetachThrowsIseOnly);
         si.clearSurfacePlanes();
         si.mPlanes = null;
         si.setDetached(true);
@@ -1408,7 +1451,7 @@
     private synchronized native void nativeClose();
     private synchronized native void nativeReleaseImage(Image i);
     private synchronized native Surface nativeGetSurface();
-    private synchronized native int nativeDetachImage(Image i);
+    private synchronized native int nativeDetachImage(Image i, boolean throwISEOnly);
     private synchronized native void nativeDiscardFreeBuffers();
 
     /**
diff --git a/media/jni/android_media_ImageReader.cpp b/media/jni/android_media_ImageReader.cpp
index 62c0d55..556f98c 100644
--- a/media/jni/android_media_ImageReader.cpp
+++ b/media/jni/android_media_ImageReader.cpp
@@ -643,7 +643,8 @@
     return ACQUIRE_SUCCESS;
 }
 
-static jint ImageReader_detachImage(JNIEnv* env, jobject thiz, jobject image) {
+static jint ImageReader_detachImage(JNIEnv* env, jobject thiz, jobject image,
+                                    jboolean throwISEOnly) {
     ALOGV("%s:", __FUNCTION__);
     JNIImageReaderContext* ctx = ImageReader_getContext(env, thiz);
     if (ctx == NULL) {
@@ -666,8 +667,12 @@
     res = bufferConsumer->detachBuffer(buffer->mSlot);
     if (res != OK) {
         ALOGE("Image detach failed: %s (%d)!!!", strerror(-res), res);
-        jniThrowRuntimeException(env,
-                "nativeDetachImage failed for image!!!");
+        if ((bool) throwISEOnly) {
+            jniThrowException(env, "java/lang/IllegalStateException",
+                    "nativeDetachImage failed for image!!!");
+        } else {
+             jniThrowRuntimeException(env, "nativeDetachImage failed for image!!!");
+        }
         return res;
     }
     return OK;
@@ -965,7 +970,7 @@
     {"nativeReleaseImage",     "(Landroid/media/Image;)V",   (void*)ImageReader_imageRelease },
     {"nativeImageSetup",       "(Landroid/media/Image;Z)I",   (void*)ImageReader_imageSetup },
     {"nativeGetSurface",       "()Landroid/view/Surface;",   (void*)ImageReader_getSurface },
-    {"nativeDetachImage",      "(Landroid/media/Image;)I",   (void*)ImageReader_detachImage },
+    {"nativeDetachImage",      "(Landroid/media/Image;Z)I",   (void*)ImageReader_detachImage },
     {"nativeCreateImagePlanes",
         "(ILandroid/graphics/GraphicBuffer;IIIIII)[Landroid/media/ImageReader$ImagePlane;",
                                                              (void*)ImageReader_createImagePlanes },