[CameraPipe] Add lock behaviors for all 3A modes for startFocusAndMetering
In camera-pipe, if AF mode is not supported, CameraControl#startFocusAndMetering request is not submitted to camera due to being discarded in Controller3A#lock3A for having null LockBehaviors for all 3A modes. This happens due to passing only AF lock behavior from camera-pipe-integration layer. Lock behaviors for AE and AWB are passed too in this fix. But this will cause camera-pipe to wait for AE/AWB convergence as well.
- Added additional lab tests to check if convergence can complete
- Refactored timeout handling in FocusMeteringControl to remove extra timeout checking in integration layer and simplify the logic further. camera-pipe should ensure the timeout provided is handled properly.
Bug: 270012294
Test: FocusMeteringDeviceTest
Change-Id: Ida378a1b2be8d5bb9b2ce4d26c05d3e0def58637
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/FocusMeteringControl.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/FocusMeteringControl.kt
index 5541a40..ed6acf5 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/FocusMeteringControl.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/FocusMeteringControl.kt
@@ -27,6 +27,7 @@
import androidx.camera.camera2.pipe.AeMode
import androidx.camera.camera2.pipe.AfMode
import androidx.camera.camera2.pipe.CameraGraph.Constants3A.METERING_REGIONS_DEFAULT
+import androidx.camera.camera2.pipe.Lock3ABehavior
import androidx.camera.camera2.pipe.Result3A
import androidx.camera.camera2.pipe.integration.adapter.asListenableFuture
import androidx.camera.camera2.pipe.integration.adapter.propagateTo
@@ -43,11 +44,11 @@
import dagger.Binds
import dagger.Module
import dagger.multibindings.IntoSet
+import java.util.concurrent.TimeUnit
import javax.inject.Inject
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.launch
-import kotlinx.coroutines.withTimeoutOrNull
/**
* Implementation of focus and metering controls exposed by [CameraControlInternal].
@@ -110,7 +111,7 @@
val signal = CompletableDeferred<FocusMeteringResult>()
useCaseCamera?.let { useCaseCamera ->
- val job = threads.sequentialScope.launch {
+ threads.sequentialScope.launch {
cancelSignal?.setCancelException("Cancelled by another startFocusAndMetering()")
updateSignal?.setCancelException("Cancelled by another startFocusAndMetering()")
updateSignal = signal
@@ -158,50 +159,54 @@
} else {
(false to autoFocusTimeoutMs)
}
- withTimeoutOrNull(timeout) {
- /**
- * If device does not support a 3A region, we should not update it at all.
- * If device does support but a region list is empty, it means any previously
- * set region should be removed, so the no-op METERING_REGIONS_DEFAULT is used.
- */
- useCaseCamera.requestControl.startFocusAndMeteringAsync(
- aeRegions = if (maxAeRegionCount > 0)
- aeRectangles.ifEmpty { METERING_REGIONS_DEFAULT.toList() }
- else null,
- afRegions = if (maxAfRegionCount > 0)
- afRectangles.ifEmpty { METERING_REGIONS_DEFAULT.toList() }
- else null,
- awbRegions = if (maxAwbRegionCount > 0)
- awbRectangles.ifEmpty { METERING_REGIONS_DEFAULT.toList() }
- else null,
- afTriggerStartAeMode = cameraProperties.getSupportedAeMode(AeMode.ON)
- ).await()
- }.let { result3A ->
- if (result3A != null) {
- if (result3A.status == Result3A.Status.SUBMIT_FAILED) {
- signal.completeExceptionally(
- OperationCanceledException("Camera is not active.")
- )
- } else {
- signal.complete(result3A.toFocusMeteringResult(
- shouldTriggerAf = afRectangles.isNotEmpty()
- ))
+
+ /**
+ * If device does not support a 3A region, we should not update it at all.
+ * If device does support but a region list is empty, it means any previously
+ * set region should be removed, so the no-op METERING_REGIONS_DEFAULT is used.
+ */
+ val result3A = useCaseCamera.requestControl.startFocusAndMeteringAsync(
+ aeRegions = if (maxAeRegionCount > 0)
+ aeRectangles.ifEmpty { METERING_REGIONS_DEFAULT.toList() }
+ else null,
+ afRegions = if (maxAfRegionCount > 0)
+ afRectangles.ifEmpty { METERING_REGIONS_DEFAULT.toList() }
+ else null,
+ awbRegions = if (maxAwbRegionCount > 0)
+ awbRectangles.ifEmpty { METERING_REGIONS_DEFAULT.toList() }
+ else null,
+ aeLockBehavior = if (maxAeRegionCount > 0)
+ Lock3ABehavior.AFTER_NEW_SCAN
+ else null,
+ afLockBehavior = if (maxAfRegionCount > 0)
+ Lock3ABehavior.AFTER_NEW_SCAN
+ else null,
+ awbLockBehavior = if (maxAwbRegionCount > 0)
+ Lock3ABehavior.AFTER_NEW_SCAN
+ else null,
+ afTriggerStartAeMode = cameraProperties.getSupportedAeMode(AeMode.ON),
+ timeLimitNs = TimeUnit.NANOSECONDS.convert(
+ timeout,
+ TimeUnit.MILLISECONDS
+ )
+ ).await()
+
+ if (result3A.status == Result3A.Status.SUBMIT_FAILED) {
+ signal.completeExceptionally(
+ OperationCanceledException("Camera is not active.")
+ )
+ } else if (result3A.status == Result3A.Status.TIME_LIMIT_REACHED) {
+ if (isCancelEnabled) {
+ if (signal.isActive) {
+ cancelFocusAndMeteringNowAsync(useCaseCamera, signal)
}
} else {
- if (isCancelEnabled) {
- if (signal.isActive) {
- cancelFocusAndMeteringNowAsync(useCaseCamera, signal)
- }
- } else {
- signal.complete(FocusMeteringResult.create(false))
- }
+ signal.complete(FocusMeteringResult.create(false))
}
- }
- }
-
- signal.invokeOnCompletion { throwable ->
- if (throwable is OperationCanceledException) {
- job.cancel()
+ } else {
+ signal.complete(result3A.toFocusMeteringResult(
+ shouldTriggerAf = afRectangles.isNotEmpty()
+ ))
}
}
} ?: run {
@@ -311,7 +316,8 @@
* in priority. On the other hand, resultAfState == null matters only if the result comes
* from a submitted request, so it should be checked after frameMetadata == null.
*
- * Ref: [FocusMeteringAction] and [Controller3A.lock3A] documentations.
+ * @see FocusMeteringAction
+ * @see androidx.camera.camera2.pipe.graph.Controller3A.lock3A
*/
val isFocusSuccessful = when {
!shouldTriggerAf -> false
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCameraRequestControl.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCameraRequestControl.kt
index 3e96a04..bfa1fc7 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCameraRequestControl.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCameraRequestControl.kt
@@ -26,6 +26,7 @@
import androidx.camera.camera2.pipe.AeMode
import androidx.camera.camera2.pipe.AfMode
import androidx.camera.camera2.pipe.AwbMode
+import androidx.camera.camera2.pipe.CameraGraph
import androidx.camera.camera2.pipe.CameraGraph.Constants3A.METERING_REGIONS_DEFAULT
import androidx.camera.camera2.pipe.Lock3ABehavior
import androidx.camera.camera2.pipe.Request
@@ -132,7 +133,11 @@
aeRegions: List<MeteringRectangle>? = null,
afRegions: List<MeteringRectangle>? = null,
awbRegions: List<MeteringRectangle>? = null,
- afTriggerStartAeMode: AeMode? = null
+ aeLockBehavior: Lock3ABehavior? = null,
+ afLockBehavior: Lock3ABehavior? = null,
+ awbLockBehavior: Lock3ABehavior? = null,
+ afTriggerStartAeMode: AeMode? = null,
+ timeLimitNs: Long = CameraGraph.Constants3A.DEFAULT_TIME_LIMIT_NS,
): Deferred<Result3A>
suspend fun cancelFocusAndMeteringAsync(): Deferred<Result3A>
@@ -219,14 +224,21 @@
aeRegions: List<MeteringRectangle>?,
afRegions: List<MeteringRectangle>?,
awbRegions: List<MeteringRectangle>?,
- afTriggerStartAeMode: AeMode?
+ aeLockBehavior: Lock3ABehavior?,
+ afLockBehavior: Lock3ABehavior?,
+ awbLockBehavior: Lock3ABehavior?,
+ afTriggerStartAeMode: AeMode?,
+ timeLimitNs: Long,
): Deferred<Result3A> = graph.acquireSession().use {
it.lock3A(
aeRegions = aeRegions,
afRegions = afRegions,
awbRegions = awbRegions,
- afLockBehavior = Lock3ABehavior.AFTER_NEW_SCAN,
- afTriggerStartAeMode = afTriggerStartAeMode
+ aeLockBehavior = aeLockBehavior,
+ afLockBehavior = afLockBehavior,
+ awbLockBehavior = awbLockBehavior,
+ afTriggerStartAeMode = afTriggerStartAeMode,
+ timeLimitNs = timeLimitNs,
)
}
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/FocusMeteringControlTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/FocusMeteringControlTest.kt
index 9898814..3b0b4c2 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/FocusMeteringControlTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/FocusMeteringControlTest.kt
@@ -29,6 +29,7 @@
import android.util.Size
import androidx.camera.camera2.pipe.CameraGraph
import androidx.camera.camera2.pipe.CameraId
+import androidx.camera.camera2.pipe.Lock3ABehavior
import androidx.camera.camera2.pipe.Result3A
import androidx.camera.camera2.pipe.integration.compat.StreamConfigurationMapCompat
import androidx.camera.camera2.pipe.integration.compat.ZoomCompat
@@ -60,7 +61,6 @@
import androidx.camera.testing.SurfaceTextureProvider
import androidx.camera.testing.fakes.FakeCamera
import androidx.camera.testing.fakes.FakeUseCase
-import androidx.concurrent.futures.await
import androidx.test.filters.MediumTest
import com.google.common.truth.Truth.assertThat
import com.google.common.truth.Truth.assertWithMessage
@@ -361,6 +361,20 @@
}
@Test
+ fun startFocusAndMetering_defaultPoint_3ALocksAreCorrect() = runBlocking {
+ startFocusMeteringAndAwait(FocusMeteringAction.Builder(point1).build())
+
+ with(fakeRequestControl.focusMeteringCalls.last()) {
+ assertWithMessage("Wrong lock behavior for AE")
+ .that(aeLockBehavior).isEqualTo(Lock3ABehavior.AFTER_NEW_SCAN)
+ assertWithMessage("Wrong lock behavior for AF")
+ .that(afLockBehavior).isEqualTo(Lock3ABehavior.AFTER_NEW_SCAN)
+ assertWithMessage("Wrong lock behavior for AWB")
+ .that(awbLockBehavior).isEqualTo(Lock3ABehavior.AFTER_NEW_SCAN)
+ }
+ }
+
+ @Test
fun startFocusAndMetering_multiplePoints_3ARectsAreCorrect() = runBlocking {
// Camera 0 i.e. Max AF count = 3, Max AE count = 3, Max AWB count = 1
startFocusMeteringAndAwait(
@@ -1268,7 +1282,7 @@
}
@Test
- fun startFocusMetering_onlyAfSupported_unsupportedRegionsNotSet() {
+ fun startFocusMetering_onlyAfSupported_unsupportedRegionsNotConfigured() {
// camera 5 supports 1 AF and 0 AE/AWB regions
focusMeteringControl = initFocusMeteringControl(cameraId = CAMERA_ID_5)
@@ -1282,8 +1296,14 @@
with(fakeRequestControl.focusMeteringCalls.last()) {
assertWithMessage("Wrong number of AE regions").that(aeRegions).isNull()
+ assertWithMessage("Wrong lock behavior for AE").that(aeLockBehavior).isNull()
+
assertWithMessage("Wrong number of AF regions").that(afRegions?.size).isEqualTo(1)
+ assertWithMessage("Wrong lock behavior for AE")
+ .that(afLockBehavior).isEqualTo(Lock3ABehavior.AFTER_NEW_SCAN)
+
assertWithMessage("Wrong number of AWB regions").that(awbRegions).isNull()
+ assertWithMessage("Wrong lock behavior for AWB").that(awbLockBehavior).isNull()
}
}
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeUseCaseCamera.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeUseCaseCamera.kt
index 215e70a..64d3363 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeUseCaseCamera.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeUseCaseCamera.kt
@@ -19,6 +19,8 @@
import android.hardware.camera2.CaptureRequest
import android.hardware.camera2.params.MeteringRectangle
import androidx.camera.camera2.pipe.AeMode
+import androidx.camera.camera2.pipe.CameraGraph
+import androidx.camera.camera2.pipe.Lock3ABehavior
import androidx.camera.camera2.pipe.Request
import androidx.camera.camera2.pipe.RequestTemplate
import androidx.camera.camera2.pipe.Result3A
@@ -31,9 +33,11 @@
import androidx.camera.core.UseCase
import androidx.camera.core.impl.CaptureConfig
import androidx.camera.core.impl.Config
+import java.util.concurrent.TimeUnit
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Job
+import kotlinx.coroutines.withTimeoutOrNull
class FakeUseCaseCameraComponentBuilder : UseCaseCameraComponent.Builder {
private var config: UseCaseCameraConfig = UseCaseCameraConfig(emptyList(), CameraStateAdapter())
@@ -101,11 +105,27 @@
aeRegions: List<MeteringRectangle>?,
afRegions: List<MeteringRectangle>?,
awbRegions: List<MeteringRectangle>?,
- afTriggerStartAeMode: AeMode?
+ aeLockBehavior: Lock3ABehavior?,
+ afLockBehavior: Lock3ABehavior?,
+ awbLockBehavior: Lock3ABehavior?,
+ afTriggerStartAeMode: AeMode?,
+ timeLimitNs: Long,
): Deferred<Result3A> {
focusMeteringCalls.add(
- FocusMeteringParams(aeRegions, afRegions, awbRegions, afTriggerStartAeMode)
+ FocusMeteringParams(
+ aeRegions, afRegions, awbRegions,
+ aeLockBehavior, afLockBehavior, awbLockBehavior,
+ afTriggerStartAeMode,
+ timeLimitNs
+ )
)
+ withTimeoutOrNull(TimeUnit.MILLISECONDS.convert(timeLimitNs, TimeUnit.NANOSECONDS)) {
+ focusMeteringResult.await()
+ }.let { result3A ->
+ if (result3A == null) {
+ focusMeteringResult.complete(Result3A(status = Result3A.Status.TIME_LIMIT_REACHED))
+ }
+ }
return focusMeteringResult
}
@@ -127,7 +147,11 @@
val aeRegions: List<MeteringRectangle>? = null,
val afRegions: List<MeteringRectangle>? = null,
val awbRegions: List<MeteringRectangle>? = null,
- val afTriggerStartAeMode: AeMode? = null
+ val aeLockBehavior: Lock3ABehavior? = null,
+ val afLockBehavior: Lock3ABehavior? = null,
+ val awbLockBehavior: Lock3ABehavior? = null,
+ val afTriggerStartAeMode: AeMode? = null,
+ val timeLimitNs: Long = CameraGraph.Constants3A.DEFAULT_TIME_LIMIT_NS,
)
data class RequestParameters(
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/LabTestRule.kt b/camera/camera-testing/src/main/java/androidx/camera/testing/LabTestRule.kt
index dae1356..dd5ac4e 100644
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/LabTestRule.kt
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/LabTestRule.kt
@@ -18,6 +18,7 @@
import android.util.Log
import androidx.annotation.RequiresApi
+import androidx.camera.core.CameraSelector
import androidx.camera.testing.LabTestRule.LabTestFrontCamera
import androidx.camera.testing.LabTestRule.LabTestOnly
import androidx.camera.testing.LabTestRule.LabTestRearCamera
@@ -142,5 +143,23 @@
fun isInLabTest(): Boolean {
return Log.isLoggable("MH", Log.DEBUG)
}
+
+ /**
+ * Checks if it is CameraX lab environment where the enabled camera uses the specified
+ * [lensFacing] direction.
+ *
+ * For example, if [lensFacing] is [CameraSelector.LENS_FACING_BACK], this method will
+ * return true if the rear camera is enabled on a device in CameraX lab environment.
+ *
+ * @param lensFacing the required camera direction relative to the device screen.
+ * @return if enabled camera is in same direction as [lensFacing] in CameraX lab environment
+ */
+ @JvmStatic
+ fun isLensFacingEnabledInLabTest(@CameraSelector.LensFacing lensFacing: Int) =
+ when (lensFacing) {
+ CameraSelector.LENS_FACING_BACK -> Log.isLoggable("rearCameraE2E", Log.DEBUG)
+ CameraSelector.LENS_FACING_FRONT -> Log.isLoggable("frontCameraE2E", Log.DEBUG)
+ else -> false
+ }
}
}
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/FocusMeteringDeviceTest.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/FocusMeteringDeviceTest.kt
index f96b0b1..42ed11b 100644
--- a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/FocusMeteringDeviceTest.kt
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/FocusMeteringDeviceTest.kt
@@ -39,6 +39,7 @@
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.testing.CameraPipeConfigTestRule
import androidx.camera.testing.CameraUtil
+import androidx.camera.testing.LabTestRule.Companion.isLensFacingEnabledInLabTest
import androidx.camera.testing.fakes.FakeLifecycleOwner
import androidx.test.core.app.ApplicationProvider
import androidx.test.filters.LargeTest
@@ -311,6 +312,85 @@
assertFutureCompletes(future)
}
+ /**
+ * The following tests check if a device can complete 3A convergence, by setting an auto
+ * cancellation with [FocusMeteringAction.Builder.setAutoCancelDuration] which ensures throwing
+ * an exception in case of a timeout.
+ *
+ * Since some devices may require a long time to complete convergence, we are setting a long
+ * [FocusMeteringAction.mAutoCancelDurationInMillis] in these tests.
+ */
+
+ @Test
+ fun futureCompletes_whenFocusMeteringStartedWithLongCancelDuration() = runBlocking {
+ Assume.assumeTrue(
+ "Not CameraX lab environment," +
+ " or lensFacing:${cameraSelector.lensFacing!!} camera is not enabled",
+ isLensFacingEnabledInLabTest(lensFacing = cameraSelector.lensFacing!!)
+ )
+
+ Assume.assumeTrue(
+ "No AF/AE/AWB region available on this device!",
+ hasMeteringRegion(cameraSelector)
+ )
+
+ val focusMeteringAction = FocusMeteringAction.Builder(validMeteringPoint)
+ .setAutoCancelDuration(5_000, TimeUnit.MILLISECONDS)
+ .build()
+
+ val resultFuture = camera.cameraControl.startFocusAndMetering(focusMeteringAction)
+
+ assertFutureCompletes(resultFuture)
+ }
+
+ @Test
+ fun futureCompletes_whenOnlyAfFocusMeteringStartedWithLongCancelDuration() = runBlocking {
+ Assume.assumeTrue(
+ "Not CameraX lab environment," +
+ " or lensFacing:${cameraSelector.lensFacing!!} camera is not enabled",
+ isLensFacingEnabledInLabTest(lensFacing = cameraSelector.lensFacing!!)
+ )
+
+ Assume.assumeTrue(
+ "No AF region available on this device!",
+ hasMeteringRegion(cameraSelector, FLAG_AF)
+ )
+
+ val focusMeteringAction = FocusMeteringAction.Builder(
+ validMeteringPoint,
+ FLAG_AF
+ ).setAutoCancelDuration(5_000, TimeUnit.MILLISECONDS)
+ .build()
+
+ val resultFuture = camera.cameraControl.startFocusAndMetering(focusMeteringAction)
+
+ assertFutureCompletes(resultFuture)
+ }
+
+ @Test
+ fun futureCompletes_whenAeAwbFocusMeteringStartedWithLongCancelDuration() = runBlocking {
+ Assume.assumeTrue(
+ "Not CameraX lab environment," +
+ " or lensFacing:${cameraSelector.lensFacing!!} camera is not enabled",
+ isLensFacingEnabledInLabTest(lensFacing = cameraSelector.lensFacing!!)
+ )
+
+ Assume.assumeTrue(
+ "No AE/AWB region available on this device!",
+ hasMeteringRegion(cameraSelector, FLAG_AE or FLAG_AWB)
+ )
+
+ val focusMeteringAction = FocusMeteringAction.Builder(
+ validMeteringPoint,
+ FLAG_AE or FLAG_AWB
+ ).setAutoCancelDuration(5_000, TimeUnit.MILLISECONDS)
+ .build()
+
+ val resultFuture = camera.cameraControl.startFocusAndMetering(focusMeteringAction)
+
+ assertFutureCompletes(resultFuture)
+ }
+
private fun hasMeteringRegion(
selector: CameraSelector,
@FocusMeteringAction.MeteringMode flags: Int = FLAG_AF or FLAG_AE or FLAG_AWB