Merge "[Camera-pipe] Fix image capture no response" into androidx-main
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/StillCaptureRequestControl.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/StillCaptureRequestControl.kt
index e4dec65..3a7e196 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/StillCaptureRequestControl.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/StillCaptureRequestControl.kt
@@ -19,7 +19,6 @@
 import androidx.annotation.GuardedBy
 import androidx.annotation.RequiresApi
 import androidx.camera.camera2.pipe.core.Log.debug
-import androidx.camera.camera2.pipe.core.Log.warn
 import androidx.camera.camera2.pipe.integration.adapter.asListenableFuture
 import androidx.camera.camera2.pipe.integration.adapter.propagateOnceTo
 import androidx.camera.camera2.pipe.integration.config.CameraScope
@@ -40,7 +39,6 @@
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.sync.Mutex
 import kotlinx.coroutines.sync.withLock
-import kotlinx.coroutines.withTimeoutOrNull
 
 @RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
 @CameraScope
@@ -144,12 +142,7 @@
         // completed. On some devices, AE preCapture triggered in submitStillCaptures may not
         // work properly if the repeating request to change the flash mode is not completed.
         debug { "StillCaptureRequestControl: Waiting for flash control" }
-        withTimeoutOrNull(1_000L) {
-            flashControl.updateSignal.join()
-        } ?: {
-            warn { "StillCaptureRequestControl: Waiting for flash control timed out" }
-        }
-        debug { "StillCaptureRequestControl: Waiting for flash control done" }
+        flashControl.updateSignal.join()
         debug { "StillCaptureRequestControl: Issuing single capture" }
         val deferredList = camera.requestControl.issueSingleCaptureAsync(
             request.captureConfigs,
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCamera.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCamera.kt
index 32c7488..1f31a5a 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCamera.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCamera.kt
@@ -139,6 +139,7 @@
         return if (closed.compareAndSet(expect = false, update = true)) {
             threads.scope.launch(start = CoroutineStart.UNDISPATCHED) {
                 debug { "Closing $this" }
+                requestControl.close()
                 useCaseGraphConfig.graph.close()
                 useCaseSurfaceManager.stopAsync().await()
             }
@@ -151,15 +152,19 @@
         key: CaptureRequest.Key<T>,
         value: T,
         priority: Config.OptionPriority,
-    ): Deferred<Unit> = setParametersAsync(mapOf(key to (value as Any)), priority)
+    ): Deferred<Unit> = runIfNotClosed {
+        setParametersAsync(mapOf(key to (value as Any)), priority)
+    } ?: canceledResult
 
     override fun setParametersAsync(
         values: Map<CaptureRequest.Key<*>, Any>,
         priority: Config.OptionPriority,
-    ): Deferred<Unit> = requestControl.addParametersAsync(
-        values = values,
-        optionPriority = priority
-    )
+    ): Deferred<Unit> = runIfNotClosed {
+        requestControl.addParametersAsync(
+            values = values,
+            optionPriority = priority
+        )
+    } ?: canceledResult
 
     override fun setActiveResumeMode(enabled: Boolean) {
         useCaseGraphConfig.graph.isForeground = enabled
@@ -167,21 +172,27 @@
 
     private fun UseCaseCameraRequestControl.setSessionConfigAsync(
         sessionConfig: SessionConfig
-    ): Deferred<Unit> = setConfigAsync(
-        type = UseCaseCameraRequestControl.Type.SESSION_CONFIG,
-        config = sessionConfig.implementationOptions,
-        tags = sessionConfig.repeatingCaptureConfig.tagBundle.toMap(),
-        listeners = setOf(
-            CameraCallbackMap.createFor(
-                sessionConfig.repeatingCameraCaptureCallbacks,
-                threads.backgroundExecutor
-            )
-        ),
-        template = RequestTemplate(sessionConfig.repeatingCaptureConfig.templateType),
-        streams = useCaseGraphConfig.getStreamIdsFromSurfaces(
-            sessionConfig.repeatingCaptureConfig.surfaces
-        ),
-    )
+    ): Deferred<Unit> = runIfNotClosed {
+        setConfigAsync(
+            type = UseCaseCameraRequestControl.Type.SESSION_CONFIG,
+            config = sessionConfig.implementationOptions,
+            tags = sessionConfig.repeatingCaptureConfig.tagBundle.toMap(),
+            listeners = setOf(
+                CameraCallbackMap.createFor(
+                    sessionConfig.repeatingCameraCaptureCallbacks,
+                    threads.backgroundExecutor
+                )
+            ),
+            template = RequestTemplate(sessionConfig.repeatingCaptureConfig.templateType),
+            streams = useCaseGraphConfig.getStreamIdsFromSurfaces(
+                sessionConfig.repeatingCaptureConfig.surfaces
+            ),
+        )
+    } ?: canceledResult
+
+    private inline fun <R> runIfNotClosed(crossinline block: () -> R): R? {
+        return if (!closed.value) block() else null
+    }
 
     override fun toString(): String = "UseCaseCamera-$debugId"
 
@@ -191,4 +202,8 @@
         @Binds
         abstract fun provideUseCaseCamera(useCaseCamera: UseCaseCameraImpl): UseCaseCamera
     }
+
+    companion object {
+        private val canceledResult = CompletableDeferred<Unit>().apply { cancel() }
+    }
 }
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCameraRequestControl.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCameraRequestControl.kt
index 6ce070e..13ac999 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCameraRequestControl.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCameraRequestControl.kt
@@ -46,6 +46,7 @@
 import dagger.Binds
 import dagger.Module
 import javax.inject.Inject
+import kotlinx.coroutines.CancellationException
 import kotlinx.coroutines.CompletableDeferred
 import kotlinx.coroutines.Deferred
 
@@ -150,6 +151,8 @@
         flashType: Int,
         flashMode: Int,
     ): List<Deferred<Void?>>
+
+    fun close()
 }
 
 @UseCaseCameraScope
@@ -161,6 +164,9 @@
 ) : UseCaseCameraRequestControl {
     private val graph = useCaseGraphConfig.graph
 
+    @Volatile
+    private var closed = false
+
     private data class InfoBundle(
         val options: Camera2ImplConfig.Builder = Camera2ImplConfig.Builder(),
         val tags: MutableMap<String, Any> = mutableMapOf(),
@@ -178,15 +184,17 @@
         optionPriority: Config.OptionPriority,
         tags: Map<String, Any>,
         listeners: Set<Request.Listener>
-    ): Deferred<Unit> = synchronized(lock) {
-        debug { "[$type] Add request option: $values" }
-        infoBundleMap.getOrPut(type) { InfoBundle() }.let {
-            it.options.addAllCaptureRequestOptionsWithPriority(values, optionPriority)
-            it.tags.putAll(tags)
-            it.listeners.addAll(listeners)
-        }
-        infoBundleMap.merge()
-    }.updateCameraStateAsync()
+    ): Deferred<Unit> = runIfNotClosed {
+        synchronized(lock) {
+            debug { "[$type] Add request option: $values" }
+            infoBundleMap.getOrPut(type) { InfoBundle() }.let {
+                it.options.addAllCaptureRequestOptionsWithPriority(values, optionPriority)
+                it.tags.putAll(tags)
+                it.listeners.addAll(listeners)
+            }
+            infoBundleMap.merge()
+        }.updateCameraStateAsync()
+    } ?: canceledResult
 
     override fun setConfigAsync(
         type: UseCaseCameraRequestControl.Type,
@@ -195,25 +203,27 @@
         streams: Set<StreamId>?,
         template: RequestTemplate?,
         listeners: Set<Request.Listener>
-    ): Deferred<Unit> = synchronized(lock) {
-        debug { "[$type] Set config: ${config?.toParameters()}" }
-        infoBundleMap[type] = InfoBundle(
-            Camera2ImplConfig.Builder().apply {
-                config?.let {
-                    insertAllOptions(it)
-                }
-            },
-            tags.toMutableMap(),
-            listeners.toMutableSet(),
-            template,
+    ): Deferred<Unit> = runIfNotClosed {
+        synchronized(lock) {
+            debug { "[$type] Set config: ${config?.toParameters()}" }
+            infoBundleMap[type] = InfoBundle(
+                Camera2ImplConfig.Builder().apply {
+                    config?.let {
+                        insertAllOptions(it)
+                    }
+                },
+                tags.toMutableMap(),
+                listeners.toMutableSet(),
+                template,
+            )
+            infoBundleMap.merge()
+        }.updateCameraStateAsync(
+            streams = streams,
         )
-        infoBundleMap.merge()
-    }.updateCameraStateAsync(
-        streams = streams,
-    )
+    } ?: canceledResult
 
-    override suspend fun setTorchAsync(enabled: Boolean): Deferred<Result3A> =
-        graph.acquireSession().use {
+    override suspend fun setTorchAsync(enabled: Boolean): Deferred<Result3A> = runIfNotClosed {
+        useGraphSessionOrFailed {
             it.setTorch(
                 when (enabled) {
                     true -> TorchState.ON
@@ -221,6 +231,7 @@
                 }
             )
         }
+    } ?: submitFailedResult
 
     override suspend fun startFocusAndMeteringAsync(
         aeRegions: List<MeteringRectangle>?,
@@ -231,54 +242,49 @@
         awbLockBehavior: Lock3ABehavior?,
         afTriggerStartAeMode: AeMode?,
         timeLimitNs: Long,
-    ): Deferred<Result3A> = graph.acquireSession().use {
-        it.lock3A(
-            aeRegions = aeRegions,
-            afRegions = afRegions,
-            awbRegions = awbRegions,
-            aeLockBehavior = aeLockBehavior,
-            afLockBehavior = afLockBehavior,
-            awbLockBehavior = awbLockBehavior,
-            afTriggerStartAeMode = afTriggerStartAeMode,
-            timeLimitNs = timeLimitNs,
-        )
-    }
+    ): Deferred<Result3A> = runIfNotClosed {
+        useGraphSessionOrFailed {
+            it.lock3A(
+                aeRegions = aeRegions,
+                afRegions = afRegions,
+                awbRegions = awbRegions,
+                aeLockBehavior = aeLockBehavior,
+                afLockBehavior = afLockBehavior,
+                awbLockBehavior = awbLockBehavior,
+                afTriggerStartAeMode = afTriggerStartAeMode,
+                timeLimitNs = timeLimitNs,
+            )
+        }
+    } ?: submitFailedResult
 
-    override suspend fun cancelFocusAndMeteringAsync(): Deferred<Result3A> {
-        graph.acquireSession().use {
+    override suspend fun cancelFocusAndMeteringAsync() = runIfNotClosed {
+        useGraphSessionOrFailed {
             it.unlock3A(ae = true, af = true, awb = true)
         }.await()
 
-        return graph.acquireSession().use {
+        useGraphSessionOrFailed {
             it.update3A(
                 aeRegions = METERING_REGIONS_DEFAULT.asList(),
                 afRegions = METERING_REGIONS_DEFAULT.asList(),
                 awbRegions = METERING_REGIONS_DEFAULT.asList()
             )
         }
-    }
+    } ?: submitFailedResult
 
     override suspend fun issueSingleCaptureAsync(
         captureSequence: List<CaptureConfig>,
         captureMode: Int,
         flashType: Int,
         flashMode: Int,
-    ): List<Deferred<Void?>> {
+    ) = runIfNotClosed {
         if (captureSequence.hasInvalidSurface()) {
-            return List(captureSequence.size) {
-                CompletableDeferred<Void?>().apply {
-                    completeExceptionally(
-                        ImageCaptureException(
-                            ImageCapture.ERROR_CAPTURE_FAILED,
-                            "Capture request failed due to invalid surface",
-                            null
-                        )
-                    )
-                }
-            }
+            failedResults(
+                captureSequence.size,
+                "Capture request failed due to invalid surface"
+            )
         }
 
-        return synchronized(lock) {
+        synchronized(lock) {
             infoBundleMap.merge()
         }.let { infoBundle ->
             debug { "UseCaseCameraRequestControl: Submitting still captures to capture pipeline" }
@@ -295,8 +301,21 @@
                 flashMode = flashMode,
             )
         }
+    } ?: failedResults(captureSequence.size, "Capture request is cancelled on closed CameraGraph")
+
+    override fun close() {
+        closed = true
     }
 
+    private fun failedResults(count: Int, message: String): List<Deferred<Void?>> =
+        List(count) {
+            CompletableDeferred<Void>().apply {
+                completeExceptionally(
+                    ImageCaptureException(ImageCapture.ERROR_CAPTURE_FAILED, message, null)
+                )
+            }
+        }
+
     private fun List<CaptureConfig>.hasInvalidSurface(): Boolean {
         forEach { captureConfig ->
             if (captureConfig.surfaces.isEmpty()) {
@@ -339,23 +358,37 @@
             }
         }
 
-    private fun InfoBundle.updateCameraStateAsync(streams: Set<StreamId>? = null): Deferred<Unit> {
-        capturePipeline.template =
-            if (template != null && template!!.value != TEMPLATE_TYPE_NONE) {
-                template!!.value
-            } else {
-                DEFAULT_REQUEST_TEMPLATE
-            }
+    private fun InfoBundle.updateCameraStateAsync(streams: Set<StreamId>? = null): Deferred<Unit> =
+        runIfNotClosed {
+            capturePipeline.template =
+                if (template != null && template!!.value != TEMPLATE_TYPE_NONE) {
+                    template!!.value
+                } else {
+                    DEFAULT_REQUEST_TEMPLATE
+                }
 
-        return state.updateAsync(
-            parameters = options.build().toParameters(),
-            appendParameters = false,
-            internalParameters = mapOf(CAMERAX_TAG_BUNDLE to toTagBundle()),
-            appendInternalParameters = false,
-            streams = streams,
-            template = template,
-            listeners = listeners,
-        )
+            state.updateAsync(
+                parameters = options.build().toParameters(),
+                appendParameters = false,
+                internalParameters = mapOf(CAMERAX_TAG_BUNDLE to toTagBundle()),
+                appendInternalParameters = false,
+                streams = streams,
+                template = template,
+                listeners = listeners,
+            )
+        } ?: canceledResult
+
+    private inline fun <R> runIfNotClosed(block: () -> R): R? {
+        return if (!closed) block() else null
+    }
+
+    private suspend inline fun useGraphSessionOrFailed(
+        crossinline block: suspend (CameraGraph.Session) -> Deferred<Result3A>
+    ): Deferred<Result3A> = try {
+        graph.acquireSession().use { block(it) }
+    } catch (e: CancellationException) {
+        debug(e) { "Cannot acquire the CameraGraph.Session" }
+        submitFailedResult
     }
 
     @Module
@@ -366,6 +399,12 @@
             requestControl: UseCaseCameraRequestControlImpl
         ): UseCaseCameraRequestControl
     }
+
+    companion object {
+        private val submitFailedResult =
+            CompletableDeferred(Result3A(Result3A.Status.SUBMIT_FAILED))
+        private val canceledResult = CompletableDeferred<Unit>().apply { cancel() }
+    }
 }
 
 fun TagBundle.toMap(): Map<String, Any> = mutableMapOf<String, Any>().also {
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCameraState.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCameraState.kt
index 75a210e..b67a339 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCameraState.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCameraState.kt
@@ -38,6 +38,7 @@
 import androidx.camera.camera2.pipe.integration.config.UseCaseGraphConfig
 import javax.inject.Inject
 import kotlinx.atomicfu.atomic
+import kotlinx.coroutines.CancellationException
 import kotlinx.coroutines.CompletableDeferred
 import kotlinx.coroutines.CoroutineStart
 import kotlinx.coroutines.Deferred
@@ -241,9 +242,14 @@
         threads.scope.launch(start = CoroutineStart.UNDISPATCHED) {
             val result: CompletableDeferred<Unit>?
             val request: Request?
-            cameraGraph.acquireSession().use {
+            try {
+                cameraGraph.acquireSession()
+            } catch (e: CancellationException) {
+                Log.debug(e) { "Cannot acquire session at ${this@UseCaseCameraState}" }
+                null
+            }.let { session ->
                 synchronized(lock) {
-                    request = if (currentStreams.isEmpty()) {
+                    request = if (currentStreams.isEmpty() || session == null) {
                         null
                     } else {
                         Request(
@@ -263,18 +269,21 @@
                     updating = false
                     updateSignal = null
                 }
-
-                if (request == null) {
-                    it.stopRepeating()
-                } else {
-                    result?.let { result ->
-                        synchronized(lock) {
-                            updateSignals.add(RequestSignal(submittedRequestCounter.value, result))
+                session?.use {
+                    if (request == null) {
+                        it.stopRepeating()
+                    } else {
+                        result?.let { result ->
+                            synchronized(lock) {
+                                updateSignals.add(
+                                    RequestSignal(submittedRequestCounter.value, result)
+                                )
+                            }
                         }
+                        Log.debug { "Update RepeatingRequest: $request" }
+                        it.startRepeating(request)
+                        it.update3A(request.parameters)
                     }
-                    Log.debug { "Update RepeatingRequest: $request" }
-                    it.startRepeating(request)
-                    it.update3A(request.parameters)
                 }
             }
 
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeUseCaseCamera.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeUseCaseCamera.kt
index d2d439e..5fd0c64 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeUseCaseCamera.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeUseCaseCamera.kt
@@ -160,6 +160,9 @@
         }
     }
 
+    override fun close() {
+    }
+
     data class FocusMeteringParams(
         val aeRegions: List<MeteringRectangle>? = null,
         val afRegions: List<MeteringRectangle>? = null,