Merge "Add Image Capture Stress test for Camera2 Extensions" into androidx-main
diff --git a/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/camera2extensions/Camera2ExtensionsImageCaptureStressTest.kt b/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/camera2extensions/Camera2ExtensionsImageCaptureStressTest.kt
new file mode 100644
index 0000000..f9d8b9d
--- /dev/null
+++ b/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/camera2extensions/Camera2ExtensionsImageCaptureStressTest.kt
@@ -0,0 +1,136 @@
+/*
+ * Copyright 2022 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 androidx.camera.integration.extensions.camera2extensions
+
+import android.content.Context
+import android.hardware.camera2.CameraDevice
+import android.hardware.camera2.CameraExtensionSession
+import android.hardware.camera2.CameraManager
+import android.hardware.camera2.params.OutputConfiguration
+import android.media.ImageReader
+import android.view.Surface
+import androidx.camera.camera2.Camera2Config
+import androidx.camera.integration.extensions.util.Camera2ExtensionsTestUtil
+import androidx.camera.integration.extensions.util.Camera2ExtensionsTestUtil.assumeCameraExtensionSupported
+import androidx.camera.integration.extensions.util.Camera2ExtensionsTestUtil.createCaptureImageReader
+import androidx.camera.integration.extensions.util.Camera2ExtensionsTestUtil.openCameraDevice
+import androidx.camera.integration.extensions.util.Camera2ExtensionsTestUtil.openExtensionSession
+import androidx.camera.integration.extensions.util.Camera2ExtensionsTestUtil.takePicture
+import androidx.camera.integration.extensions.util.assertImageIsValid
+import androidx.camera.integration.extensions.utils.CameraIdExtensionModePair
+import androidx.camera.testing.CameraUtil
+import androidx.camera.testing.StressTestRule
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.filters.LargeTest
+import androidx.test.filters.SdkSuppress
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.runBlocking
+import org.junit.After
+import org.junit.Assume.assumeTrue
+import org.junit.Before
+import org.junit.ClassRule
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+/**
+ * Stress test to verify that the camera can successfully capture images for all supported
+ * extension modes for each cameras ID.
+ */
+@LargeTest
+@RunWith(Parameterized::class)
+@SdkSuppress(minSdkVersion = 31)
+class Camera2ExtensionsImageCaptureStressTest(private val config: CameraIdExtensionModePair) {
+    @get:Rule
+    val useCamera =
+        CameraUtil.grantCameraPermissionAndPreTest(
+            CameraUtil.PreTestCameraIdList(Camera2Config.defaultConfig())
+        )
+
+    companion object {
+        @ClassRule
+        @JvmField val stressTest = StressTestRule()
+
+        @Parameterized.Parameters(name = "config = {0}")
+        @JvmStatic
+        fun parameters() = Camera2ExtensionsTestUtil.getAllCameraIdExtensionModeCombinations()
+    }
+
+    private val context = ApplicationProvider.getApplicationContext<Context>()
+    private val cameraManager = context.getSystemService(Context.CAMERA_SERVICE) as CameraManager
+
+    private lateinit var cameraDevice: CameraDevice
+    private lateinit var imageReader: ImageReader
+    private lateinit var captureSurface: Surface
+    private lateinit var extensionSession: CameraExtensionSession
+
+    @Before
+    fun setUp(): Unit = runBlocking {
+        assumeTrue(Camera2ExtensionsTestUtil.isTargetDeviceExcludedForExtensionsTest())
+
+        val (cameraId, extensionMode) = config
+
+        val extensionsCharacteristics = cameraManager.getCameraExtensionCharacteristics(cameraId)
+        assumeCameraExtensionSupported(extensionMode, extensionsCharacteristics)
+
+        cameraDevice = openCameraDevice(cameraManager, cameraId)
+        imageReader = createCaptureImageReader(extensionsCharacteristics, extensionMode)
+        captureSurface = imageReader.surface
+        val outputConfigurationCapture = OutputConfiguration(captureSurface)
+        extensionSession = openExtensionSession(
+            cameraDevice,
+            extensionMode,
+            listOf(outputConfigurationCapture)
+        )
+        assertThat(extensionSession).isNotNull()
+    }
+
+    @After
+    fun tearDown() {
+        if (::extensionSession.isInitialized) {
+            extensionSession.close()
+        }
+
+        if (::cameraDevice.isInitialized) {
+            cameraDevice.close()
+        }
+
+        if (::imageReader.isInitialized) {
+            imageReader.close()
+        }
+
+        if (::captureSurface.isInitialized) {
+            captureSurface.release()
+        }
+    }
+
+    @Test
+    fun captureImage(): Unit = runBlocking {
+        repeat(Camera2ExtensionsTestUtil.getStressTestRepeatingCount()) {
+            val image = takePicture(cameraDevice, extensionSession, imageReader)
+            assertThat(image).isNotNull()
+
+            image?.let {
+                assertThat(it.timestamp).isGreaterThan(0)
+                assertImageIsValid(it, imageReader.width, imageReader.height)
+            }
+
+            image!!.close()
+        }
+    }
+}
\ No newline at end of file
diff --git a/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/util/Camera2ExtensionsTestUtil.kt b/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/util/Camera2ExtensionsTestUtil.kt
index 18051b9..429f3c8 100644
--- a/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/util/Camera2ExtensionsTestUtil.kt
+++ b/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/util/Camera2ExtensionsTestUtil.kt
@@ -22,6 +22,7 @@
 import android.hardware.camera2.CameraAccessException
 import android.hardware.camera2.CameraCharacteristics
 import android.hardware.camera2.CameraDevice
+import android.hardware.camera2.CameraExtensionCharacteristics
 import android.hardware.camera2.CameraExtensionSession
 import android.hardware.camera2.CameraManager
 import android.hardware.camera2.CaptureRequest
@@ -88,15 +89,7 @@
         verifyOutput: Boolean = false
     ) {
         val extensionsCharacteristics = cameraManager.getCameraExtensionCharacteristics(cameraId)
-        assumeTrue(extensionsCharacteristics.supportedExtensions.contains(extensionMode))
-        assumeTrue(
-            extensionsCharacteristics
-                .getExtensionSupportedSizes(extensionMode, SurfaceTexture::class.java).isNotEmpty()
-        )
-        assumeTrue(
-            extensionsCharacteristics
-                .getExtensionSupportedSizes(extensionMode, ImageFormat.JPEG).isNotEmpty()
-        )
+        assumeCameraExtensionSupported(extensionMode, extensionsCharacteristics)
 
         // Preview surface
         val previewSize = extensionsCharacteristics
@@ -119,11 +112,7 @@
         val previewSurface = Surface(surfaceTextureHolder.surfaceTexture)
 
         // Still capture surface
-        val captureSize = extensionsCharacteristics
-            .getExtensionSupportedSizes(extensionMode, ImageFormat.JPEG)
-            .maxBy { it.width * it.height }
-        val imageReader = ImageReader
-            .newInstance(captureSize.width, captureSize.height, ImageFormat.JPEG, 2)
+        val imageReader = createCaptureImageReader(extensionsCharacteristics, extensionMode)
         val captureSurface = imageReader.surface
 
         val cameraDevice = openCameraDevice(cameraManager, cameraId)
@@ -197,9 +186,39 @@
     }
 
     /**
+     * Check if the device supports the [extensionMode] and other extension specific characteristics
+     * required for testing. Halt the test if any criteria is not satisfied.
+     */
+    fun assumeCameraExtensionSupported(
+        extensionMode: Int,
+        extensionsCharacteristics: CameraExtensionCharacteristics
+    ) {
+        assumeTrue(extensionsCharacteristics.supportedExtensions.contains(extensionMode))
+        assumeTrue(
+            extensionsCharacteristics
+                .getExtensionSupportedSizes(extensionMode, SurfaceTexture::class.java).isNotEmpty()
+        )
+        assumeTrue(
+            extensionsCharacteristics
+                .getExtensionSupportedSizes(extensionMode, ImageFormat.JPEG).isNotEmpty()
+        )
+    }
+
+    fun createCaptureImageReader(
+        extensionsCharacteristics: CameraExtensionCharacteristics,
+        extensionMode: Int
+    ): ImageReader {
+        val captureSize = extensionsCharacteristics
+            .getExtensionSupportedSizes(extensionMode, ImageFormat.JPEG)
+            .maxBy { it.width * it.height }
+        return ImageReader
+            .newInstance(captureSize.width, captureSize.height, ImageFormat.JPEG, 2)
+    }
+
+    /**
      * Open the camera device and return the [CameraDevice] instance.
      */
-    private suspend fun openCameraDevice(
+    suspend fun openCameraDevice(
         cameraManager: CameraManager,
         cameraId: String
     ): CameraDevice {
@@ -228,7 +247,7 @@
     /**
      * Open the [CameraExtensionSession] and return the instance.
      */
-    private suspend fun openExtensionSession(
+    suspend fun openExtensionSession(
         cameraDevice: CameraDevice,
         extensionMode: Int,
         outputConfigs: List<OutputConfiguration>
@@ -256,7 +275,11 @@
         return deferred.await()
     }
 
-    private suspend fun takePicture(
+    /**
+     * Take a picture with the provided [session] and output the contents to the [imageReader]. The
+     * latest image written to the [imageReader] is returned.
+     */
+    suspend fun takePicture(
         cameraDevice: CameraDevice,
         session: CameraExtensionSession,
         imageReader: ImageReader
diff --git a/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/util/ImageCaptureTestUtil.kt b/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/util/ImageCaptureTestUtil.kt
new file mode 100644
index 0000000..1875421
--- /dev/null
+++ b/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/util/ImageCaptureTestUtil.kt
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2022 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 androidx.camera.integration.extensions.util
+
+import android.graphics.BitmapFactory
+import android.graphics.ImageFormat
+import android.media.Image
+import com.google.common.truth.Truth.assertThat
+import junit.framework.TestCase.assertNotNull
+import junit.framework.TestCase.assertTrue
+
+/**
+ * Validate the image can be correctly decoded from a jpeg format to a bitmap.
+ */
+fun assertImageIsValid(image: Image, width: Int, height: Int) {
+    assertThat(image.width).isEqualTo(width)
+    assertThat(image.height).isEqualTo(height)
+    assertThat(image.format).isEqualTo(ImageFormat.JPEG)
+
+    val data = imageData(image)
+    assertTrue("Invalid image data", data.isNotEmpty())
+
+    val bmpOptions = BitmapFactory.Options().apply {
+        inJustDecodeBounds = true
+    }
+
+    BitmapFactory.decodeByteArray(data, 0, data.size, bmpOptions)
+
+    assertThat(width).isEqualTo(bmpOptions.outWidth)
+    assertThat(height).isEqualTo(bmpOptions.outHeight)
+
+    val bitmap = BitmapFactory.decodeByteArray(data, 0, data.size)
+    assertNotNull("Decoding jpeg failed", bitmap)
+}
+
+private fun imageData(image: Image): ByteArray {
+    val planes = image.planes
+    assertTrue("Fail to get image planes", planes != null && planes.isNotEmpty())
+
+    val buffer = planes[0].buffer
+    assertNotNull("Fail to get jpeg ByteBuffer", buffer)
+
+    val data = ByteArray(buffer.remaining())
+    buffer.get(data)
+    buffer.rewind()
+    return data
+}
\ No newline at end of file