[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