@@ -19,27 +19,39 @@ import android.app.Application
1919import android.content.ContentResolver
2020import android.graphics.SurfaceTexture
2121import android.net.Uri
22+ import android.util.Log
2223import android.view.Surface
2324import androidx.annotation.GuardedBy
2425import androidx.concurrent.futures.DirectExecutor
2526import androidx.test.ext.junit.runners.AndroidJUnit4
2627import androidx.test.filters.LargeTest
2728import androidx.test.platform.app.InstrumentationRegistry
2829import androidx.test.rule.GrantPermissionRule
30+ import com.google.common.truth.Truth.assertThat
31+ import com.google.jetpackcamera.core.camera.CameraXCameraSystemTest.Feature.DYNAMIC_RANGE_HLG10
32+ import com.google.jetpackcamera.core.camera.CameraXCameraSystemTest.Feature.FPS_60
33+ import com.google.jetpackcamera.core.camera.CameraXCameraSystemTest.Feature.IMAGE_FORMAT_JPEG_ULTRA_HDR
34+ import com.google.jetpackcamera.core.camera.CameraXCameraSystemTest.Feature.STABILIZATION_MODE_ON
35+ import com.google.jetpackcamera.core.camera.CameraXCameraSystemTest.Feature.STREAM_CONFIG_SINGLE
36+ import com.google.jetpackcamera.core.camera.CameraXCameraSystemTest.Feature.VIDEO_QUALITY_UHD
2937import com.google.jetpackcamera.core.camera.OnVideoRecordEvent.OnVideoRecordError
3038import com.google.jetpackcamera.core.camera.OnVideoRecordEvent.OnVideoRecorded
3139import com.google.jetpackcamera.core.camera.utils.APP_REQUIRED_PERMISSIONS
3240import com.google.jetpackcamera.core.common.FakeFilePathGenerator
3341import com.google.jetpackcamera.model.DynamicRange
3442import com.google.jetpackcamera.model.FlashMode
3543import com.google.jetpackcamera.model.Illuminant
44+ import com.google.jetpackcamera.model.ImageOutputFormat
3645import com.google.jetpackcamera.model.LensFacing
3746import com.google.jetpackcamera.model.SaveLocation
47+ import com.google.jetpackcamera.model.StabilizationMode
48+ import com.google.jetpackcamera.model.StreamConfig
3849import com.google.jetpackcamera.model.VideoQuality
3950import com.google.jetpackcamera.settings.ConstraintsRepository
4051import com.google.jetpackcamera.settings.SettableConstraintsRepository
4152import com.google.jetpackcamera.settings.SettableConstraintsRepositoryImpl
4253import com.google.jetpackcamera.settings.model.CameraAppSettings
54+ import com.google.jetpackcamera.settings.model.CameraConstraints
4355import com.google.jetpackcamera.settings.model.CameraSystemConstraints
4456import com.google.jetpackcamera.settings.model.DEFAULT_CAMERA_APP_SETTINGS
4557import java.io.File
@@ -80,6 +92,7 @@ class CameraXCameraSystemTest {
8092 private const val GENERAL_TIMEOUT_MS = 3_000L
8193 private const val RECORDING_TIMEOUT_MS = 10_000L
8294 private const val RECORDING_START_DURATION_MS = 500L
95+ private const val TAG = " CameraXCameraSystemTest"
8396 }
8497
8598 @get:Rule
@@ -171,65 +184,129 @@ class CameraXCameraSystemTest {
171184 }
172185
173186 @Test
174- fun setMultipleFeatures_systemConstraintsUpdatedAndFeaturesSettableIfSupported () = runBlocking {
175- // Arrange.
176- val constraintsRepository = ObservableConstraintsRepository ()
177- val cameraSystem = createAndInitCameraXCameraSystem(
178- constraintsRepository = constraintsRepository
187+ fun setMultipleFeatures_systemConstraintsUpdatedAndFeaturesSetIfSupported () = runBlocking {
188+ // TODO: Add STREAM_CONFIG_SINGLE to the featuresToTest list. This currently leads to flaky
189+ // crashes due to some camera effect related surface not being cleaned up properly somehow.
190+ // This doesn't seem to be related to the primary purpose of this test, so simply excluding
191+ // it for now.
192+ val featuresToTest = listOf (
193+ DYNAMIC_RANGE_HLG10 ,
194+ FPS_60 ,
195+ VIDEO_QUALITY_UHD
179196 )
180197
181- // Each camera run/update should lead to a new systemConstraints update
182- var systemConstraints = constraintsRepository.observeNextUpdate()
183- cameraSystem.runCameraOnMain()
198+ featuresToTest.permutations().forEach { orderedFeatures ->
199+ Log .d(TAG , " Testing $orderedFeatures " )
184200
185- val lensFacing = cameraSystem.getCurrentSettings().value?.cameraLensFacing
201+ // Setup
202+ val constraintsRepository = ObservableConstraintsRepository ()
203+ val cameraSystem = createAndInitCameraXCameraSystem(
204+ constraintsRepository = constraintsRepository
205+ )
186206
187- // Act: For each of the features — HDR, 60 FPS, UHD recording, await previous constraints
188- // update and set the feature if the constraints supports it.
207+ // Initial run: each camera run/update should lead to a new systemConstraints update
208+ var currentConstraints = constraintsRepository.observeNextUpdate().let {
209+ cameraSystem.runCameraOnMain()
210+ it.awaitUntil()
211+ }
189212
190- if (
191- systemConstraints
192- .awaitUntil()
193- .perLensConstraints[lensFacing]
194- ?.supportedDynamicRanges
195- ?.contains(DynamicRange .HLG10 ) == true
196- ) {
197- systemConstraints = constraintsRepository.observeNextUpdate()
198- cameraSystem.setDynamicRange(DynamicRange .HLG10 )
199- }
213+ val lensFacing =
214+ requireNotNull(cameraSystem.getCurrentSettings().value?.cameraLensFacing)
215+
216+ orderedFeatures.forEach { feature ->
217+ currentConstraints = when (feature) {
218+ DYNAMIC_RANGE_HLG10 -> feature.tryApplyFeature(
219+ expectedValue = DynamicRange .HLG10 ,
220+ lensFacing = lensFacing,
221+ cameraSystemConstraints = currentConstraints,
222+ constraintsRepository = constraintsRepository,
223+ cameraSystem = cameraSystem,
224+ setFeature = { cameraSystem.setDynamicRange(DynamicRange .HLG10 ) },
225+ getNewFeatureValue = { it?.dynamicRange }
226+ ) { constraints ->
227+ constraints
228+ ?.supportedDynamicRanges
229+ ?.contains(DynamicRange .HLG10 ) == true
230+ }
200231
201- if (
202- systemConstraints
203- .awaitUntil()
204- .perLensConstraints[lensFacing]
205- ?.supportedFixedFrameRates
206- ?.contains(60 ) == true
207- ) {
208- systemConstraints = constraintsRepository.observeNextUpdate()
209- cameraSystem.setTargetFrameRate(60 )
210- }
232+ FPS_60 -> feature.tryApplyFeature(
233+ expectedValue = 60 ,
234+ lensFacing = lensFacing,
235+ cameraSystemConstraints = currentConstraints,
236+ constraintsRepository = constraintsRepository,
237+ cameraSystem = cameraSystem,
238+ setFeature = { cameraSystem.setTargetFrameRate(60 ) },
239+ getNewFeatureValue = { it?.targetFrameRate }
240+ ) { constraints ->
241+ constraints
242+ ?.supportedFixedFrameRates
243+ ?.contains(60 ) == true
244+ }
211245
212- if (
213- systemConstraints
214- .awaitUntil()
215- .perLensConstraints[lensFacing]
216- ?.supportedVideoQualitiesMap
217- ?.get(cameraSystem.getCurrentSettings().value?.dynamicRange)
218- ?.contains(VideoQuality .UHD ) == true
219- ) {
220- systemConstraints = constraintsRepository.observeNextUpdate()
221- cameraSystem.setVideoQuality(VideoQuality .UHD )
222- }
246+ VIDEO_QUALITY_UHD -> feature.tryApplyFeature(
247+ expectedValue = VideoQuality .UHD ,
248+ lensFacing = lensFacing,
249+ cameraSystemConstraints = currentConstraints,
250+ constraintsRepository = constraintsRepository,
251+ cameraSystem = cameraSystem,
252+ setFeature = { cameraSystem.setVideoQuality(VideoQuality .UHD ) },
253+ getNewFeatureValue = { it?.videoQuality }
254+ ) { constraints ->
255+ constraints
256+ ?.supportedVideoQualitiesMap
257+ ?.get(cameraSystem.getCurrentSettings().value?.dynamicRange)
258+ ?.contains(VideoQuality .UHD ) == true
259+ }
223260
224- // Wait to ensure the async updateSystemConstraintsByFeatureGroups has time to run
225- // and potentially crash if there's an issue.
226- systemConstraints.awaitUntil()
261+ STABILIZATION_MODE_ON -> feature.tryApplyFeature(
262+ expectedValue = StabilizationMode .ON ,
263+ lensFacing = lensFacing,
264+ cameraSystemConstraints = currentConstraints,
265+ constraintsRepository = constraintsRepository,
266+ cameraSystem = cameraSystem,
267+ setFeature = { cameraSystem.setStabilizationMode(StabilizationMode .ON ) },
268+ getNewFeatureValue = { it?.stabilizationMode }
269+ ) { constraints ->
270+ constraints
271+ ?.supportedStabilizationModes
272+ ?.contains(StabilizationMode .ON ) == true
273+ }
227274
228- // Assert.
229- // If the test reaches here without crashing, it passes.
230- // This ensures that the feature group logic doesn't cause runtime exceptions
231- // even when high-end features are requested.
232- return @runBlocking
275+ IMAGE_FORMAT_JPEG_ULTRA_HDR -> feature.tryApplyFeature(
276+ expectedValue = ImageOutputFormat .JPEG_ULTRA_HDR ,
277+ lensFacing = lensFacing,
278+ cameraSystemConstraints = currentConstraints,
279+ constraintsRepository = constraintsRepository,
280+ cameraSystem = cameraSystem,
281+ setFeature = {
282+ cameraSystem.setImageFormat(
283+ ImageOutputFormat .JPEG_ULTRA_HDR
284+ )
285+ },
286+ getNewFeatureValue = { it?.imageFormat }
287+ ) { constraints ->
288+ constraints
289+ ?.supportedImageFormatsMap
290+ ?.get(cameraSystem.getCurrentSettings().value?.streamConfig)
291+ ?.contains(ImageOutputFormat .JPEG_ULTRA_HDR ) == true
292+ }
293+
294+ STREAM_CONFIG_SINGLE -> feature.tryApplyFeature(
295+ expectedValue = StreamConfig .SINGLE_STREAM ,
296+ lensFacing = lensFacing,
297+ cameraSystemConstraints = currentConstraints,
298+ constraintsRepository = constraintsRepository,
299+ cameraSystem = cameraSystem,
300+ setFeature = { cameraSystem.setStreamConfig(StreamConfig .SINGLE_STREAM ) },
301+ getNewFeatureValue = { it?.streamConfig }
302+ ) { constraints ->
303+ constraints
304+ ?.supportedStreamConfigs
305+ ?.contains(StreamConfig .SINGLE_STREAM ) == true
306+ }
307+ }
308+ }
309+ }
233310 }
234311
235312 suspend fun <T > Deferred<T>.awaitUntil (timeout : Duration = 2.seconds): T {
@@ -285,7 +362,8 @@ class CameraXCameraSystemTest {
285362 getCurrentCameraState().transform { cameraState ->
286363 (cameraState.videoRecordingState as ? VideoRecordingState .Active )?.let {
287364 emit(
288- it.elapsedTimeNanos.toDuration(DurationUnit .NANOSECONDS ).inWholeMilliseconds
365+ it.elapsedTimeNanos
366+ .toDuration(DurationUnit .NANOSECONDS ).inWholeMilliseconds
289367 )
290368 }
291369 }.first { elapsedTimeMs ->
@@ -334,6 +412,71 @@ class CameraXCameraSystemTest {
334412 }
335413 }
336414
415+ suspend fun <T > Feature.tryApplyFeature (
416+ expectedValue : T ,
417+ lensFacing : LensFacing ,
418+ cameraSystemConstraints : CameraSystemConstraints ,
419+ constraintsRepository : ObservableConstraintsRepository ,
420+ cameraSystem : CameraSystem ,
421+ setFeature : suspend () -> Unit ,
422+ getNewFeatureValue : (CameraAppSettings ? ) -> T ? ,
423+ isSupported : (CameraConstraints ? ) -> Boolean
424+ ): CameraSystemConstraints {
425+ // Check support
426+ if (! isSupported(cameraSystemConstraints.perLensConstraints[lensFacing])) {
427+ Log .d(TAG , " Skipping $this : Not supported by current constraints." )
428+ return cameraSystemConstraints
429+ }
430+
431+ Log .d(TAG , " Applying $this ..." )
432+
433+ // Prepare observer
434+ val nextUpdate = constraintsRepository.observeNextUpdate()
435+
436+ setFeature()
437+
438+ // Wait to verify constraints is updated
439+ val newConstraints = nextUpdate.awaitUntil()
440+
441+ // Verify feature is set according to current settings
442+ assertThat(getNewFeatureValue(cameraSystem.getCurrentSettings().value)).isEqualTo(
443+ expectedValue
444+ )
445+
446+ return newConstraints
447+ }
448+
449+ fun <T > List<T>.permutations (): List <List <T >> {
450+ if (isEmpty()) {
451+ // Base case: an empty list has one permutation (the empty list itself)
452+ return listOf (emptyList())
453+ }
454+
455+ val result = mutableListOf<List <T >>()
456+ val head = first() // Take the first element
457+ val tail = drop(1 ) // Get the rest of the list
458+
459+ // Recursively get permutations of the tail
460+ tail.permutations().forEach { permOfTail ->
461+ // Insert the head element at all possible positions in each permutation of the tail
462+ for (i in 0 .. permOfTail.size) {
463+ val newPerm = permOfTail.toMutableList()
464+ newPerm.add(i, head)
465+ result.add(newPerm)
466+ }
467+ }
468+ return result
469+ }
470+
471+ enum class Feature {
472+ DYNAMIC_RANGE_HLG10 ,
473+ FPS_60 ,
474+ VIDEO_QUALITY_UHD ,
475+ STABILIZATION_MODE_ON ,
476+ IMAGE_FORMAT_JPEG_ULTRA_HDR ,
477+ STREAM_CONFIG_SINGLE
478+ }
479+
337480 class ObservableConstraintsRepository : SettableConstraintsRepository {
338481 private val lock = Object ()
339482
0 commit comments