Merge "Allow GraphProcessor to submit parameters before the graph starts" into androidx-main
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/Controller3A.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/Controller3A.kt
index 1def65c..f1360d3 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/Controller3A.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/Controller3A.kt
@@ -187,7 +187,7 @@
         afRegions?.let { extra3AParams.put(CaptureRequest.CONTROL_AF_REGIONS, it.toTypedArray()) }
         awbRegions?.let { extra3AParams.put(CaptureRequest.CONTROL_AWB_REGIONS, it.toTypedArray()) }
 
-        if (!graphProcessor.submit(extra3AParams)) {
+        if (!graphProcessor.trySubmit(extra3AParams)) {
             graphListener3A.removeListener(listener)
             return CompletableDeferred(result3ASubmitFailed)
         }
@@ -245,7 +245,7 @@
         // a single request with TRIGGER = TRIGGER_CANCEL so that af can start a fresh scan.
         if (afLockBehaviorSanitized.shouldUnlockAf()) {
             debug { "lock3A - sending a request to unlock af first." }
-            if (!graphProcessor.submit(parameterForAfTriggerCancel)) {
+            if (!graphProcessor.trySubmit(parameterForAfTriggerCancel)) {
                 return CompletableDeferred(result3ASubmitFailed)
             }
         }
@@ -333,7 +333,7 @@
         // a single request with TRIGGER = TRIGGER_CANCEL so that af can start a fresh scan.
         if (afSanitized == true) {
             debug { "unlock3A - sending a request to unlock af first." }
-            if (!graphProcessor.submit(parameterForAfTriggerCancel)) {
+            if (!graphProcessor.trySubmit(parameterForAfTriggerCancel)) {
                 debug { "unlock3A - request to unlock af failed, returning early." }
                 return CompletableDeferred(result3ASubmitFailed)
             }
@@ -373,7 +373,7 @@
         graphListener3A.addListener(listener)
 
         debug { "lock3AForCapture - sending a request to trigger ae precapture metering and af." }
-        if (!graphProcessor.submit(parametersForAePrecaptureAndAfTrigger)) {
+        if (!graphProcessor.trySubmit(parametersForAePrecaptureAndAfTrigger)) {
             debug {
                 "lock3AForCapture - request to trigger ae precapture metering and af failed, " +
                     "returning early."
@@ -401,7 +401,7 @@
      */
     private suspend fun unlock3APostCaptureAndroidLAndBelow(): Deferred<Result3A> {
         debug { "unlock3AForCapture - sending a request to cancel af and turn on ae." }
-        if (!graphProcessor.submit(
+        if (!graphProcessor.trySubmit(
                 mapOf(CONTROL_AF_TRIGGER to CONTROL_AF_TRIGGER_CANCEL, CONTROL_AE_LOCK to true)
             )
         ) {
@@ -415,7 +415,10 @@
         graphListener3A.addListener(listener)
 
         debug { "unlock3AForCapture - sending a request to turn off ae." }
-        if (!graphProcessor.submit(mapOf<CaptureRequest.Key<*>, Any>(CONTROL_AE_LOCK to false))) {
+        if (!graphProcessor.trySubmit(
+                mapOf<CaptureRequest.Key<*>, Any>(CONTROL_AE_LOCK to false)
+            )
+        ) {
             debug { "unlock3AForCapture - request to unlock ae failed." }
             graphListener3A.removeListener(listener)
             return CompletableDeferred(result3ASubmitFailed)
@@ -438,7 +441,7 @@
                 CONTROL_AE_PRECAPTURE_TRIGGER to
                     CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_CANCEL
             )
-        if (!graphProcessor.submit(parametersForAePrecaptureAndAfCancel)) {
+        if (!graphProcessor.trySubmit(parametersForAePrecaptureAndAfCancel)) {
             debug {
                 "unlock3APostCapture - request to reset af and ae precapture metering failed, " +
                     "returning early."
@@ -511,7 +514,7 @@
         }
 
         debug { "lock3A - submitting a request to lock af." }
-        val submitSuccess = graphProcessor.submit(parameterForAfTriggerStart)
+        val submitSuccess = graphProcessor.trySubmit(parameterForAfTriggerStart)
 
         lastAeMode?.let {
             graphState3A.update(aeMode = it)
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/GraphProcessor.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/GraphProcessor.kt
index 355643f..e195621 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/GraphProcessor.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/GraphProcessor.kt
@@ -38,6 +38,7 @@
 import androidx.camera.camera2.pipe.formatForLogs
 import androidx.camera.camera2.pipe.putAllMetadata
 import javax.inject.Inject
+import kotlinx.coroutines.CompletableDeferred
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.StateFlow
@@ -55,7 +56,31 @@
 
     fun submit(request: Request)
     fun submit(requests: List<Request>)
-    suspend fun submit(parameters: Map<*, Any?>): Boolean
+
+    /**
+     * This tries to submit a list of parameters — essentially a list of request settings usually
+     * from 3A methods. It does this by setting the given parameters onto the current repeating
+     * request on a best-effort basis.
+     *
+     * If the CameraGraph hasn't been started yet, or we haven't yet submitted a repeating request,
+     * the method will suspend until we've met the criteria and only then submits the parameters.
+     *
+     * This behavior is required if users call 3A methods immediately after start. For example:
+     *
+     * ```
+     * cameraGraph.start()
+     * cameraGraph.acquireSession().use {
+     *     it.startRepeating(request)
+     *     it.lock3A(...)
+     * }
+     * ```
+     *
+     * Under this scenario, developers should reasonably expect things to work, and therefore
+     * the implementation handles this on a best-effort basis for the developer.
+     *
+     * Please read b/263211462 for more context.
+     */
+    suspend fun trySubmit(parameters: Map<*, Any?>): Boolean
 
     fun startRepeating(request: Request)
     fun stopRepeating()
@@ -114,6 +139,12 @@
     @GuardedBy("lock")
     private var closed = false
 
+    @GuardedBy("lock")
+    private var pendingParameters: Map<*, Any?>? = null
+
+    @GuardedBy("lock")
+    private var pendingParametersDeferred: CompletableDeferred<Boolean>? = null
+
     private val _graphState = MutableStateFlow<GraphState>(GraphStateStopped)
 
     override val graphState: StateFlow<GraphState>
@@ -248,11 +279,12 @@
     }
 
     /** Submit a request to the camera using only the current repeating request. */
-    override suspend fun submit(parameters: Map<*, Any?>): Boolean =
+    override suspend fun trySubmit(parameters: Map<*, Any?>): Boolean =
         withContext(threads.lightweightDispatcher) {
             val processor: GraphRequestProcessor?
             val request: Request?
             val requiredParameters: MutableMap<Any, Any?> = mutableMapOf()
+            var deferredResult: CompletableDeferred<Boolean>? = null
 
             synchronized(lock) {
                 if (closed) return@withContext false
@@ -262,10 +294,20 @@
                 requiredParameters.putAllMetadata(parameters.toMutableMap())
                 graphState3A.writeTo(requiredParameters)
                 requiredParameters.putAllMetadata(cameraGraphConfig.requiredParameters)
+
+                if (processor == null || request == null) {
+                    // If a previous set of parameters haven't been submitted yet, consider it stale
+                    pendingParametersDeferred?.complete(false)
+
+                    debug { "Holding parameters to be submitted later" }
+                    deferredResult = CompletableDeferred<Boolean>()
+                    pendingParametersDeferred = deferredResult
+                    pendingParameters = requiredParameters
+                }
             }
 
             return@withContext when {
-                processor == null || request == null -> false
+                processor == null || request == null -> deferredResult?.await() == true
                 else ->
                     processor.submit(
                         isRepeating = false,
@@ -389,6 +431,7 @@
                     synchronized(lock) {
                         if (processor === _requestProcessor) {
                             currentRepeatingRequest = request
+                            trySubmitPendingParameters(processor, request)
                         }
                     }
                     succeededIndex = index
@@ -411,6 +454,25 @@
         }
     }
 
+    @GuardedBy("lock")
+    private fun trySubmitPendingParameters(processor: GraphRequestProcessor, request: Request) {
+        val parameters = pendingParameters
+        val deferred = pendingParametersDeferred
+        if (parameters != null && deferred != null) {
+            val resubmitResult = processor.submit(
+                isRepeating = false,
+                requests = listOf(request),
+                defaultParameters = cameraGraphConfig.defaultParameters,
+                requiredParameters = parameters,
+                listeners = graphListeners
+            )
+            deferred.complete(resubmitResult)
+
+            pendingParameters = null
+            pendingParametersDeferred = null
+        }
+    }
+
     private fun submitLoop() {
         var burst: List<Request>
         var processor: GraphRequestProcessor
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/GraphProcessorTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/GraphProcessorTest.kt
index 29210c7..d95bfd2 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/GraphProcessorTest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/GraphProcessorTest.kt
@@ -18,6 +18,7 @@
 
 import android.graphics.SurfaceTexture
 import android.hardware.camera2.CaptureRequest
+import android.hardware.camera2.CaptureRequest.CONTROL_AE_LOCK
 import android.os.Build
 import android.view.Surface
 import androidx.camera.camera2.pipe.CameraError
@@ -33,6 +34,7 @@
 import androidx.camera.camera2.pipe.testing.RobolectricCameraPipeTestRunner
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.async
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.flow.first
 import kotlinx.coroutines.flow.firstOrNull
@@ -430,6 +432,67 @@
     }
 
     @Test
+    fun graphProcessorResubmitsParametersAfterGraphStarts() = runTest {
+        val graphProcessor =
+            GraphProcessorImpl(
+                FakeThreads.fromTestScope(this),
+                FakeGraphConfigs.graphConfig,
+                graphState3A,
+                this,
+                arrayListOf(globalListener)
+            )
+
+        val result = async {
+            graphProcessor.trySubmit(mapOf<CaptureRequest.Key<*>, Any>(CONTROL_AE_LOCK to false))
+        }
+        advanceUntilIdle()
+
+        graphProcessor.onGraphStarted(graphRequestProcessor1)
+        graphProcessor.startRepeating(request1)
+        advanceUntilIdle()
+
+        assertThat(result.await()).isTrue()
+    }
+
+    @Test
+    fun graphProcessorSubmitsLatestParametersWhenSubmittedTwiceBeforeGraphStarts() = runTest {
+        val graphProcessor =
+            GraphProcessorImpl(
+                FakeThreads.fromTestScope(this),
+                FakeGraphConfigs.graphConfig,
+                graphState3A,
+                this,
+                arrayListOf(globalListener)
+            )
+
+        val result1 = async {
+            graphProcessor.trySubmit(mapOf<CaptureRequest.Key<*>, Any>(CONTROL_AE_LOCK to false))
+        }
+        advanceUntilIdle()
+        val result2 = async {
+            graphProcessor.trySubmit(mapOf<CaptureRequest.Key<*>, Any>(CONTROL_AE_LOCK to true))
+        }
+        advanceUntilIdle()
+
+        graphProcessor.onGraphStarted(graphRequestProcessor1)
+        advanceUntilIdle()
+
+        graphProcessor.startRepeating(request1)
+        advanceUntilIdle()
+
+        val event1 = fakeProcessor1.nextEvent()
+        assertThat(event1.requestSequence?.repeating).isTrue()
+        val event2 = fakeProcessor1.nextEvent()
+        assertThat(event2.requestSequence?.repeating).isFalse()
+        assertThat(
+            event2.requestSequence?.requestMetadata?.get(request1)?.get(CONTROL_AE_LOCK)
+        ).isTrue()
+
+        assertThat(result1.await()).isFalse()
+        assertThat(result2.await()).isTrue()
+    }
+
+    @Test
     fun graphProcessorChangesGraphStateOnError() = runTest {
         val graphProcessor =
             GraphProcessorImpl(
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeGraphProcessor.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeGraphProcessor.kt
index 128e70c..6f450b9 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeGraphProcessor.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeGraphProcessor.kt
@@ -71,7 +71,7 @@
         _requestQueue.add(requests)
     }
 
-    override suspend fun submit(parameters: Map<*, Any?>): Boolean {
+    override suspend fun trySubmit(parameters: Map<*, Any?>): Boolean {
         if (closed) {
             return false
         }