Refactor GraphProcessor to use GraphLoop
This makes many of the 3A tests more deterministic, reduces the
need for suspend calls with deferred results, and improves the
signals that can be used to avoid hanging 3A requests.
* Add a Listener to GraphLoop with stop / close events.
* Refactor GraphProcessor to use GraphLoop
* Update GraphProcessor interface to return better signals
* Update Controller3A to take advantage of synchronous signals
* Update tests and classes to match the new interfaces.
Bug: 263211462
Bug: 247166179
Bug: 287020251
Bug: 289284907
Test: ./gradlew\
:camera:camera-camera2-pipe:testDebugUnitTest\
:camera:camera-camera2-pipe-testing:testDebugUnitTest\
:camera:camera-camera2-pipe-integration:testDebugUnitTest
Change-Id: Iebc75538e99f5d53943c4623e22bbccfa250842f
diff --git a/camera/camera-camera2-pipe-testing/src/main/java/androidx/camera/camera2/pipe/testing/FakeCaptureSequenceProcessor.kt b/camera/camera-camera2-pipe-testing/src/main/java/androidx/camera/camera2/pipe/testing/FakeCaptureSequenceProcessor.kt
index c6bb946..5010c90 100644
--- a/camera/camera-camera2-pipe-testing/src/main/java/androidx/camera/camera2/pipe/testing/FakeCaptureSequenceProcessor.kt
+++ b/camera/camera-camera2-pipe-testing/src/main/java/androidx/camera/camera2/pipe/testing/FakeCaptureSequenceProcessor.kt
@@ -138,7 +138,7 @@
requestSequence?.invokeOnSequenceAborted()
}
- override fun close() {
+ override suspend fun shutdown() {
synchronized(lock) {
rejectRequests = true
check(eventChannel.trySend(Event(close = true)).isSuccess)
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CaptureSequenceProcessor.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CaptureSequenceProcessor.kt
index 5b4721e..77c8444 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CaptureSequenceProcessor.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CaptureSequenceProcessor.kt
@@ -71,5 +71,5 @@
* Signal that this [CaptureSequenceProcessor] is no longer in use. Active requests may continue
* to be processed, and [abortCaptures] and [stopRepeating] may still be invoked.
*/
- public fun close()
+ public suspend fun shutdown()
}
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CaptureSequenceProcessor.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CaptureSequenceProcessor.kt
index 7549fe4..17fb37e 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CaptureSequenceProcessor.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CaptureSequenceProcessor.kt
@@ -37,8 +37,6 @@
import androidx.camera.camera2.pipe.core.Debug
import androidx.camera.camera2.pipe.core.Log
import androidx.camera.camera2.pipe.core.Log.MonitoredLogMessages.REPEATING_REQUEST_STARTED_TIMEOUT
-import androidx.camera.camera2.pipe.core.Log.rethrowExceptionAfterLogging
-import androidx.camera.camera2.pipe.core.Threading.runBlockingWithTimeout
import androidx.camera.camera2.pipe.core.Threads
import androidx.camera.camera2.pipe.graph.StreamGraphImpl
import androidx.camera.camera2.pipe.media.AndroidImageWriter
@@ -47,6 +45,8 @@
import javax.inject.Inject
import kotlin.reflect.KClass
import kotlinx.atomicfu.atomic
+import kotlinx.coroutines.TimeoutCancellationException
+import kotlinx.coroutines.withTimeout
internal interface Camera2CaptureSequenceProcessorFactory {
fun create(
@@ -181,14 +181,13 @@
// Finally, write required parameters to the request builder. This will override any
// value that has ben previously set.
//
- // TODO(sushilnath@): Implement one of the two options. (1) Apply the 3A parameters
- // from internal 3A state machine at last and provide a flag in the Request object
- // to
- // specify when the clients want to explicitly override some of the 3A parameters
- // directly. Add code to handle the flag. (2) Let clients override the 3A parameters
- // freely and when that happens intercept those parameters from the request and keep
- // the
- // internal 3A state machine in sync.
+ // TODO(sushilnath@): Implement one of the two options
+ // (1) Apply the 3A parameters from internal 3A state machine at last and provide
+ // a flag in the Request object to specify when the clients want to explicitly
+ // override some of the 3A parameters directly. Add code to handle the flag.
+ // (2) Let clients override the 3A parameters freely and when that happens
+ // intercept those parameters from the request and keep the internal 3A state
+ // machine in sync.
requestBuilder.writeParameters(requiredParameters)
}
val requestNumber = nextRequestNumber()
@@ -284,9 +283,7 @@
override fun submit(captureSequence: Camera2CaptureSequence): Int? =
synchronized(lock) {
if (closed) {
- Log.warn {
- "Capture sequence processor closed. $captureSequence won't be submitted"
- }
+ Log.warn { "$this closed. $captureSequence won't be submitted" }
return null
}
val captureCallback = captureSequence as CameraCaptureSession.CaptureCallback
@@ -327,48 +324,52 @@
session.stopRepeating()
}
- override fun close() =
+ override suspend fun shutdown() {
+ val captureSequence: Camera2CaptureSequence?
synchronized(lock) {
if (closed) {
- return@synchronized
+ return
}
- // Close should not shut down
- Debug.trace("$this#close") {
- if (shouldWaitForRepeatingRequest) {
- lastSingleRepeatingRequestSequence?.let {
- Log.debug { "Waiting for the last repeating request sequence $it" }
- // On certain devices, the submitted repeating request sequence may not give
- // us
- // onCaptureStarted() or onCaptureSequenceAborted() [1]. Hence we wrap the
- // wait
- // under a timeout to prevent us from waiting forever.
- //
- // [1] b/307588161 - [ANR] at
- //
- // androidx.camera.camera2.pipe.compat.Camera2CaptureSequenceProcessor.close
- rethrowExceptionAfterLogging(
- "$this#close: $REPEATING_REQUEST_STARTED_TIMEOUT" +
- ", lastSingleRepeatingRequestSequence = $it"
- ) {
- runBlockingWithTimeout(
- threads.backgroundDispatcher,
- WAIT_FOR_REPEATING_TIMEOUT_MS
- ) {
- it.awaitStarted()
- }
- }
- }
- }
+ closed = true
+ captureSequence = lastSingleRepeatingRequestSequence
+ }
+
+ if (shouldWaitForRepeatingRequest && captureSequence != null) {
+ awaitRepeatingRequestStarted(captureSequence)
+ }
+
+ // Shutdown is responsible for releasing resources that are no longer in use.
+ Debug.trace("$this#close") {
+ synchronized(lock) {
imageWriter?.close()
session.inputSurface?.release()
- closed = true
}
}
+ }
override fun toString(): String {
return "Camera2CaptureSequenceProcessor-$debugId"
}
+ private suspend fun awaitRepeatingRequestStarted(captureSequence: Camera2CaptureSequence) {
+ Log.debug { "Waiting for the last repeating request sequence: $captureSequence" }
+ // On certain devices, the submitted repeating request sequence may not give
+ // us onCaptureStarted() or onCaptureSequenceAborted() [1]. Hence we wrap
+ // the wait under a timeout to prevent us from waiting forever.
+ //
+ // [1] b/307588161 - [ANR] at
+ // androidx.camera.camera2.pipe.compat.Camera2CaptureSequenceProcessor.close
+ try {
+ withTimeout(WAIT_FOR_REPEATING_TIMEOUT_MS) { captureSequence.awaitStarted() }
+ } catch (e: TimeoutCancellationException) {
+ Log.error {
+ "$this#close: $REPEATING_REQUEST_STARTED_TIMEOUT" +
+ ", lastSingleRepeatingRequestSequence = $captureSequence"
+ }
+ throw e
+ }
+ }
+
/**
* The [ImageWriterWrapper] is created once per capture session when the capture session is
* created, assuming it's a reprocessing session.
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/ExternalRequestProcessor.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/ExternalRequestProcessor.kt
index 0379996..15425cb 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/ExternalRequestProcessor.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/ExternalRequestProcessor.kt
@@ -40,6 +40,7 @@
import androidx.camera.camera2.pipe.graph.GraphRequestProcessor
import kotlin.reflect.KClass
import kotlinx.atomicfu.atomic
+import kotlinx.coroutines.runBlocking
public class ExternalCameraController(
private val graphId: CameraGraphId,
@@ -78,7 +79,9 @@
}
override fun close() {
- graphProcessor.close()
+ // TODO: ExternalRequestProcessor will be deprecated. This is a temporary patch to allow
+ // graphProcessor to have a suspending shutdown function.
+ runBlocking { graphProcessor.shutdown() }
}
override fun updateSurfaceMap(surfaceMap: Map<StreamId, Surface>) {
@@ -189,7 +192,7 @@
processor.stopRepeating()
}
- override fun close() {
+ override suspend fun shutdown() {
if (closed.compareAndSet(expect = false, update = true)) {
processor.close()
}
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/core/Debug.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/core/Debug.kt
index b805289..a01f102 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/core/Debug.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/core/Debug.kt
@@ -44,7 +44,7 @@
* @param label A name of the code section to appear in the trace.
* @param block A block of code which is being traced.
*/
- public inline fun <T> trace(label: String, block: () -> T): T {
+ public inline fun <T> trace(label: String, crossinline block: () -> T): T {
try {
traceStart { label }
return block()
@@ -54,7 +54,7 @@
}
/** Wrap the specified [block] in a trace and timing calls. */
- internal inline fun <T> instrument(label: String, block: () -> T): T {
+ internal inline fun <T> instrument(label: String, crossinline block: () -> T): T {
val start = systemTimeSource.now()
try {
traceStart { label }
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/CameraGraphSessionImpl.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/CameraGraphSessionImpl.kt
index eaef6a4..c552afc 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/CameraGraphSessionImpl.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/CameraGraphSessionImpl.kt
@@ -67,7 +67,7 @@
override fun startRepeating(request: Request) {
check(!token.released) { "Cannot call startRepeating on $this after close." }
- graphProcessor.startRepeating(request)
+ graphProcessor.repeatingRequest = request
}
override fun abort() {
@@ -77,8 +77,7 @@
override fun stopRepeating() {
check(!token.released) { "Cannot call stopRepeating on $this after close." }
- graphProcessor.stopRepeating()
- controller3A.onStopRepeating()
+ graphProcessor.repeatingRequest = null
}
override fun close() {
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/CaptureLimiter.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/CaptureLimiter.kt
new file mode 100644
index 0000000..492d7f9
--- /dev/null
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/CaptureLimiter.kt
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2024 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.camera2.pipe.graph
+
+import androidx.camera.camera2.pipe.FrameInfo
+import androidx.camera.camera2.pipe.FrameNumber
+import androidx.camera.camera2.pipe.Request
+import androidx.camera.camera2.pipe.RequestMetadata
+import androidx.camera.camera2.pipe.core.Log
+import kotlinx.atomicfu.atomic
+import kotlinx.atomicfu.update
+import kotlinx.atomicfu.updateAndGet
+
+/**
+ * On some devices, we need to wait for 10 frames to complete before we can guarantee the success of
+ * single capture requests. This is a quirk identified as part of b/287020251 and reported in
+ * b/289284907.
+ *
+ * During initialization, setting the graphLoop will disableCaptureProcessing until after the
+ * required number of frames have been completed.
+ */
+internal class CaptureLimiter(private val requestsUntilActive: Long) :
+ Request.Listener, GraphLoop.Listener {
+ init {
+ require(requestsUntilActive > 0)
+ }
+
+ private val frameCount = atomic(0L)
+ private var _graphLoop: GraphLoop? = null
+ var graphLoop: GraphLoop
+ get() = _graphLoop!!
+ set(value) {
+ check(_graphLoop == null) { "GraphLoop has already been set!" }
+ _graphLoop = value
+ value.captureProcessingEnabled = false
+ Log.warn {
+ "Capture processing has been disabled for $value until $requestsUntilActive " +
+ "frames have been completed."
+ }
+ }
+
+ override fun onComplete(
+ requestMetadata: RequestMetadata,
+ frameNumber: FrameNumber,
+ result: FrameInfo
+ ) {
+ val count = frameCount.updateAndGet { if (it == -1L) -1 else it + 1 }
+ if (count == requestsUntilActive) {
+ Log.warn { "Capture processing is now enabled for $_graphLoop after $count frames." }
+ graphLoop.captureProcessingEnabled = true
+ }
+ }
+
+ override fun onStopRepeating() {
+ // Ignored
+ }
+
+ override fun onGraphStopped() {
+ // If the cameraGraph is stopped, reset the counter
+ frameCount.update { if (it == -1L) -1 else 0 }
+ graphLoop.captureProcessingEnabled = false
+ Log.warn {
+ "Capture processing has been disabled for $graphLoop until $requestsUntilActive " +
+ "frames have been completed."
+ }
+ }
+
+ override fun onGraphShutdown() {
+ frameCount.value = -1
+ graphLoop.captureProcessingEnabled = false
+ }
+}
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 841683ec..177ce94 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
@@ -186,7 +186,7 @@
): Deferred<Result3A> {
// If the GraphProcessor does not have a repeating request we should update the current
// parameters, but should not invalidate or trigger set a new listener.
- if (!graphProcessor.hasRepeatingRequest()) {
+ if (graphProcessor.repeatingRequest == null) {
graphState3A.update(
aeMode,
afMode,
@@ -222,7 +222,7 @@
return result
}
- suspend fun submit3A(
+ fun submit3A(
aeMode: AeMode? = null,
afMode: AfMode? = null,
awbMode: AwbMode? = null,
@@ -231,9 +231,10 @@
awbRegions: List<MeteringRectangle>? = null
): Deferred<Result3A> {
// If the GraphProcessor does not have a repeating request, we should fail immediately.
- if (!graphProcessor.hasRepeatingRequest()) {
+ if (graphProcessor.repeatingRequest == null) {
return deferredResult3ASubmitFailed
}
+
// Add the listener to a global pool of 3A listeners to monitor the state change to the
// desired one.
val listener = createListenerFor3AParams(aeMode, afMode, awbMode)
@@ -247,7 +248,7 @@
afRegions?.let { extra3AParams.put(CaptureRequest.CONTROL_AF_REGIONS, it.toTypedArray()) }
awbRegions?.let { extra3AParams.put(CaptureRequest.CONTROL_AWB_REGIONS, it.toTypedArray()) }
- if (!graphProcessor.trySubmit(extra3AParams)) {
+ if (!graphProcessor.submit(extra3AParams)) {
graphListener3A.removeListener(listener)
return deferredResult3ASubmitFailed
}
@@ -308,7 +309,7 @@
// If the GraphProcessor does not have a repeating request we should update the current
// parameters, but should not invalidate or trigger set a new listener.
- if (!graphProcessor.hasRepeatingRequest()) {
+ if (graphProcessor.repeatingRequest == null) {
return deferredResult3ASubmitFailed
}
@@ -316,7 +317,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.trySubmit(parameterForAfTriggerCancel)) {
+ if (!graphProcessor.submit(parameterForAfTriggerCancel)) {
return deferredResult3ASubmitFailed
}
}
@@ -394,7 +395,7 @@
* There are two requests involved in this operation, (a) a single request with af trigger =
* cancel, to unlock af, and then (a) a repeating request to unlock ae, awb.
*/
- suspend fun unlock3A(
+ fun unlock3A(
ae: Boolean? = null,
af: Boolean? = null,
awb: Boolean? = null,
@@ -410,17 +411,14 @@
return CompletableDeferred(Result3A(Status.OK, /* frameMetadata= */ null))
}
// If the GraphProcessor does not have a repeating request, we should fail immediately.
- if (!graphProcessor.hasRepeatingRequest()) {
+ if (graphProcessor.repeatingRequest == null) {
return deferredResult3ASubmitFailed
}
// If we explicitly need to unlock af first before proceeding to lock it, we need to send
// 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.trySubmit(parameterForAfTriggerCancel)) {
- debug { "unlock3A - request to unlock af failed, returning early." }
- return deferredResult3ASubmitFailed
- }
+ graphProcessor.submit(parameterForAfTriggerCancel)
}
// As needed unlock ae, awb and wait for ae, af and awb to converge.
@@ -463,7 +461,7 @@
* which the locks were applied or the frame number at which the method returned early because
* either frame limit or time limit was reached.
*/
- suspend fun lock3AForCapture(
+ fun lock3AForCapture(
lockedCondition: ((FrameMetadata) -> Boolean)? = null,
frameLimit: Int = DEFAULT_FRAME_LIMIT,
timeLimitNs: Long = DEFAULT_TIME_LIMIT_NS,
@@ -496,7 +494,7 @@
* which the locks were applied or the frame number at which the method returned early because
* either frame limit or time limit was reached.
*/
- suspend fun lock3AForCapture(
+ fun lock3AForCapture(
triggerAf: Boolean = true,
waitForAwb: Boolean = false,
frameLimit: Int = DEFAULT_FRAME_LIMIT,
@@ -538,14 +536,14 @@
* which the locks were applied or the frame number at which the method returned early because
* either frame limit or time limit was reached.
*/
- private suspend fun lock3AForCapture(
+ private fun lock3AForCapture(
triggerCondition: Map<CaptureRequest.Key<*>, Any>? = null,
lockedCondition: ((FrameMetadata) -> Boolean)? = null,
frameLimit: Int = DEFAULT_FRAME_LIMIT,
timeLimitNs: Long = DEFAULT_TIME_LIMIT_NS,
): Deferred<Result3A> {
// If the GraphProcessor does not have a repeating request, we should fail immediately.
- if (!graphProcessor.hasRepeatingRequest()) {
+ if (graphProcessor.repeatingRequest == null) {
return deferredResult3ASubmitFailed
}
@@ -569,24 +567,19 @@
)
graphListener3A.addListener(listener)
-
debug { "lock3AForCapture - sending a request to trigger ae precapture metering and af." }
- if (!graphProcessor.trySubmit(finalTriggerCondition)) {
- debug {
- "lock3AForCapture - request to trigger ae precapture metering and af failed, " +
- "returning early."
- }
+
+ if (!graphProcessor.submit(finalTriggerCondition)) {
graphListener3A.removeListener(listener)
return deferredResult3ASubmitFailed
}
-
graphProcessor.invalidate()
return listener.result
}
- suspend fun unlock3APostCapture(cancelAf: Boolean = true): Deferred<Result3A> {
+ fun unlock3APostCapture(cancelAf: Boolean = true): Deferred<Result3A> {
// If the GraphProcessor does not have a repeating request, we should fail immediately.
- if (!graphProcessor.hasRepeatingRequest()) {
+ if (graphProcessor.repeatingRequest == null) {
return deferredResult3ASubmitFailed
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
@@ -601,22 +594,15 @@
* REF :
* https://developer.android.com/reference/android/hardware/camera2/CaptureRequest#CONTROL_AE_PRECAPTURE_TRIGGER
*/
- private suspend fun unlock3APostCaptureAndroidLAndBelow(
- cancelAf: Boolean = true
- ): Deferred<Result3A> {
+ private fun unlock3APostCaptureAndroidLAndBelow(cancelAf: Boolean = true): Deferred<Result3A> {
debug { "unlock3AForCapture - sending a request to cancel af and turn on ae." }
- if (
- !graphProcessor.trySubmit(
- if (cancelAf) {
- unlock3APostCaptureLockAeAndCancelAfParams
- } else {
- unlock3APostCaptureLockAeParams
- }
- )
- ) {
- debug { "unlock3AForCapture - request to cancel af and lock ae as failed." }
- return deferredResult3ASubmitFailed
- }
+ val cancelParams =
+ if (cancelAf) {
+ unlock3APostCaptureLockAeAndCancelAfParams
+ } else {
+ unlock3APostCaptureLockAeParams
+ }
+ if (!graphProcessor.submit(cancelParams)) return deferredResult3ASubmitFailed
// Listener to monitor when we receive the capture result corresponding to the request
// below.
@@ -624,8 +610,7 @@
graphListener3A.addListener(listener)
debug { "unlock3AForCapture - sending a request to turn off ae." }
- if (!graphProcessor.trySubmit(unlock3APostCaptureUnlockAeParams)) {
- debug { "unlock3AForCapture - request to unlock ae failed." }
+ if (!graphProcessor.submit(unlock3APostCaptureUnlockAeParams)) {
graphListener3A.removeListener(listener)
return deferredResult3ASubmitFailed
}
@@ -639,16 +624,10 @@
* https://developer.android.com/reference/android/hardware/camera2/CaptureRequest#CONTROL_AE_PRECAPTURE_TRIGGER
*/
@RequiresApi(23)
- private suspend fun unlock3APostCaptureAndroidMAndAbove(
- cancelAf: Boolean = true
- ): Deferred<Result3A> {
+ private fun unlock3APostCaptureAndroidMAndAbove(cancelAf: Boolean = true): Deferred<Result3A> {
debug { "unlock3APostCapture - sending a request to reset af and ae precapture metering." }
val cancelParams = if (cancelAf) aePrecaptureAndAfCancelParams else aePrecaptureCancelParams
- if (!graphProcessor.trySubmit(cancelParams)) {
- debug {
- "unlock3APostCapture - request to reset af and ae precapture metering failed, " +
- "returning early."
- }
+ if (!graphProcessor.submit(cancelParams)) {
return deferredResult3ASubmitFailed
}
@@ -677,11 +656,7 @@
return update3A(aeMode = desiredAeMode, flashMode = flashMode)
}
- internal fun onStopRepeating() {
- graphListener3A.onStopRepeating()
- }
-
- private suspend fun lock3ANow(
+ private fun lock3ANow(
aeLockBehavior: Lock3ABehavior?,
afLockBehavior: Lock3ABehavior?,
awbLockBehavior: Lock3ABehavior?,
@@ -724,7 +699,9 @@
}
debug { "lock3A - submitting a request to lock af." }
- val submitSuccess = graphProcessor.trySubmit(parameterForAfTriggerStart)
+ if (!graphProcessor.submit(parameterForAfTriggerStart)) {
+ return deferredResult3ASubmitFailed
+ }
lastAeMode?.let {
graphState3A.update(aeMode = it)
@@ -733,14 +710,6 @@
// w.r.t. building the parameter snapshot
graphProcessor.invalidate()
}
-
- if (!submitSuccess) {
- // TODO(sushilnath@): Change the error code to a more specific code so it's clear
- // that one of the request in sequence of requests failed and the caller should
- // unlock 3A to bring the 3A system to an initial state and then try again if they
- // want to. The other option is to reset or restore the 3A state here.
- return deferredResult3ASubmitFailed
- }
return resultForLocked!!
}
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/GraphLoop.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/GraphLoop.kt
index 7811490..941a3ec 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/GraphLoop.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/GraphLoop.kt
@@ -17,6 +17,7 @@
package androidx.camera.camera2.pipe.graph
import androidx.annotation.GuardedBy
+import androidx.camera.camera2.pipe.CameraGraphId
import androidx.camera.camera2.pipe.Request
import androidx.camera.camera2.pipe.core.Debug
import androidx.camera.camera2.pipe.core.Log
@@ -24,6 +25,7 @@
import androidx.camera.camera2.pipe.core.ProcessingQueue.Companion.processIn
import androidx.camera.camera2.pipe.putAllMetadata
import java.io.Closeable
+import kotlinx.atomicfu.atomic
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.CoroutineScope
@@ -36,12 +38,22 @@
* cleanup of pending requests during shutdown.
*/
internal class GraphLoop(
- private val defaultParameters: Map<Any, Any?>,
- private val requiredParameters: Map<Any, Any?>,
- private val listeners: List<Request.Listener>,
+ private val cameraGraphId: CameraGraphId,
+ private val defaultParameters: Map<*, Any?>,
+ private val requiredParameters: Map<*, Any?>,
+ private val graphListeners: List<Request.Listener>,
private val graphState3A: GraphState3A?,
+ private val listeners: List<GraphLoop.Listener>,
dispatcher: CoroutineDispatcher
) : Closeable {
+ internal interface Listener {
+ fun onStopRepeating()
+
+ fun onGraphStopped()
+
+ fun onGraphShutdown()
+ }
+
private val lock = Any()
private val graphProcessorScope =
CoroutineScope(dispatcher.plus(CoroutineName("CXCP-GraphLoop")))
@@ -57,71 +69,108 @@
var requestProcessor: GraphRequestProcessor?
get() = synchronized(lock) { _requestProcessor }
- set(value) =
+ set(value) {
synchronized(lock) {
- check(!closed)
val previous = _requestProcessor
_requestProcessor = value
- // Ignore duplicate calls to set with the same value.
- if (previous !== value) {
- if (previous != null) {
- // Closing the request processor can (sometimes) block the calling thread.
- // Make
- // sure this is invoked in the background.
- processingQueue.tryEmit(CloseRequestProcessor(previous))
- }
+ check(value == null || !closed) {
+ "Cannot set requestProcessor after $this is closed."
+ }
- if (value != null) {
- val repeatingRequest = _repeatingRequest
- if (repeatingRequest == null) {
- // This handles the case where a single request has been issued before
- // the GraphRequestProcessor was configured when there is not repeating
- // request.
- processingQueue.tryEmit(Invalidate)
- } else {
- // If there is an active repeating request, make sure the request is
- // issued to the new request processor.
- processingQueue.tryEmit(StartRepeating(repeatingRequest))
- }
+ // Ignore duplicate calls to set with the same value.
+ if (previous === value) {
+ return@synchronized
+ }
+
+ if (previous != null) {
+ // Closing the request processor can (sometimes) block the calling thread.
+ // Make sure this is invoked in the background.
+ processingQueue.tryEmit(CloseRequestProcessor(previous))
+ }
+
+ if (value != null) {
+ val repeatingRequest = _repeatingRequest
+ if (repeatingRequest == null) {
+ // This handles the case where a single request has been issued before
+ // the GraphRequestProcessor was configured when there is not repeating
+ // request. Invalidate will cause the commandLoop to re-evaluate, which
+ // may succeed now that a valid request processor has been configured.
+ processingQueue.tryEmit(Invalidate)
+ } else {
+ // If there is an active repeating request, make sure the request is
+ // issued to the new request processor. This serves the same purpose as
+ // Invalidate which re-triggers the commandLoop.
+ processingQueue.tryEmit(StartRepeating(repeatingRequest))
}
}
}
+ if (value == null) {
+ for (i in listeners.indices) {
+ listeners[i].onGraphStopped()
+ }
+ }
+ }
+
var repeatingRequest: Request?
get() = synchronized(lock) { _repeatingRequest }
- set(value) =
+ set(value) {
synchronized(lock) {
val previous = _repeatingRequest
_repeatingRequest = value
- // Ignore duplicate calls to set with the same value.
- if (previous !== value) {
- if (value != null) {
- processingQueue.tryEmit(StartRepeating(value))
- } else {
- // If the repeating request is set to null, stop repeating (using the
- // current request processor instance), this is allowed because stop and
- // abort can be called on a requestProcessor that has, or is in the
- // process, of being released.
- processingQueue.tryEmit(StopRepeating(_requestProcessor))
- }
+ // Ignore duplicate calls to set null, this avoids multiple stopRepeating calls from
+ // being invoked.
+ if (previous == null && value == null) {
+ return@synchronized
+ }
+
+ if (value != null) {
+ processingQueue.tryEmit(StartRepeating(value))
+ } else {
+ // If the repeating request is set to null, stop repeating (using the current
+ // request processor instance). This is allowed because stop and abort can be
+ // called on a requestProcessor that has, or is in the process, of being
+ // released.
+ processingQueue.tryEmit(StopRepeating(_requestProcessor))
}
}
- fun submit(requests: List<Request>) {
+ if (value == null) {
+ for (i in listeners.indices) {
+ listeners[i].onStopRepeating()
+ }
+ }
+ }
+
+ private val _captureProcessingEnabled = atomic(true)
+ var captureProcessingEnabled: Boolean
+ get() = _captureProcessingEnabled.value
+ set(value) {
+ _captureProcessingEnabled.value = value
+ if (value) {
+ invalidate()
+ }
+ }
+
+ fun submit(request: Request): Boolean = submit(listOf(request))
+
+ fun submit(requests: List<Request>): Boolean {
if (!processingQueue.tryEmit(SubmitCapture(requests))) {
abortRequests(requests)
+ return false
}
+ return true
}
- fun submit(parameters: Map<Any, Any?>) {
+ fun submit(parameters: Map<*, Any?>): Boolean {
synchronized(lock) {
val currentRepeatingRequest = _repeatingRequest
check(currentRepeatingRequest != null) {
"Cannot submit parameters without an active repeating request!"
}
- processingQueue.tryEmit(SubmitParameters(currentRepeatingRequest, parameters))
+ return processingQueue.tryEmit(SubmitParameters(currentRepeatingRequest, parameters))
}
}
@@ -155,6 +204,10 @@
// close the request processor before cancelling the scope.
processingQueue.tryEmit(Shutdown(previousRequestProcessor))
}
+
+ for (i in listeners.indices) {
+ listeners[i].onGraphShutdown()
+ }
}
/**
@@ -165,12 +218,12 @@
// Internal listeners
for (rIdx in requests.indices) {
val request = requests[rIdx]
- for (listenerIdx in listeners.indices) {
- listeners[listenerIdx].onAborted(request)
+ for (listenerIdx in graphListeners.indices) {
+ graphListeners[listenerIdx].onAborted(request)
}
}
- // Request listeners
+ // Request-specific listeners
for (rIdx in requests.indices) {
val request = requests[rIdx]
for (listenerIdx in request.listeners.indices) {
@@ -191,7 +244,7 @@
private var lastRepeatingRequest: Request? = null
- private fun commandLoop(commands: MutableList<GraphCommand>) {
+ private suspend fun commandLoop(commands: MutableList<GraphCommand>) {
// Command Loop Design:
//
// 1. Iterate through commands, newest first.
@@ -257,19 +310,22 @@
// If the request processor is not null, shut it down. Consider making this a
// suspending call instead of just blocking to allow suspend-with-timeout.
- command.requestProcessor?.close()
+ command.requestProcessor?.shutdown()
- // Cancel the scope.
+ // Cancel the scope. This will trigger the onUnprocessedItems callback after after
+ // this hits the next suspension point.
graphProcessorScope.cancel()
}
is CloseRequestProcessor -> {
commands.removeAt(idx)
- // TODO: Consider making this a suspending call. This would allow things like
- // "await repeating request" to suspend-with-timeout instead of blocking.
- command.requestProcessor.close()
+ command.requestProcessor.shutdown()
}
is AbortCaptures -> {
commands.removeAt(idx)
+
+ // Attempt to abort captures in the approximate order they were submitted:
+ // 1. Abort captures submitted to the camera
+ // 2. Invoke abort on captures that have not yet been submitted to the camera.
if (command.requestProcessor != null) {
command.requestProcessor.abortCaptures()
}
@@ -315,6 +371,13 @@
commands.removeUpTo(idx) { it is StartRepeating }
}
is SubmitCapture -> {
+ if (!_captureProcessingEnabled.value) {
+ Log.warn {
+ "Skipping SubmitCapture because capture processing is paused: " +
+ "${command.requests}"
+ }
+ return
+ }
val success =
requestProcessor?.buildAndSubmit(
isRepeating = false,
@@ -330,6 +393,14 @@
}
}
is SubmitParameters -> {
+ if (!_captureProcessingEnabled.value) {
+ Log.warn {
+ "Skipping SubmitParameters because capture processing is paused: " +
+ "${command.parameters}"
+ }
+ return
+ }
+
val success =
requestProcessor?.buildAndSubmit(
isRepeating = false,
@@ -368,7 +439,7 @@
private fun GraphRequestProcessor.buildAndSubmit(
isRepeating: Boolean,
requests: List<Request>,
- parameters: Map<Any, Any?> = emptyMap()
+ parameters: Map<*, Any?> = emptyMap<Any, Any?>()
): Boolean {
val graphRequiredParameters = buildMap {
// Build the required parameter map:
@@ -384,10 +455,12 @@
requests = requests,
defaultParameters = defaultParameters,
requiredParameters = graphRequiredParameters,
- listeners = listeners
+ listeners = graphListeners
)
}
+ override fun toString(): String = "GraphLoop($cameraGraphId)"
+
private sealed class GraphCommand
private object Invalidate : GraphCommand()
@@ -405,6 +478,6 @@
private class SubmitCapture(val requests: List<Request>) : GraphCommand()
- private class SubmitParameters(val request: Request, val parameters: Map<Any, Any?>) :
+ private class SubmitParameters(val request: Request, val parameters: Map<*, Any?>) :
GraphCommand()
}
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 42035e2..45f5d76 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
@@ -15,8 +15,6 @@
*/
package androidx.camera.camera2.pipe.graph
-import android.os.Build
-import androidx.annotation.GuardedBy
import androidx.camera.camera2.pipe.CameraGraph
import androidx.camera.camera2.pipe.CameraGraphId
import androidx.camera.camera2.pipe.CaptureSequenceProcessor
@@ -34,25 +32,14 @@
import androidx.camera.camera2.pipe.compat.CameraPipeKeys
import androidx.camera.camera2.pipe.config.CameraGraphScope
import androidx.camera.camera2.pipe.config.ForCameraGraph
-import androidx.camera.camera2.pipe.core.CoroutineMutex
-import androidx.camera.camera2.pipe.core.Debug
import androidx.camera.camera2.pipe.core.Log.debug
import androidx.camera.camera2.pipe.core.Log.info
-import androidx.camera.camera2.pipe.core.Log.warn
import androidx.camera.camera2.pipe.core.Threads
-import androidx.camera.camera2.pipe.core.withLockLaunch
-import androidx.camera.camera2.pipe.formatForLogs
-import androidx.camera.camera2.pipe.putAllMetadata
import java.util.concurrent.CountDownLatch
-import java.util.concurrent.TimeUnit
import javax.inject.Inject
-import kotlinx.coroutines.CompletableDeferred
-import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.withContext
/**
* The [GraphProcessor] is responsible for queuing and then submitting them to a
@@ -62,18 +49,20 @@
internal interface GraphProcessor {
val graphState: StateFlow<GraphState>
- fun submit(request: Request)
+ /**
+ * The currently configured repeating request. Setting this value to null will attempt to call
+ * stopRepeating on the Camera.
+ */
+ var repeatingRequest: Request?
- fun submit(requests: List<Request>)
+ fun submit(request: Request): Boolean
+
+ fun submit(requests: List<Request>): 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, but we do have a pending repeating request
- * queued, the method will suspend until we have a submitted repeating request and only then
- * submits the parameters.
+ * This tries to submit a list of parameters based on the current repeating request. If the
+ * CameraGraph hasn't been started but a valid repeating request has already been set this
+ * method will enqueue the submission based on the repeating request.
*
* This behavior is required if users call 3A methods immediately after start. For example:
* ```
@@ -88,21 +77,9 @@
* implementation handles this on a best-effort basis for the developer. Please read b/263211462
* for more context.
*
- * However, if the CameraGraph does NOT have a current repeating request or any repeating
- * requests queued up, the method will return false.
+ * This method will throw a checked exception if no repeating request has been configured.
*/
- suspend fun trySubmit(parameters: Map<*, Any?>): Boolean
-
- fun startRepeating(request: Request)
-
- fun stopRepeating()
-
- /**
- * Checks whether we have a repeating request in progress. Returns true when we have a repeating
- * request already submitted or is being submitted. This is used to check whether we can try to
- * submit parameters (used by 3A methods).
- */
- fun hasRepeatingRequest(): Boolean
+ fun submit(parameters: Map<*, Any?>): Boolean
/**
* Indicates that internal parameters may have changed, and that the repeating request should be
@@ -128,29 +105,55 @@
internal class GraphProcessorImpl
@Inject
constructor(
- private val threads: Threads,
+ threads: Threads,
private val cameraGraphId: CameraGraphId,
private val cameraGraphConfig: CameraGraph.Config,
- private val graphState3A: GraphState3A,
- @ForCameraGraph private val graphScope: CoroutineScope,
- @ForCameraGraph private val graphListeners: List<@JvmSuppressWildcards Request.Listener>
+ graphState3A: GraphState3A,
+ graphListener3A: Listener3A,
+ @ForCameraGraph graphListeners: List<@JvmSuppressWildcards Request.Listener>
) : GraphProcessor, GraphListener {
- private val lock = Any()
- private val tryStartRepeatingExecutionLock = Any()
- private val coroutineMutex = CoroutineMutex()
- @GuardedBy("lock") private val submitQueue: MutableList<List<Request>> = ArrayList()
- @GuardedBy("lock") private val repeatingQueue: MutableList<Request> = ArrayList()
- @GuardedBy("lock") private var currentRepeatingRequest: Request? = null
- @GuardedBy("lock") private var _requestProcessor: GraphRequestProcessor? = null
- @GuardedBy("lock") private var submitting = false
- @GuardedBy("lock") private var dirty = false
- @GuardedBy("lock") private var closed = false
- @GuardedBy("lock") private var pendingParameters: Map<*, Any?>? = null
- @GuardedBy("lock") private var pendingParametersDeferred: CompletableDeferred<Boolean>? = null
+ private val graphLoop: GraphLoop
+
+ init {
+ val defaultParameters = cameraGraphConfig.defaultParameters
+ val requiredParameters = cameraGraphConfig.requiredParameters
+ val ignore3AState =
+ (defaultParameters[CameraPipeKeys.ignore3ARequiredParameters] == true) ||
+ (requiredParameters[CameraPipeKeys.ignore3ARequiredParameters] == true)
+
+ if (ignore3AState) {
+ info {
+ "${CameraPipeKeys.ignore3ARequiredParameters} is set to true, " +
+ "ignoring GraphState3A parameters."
+ }
+ }
+
+ val captureLimiter =
+ if (Camera2Quirks.shouldWaitForRepeatingBeforeCapture()) {
+ CaptureLimiter(10)
+ } else {
+ null
+ }
+
+ graphLoop =
+ GraphLoop(
+ cameraGraphId = cameraGraphId,
+ defaultParameters = defaultParameters,
+ requiredParameters = requiredParameters,
+ graphListeners = graphListeners + listOfNotNull(captureLimiter),
+ graphState3A = if (ignore3AState) null else graphState3A,
+ listeners = listOfNotNull(graphListener3A, captureLimiter),
+ dispatcher = threads.lightweightDispatcher
+ )
+
+ captureLimiter?.graphLoop = graphLoop
+ }
+
// On some devices, we need to wait for 10 frames to complete before we can guarantee the
// success of single capture requests. This is a quirk identified as part of b/287020251 and
// reported in b/289284907.
private var repeatingRequestsCompleted = CountDownLatch(10)
+
// Graph listener added to repeating requests in order to handle the aforementioned quirk.
private val graphProcessorRepeatingListeners =
if (!Camera2Quirks.shouldWaitForRepeatingBeforeCapture()) {
@@ -171,6 +174,12 @@
override val graphState: StateFlow<GraphState>
get() = _graphState
+ override var repeatingRequest: Request?
+ get() = graphLoop.repeatingRequest
+ set(value) {
+ graphLoop.repeatingRequest = value
+ }
+
override fun onGraphStarting() {
debug { "$this onGraphStarting" }
_graphState.value = GraphStateStarting
@@ -179,22 +188,7 @@
override fun onGraphStarted(requestProcessor: GraphRequestProcessor) {
debug { "$this onGraphStarted" }
_graphState.value = GraphStateStarted
- var old: GraphRequestProcessor? = null
- synchronized(lock) {
- if (closed) {
- requestProcessor.close()
- return
- }
- if (_requestProcessor != null && _requestProcessor !== requestProcessor) {
- old = _requestProcessor
- }
- _requestProcessor = requestProcessor
- }
- val processorToClose = old
- if (processorToClose != null) {
- synchronized(processorToClose) { processorToClose.close() }
- }
- resubmit()
+ graphLoop.requestProcessor = requestProcessor
}
override fun onGraphStopping() {
@@ -205,39 +199,12 @@
override fun onGraphStopped(requestProcessor: GraphRequestProcessor?) {
debug { "$this onGraphStopped" }
_graphState.value = GraphStateStopped
- if (requestProcessor == null) return
- var old: GraphRequestProcessor? = null
- synchronized(lock) {
- if (closed) {
- return
- }
- if (requestProcessor === _requestProcessor) {
- old = _requestProcessor
- _requestProcessor = null
- } else {
- warn {
- "Refusing to detach $requestProcessor. " +
- "It is different from $_requestProcessor"
- }
- }
- }
- val processorToClose = old
- if (processorToClose != null) {
- synchronized(processorToClose) { processorToClose.close() }
- }
+ graphLoop.requestProcessor = null
}
override fun onGraphModified(requestProcessor: GraphRequestProcessor) {
debug { "$this onGraphModified" }
- synchronized(lock) {
- if (closed) {
- return
- }
- if (requestProcessor !== _requestProcessor) {
- return
- }
- }
- resubmit()
+ graphLoop.invalidate()
}
override fun onGraphError(graphStateError: GraphStateError) {
@@ -251,57 +218,19 @@
}
}
- override fun startRepeating(request: Request) {
- synchronized(lock) {
- if (closed) return
- currentRepeatingRequest = request
- repeatingQueue.add(request)
- debug { "startRepeating with ${request.formatForLogs()}" }
- coroutineMutex.withLockLaunch(graphScope) { tryStartRepeating() }
- }
- }
+ override fun submit(request: Request): Boolean = submit(listOf(request))
- override fun stopRepeating() {
- val processor: GraphRequestProcessor?
- synchronized(lock) {
- processor = _requestProcessor
- repeatingQueue.clear()
- currentRepeatingRequest = null
- coroutineMutex.withLockLaunch(graphScope) {
- Debug.traceStart { "$this#stopRepeating" }
- // Start with requests that have already been submitted
- if (processor != null) {
- synchronized(processor) { processor.stopRepeating() }
- }
- Debug.traceStop()
+ override fun submit(requests: List<Request>): Boolean {
+ val reprocessingRequest = requests.firstOrNull { it.inputRequest != null }
+ if (reprocessingRequest != null) {
+ checkNotNull(cameraGraphConfig.input) {
+ "Cannot submit $reprocessingRequest with input request " +
+ "${reprocessingRequest.inputRequest} to $this because CameraGraph was not " +
+ "configured to support reprocessing"
}
}
- }
- override fun submit(request: Request) {
- submit(listOf(request))
- }
-
- override fun submit(requests: List<Request>) {
- requests
- .firstOrNull { it.inputRequest != null }
- ?.let {
- check(Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
- "Reprocessing not supported on Android ${Build.VERSION.SDK_INT} devices"
- }
- checkNotNull(cameraGraphConfig.input) {
- "Cannot submit request $it with input request ${it.inputRequest} " +
- "to $this because CameraGraph was not configured to support reprocessing"
- }
- }
- synchronized(lock) {
- if (closed) {
- graphScope.launch(threads.lightweightDispatcher) { abortBurst(requests) }
- return
- }
- submitQueue.add(requests)
- }
- graphScope.launch(threads.lightweightDispatcher) { submitLoop() }
+ return graphLoop.submit(requests)
}
/**
@@ -310,285 +239,18 @@
* false. Otherwise, the method tries to submit the provided [parameters] and suspends until it
* finishes.
*/
- 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
- processor = _requestProcessor
- // If there is no current repeating request and no repeating requests are in the
- // queue (i.e., startRepeating wasn't called before the 3A methods), we should just
- // fail immediately.
- if (currentRepeatingRequest == null) {
- return@withContext false
- }
- request = currentRepeatingRequest as Request
- requiredParameters.putAllMetadata(parameters.toMutableMap())
- graphState3A.writeTo(requiredParameters)
- requiredParameters.putAllMetadata(cameraGraphConfig.requiredParameters)
- if (processor == 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 -> deferredResult?.await() == true
- else ->
- processor.submit(
- isRepeating = false,
- requests = listOf(request),
- defaultParameters = cameraGraphConfig.defaultParameters,
- requiredParameters = requiredParameters,
- listeners = graphListeners
- )
- }
- }
-
- override fun hasRepeatingRequest() = synchronized(lock) { currentRepeatingRequest != null }
+ override fun submit(parameters: Map<*, Any?>): Boolean = graphLoop.submit(parameters)
override fun invalidate() {
- // Invalidate is only used for updates to internal state (listeners, parameters, etc) and
- // should not (currently) attempt to resubmit the normal request queue.
- graphScope.launch(threads.lightweightDispatcher) { tryStartRepeating() }
+ graphLoop.invalidate()
}
override fun abort() {
- val processor: GraphRequestProcessor?
- val requests: List<List<Request>>
- synchronized(lock) {
- processor = _requestProcessor
- requests = submitQueue.toList()
- submitQueue.clear()
- }
- graphScope.launch(threads.lightweightDispatcher) {
- Debug.traceStart { "$this#abort" }
- // Start with requests that have already been submitted
- if (processor != null) {
- synchronized(processor) { processor.abortCaptures() }
- }
- // Then abort requests that have not been submitted
- for (burst in requests) {
- abortBurst(burst)
- }
- Debug.traceStop()
- }
+ graphLoop.abort()
}
override fun close() {
- val processor: GraphRequestProcessor?
- synchronized(lock) {
- if (closed) {
- return
- }
- closed = true
- processor = _requestProcessor
- _requestProcessor = null
- }
- processor?.close()
- abort()
- }
-
- private fun resubmit() {
- graphScope.launch(threads.lightweightDispatcher) {
- tryStartRepeating()
- submitLoop()
- }
- }
-
- private fun abortBurst(requests: List<Request>) {
- for (request in requests) {
- abortRequest(request)
- }
- }
-
- private fun abortRequest(request: Request) {
- for (listenerIdx in graphListeners.indices) {
- graphListeners[listenerIdx].onAborted(request)
- }
- for (listenerIdx in request.listeners.indices) {
- request.listeners[listenerIdx].onAborted(request)
- }
- }
-
- private fun tryStartRepeating() =
- synchronized(tryStartRepeatingExecutionLock) {
- val processor: GraphRequestProcessor
- val requests = mutableListOf<Request>()
- var shouldRetryRequests = false
- synchronized(lock) {
- if (closed || _requestProcessor == null) return
- processor = _requestProcessor!!
- if (repeatingQueue.isNotEmpty()) {
- requests.addAll(repeatingQueue)
- repeatingQueue.clear()
- shouldRetryRequests = true
- } else {
- currentRepeatingRequest?.let { requests.add(it) }
- }
- }
- if (requests.isEmpty()) return
- Debug.traceStart { "$this#startRepeating" }
- var succeededIndex = -1
- synchronized(processor) {
- // Here an important optimization is applied. Newer repeating requests should always
- // supersede older ones. Instead of going from oldest request to newest, we can
- // start
- // from the newest request and immediately break when a request submission succeeds.
- for ((index, request) in requests.reversed().withIndex()) {
- val requiredParameters = mutableMapOf<Any, Any?>()
- graphState3A.writeTo(requiredParameters)
- requiredParameters.putAllMetadata(cameraGraphConfig.requiredParameters)
- if (
- processor.submit(
- isRepeating = true,
- requests = listOf(request),
- defaultParameters = cameraGraphConfig.defaultParameters,
- requiredParameters = requiredParameters,
- listeners = graphProcessorRepeatingListeners,
- )
- ) {
- // ONLY update the current repeating request if the update succeeds
- synchronized(lock) {
- if (processor === _requestProcessor) {
- trySubmitPendingParameters(processor, request)
- }
- }
- succeededIndex = index
- break
- }
- }
- }
- Debug.traceStop()
- if (shouldRetryRequests) {
- synchronized(lock) {
- // We should only retry the requests newer than the succeeded request, since the
- // succeeded request would prevail over the preceding requests that failed.
- val requestsToRetry = requests.slice(succeededIndex + 1 until requests.size)
- // We might have new repeating requests at this point, and these requests to
- // retry
- // should be placed in the front in order to preserve FIFO order.
- repeatingQueue.addAll(0, requestsToRetry)
- }
- }
- }
-
- @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() {
- if (Camera2Quirks.shouldWaitForRepeatingBeforeCapture() && hasRepeatingRequest()) {
- debug {
- "Quirk: Waiting for 10 repeating requests to complete before submitting requests"
- }
- if (!repeatingRequestsCompleted.await(2, TimeUnit.SECONDS)) {
- warn { "Failed to wait for 10 repeating requests to complete after 2 seconds" }
- }
- }
- var burst: List<Request>
- var processor: GraphRequestProcessor
- synchronized(lock) {
- if (closed) return
- if (submitting) {
- dirty = true
- return
- }
- val nullableProcessor = _requestProcessor
- val nullableBurst = submitQueue.firstOrNull()
- if (nullableProcessor == null || nullableBurst == null) {
- return
- }
- processor = nullableProcessor
- burst = nullableBurst
- submitting = true
- }
- while (true) {
- var submitted = false
- Debug.traceStart { "$this#submit" }
- try {
- submitted =
- synchronized(processor) {
- val requiredParameters = mutableMapOf<Any, Any?>()
- if (
- cameraGraphConfig.defaultParameters[
- CameraPipeKeys.ignore3ARequiredParameters] == true ||
- cameraGraphConfig.requiredParameters[
- CameraPipeKeys.ignore3ARequiredParameters] == true
- ) {
- info {
- "${CameraPipeKeys.ignore3ARequiredParameters} is set to true, " +
- "ignoring 3A required parameters"
- }
- } else {
- graphState3A.writeTo(requiredParameters)
- }
- requiredParameters.putAllMetadata(cameraGraphConfig.requiredParameters)
- processor.submit(
- isRepeating = false,
- requests = burst,
- defaultParameters = cameraGraphConfig.defaultParameters,
- requiredParameters = requiredParameters,
- listeners = graphListeners
- )
- }
- } finally {
- Debug.traceStop()
- synchronized(lock) {
- if (submitted) {
- // submitQueue can potentially be cleared by abort() before entering here.
- check(submitQueue.isEmpty() || submitQueue.removeAt(0) === burst)
- val nullableBurst = submitQueue.firstOrNull()
- if (nullableBurst == null) {
- dirty = false
- submitting = false
- return
- }
- burst = nullableBurst
- } else if (!dirty) {
- debug { "Failed to submit $burst, and the queue is not dirty." }
- // If we did not submit, and we are also not dirty, then exit the loop
- submitting = false
- return
- } else {
- debug {
- "Failed to submit $burst but the request queue or processor is " +
- "dirty. Clearing dirty flag and attempting retry."
- }
- dirty = false
- // One possible situation is that the _requestProcessor was replaced or
- // set to null. If this happens, try to update the requestProcessor we
- // are currently using. If the current request processor is null, then
- // we cannot submit anyways.
- val nullableProcessor = _requestProcessor
- if (nullableProcessor != null) {
- processor = nullableProcessor
- }
- }
- }
- }
- }
+ graphLoop.close()
}
override fun toString(): String = "GraphProcessor(cameraGraph: $cameraGraphId)"
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/GraphRequestProcessor.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/GraphRequestProcessor.kt
index 0b2c1a3..cf9d3c2 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/GraphRequestProcessor.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/GraphRequestProcessor.kt
@@ -104,10 +104,10 @@
captureSequenceProcessor.stopRepeating()
}
- internal fun close() {
+ internal suspend fun shutdown() {
Log.debug { "Closing $this" }
if (closed.compareAndSet(expect = false, update = true)) {
- captureSequenceProcessor.close()
+ captureSequenceProcessor.shutdown()
}
}
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/Listener3A.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/Listener3A.kt
index 5cb02d3..f3477ffb 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/Listener3A.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/Listener3A.kt
@@ -33,7 +33,7 @@
* for desired 3A state changes.
*/
@CameraGraphScope
-internal class Listener3A @Inject constructor() : Request.Listener {
+internal class Listener3A @Inject constructor() : Request.Listener, GraphLoop.Listener {
private val listeners: CopyOnWriteArrayList<Result3AStateListener> = CopyOnWriteArrayList()
override fun onRequestSequenceCreated(requestMetadata: RequestMetadata) {
@@ -66,12 +66,6 @@
listeners.remove(listener)
}
- internal fun onStopRepeating() {
- for (listener in listeners) {
- listener.onRequestSequenceStopped()
- }
- }
-
private fun updateListeners(requestNumber: RequestNumber, metadata: FrameMetadata) {
for (listener in listeners) {
if (listener.update(requestNumber, metadata)) {
@@ -79,4 +73,22 @@
}
}
}
+
+ override fun onStopRepeating() {
+ for (listener in listeners) {
+ listener.onStopRepeating()
+ }
+ }
+
+ override fun onGraphStopped() {
+ for (listener in listeners) {
+ listener.onStopRepeating()
+ }
+ }
+
+ override fun onGraphShutdown() {
+ for (listener in listeners) {
+ listener.onStopRepeating()
+ }
+ }
}
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/Result3AStateListener.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/Result3AStateListener.kt
index 17aca1f..d338a9e 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/Result3AStateListener.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/Result3AStateListener.kt
@@ -38,12 +38,10 @@
* This update method can be called multiple times as we get newer [CaptureResult]s from the camera
* device. This class also exposes a [Deferred] to query the status of desired state.
*/
-internal interface Result3AStateListener {
+internal interface Result3AStateListener : GraphLoop.Listener {
fun onRequestSequenceCreated(requestNumber: RequestNumber)
fun update(requestNumber: RequestNumber, frameMetadata: FrameMetadata): Boolean
-
- fun onRequestSequenceStopped()
}
internal class Result3AStateListenerImpl(
@@ -133,12 +131,16 @@
return true
}
- override fun onRequestSequenceStopped() {
+ override fun onStopRepeating() {
_result.complete(Result3A(Result3A.Status.SUBMIT_CANCELLED))
}
- fun getDeferredResult(): Deferred<Result3A> {
- return _result
+ override fun onGraphStopped() {
+ _result.complete(Result3A(Result3A.Status.SUBMIT_CANCELLED))
+ }
+
+ override fun onGraphShutdown() {
+ _result.complete(Result3A(Result3A.Status.SUBMIT_CANCELLED))
}
}
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/CameraGraphSessionImplTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/CameraGraphSessionImplTest.kt
index 15db1bd..8c3b7bc 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/CameraGraphSessionImplTest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/CameraGraphSessionImplTest.kt
@@ -57,7 +57,11 @@
private val graphState3A = GraphState3A()
private val listener3A = Listener3A()
private val graphProcessor =
- FakeGraphProcessor(graphState3A = graphState3A, defaultListeners = listOf(listener3A))
+ FakeGraphProcessor(
+ graphState3A = graphState3A,
+ graphListener3A = listener3A,
+ defaultListeners = listOf(listener3A)
+ )
private val fakeCaptureSequenceProcessor = FakeCaptureSequenceProcessor()
private val fakeGraphRequestProcessor = GraphRequestProcessor.from(fakeCaptureSequenceProcessor)
private val controller3A =
@@ -102,12 +106,16 @@
session.startRepeating(Request(streams = listOf(StreamId(1))))
graphProcessor.invalidate()
- val result = session.lock3A(aeLockBehavior = Lock3ABehavior.IMMEDIATE)
+ val deferred = session.lock3A(aeLockBehavior = Lock3ABehavior.IMMEDIATE)
+
+ assertThat(deferred.isCompleted).isFalse()
// Don't return any results to simulate that the 3A conditions haven't been met, but the
// app calls stopRepeating(). In which case, we should fail here with SUBMIT_CANCELLED.
session.stopRepeating()
- assertThat(result.await().status).isEqualTo(Result3A.Status.SUBMIT_CANCELLED)
+ assertThat(deferred.isCompleted).isTrue()
+ val result = deferred.await()
+ assertThat(result.status).isEqualTo(Result3A.Status.SUBMIT_CANCELLED)
}
@Test
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/CaptureLimiterTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/CaptureLimiterTest.kt
new file mode 100644
index 0000000..983c2fd
--- /dev/null
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/CaptureLimiterTest.kt
@@ -0,0 +1,105 @@
+/*
+ * Copyright 2024 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.camera2.pipe.graph
+
+import android.os.Build
+import androidx.camera.camera2.pipe.CameraGraphId
+import androidx.camera.camera2.pipe.FrameNumber
+import androidx.camera.camera2.pipe.testing.FakeFrameInfo
+import androidx.camera.camera2.pipe.testing.FakeRequestMetadata
+import com.google.common.truth.Truth.assertThat
+import kotlin.test.Test
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runTest
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.robolectric.annotation.Config
+
+@RunWith(JUnit4::class)
+@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
+class CaptureLimiterTest {
+ private val testScope = TestScope()
+ private val testDispatcher = StandardTestDispatcher(testScope.testScheduler)
+ private val graphState3A = GraphState3A()
+
+ private val fakeRequestMetadata = FakeRequestMetadata()
+ private val fakeFrameInfo = FakeFrameInfo()
+
+ private val captureLimiter = CaptureLimiter(3)
+ private val cameraGraphId = CameraGraphId.nextId()
+
+ private val graphLoop =
+ GraphLoop(
+ cameraGraphId = cameraGraphId,
+ defaultParameters = emptyMap<Any, Any?>(),
+ requiredParameters = emptyMap<Any, Any?>(),
+ graphListeners = listOf(),
+ graphState3A = graphState3A,
+ listeners = listOf(captureLimiter),
+ dispatcher = testDispatcher,
+ )
+
+ init {
+ captureLimiter.graphLoop = graphLoop
+ }
+
+ @Test
+ fun captureLimiterEnablesCaptureProcessingAfterFrameCount() {
+ assertThat(graphLoop.captureProcessingEnabled).isFalse()
+
+ // ACT
+ simulateFrames(2)
+ assertThat(graphLoop.captureProcessingEnabled).isFalse()
+
+ // ACT
+ simulateFrames(1)
+ assertThat(graphLoop.captureProcessingEnabled).isTrue()
+ }
+
+ @Test
+ fun captureLimiterResetsAfterGraphProcessorIsRemoved() {
+ simulateFrames(3)
+ assertThat(graphLoop.captureProcessingEnabled).isTrue()
+
+ // ACT
+ graphLoop.requestProcessor = null
+
+ assertThat(graphLoop.captureProcessingEnabled).isFalse()
+ simulateFrames(3)
+ assertThat(graphLoop.captureProcessingEnabled).isTrue()
+ }
+
+ @Test
+ fun captureLimiterPermanentlyDisablesAfterClose() =
+ testScope.runTest {
+ simulateFrames(3)
+ assertThat(graphLoop.captureProcessingEnabled).isTrue()
+
+ // ACT
+ graphLoop.close()
+ assertThat(graphLoop.captureProcessingEnabled).isFalse()
+ simulateFrames(3)
+ assertThat(graphLoop.captureProcessingEnabled).isFalse()
+ }
+
+ private fun simulateFrames(count: Long) {
+ for (i in 1L..count) {
+ captureLimiter.onComplete(fakeRequestMetadata, FrameNumber(i), fakeFrameInfo)
+ }
+ }
+}
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3ASubmit3ATest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3ASubmit3ATest.kt
index 3af94c5..3d72cac 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3ASubmit3ATest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3ASubmit3ATest.kt
@@ -34,6 +34,7 @@
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.launch
+import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.runTest
import org.junit.After
import org.junit.Test
@@ -231,9 +232,12 @@
// There are different conditions that can lead to the request processor not being able
// to successfully submit the desired request. For this test we are closing the processor.
graphProcessor.close()
+ advanceUntilIdle()
// Since the request processor is closed the submit3A method call will fail.
- val result = controller3A.submit3A(aeMode = AeMode.ON_ALWAYS_FLASH).await()
+ val deferred = controller3A.submit3A(aeMode = AeMode.ON_ALWAYS_FLASH)
+ assertThat(deferred.isCompleted)
+ val result = deferred.await()
assertThat(result.frameMetadata).isNull()
assertThat(result.status).isEqualTo(Result3A.Status.SUBMIT_FAILED)
}
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3AUpdate3ATest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3AUpdate3ATest.kt
index 0b14071..93f81ba 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3AUpdate3ATest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3AUpdate3ATest.kt
@@ -253,6 +253,6 @@
private fun initGraphProcessor() {
graphProcessor.onGraphStarted(fakeGraphRequestProcessor)
- graphProcessor.startRepeating(Request(streams = listOf(StreamId(1))))
+ graphProcessor.repeatingRequest = Request(streams = listOf(StreamId(1)))
}
}
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/GraphLoopTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/GraphLoopTest.kt
index 419d94e..629f63a 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/GraphLoopTest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/GraphLoopTest.kt
@@ -18,10 +18,12 @@
import android.os.Build
import android.view.Surface
+import androidx.camera.camera2.pipe.CameraGraphId
import androidx.camera.camera2.pipe.CameraId
import androidx.camera.camera2.pipe.CaptureSequence
import androidx.camera.camera2.pipe.CaptureSequenceProcessor
import androidx.camera.camera2.pipe.Request
+import androidx.camera.camera2.pipe.Result3A
import androidx.camera.camera2.pipe.StreamId
import androidx.camera.camera2.pipe.testing.FakeCameraMetadata
import androidx.camera.camera2.pipe.testing.FakeCaptureSequence
@@ -52,6 +54,7 @@
private val testDispatcher = StandardTestDispatcher(testScope.testScheduler)
private val graphState3A = GraphState3A()
+ private val listener3A = Listener3A()
private val defaultParameters = emptyMap<Any, Any?>()
private val requiredParameters = emptyMap<Any, Any?>()
private val mockListener: Request.Listener = mock<Request.Listener>()
@@ -76,13 +79,16 @@
private val request1 = Request(streams = listOf(stream1))
private val request2 = Request(streams = listOf(stream2))
private val request3 = Request(streams = listOf(stream1, stream2))
+ private val cameraGraphId = CameraGraphId.nextId()
private val graphLoop =
GraphLoop(
+ cameraGraphId = cameraGraphId,
defaultParameters = defaultParameters,
requiredParameters = requiredParameters,
- listeners = listOf(mockListener),
+ graphListeners = listOf(mockListener),
graphState3A = graphState3A,
+ listeners = listOf(listener3A),
dispatcher = testDispatcher,
)
@@ -238,7 +244,7 @@
fun changingRequestProcessorsReIssuesCaptureRequests() =
testScope.runTest {
graphLoop.requestProcessor = grp1
- csp1.close() // Reject requests
+ csp1.shutdown() // reject incoming requests
graphLoop.submit(listOf(request1))
graphLoop.submit(listOf(request2))
advanceUntilIdle()
@@ -257,7 +263,7 @@
fun capturesThatFailCanBeRetried() =
testScope.runTest {
graphLoop.requestProcessor = grp1
- csp1.close() // reject incoming requests
+ csp1.shutdown() // reject incoming requests
graphLoop.repeatingRequest = request1
advanceUntilIdle()
@@ -456,10 +462,12 @@
testScope.runTest {
val gl =
GraphLoop(
+ cameraGraphId = cameraGraphId,
defaultParameters = mapOf<Any, Any?>(TEST_KEY to 10),
requiredParameters = requiredParameters,
- listeners = listOf(mockListener),
+ graphListeners = listOf(mockListener),
graphState3A = graphState3A,
+ listeners = listOf(listener3A),
dispatcher = testDispatcher,
)
@@ -491,10 +499,12 @@
testScope.runTest {
val gl =
GraphLoop(
- defaultParameters = emptyMap(),
+ cameraGraphId = cameraGraphId,
+ defaultParameters = emptyMap<Any, Any?>(),
requiredParameters = mapOf<Any, Any?>(TEST_KEY to 10),
- listeners = listOf(mockListener),
+ graphListeners = listOf(mockListener),
graphState3A = graphState3A,
+ listeners = listOf(listener3A),
dispatcher = testDispatcher,
)
@@ -525,7 +535,7 @@
fun requestsSubmittedToClosedRequestProcessorAreEnqueuedToTheNextOne() =
testScope.runTest {
graphLoop.requestProcessor = grp1
- grp1.close()
+ grp1.shutdown()
graphLoop.repeatingRequest = request1
graphLoop.submit(mapOf<Any, Any?>(TEST_KEY to 42))
graphLoop.submit(listOf(request2))
@@ -623,6 +633,153 @@
.contains("Test Exception")
}
+ @Test
+ fun stopRepeatingCancelsTriggers() =
+ testScope.runTest {
+ val listener = Result3AStateListenerImpl({ _ -> true }, 10, 1_000_000_000)
+ listener3A.addListener(listener)
+ assertThat(listener.result.isCompleted).isFalse()
+
+ graphLoop.repeatingRequest = null
+
+ assertThat(listener.result.isCompleted).isTrue()
+ assertThat(listener.result.await().status).isEqualTo(Result3A.Status.SUBMIT_CANCELLED)
+ }
+
+ @Test
+ fun clearingRequestProcessorCancelsTriggers() =
+ testScope.runTest {
+ // Setup the graph loop so that the repeating request and trigger are enqueued before
+ // the graphRequestProcessor is configured. Assert that the listener is not invoked
+ // until after the requestProcessor is stopped.
+ graphLoop.repeatingRequest = request1
+ val listener = Result3AStateListenerImpl({ _ -> true }, 10, 1_000_000_000)
+ listener3A.addListener(listener)
+ graphLoop.submit(mapOf<Any, Any?>(TEST_KEY to 42))
+ graphLoop.requestProcessor = grp1
+ advanceUntilIdle()
+
+ assertThat(listener.result.isCompleted).isFalse()
+
+ graphLoop.requestProcessor = null
+ advanceUntilIdle()
+
+ assertThat(listener.result.isCompleted).isTrue()
+ assertThat(listener.result.await().status).isEqualTo(Result3A.Status.SUBMIT_CANCELLED)
+ }
+
+ @Test
+ fun shutdownRequestProcessorCancelsTriggers() =
+ testScope.runTest {
+ // Arrange
+ val listener = Result3AStateListenerImpl({ _ -> true }, 10, 1_000_000_000)
+ listener3A.addListener(listener)
+
+ // Act
+ graphLoop.requestProcessor = null
+
+ // Assert
+ assertThat(listener.result.isCompleted).isTrue()
+ assertThat(listener.result.await().status).isEqualTo(Result3A.Status.SUBMIT_CANCELLED)
+ }
+
+ @Test
+ fun swappingRequestProcessorsDoesNotCancelTriggers() {
+ testScope.runTest {
+ // Arrange
+
+ // Setup the graph loop so that the repeating request and trigger are enqueued before
+ // the graphRequestProcessor is configured. Assert that the listener is not invoked
+ // until after the requestProcessor is stopped.
+ graphLoop.requestProcessor = grp1
+ val listener = Result3AStateListenerImpl({ _ -> true }, 10, 1_000_000_000)
+ listener3A.addListener(listener)
+ graphLoop.requestProcessor = grp2 // Does not cancel trigger
+ advanceUntilIdle()
+ assertThat(listener.result.isCompleted).isFalse()
+
+ // Act
+ graphLoop.requestProcessor = null // Cancel triggers
+
+ // Assert
+ assertThat(listener.result.isCompleted).isTrue()
+ assertThat(listener.result.await().status).isEqualTo(Result3A.Status.SUBMIT_CANCELLED)
+ }
+ }
+
+ @Test
+ fun pausingCaptureProcessingPreventsCaptureRequests() =
+ testScope.runTest {
+ // Arrange
+ graphLoop.requestProcessor = grp1
+ graphLoop.captureProcessingEnabled = false // Disable captureProcessing
+
+ // Act
+ graphLoop.submit(listOf(request1))
+ graphLoop.submit(listOf(request2))
+ advanceUntilIdle()
+
+ // Assert: Events are not processed
+ assertThat(csp1.events.size).isEqualTo(0)
+ }
+
+ @Test
+ fun resumingCaptureProcessingResumesCaptureRequests() =
+ testScope.runTest {
+ // Arrange
+ graphLoop.requestProcessor = grp1
+ graphLoop.captureProcessingEnabled = false // Disable captureProcessing
+
+ // Act
+ graphLoop.submit(listOf(request1))
+ graphLoop.submit(listOf(request2))
+ advanceUntilIdle()
+ graphLoop.captureProcessingEnabled = true // Enable processing
+ advanceUntilIdle()
+
+ // Assert: Events are not processed
+ assertThat(csp1.events.size).isEqualTo(2)
+
+ assertThat(csp1.events[0].isCapture).isTrue()
+ assertThat(csp1.events[0].requests).containsExactly(request1)
+
+ assertThat(csp1.events[1].isCapture).isTrue()
+ assertThat(csp1.events[1].requests).containsExactly(request2)
+ }
+
+ @Test
+ fun disablingCaptureProcessingAllowsRepeatingRequests() =
+ testScope.runTest {
+ // Arrange
+ graphLoop.requestProcessor = grp1
+
+ // Act
+ graphLoop.captureProcessingEnabled = false // Disable captureProcessing
+ graphLoop.repeatingRequest = request1
+ advanceUntilIdle()
+
+ // Assert
+ assertThat(csp1.events.size).isEqualTo(1)
+ assertThat(csp1.events[0].isRepeating).isTrue()
+ assertThat(csp1.events[0].requests).containsExactly(request1)
+ }
+
+ @Test
+ fun settingNullForRequestProcessorAfterCloseDoesNotCrash() =
+ testScope.runTest {
+ // Arrange
+ graphLoop.requestProcessor = grp1
+ graphLoop.close()
+
+ // Act
+ graphLoop.requestProcessor = null
+ advanceUntilIdle()
+
+ // Assert: does not crash, and only Close is invoked.
+ assertThat(csp1.events.size).isEqualTo(1)
+ assertThat(csp1.events[0].isClose).isTrue()
+ }
+
private val SimpleCSP.SimpleCSPEvent.requests: List<Request>
get() = (this as SimpleCSP.Submit).captureSequence.captureRequestList
@@ -686,7 +843,7 @@
events.add(StopRepeating)
}
- override fun close() {
+ override suspend fun shutdown() {
closed = true
events.add(Close)
}
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 d95aa17..13e29b9 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
@@ -33,9 +33,9 @@
import androidx.camera.camera2.pipe.testing.FakeRequestListener
import androidx.camera.camera2.pipe.testing.FakeThreads
import androidx.camera.camera2.pipe.testing.RobolectricCameraPipeTestRunner
+import androidx.testutils.assertThrows
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
@@ -54,6 +54,7 @@
internal class GraphProcessorTest {
private val globalListener = FakeRequestListener()
private val graphState3A = GraphState3A()
+ private val graphListener3A = Listener3A()
private val streamId = StreamId(0)
private val surfaceMap = mapOf(streamId to Surface(SurfaceTexture(1)))
@@ -82,7 +83,7 @@
CameraGraphId.nextId(),
FakeGraphConfigs.graphConfig,
graphState3A,
- this,
+ graphListener3A,
arrayListOf(globalListener)
)
graphProcessor.onGraphStarted(graphRequestProcessor1)
@@ -104,7 +105,7 @@
CameraGraphId.nextId(),
FakeGraphConfigs.graphConfig,
graphState3A,
- this,
+ graphListener3A,
arrayListOf(globalListener)
)
@@ -128,7 +129,7 @@
CameraGraphId.nextId(),
FakeGraphConfigs.graphConfig,
graphState3A,
- this,
+ graphListener3A,
arrayListOf(globalListener)
)
@@ -156,7 +157,7 @@
CameraGraphId.nextId(),
FakeGraphConfigs.graphConfig,
graphState3A,
- this,
+ graphListener3A,
arrayListOf(globalListener)
)
@@ -176,7 +177,7 @@
CameraGraphId.nextId(),
FakeGraphConfigs.graphConfig,
graphState3A,
- this,
+ graphListener3A,
arrayListOf(globalListener)
)
@@ -208,7 +209,7 @@
CameraGraphId.nextId(),
FakeGraphConfigs.graphConfig,
graphState3A,
- this,
+ graphListener3A,
arrayListOf(globalListener)
)
@@ -246,13 +247,13 @@
CameraGraphId.nextId(),
FakeGraphConfigs.graphConfig,
graphState3A,
- this,
+ graphListener3A,
arrayListOf(globalListener)
)
graphProcessor.onGraphStarted(graphRequestProcessor1)
- graphProcessor.startRepeating(request1)
- graphProcessor.startRepeating(request2)
+ graphProcessor.repeatingRequest = request1
+ graphProcessor.repeatingRequest = request2
advanceUntilIdle()
val event =
@@ -271,19 +272,19 @@
CameraGraphId.nextId(),
FakeGraphConfigs.graphConfig,
graphState3A,
- this,
+ graphListener3A,
arrayListOf(globalListener)
)
fakeProcessor1.rejectRequests = true
graphProcessor.onGraphStarted(graphRequestProcessor1)
- graphProcessor.startRepeating(request1)
+ graphProcessor.repeatingRequest = request1
val event1 = fakeProcessor1.nextEvent()
assertThat(event1.rejected).isTrue()
assertThat(event1.requestSequence!!.captureRequestList[0]).isSameInstanceAs(request1)
- graphProcessor.startRepeating(request2)
+ graphProcessor.repeatingRequest = request2
val event2 = fakeProcessor1.nextEvent()
assertThat(event2.rejected).isTrue()
fakeProcessor1.awaitEvent(request = request2) {
@@ -291,7 +292,7 @@
}
fakeProcessor1.rejectRequests = false
- graphProcessor.onGraphStarted(graphRequestProcessor1)
+ graphProcessor.invalidate()
fakeProcessor1.awaitEvent(request = request2) {
it.submit && it.requestSequence?.repeating == true
@@ -306,12 +307,12 @@
CameraGraphId.nextId(),
FakeGraphConfigs.graphConfig,
graphState3A,
- this,
+ graphListener3A,
arrayListOf(globalListener)
)
graphProcessor.onGraphStarted(graphRequestProcessor1)
- graphProcessor.startRepeating(request1)
+ graphProcessor.repeatingRequest = request1
advanceUntilIdle()
fakeProcessor1.awaitEvent(request = request1) {
@@ -334,13 +335,13 @@
CameraGraphId.nextId(),
FakeGraphConfigs.graphConfig,
graphState3A,
- this,
+ graphListener3A,
arrayListOf(globalListener)
)
fakeProcessor1.rejectRequests = true
graphProcessor.onGraphStarted(graphRequestProcessor1)
- graphProcessor.startRepeating(request1)
+ graphProcessor.repeatingRequest = request1
fakeProcessor1.awaitEvent(request = request1) { it.rejected }
graphProcessor.onGraphStarted(graphRequestProcessor2)
@@ -357,11 +358,11 @@
CameraGraphId.nextId(),
FakeGraphConfigs.graphConfig,
graphState3A,
- this,
+ graphListener3A,
arrayListOf(globalListener)
)
- graphProcessor.startRepeating(request1)
+ graphProcessor.repeatingRequest = request1
graphProcessor.submit(request2)
delay(50)
@@ -393,11 +394,11 @@
CameraGraphId.nextId(),
FakeGraphConfigs.graphConfig,
graphState3A,
- this,
+ graphListener3A,
arrayListOf(globalListener)
)
- graphProcessor.startRepeating(request1)
+ graphProcessor.repeatingRequest = request1
graphProcessor.submit(request2)
// Abort queued and in-flight requests.
@@ -426,14 +427,15 @@
CameraGraphId.nextId(),
FakeGraphConfigs.graphConfig,
graphState3A,
- this,
+ graphListener3A,
arrayListOf(globalListener)
)
graphProcessor.close()
+ advanceUntilIdle()
// Abort queued and in-flight requests.
- graphProcessor.onGraphStarted(graphRequestProcessor1)
- graphProcessor.startRepeating(request1)
+ // graphProcessor.onGraphStarted(graphRequestProcessor1)
+ graphProcessor.repeatingRequest = request1
graphProcessor.submit(request2)
val abortEvent1 =
@@ -441,8 +443,6 @@
val abortEvent2 = requestListener2.onAbortedFlow.first()
assertThat(abortEvent1).isNull()
assertThat(abortEvent2.request).isSameInstanceAs(request2)
-
- assertThat(fakeProcessor1.nextEvent().close).isTrue()
}
@Test
@@ -453,23 +453,25 @@
CameraGraphId.nextId(),
FakeGraphConfigs.graphConfig,
graphState3A,
- this,
+ graphListener3A,
arrayListOf(globalListener)
)
// Submit a repeating request first to make sure we have one in progress.
- graphProcessor.startRepeating(request1)
+ graphProcessor.repeatingRequest = request1
advanceUntilIdle()
- val result = async {
- graphProcessor.trySubmit(mapOf<CaptureRequest.Key<*>, Any>(CONTROL_AE_LOCK to false))
- }
+ graphProcessor.submit(mapOf<CaptureRequest.Key<*>, Any>(CONTROL_AE_LOCK to false))
advanceUntilIdle()
graphProcessor.onGraphStarted(graphRequestProcessor1)
advanceUntilIdle()
-
- assertThat(result.await()).isTrue()
+ 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))
+ .isFalse()
}
@Test
@@ -480,21 +482,14 @@
CameraGraphId.nextId(),
FakeGraphConfigs.graphConfig,
graphState3A,
- this,
+ graphListener3A,
arrayListOf(globalListener)
)
// Submit a repeating request first to make sure we have one in progress.
- graphProcessor.startRepeating(request1)
- advanceUntilIdle()
-
- 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))
- }
+ graphProcessor.repeatingRequest = request1
+ graphProcessor.submit(mapOf<CaptureRequest.Key<*>, Any>(CONTROL_AE_LOCK to false))
+ graphProcessor.submit(mapOf<CaptureRequest.Key<*>, Any>(CONTROL_AE_LOCK to true))
advanceUntilIdle()
graphProcessor.onGraphStarted(graphRequestProcessor1)
@@ -505,10 +500,11 @@
val event2 = fakeProcessor1.nextEvent()
assertThat(event2.requestSequence?.repeating).isFalse()
assertThat(event2.requestSequence?.requestMetadata?.get(request1)?.get(CONTROL_AE_LOCK))
+ .isFalse()
+ val event3 = fakeProcessor1.nextEvent()
+ assertThat(event3.requestSequence?.repeating).isFalse()
+ assertThat(event3.requestSequence?.requestMetadata?.get(request1)?.get(CONTROL_AE_LOCK))
.isTrue()
-
- assertThat(result1.await()).isFalse()
- assertThat(result2.await()).isTrue()
}
@Test
@@ -519,16 +515,16 @@
CameraGraphId.nextId(),
FakeGraphConfigs.graphConfig,
graphState3A,
- this,
+ graphListener3A,
arrayListOf(globalListener)
)
graphProcessor.onGraphStarted(graphRequestProcessor1)
advanceUntilIdle()
- val result =
- graphProcessor.trySubmit(mapOf<CaptureRequest.Key<*>, Any>(CONTROL_AE_LOCK to true))
- assertThat(result).isFalse()
+ assertThrows<IllegalStateException> {
+ graphProcessor.submit(mapOf<CaptureRequest.Key<*>, Any>(CONTROL_AE_LOCK to true))
+ }
}
@Test
@@ -539,7 +535,7 @@
CameraGraphId.nextId(),
FakeGraphConfigs.graphConfig,
graphState3A,
- this,
+ graphListener3A,
arrayListOf(globalListener)
)
assertThat(graphProcessor.graphState.value).isEqualTo(GraphStateStopped)
@@ -559,7 +555,7 @@
CameraGraphId.nextId(),
FakeGraphConfigs.graphConfig,
graphState3A,
- this,
+ graphListener3A,
arrayListOf(globalListener)
)
assertThat(graphProcessor.graphState.value).isEqualTo(GraphStateStopped)
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/GraphTestContext.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/GraphTestContext.kt
index 93d4a6e..dbeb67d 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/GraphTestContext.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/GraphTestContext.kt
@@ -33,7 +33,7 @@
init {
captureSequenceProcessor.surfaceMap = surfaceMap
graphProcessor.onGraphStarted(graphRequestProcessor)
- graphProcessor.startRepeating(Request(streams = listOf(streamId)))
+ graphProcessor.repeatingRequest = Request(streams = listOf(streamId))
}
override fun close() {
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 0137588..4205aef 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
@@ -27,14 +27,17 @@
import androidx.camera.camera2.pipe.graph.GraphProcessor
import androidx.camera.camera2.pipe.graph.GraphRequestProcessor
import androidx.camera.camera2.pipe.graph.GraphState3A
+import androidx.camera.camera2.pipe.graph.Listener3A
import androidx.camera.camera2.pipe.putAllMetadata
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.runBlocking
/** Fake implementation of a [GraphProcessor] for tests. */
internal class FakeGraphProcessor(
val graphState3A: GraphState3A = GraphState3A(),
+ val graphListener3A: Listener3A = Listener3A(),
val defaultParameters: Map<*, Any?> = emptyMap<Any, Any?>(),
val defaultListeners: List<Request.Listener> = emptyList()
) : GraphProcessor, GraphListener {
@@ -44,8 +47,15 @@
var closed = false
private set
- var repeatingRequest: Request? = null
- private set
+ private var _repeatingRequest: Request? = null
+ override var repeatingRequest: Request?
+ get() = _repeatingRequest
+ set(value) {
+ _repeatingRequest = value
+ if (value == null) {
+ graphListener3A.onStopRepeating()
+ }
+ }
val requestQueue: List<List<Request>>
get() = _requestQueue
@@ -58,29 +68,17 @@
override val graphState: StateFlow<GraphState>
get() = _graphState
- override fun startRepeating(request: Request) {
- repeatingRequest = request
- }
+ override fun submit(request: Request): Boolean = submit(listOf(request))
- override fun stopRepeating() {
- repeatingRequest = null
- }
-
- override fun hasRepeatingRequest() = repeatingRequest != null
-
- override fun submit(request: Request) {
- submit(listOf(request))
- }
-
- override fun submit(requests: List<Request>) {
+ override fun submit(requests: List<Request>): Boolean {
+ if (closed) return false
_requestQueue.add(requests)
+ return true
}
- override suspend fun trySubmit(parameters: Map<*, Any?>): Boolean {
- if (closed) {
- return false
- }
- if (repeatingRequest == null) return false
+ override fun submit(parameters: Map<*, Any?>): Boolean {
+ check(repeatingRequest != null)
+ if (closed) return false
val currProcessor = processor
val currRepeatingRequest = repeatingRequest
@@ -88,22 +86,37 @@
requiredParameters.putAllMetadata(parameters)
graphState3A.writeTo(requiredParameters)
- return when {
- currProcessor == null || currRepeatingRequest == null -> false
- else ->
- currProcessor.submit(
- isRepeating = false,
- requests = listOf(currRepeatingRequest),
- defaultParameters = defaultParameters,
- requiredParameters = requiredParameters,
- listeners = defaultListeners
- )
+ if (currProcessor != null && currRepeatingRequest != null) {
+ currProcessor.submit(
+ isRepeating = false,
+ requests = listOf(currRepeatingRequest),
+ defaultParameters = defaultParameters,
+ requiredParameters = requiredParameters,
+ listeners = defaultListeners
+ )
}
+ return true
}
override fun abort() {
+ val requests = _requestQueue.toList()
_requestQueue.clear()
- // TODO: Invoke abort on the listeners in the queue.
+
+ for (burst in requests) {
+ for (request in burst) {
+ for (listener in defaultListeners) {
+ listener.onAborted(request)
+ }
+ }
+ }
+
+ for (burst in requests) {
+ for (request in burst) {
+ for (listener in request.listeners) {
+ listener.onAborted(request)
+ }
+ }
+ }
}
override fun close() {
@@ -113,6 +126,7 @@
closed = true
active = false
_requestQueue.clear()
+ graphListener3A.onGraphShutdown()
}
override fun onGraphStarting() {
@@ -123,11 +137,12 @@
_graphState.value = GraphStateStarted
val old = processor
processor = requestProcessor
- old?.close()
+ runBlocking { old?.shutdown() }
}
override fun onGraphStopping() {
_graphState.value = GraphStateStopping
+ graphListener3A.onGraphStopped()
}
override fun onGraphStopped(requestProcessor: GraphRequestProcessor?) {
@@ -136,7 +151,7 @@
val old = processor
if (requestProcessor === old) {
processor = null
- old.close()
+ runBlocking { old.shutdown() }
}
}
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/UpdateCounting3AStateListener.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/UpdateCounting3AStateListener.kt
index 8c4b838..11779e4 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/UpdateCounting3AStateListener.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/UpdateCounting3AStateListener.kt
@@ -37,5 +37,9 @@
return listener.update(requestNumber, frameMetadata)
}
- override fun onRequestSequenceStopped() {}
+ override fun onStopRepeating() {}
+
+ override fun onGraphStopped() {}
+
+ override fun onGraphShutdown() {}
}