1 /*
<lambda>null2  * Copyright 2024 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package androidx.camera.integration.core
18 
19 import android.content.Context
20 import android.hardware.camera2.CameraCaptureSession
21 import android.hardware.camera2.CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES
22 import android.hardware.camera2.CameraCharacteristics.CONTROL_AVAILABLE_VIDEO_STABILIZATION_MODES
23 import android.hardware.camera2.CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL
24 import android.hardware.camera2.CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY
25 import android.hardware.camera2.CameraMetadata.CONTROL_VIDEO_STABILIZATION_MODE_OFF
26 import android.hardware.camera2.CameraMetadata.CONTROL_VIDEO_STABILIZATION_MODE_ON
27 import android.hardware.camera2.CameraMetadata.CONTROL_VIDEO_STABILIZATION_MODE_PREVIEW_STABILIZATION
28 import android.hardware.camera2.CaptureRequest
29 import android.hardware.camera2.CaptureRequest.CONTROL_VIDEO_STABILIZATION_MODE
30 import android.hardware.camera2.TotalCaptureResult
31 import android.util.Range
32 import androidx.camera.camera2.Camera2Config
33 import androidx.camera.camera2.interop.Camera2Interop
34 import androidx.camera.camera2.pipe.integration.CameraPipeConfig
35 import androidx.camera.camera2.pipe.integration.adapter.awaitUntil
36 import androidx.camera.core.CameraSelector
37 import androidx.camera.core.CameraXConfig
38 import androidx.camera.core.ImageAnalysis
39 import androidx.camera.core.ImageCapture
40 import androidx.camera.core.Preview
41 import androidx.camera.core.UseCase
42 import androidx.camera.core.impl.CameraInfoInternal
43 import androidx.camera.core.impl.UseCaseConfig
44 import androidx.camera.core.impl.utils.executor.CameraXExecutors
45 import androidx.camera.core.internal.compat.quirk.AeFpsRangeQuirk
46 import androidx.camera.integration.core.util.Camera2InteropUtil
47 import androidx.camera.lifecycle.ProcessCameraProvider
48 import androidx.camera.testing.impl.CameraPipeConfigTestRule
49 import androidx.camera.testing.impl.CameraUtil
50 import androidx.camera.testing.impl.SurfaceTextureProvider
51 import androidx.camera.testing.impl.WakelockEmptyActivityRule
52 import androidx.camera.testing.impl.fakes.FakeLifecycleOwner
53 import androidx.camera.video.Recorder
54 import androidx.camera.video.VideoCapture
55 import androidx.test.core.app.ApplicationProvider
56 import androidx.test.filters.LargeTest
57 import androidx.test.filters.SdkSuppress
58 import com.google.common.truth.Truth.assertWithMessage
59 import java.util.concurrent.TimeUnit
60 import kotlinx.coroutines.CompletableDeferred
61 import kotlinx.coroutines.Deferred
62 import kotlinx.coroutines.Dispatchers
63 import kotlinx.coroutines.runBlocking
64 import kotlinx.coroutines.withContext
65 import org.junit.After
66 import org.junit.Assume
67 import org.junit.Assume.assumeFalse
68 import org.junit.Assume.assumeTrue
69 import org.junit.Before
70 import org.junit.Rule
71 import org.junit.Test
72 import org.junit.runner.RunWith
73 import org.junit.runners.Parameterized
74 
75 /**
76  * Tests for checking if a capture option is submitted from end-to-end.
77  *
78  * Usually, these tests only check if the corresponding capture request is submitted properly by
79  * checking the [CaptureRequest] with `Camera2Interop` capture callback. This does not mean the
80  * framework will always honor these capture request options, so we don't usually care about the
81  * result. If the [TotalCaptureResult] or response by camera also need to be verified for some
82  * specific cases, we may need to have additional considerations and will probably vary in a
83  * case-to-case basis.
84  *
85  * TODO: Will probably be better to use CameraController whenever possible to increase the scope.
86  */
87 @LargeTest
88 @RunWith(Parameterized::class)
89 @SdkSuppress(minSdkVersion = 21)
90 class CaptureOptionSubmissionTest(
91     private val selectorName: String,
92     private val cameraSelector: CameraSelector,
93     private val implName: String,
94     private val cameraConfig: CameraXConfig
95 ) {
96     @get:Rule
97     val cameraPipeConfigTestRule =
98         CameraPipeConfigTestRule(
99             active = implName == CameraPipeConfig::class.simpleName,
100         )
101 
102     @get:Rule
103     val cameraRule =
104         CameraUtil.grantCameraPermissionAndPreTestAndPostTest(
105             CameraUtil.PreTestCameraIdList(cameraConfig)
106         )
107 
108     @get:Rule val wakelockEmptyActivityRule = WakelockEmptyActivityRule()
109 
110     private val context = ApplicationProvider.getApplicationContext<Context>()
111     private lateinit var cameraProvider: ProcessCameraProvider
112     private lateinit var fakeLifecycleOwner: FakeLifecycleOwner
113 
114     // Capture callback added to session, so only a repeating capture callback, not non-repeating
115     private lateinit var sessionCaptureCallback: CaptureCallback
116 
117     @Before
118     fun setUp(): Unit = runBlocking {
119         assumeTrue(CameraUtil.hasCameraWithLensFacing(cameraSelector.lensFacing!!))
120 
121         ProcessCameraProvider.configureInstance(cameraConfig)
122         cameraProvider = ProcessCameraProvider.getInstance(context)[10, TimeUnit.SECONDS]
123         sessionCaptureCallback = CaptureCallback()
124 
125         withContext(Dispatchers.Main) {
126             fakeLifecycleOwner = FakeLifecycleOwner()
127             fakeLifecycleOwner.startAndResume()
128         }
129     }
130 
131     @After
132     fun tearDown(): Unit = runBlocking {
133         if (::cameraProvider.isInitialized) {
134             withContext(Dispatchers.Main) { cameraProvider.shutdownAsync()[10, TimeUnit.SECONDS] }
135         }
136     }
137 
138     /*
139      * Only testing if a supported FPS range is submitted to camera in [CaptureRequest] without
140      * caring about the result. This test basically checks if [Preview.Builder.setTargetFrameRate]
141      * works properly.
142      */
143 
144     @Test
145     fun canSubmitSupportedAeTargetFpsRanges_whenTargetFrameRateSetToPreviewOnly() = runBlocking {
146         // At least 2 FPS ranges should be checked as the submitted range may just be from template
147         getSupportedFpsRanges().forEach { targetFpsRange ->
148             if (targetFpsRange.upper > 30) {
149                 // TODO: b/332464740 - High FPS may not be supported as per stream config map
150                 return@forEach
151             }
152 
153             var lastSubmittedFpsRange: Range<Int>? = null
154             val result =
155                 sessionCaptureCallback.verify { captureRequest, _ ->
156                     captureRequest[CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE]?.let {
157                         lastSubmittedFpsRange = it
158                     }
159                     captureRequest[CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE] == targetFpsRange
160                 }
161 
162             bindUseCases(listOf(Preview.Builder().setTargetFrameRate(targetFpsRange)))
163 
164             val isCompleted = result.awaitUntil(timeoutMillis = 10000)
165             assertWithMessage(
166                     "Test failed for targetFpsRange = $targetFpsRange" +
167                         ", lastSubmittedFpsRange = $lastSubmittedFpsRange"
168                 )
169                 .that(isCompleted)
170                 .isTrue()
171 
172             unbindAllUseCases()
173         }
174     }
175 
176     @Test
177     fun canSubmitSupportedAeTargetFpsRanges_whenTargetFrameRateSetToVideoCaptureOnly() =
178         runBlocking {
179             // At least 2 FPS ranges should be checked as the submitted range may be from template
180             getSupportedFpsRanges().forEach { targetFpsRange ->
181                 if (targetFpsRange.upper > 30) {
182                     // TODO: b/332464740 - High FPS may not be supported as per stream config map
183                     return@forEach
184                 }
185 
186                 var lastSubmittedFpsRange: Range<Int>? = null
187                 val result =
188                     sessionCaptureCallback.verify { captureRequest, _ ->
189                         captureRequest[CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE]?.let {
190                             lastSubmittedFpsRange = it
191                         }
192                         captureRequest[CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE] == targetFpsRange
193                     }
194 
195                 bindUseCases(
196                     listOf(
197                         // Binds Preview together to ensure that a repeating will be started
198                         Preview.Builder(),
199                         VideoCapture.Builder(Recorder.Builder().build())
200                             .setTargetFrameRate(targetFpsRange),
201                     )
202                 )
203 
204                 val isCompleted = result.awaitUntil(timeoutMillis = 10000)
205                 assertWithMessage(
206                         "Test failed for targetFpsRange = $targetFpsRange" +
207                             ", lastSubmittedFpsRange = $lastSubmittedFpsRange"
208                     )
209                     .that(isCompleted)
210                     .isTrue()
211 
212                 unbindAllUseCases()
213             }
214         }
215 
216     @Test
217     fun canApplyAeFpsRangeWorkaround() = runBlocking {
218         val targetFpsRange = getAeFpsRangeFromQuirks()
219         assumeFalse(
220             "AeFpsRange workaround is applied only on LEGACY level devices.",
221             targetFpsRange == null
222         )
223 
224         var lastSubmittedFpsRange: Range<Int>? = null
225         val result =
226             sessionCaptureCallback.verify { captureRequest, _ ->
227                 captureRequest[CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE]?.let {
228                     lastSubmittedFpsRange = it
229                 }
230                 captureRequest[CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE] == targetFpsRange
231             }
232 
233         bindUseCases(listOf(Preview.Builder()))
234 
235         val isCompleted = result.awaitUntil(timeoutMillis = 10000)
236         assertWithMessage(
237                 "Test failed for targetFpsRange = $targetFpsRange" +
238                     ", lastSubmittedFpsRange = $lastSubmittedFpsRange"
239             )
240             .that(isCompleted)
241             .isTrue()
242     }
243 
244     private fun getAeFpsRangeFromQuirks(): Range<Int>? = runBlocking {
245         val camera =
246             withContext(Dispatchers.Main) {
247                 cameraProvider.bindToLifecycle(fakeLifecycleOwner, cameraSelector)
248             }
249 
250         val quirks = (camera.cameraInfo as CameraInfoInternal).cameraQuirks
251         quirks.getAll(AeFpsRangeQuirk::class.java).firstOrNull()?.targetAeFpsRange
252     }
253 
254     // TODO: b/332464991 - Add a FPS test adding different FPS ranges to Preview & VideoCapture
255 
256     @Test
257     fun canSetAeTargetFpsRangeWithCamera2Interop() = runBlocking {
258         // At least 2 FPS ranges should be checked as the submitted range may just be from template
259         getSupportedFpsRanges().forEach { targetFpsRange ->
260             if (targetFpsRange.upper > 30) {
261                 // TODO: b/332464740 - High FPS may not be supported as per stream config map
262                 return@forEach
263             }
264 
265             var lastSubmittedFpsRange: Range<Int>? = null
266             val result =
267                 sessionCaptureCallback.verify { captureRequest, _ ->
268                     captureRequest[CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE]?.let {
269                         lastSubmittedFpsRange = it
270                     }
271                     captureRequest[CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE] == targetFpsRange
272                 }
273 
274             bindUseCases(
275                 listOf(
276                     // since Preview & VideoCapture already has FPS APIs, Camera2Interop isn't
277                     // needed
278                     // when they are bound. Also, ImageCapture-only is more complex due to
279                     // MeteringRepeating and may pick up further issues.
280                     ImageCapture.Builder().also {
281                         Camera2Interop.Extender(it)
282                             .setCaptureRequestOption(
283                                 CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE,
284                                 targetFpsRange
285                             )
286                     }
287                 )
288             )
289 
290             val isCompleted = result.awaitUntil(timeoutMillis = 10000)
291             assertWithMessage(
292                     "Test failed for FPS range = $targetFpsRange" +
293                         ", lastSubmittedFpsRange = $lastSubmittedFpsRange"
294                 )
295                 .that(isCompleted)
296                 .isTrue()
297 
298             unbindAllUseCases()
299 
300             // Checking for first supported & testable FPS range only
301             return@forEach
302         }
303     }
304 
305     @Test
306     fun canOverwriteFpsRangeWithCamera2Interop_whenAnotherSetViaSetTargetFrameRate() = runBlocking {
307         val targetFpsRange = getSupportedFpsRanges().firstOrNull { it.upper <= 30 }
308         val interopFpsRange = getSupportedFpsRanges().lastOrNull { it.upper <= 30 }
309         assumeTrue(
310             "Run the test only when two different supported FPS ranges can be found.",
311             targetFpsRange != null && interopFpsRange != null && targetFpsRange != interopFpsRange
312         )
313 
314         var lastSubmittedFpsRange: Range<Int>? = null
315         val result =
316             sessionCaptureCallback.verify { captureRequest, _ ->
317                 captureRequest[CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE]?.let {
318                     lastSubmittedFpsRange = it
319                 }
320                 captureRequest[CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE] == interopFpsRange
321             }
322 
323         bindUseCases(
324             listOf(
325                 Preview.Builder().setTargetFrameRate(targetFpsRange!!),
326                 // since Preview & VideoCapture already has FPS APIs, Camera2Interop isn't needed
327                 // when they are bound.
328                 ImageCapture.Builder().also {
329                     Camera2Interop.Extender(it)
330                         .setCaptureRequestOption(
331                             CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE,
332                             interopFpsRange!!
333                         )
334                 }
335             )
336         )
337 
338         val isCompleted = result.awaitUntil(timeoutMillis = 10000)
339         assertWithMessage(
340                 "Test failed for FPS range = $interopFpsRange" +
341                     ", lastSubmittedFpsRange = $lastSubmittedFpsRange"
342             )
343             .that(isCompleted)
344             .isTrue()
345     }
346 
347     @Test
348     @SdkSuppress(minSdkVersion = 33)
349     fun canEnablePreviewStabilization() = runBlocking {
350         val targetStabilizationMode = CONTROL_VIDEO_STABILIZATION_MODE_PREVIEW_STABILIZATION
351 
352         assumeTrue(
353             "Preview stabilization not supported",
354             getSupportedStabilizationModes().contains(targetStabilizationMode)
355         )
356 
357         var lastSubmittedMode: Int? = null
358         val result =
359             sessionCaptureCallback.verify { captureRequest, _ ->
360                 captureRequest[CONTROL_VIDEO_STABILIZATION_MODE]?.let { lastSubmittedMode = it }
361                 captureRequest[CONTROL_VIDEO_STABILIZATION_MODE] == targetStabilizationMode
362             }
363 
364         bindUseCases(
365             listOf(
366                 Preview.Builder().setPreviewStabilizationEnabled(true),
367                 VideoCapture.Builder(Recorder.Builder().build())
368             )
369         )
370 
371         val isCompleted = result.awaitUntil(timeoutMillis = 10000)
372         assertWithMessage(
373                 "Test failed for stabilization mode = $targetStabilizationMode" +
374                     ", lastSubmittedMode = $lastSubmittedMode"
375             )
376             .that(isCompleted)
377             .isTrue()
378     }
379 
380     @Test
381     fun canEnableVideoStabilization() = runBlocking {
382         val targetStabilizationMode = CONTROL_VIDEO_STABILIZATION_MODE_ON
383 
384         assumeTrue(
385             "Video stabilization not supported",
386             getSupportedStabilizationModes().contains(targetStabilizationMode)
387         )
388 
389         var lastSubmittedMode: Int? = null
390         val result =
391             sessionCaptureCallback.verify { captureRequest, _ ->
392                 captureRequest[CONTROL_VIDEO_STABILIZATION_MODE]?.let { lastSubmittedMode = it }
393                 captureRequest[CONTROL_VIDEO_STABILIZATION_MODE] == targetStabilizationMode
394             }
395 
396         bindUseCases(
397             listOf(
398                 Preview.Builder(),
399                 VideoCapture.Builder(Recorder.Builder().build()).setVideoStabilizationEnabled(true)
400             )
401         )
402 
403         val isCompleted = result.awaitUntil(timeoutMillis = 10000)
404         assertWithMessage(
405                 "Test failed for stabilization mode = $targetStabilizationMode" +
406                     ", lastSubmittedMode = $lastSubmittedMode"
407             )
408             .that(isCompleted)
409             .isTrue()
410     }
411 
412     @Test
413     @SdkSuppress(minSdkVersion = 33)
414     fun canEnablePreviewStabilization_whenBothPreviewAndVideoStabilizationEnabled() = runBlocking {
415         val targetStabilizationMode = CONTROL_VIDEO_STABILIZATION_MODE_PREVIEW_STABILIZATION
416 
417         assumeTrue(
418             "Preview stabilization not supported",
419             getSupportedStabilizationModes().contains(targetStabilizationMode)
420         )
421 
422         assumeTrue(
423             "Video stabilization not supported",
424             getSupportedStabilizationModes().contains(CONTROL_VIDEO_STABILIZATION_MODE_ON)
425         )
426 
427         var lastSubmittedMode: Int? = null
428         val result =
429             sessionCaptureCallback.verify { captureRequest, _ ->
430                 captureRequest[CONTROL_VIDEO_STABILIZATION_MODE]?.let { lastSubmittedMode = it }
431                 captureRequest[CONTROL_VIDEO_STABILIZATION_MODE] == targetStabilizationMode
432             }
433 
434         bindUseCases(
435             listOf(
436                 Preview.Builder().setPreviewStabilizationEnabled(true),
437                 VideoCapture.Builder(Recorder.Builder().build()).setVideoStabilizationEnabled(true)
438             )
439         )
440 
441         val isCompleted = result.awaitUntil(timeoutMillis = 10000)
442         assertWithMessage(
443                 "Test failed for stabilization mode = $targetStabilizationMode" +
444                     ", lastSubmittedMode = $lastSubmittedMode"
445             )
446             .that(isCompleted)
447             .isTrue()
448     }
449 
450     @Test
451     fun canSetStabilizationModeWithCamera2Interop() = runBlocking {
452         val targetStabilizationMode = CONTROL_VIDEO_STABILIZATION_MODE_ON
453 
454         assumeTrue(
455             "Video stabilization not supported",
456             getSupportedStabilizationModes().contains(targetStabilizationMode)
457         )
458 
459         var lastSubmittedMode: Int? = null
460         val result =
461             sessionCaptureCallback.verify { captureRequest, _ ->
462                 captureRequest[CONTROL_VIDEO_STABILIZATION_MODE]?.let { lastSubmittedMode = it }
463                 captureRequest[CONTROL_VIDEO_STABILIZATION_MODE] == targetStabilizationMode
464             }
465 
466         bindUseCases(
467             listOf(
468                 // since Preview & VideoCapture already has stabilization APIs, Camera2Interop isn't
469                 // needed when they are bound. Also, ImageCapture-only is more complex due to
470                 // MeteringRepeating and may pick up further issues.
471                 ImageCapture.Builder().also {
472                     Camera2Interop.Extender(it)
473                         .setCaptureRequestOption(
474                             CONTROL_VIDEO_STABILIZATION_MODE,
475                             targetStabilizationMode
476                         )
477                 }
478             )
479         )
480 
481         val isCompleted = result.awaitUntil(timeoutMillis = 10000)
482         assertWithMessage(
483                 "Test failed for stabilization mode = $targetStabilizationMode" +
484                     ", lastSubmittedMode = $lastSubmittedMode"
485             )
486             .that(isCompleted)
487             .isTrue()
488     }
489 
490     @Test
491     fun canOverwriteStabilizationWithCamera2Interop_whenEnabledAtVideoCapture() = runBlocking {
492         val targetStabilizationMode = CONTROL_VIDEO_STABILIZATION_MODE_OFF
493 
494         assumeTrue(
495             "Video stabilization not supported",
496             getSupportedStabilizationModes().contains(CONTROL_VIDEO_STABILIZATION_MODE_ON)
497         )
498 
499         var lastSubmittedMode: Int? = null
500         val result =
501             sessionCaptureCallback.verify { captureRequest, _ ->
502                 captureRequest[CONTROL_VIDEO_STABILIZATION_MODE]?.let { lastSubmittedMode = it }
503                 captureRequest[CONTROL_VIDEO_STABILIZATION_MODE] == targetStabilizationMode
504             }
505 
506         bindUseCases(
507             listOf(
508                 // since Preview & VideoCapture already has stabilization APIs, Camera2Interop isn't
509                 // needed when they are bound. Also, ImageCapture-only is more complex due to
510                 // MeteringRepeating and may pick up further issues.
511                 ImageAnalysis.Builder().also {
512                     Camera2Interop.Extender(it)
513                         .setCaptureRequestOption(
514                             CONTROL_VIDEO_STABILIZATION_MODE,
515                             targetStabilizationMode
516                         )
517                 },
518                 VideoCapture.Builder(Recorder.Builder().build()).setVideoStabilizationEnabled(true)
519             )
520         )
521 
522         val isCompleted = result.awaitUntil(timeoutMillis = 10000)
523         assertWithMessage(
524                 "Test failed for stabilization mode = $targetStabilizationMode" +
525                     ", lastSubmittedMode = $lastSubmittedMode"
526             )
527             .that(isCompleted)
528             .isTrue()
529     }
530 
531     // TODO - Adds tests to check capture option is consistent for both non-repeating and repeating
532     //  captures. E.g., FPS range is not submitted for non-repeating capture right now. But this
533     //  will probably require us to add Camera2Interop callback for non-repeating captures as well,
534     //  something that comes up every now and then, although low priority.
535 
536     private fun getSupportedFpsRanges(): Array<Range<Int>> {
537         val cameraCharacteristics = CameraUtil.getCameraCharacteristics(cameraSelector.lensFacing!!)
538         Assume.assumeNotNull(cameraCharacteristics)
539 
540         val fpsRanges = cameraCharacteristics!!.get(CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES)
541         Assume.assumeNotNull(fpsRanges)
542 
543         return fpsRanges!!
544     }
545 
546     private fun getSupportedStabilizationModes(): IntArray {
547         val cameraCharacteristics = CameraUtil.getCameraCharacteristics(cameraSelector.lensFacing!!)
548         Assume.assumeNotNull(cameraCharacteristics)
549 
550         val modes = cameraCharacteristics!!.get(CONTROL_AVAILABLE_VIDEO_STABILIZATION_MODES)
551         Assume.assumeNotNull(modes)
552 
553         return modes!!
554     }
555 
556     private fun isHwLevelLegacy(): Boolean {
557         val cameraCharacteristics = CameraUtil.getCameraCharacteristics(cameraSelector.lensFacing!!)
558         Assume.assumeNotNull(cameraCharacteristics)
559 
560         val hwLevel = cameraCharacteristics!!.get(INFO_SUPPORTED_HARDWARE_LEVEL)
561         Assume.assumeNotNull(hwLevel)
562 
563         return hwLevel == INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY
564     }
565 
566     private suspend fun bindUseCases(
567         useCaseBuilders: List<UseCaseConfig.Builder<*, *, *>> = listOf(Preview.Builder())
568     ) {
569         if (useCaseBuilders.isEmpty()) {
570             return
571         }
572 
573         withContext(Dispatchers.Main) {
574             val useCases = mutableListOf<UseCase>()
575 
576             useCaseBuilders.forEachIndexed { index, builder ->
577                 useCases.add(
578                     builder
579                         .also {
580                             if (index == 0) { // adding to just one use case is enough
581                                 Camera2InteropUtil.setCameraCaptureSessionCallback(
582                                     implName,
583                                     it,
584                                     sessionCaptureCallback
585                                 )
586                             }
587                         }
588                         .build()
589                         .apply {
590                             if (this is Preview) {
591                                 setSurfaceProvider(
592                                     SurfaceTextureProvider.createSurfaceTextureProvider()
593                                 )
594                             }
595                             if (this is ImageAnalysis) {
596                                 setAnalyzer(CameraXExecutors.directExecutor()) { imageProxy ->
597                                     imageProxy.close()
598                                 }
599                             }
600                         }
601                 )
602             }
603 
604             cameraProvider.bindToLifecycle(
605                 fakeLifecycleOwner,
606                 cameraSelector,
607                 *useCases.toTypedArray()
608             )
609         }
610     }
611 
612     private suspend fun unbindAllUseCases() {
613         withContext(Dispatchers.Main) { cameraProvider.unbindAll() }
614     }
615 
616     class CaptureCallback : CameraCaptureSession.CaptureCallback() {
617         data class Verification(
618             val condition:
619                 (captureRequest: CaptureRequest, captureResult: TotalCaptureResult) -> Boolean,
620             val isVerified: CompletableDeferred<Unit>
621         )
622 
623         private var pendingVerifications = mutableListOf<Verification>()
624 
625         /** Returns a [Deferred] representing if verification has been completed */
626         fun verify(
627             condition:
628                 (captureRequest: CaptureRequest, captureResult: TotalCaptureResult) -> Boolean =
629                 { _, _ ->
630                     false
631                 },
632         ): Deferred<Unit> =
633             CompletableDeferred<Unit>().apply {
634                 val verification = Verification(condition, this)
635                 pendingVerifications.add(verification)
636 
637                 invokeOnCompletion { pendingVerifications.remove(verification) }
638             }
639 
640         override fun onCaptureCompleted(
641             session: CameraCaptureSession,
642             request: CaptureRequest,
643             result: TotalCaptureResult
644         ) {
645             pendingVerifications.forEach {
646                 if (it.condition(request, result)) {
647                     it.isVerified.complete(Unit)
648                 }
649             }
650         }
651     }
652 
653     companion object {
654         @JvmStatic
655         @Parameterized.Parameters(name = "selector={0},config={2}")
656         fun data() =
657             listOf(
658                 arrayOf(
659                     "back",
660                     CameraSelector.DEFAULT_BACK_CAMERA,
661                     Camera2Config::class.simpleName,
662                     Camera2Config.defaultConfig()
663                 ),
664                 arrayOf(
665                     "back",
666                     CameraSelector.DEFAULT_BACK_CAMERA,
667                     CameraPipeConfig::class.simpleName,
668                     CameraPipeConfig.defaultConfig()
669                 ),
670                 // front camera is not important with the current test, but may be required in
671                 // future
672             )
673     }
674 }
675