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
}