From b222813c5830511af0403655db4f558e4764e583 Mon Sep 17 00:00:00 2001 From: Kimberly Crevecoeur Date: Mon, 1 Jun 2026 18:22:48 -0700 Subject: [PATCH 1/7] Decouple HDR settings from capture modes - Decoupled dynamic range (video HDR) from image format (image HDR) settings across UI, controller, and CameraX configuration layers. - Removed dynamic range constraints from the createImageUseCase configuration in CameraSession.kt, enabling independent Ultra HDR image capture. - Updated QuickSettings bottom sheet click handlers to mutate only the HDR setting relevant to the active capture mode. - Enforced specialized Low Light Boost vs Ultra HDR conflicts in CameraXCameraSystem.kt, prioritizing Low Light Boost. - Created HdrUiStateAdapterTest.kt covering all HDR availability states and flash conflicts. - Refactored CameraXCameraSystemTest.kt to run parameterized HDR decoupling tests on both front and rear lenses. --- .../core/camera/CameraXCameraSystemTest.kt | 187 +++++++++++++++ .../core/camera/CameraSession.kt | 6 +- .../core/camera/CameraXCameraSystem.kt | 48 ++-- .../quicksettings/QuickSettingsScreen.kt | 19 +- .../capture/HdrUiStateAdapter.kt | 70 +++--- .../capture/HdrUiStateAdapterTest.kt | 216 ++++++++++++++++++ 6 files changed, 476 insertions(+), 70 deletions(-) create mode 100644 ui/uistateadapter/capture/src/test/java/com/google/jetpackcamera/ui/uistateadapter/capture/HdrUiStateAdapterTest.kt diff --git a/core/camera/src/androidTest/java/com/google/jetpackcamera/core/camera/CameraXCameraSystemTest.kt b/core/camera/src/androidTest/java/com/google/jetpackcamera/core/camera/CameraXCameraSystemTest.kt index f38b7e2e2..df5ebd601 100644 --- a/core/camera/src/androidTest/java/com/google/jetpackcamera/core/camera/CameraXCameraSystemTest.kt +++ b/core/camera/src/androidTest/java/com/google/jetpackcamera/core/camera/CameraXCameraSystemTest.kt @@ -34,8 +34,10 @@ import com.google.jetpackcamera.core.camera.utils.APP_REQUIRED_PERMISSIONS import com.google.jetpackcamera.core.camera.utils.provideUpdatingSurface import com.google.jetpackcamera.core.common.testing.FakeFilePathGenerator import com.google.jetpackcamera.model.CaptureMode +import com.google.jetpackcamera.model.DynamicRange import com.google.jetpackcamera.model.FlashMode import com.google.jetpackcamera.model.Illuminant +import com.google.jetpackcamera.model.ImageOutputFormat import com.google.jetpackcamera.model.LensFacing import com.google.jetpackcamera.model.SaveLocation import com.google.jetpackcamera.model.StabilizationMode @@ -454,6 +456,191 @@ class CameraXCameraSystemTest { ) ) } + + @Test + fun switchCaptureMode_toStandard_disablesHdr_back(): Unit = runBlocking { + runSwitchCaptureMode_toStandard_disablesHdr_test(LensFacing.BACK) + } + + @Test + fun switchCaptureMode_toStandard_disablesHdr_front(): Unit = runBlocking { + runSwitchCaptureMode_toStandard_disablesHdr_test(LensFacing.FRONT) + } + + @Test + fun switchCaptureMode_preservesVideoHdr_back(): Unit = runBlocking { + runSwitchCaptureMode_preservesVideoHdr_test(LensFacing.BACK) + } + + @Test + fun switchCaptureMode_preservesVideoHdr_front(): Unit = runBlocking { + runSwitchCaptureMode_preservesVideoHdr_test(LensFacing.FRONT) + } + + @Test + fun switchCaptureMode_preservesImageHdr_back(): Unit = runBlocking { + runSwitchCaptureMode_preservesImageHdr_test(LensFacing.BACK) + } + + @Test + fun switchCaptureMode_preservesImageHdr_front(): Unit = runBlocking { + runSwitchCaptureMode_preservesImageHdr_test(LensFacing.FRONT) + } + + private suspend fun CoroutineScope.runSwitchCaptureMode_toStandard_disablesHdr_test( + lensFacing: LensFacing + ) { + // Arrange. Initialize with default settings to query constraints safely. + val cameraSystem = createAndInitCameraXCameraSystem() + val systemConstraints = cameraSystem.getSystemConstraints().value + val cameraConstraints = systemConstraints?.perLensConstraints?.get(lensFacing) + + // This instrumented test runs on real hardware/emulator. Since we cannot mock the + // device's actual HDR capabilities, we use assume() to gracefully skip the test + // if the specified lens is not available or does not support HDR video (HLG10). + assume().withMessage("HDR video not supported on $lensFacing, skip the test.") + .that( + cameraConstraints != null && + cameraConstraints.supportedDynamicRanges.contains(DynamicRange.HLG10) + ).isTrue() + + // Configure the camera to use the target lens and enable HDR video + cameraSystem.setLensFacing(lensFacing) + cameraSystem.setCaptureMode(CaptureMode.VIDEO_ONLY) + cameraSystem.setDynamicRange(DynamicRange.HLG10) + + cameraSystem.startCameraAndWaitUntilRunning() + + val dynamicRangeCheck = cameraSystem.getCurrentSettings() + .filterNotNull() + .map { it.dynamicRange } + .produceIn(this) + + // Ensure we start in HLG10 + dynamicRangeCheck.awaitValue(DynamicRange.HLG10) + + // Act. Switch to STANDARD mode + cameraSystem.setCaptureMode(CaptureMode.STANDARD) + + // Assert. Dynamic range should fallback to SDR because STANDARD doesn't support HDR + dynamicRangeCheck.awaitValue(DynamicRange.SDR) + + // Clean-up. + dynamicRangeCheck.cancel() + } + + private suspend fun CoroutineScope.runSwitchCaptureMode_preservesVideoHdr_test( + lensFacing: LensFacing + ) { + // Arrange. Initialize with default settings to query constraints safely. + val cameraSystem = createAndInitCameraXCameraSystem() + val systemConstraints = cameraSystem.getSystemConstraints().value + val cameraConstraints = systemConstraints?.perLensConstraints?.get(lensFacing) + + // This instrumented test runs on real hardware/emulator. Since we cannot mock the + // device's actual HDR capabilities, we use assume() to gracefully skip the test + // if the specified lens is not available or does not support HDR video (HLG10). + assume().withMessage("HDR video not supported on $lensFacing, skip the test.") + .that( + cameraConstraints != null && + cameraConstraints.supportedDynamicRanges.contains(DynamicRange.HLG10) + ).isTrue() + + // Configure the camera to use the target lens and enable HDR video + cameraSystem.setLensFacing(lensFacing) + cameraSystem.setCaptureMode(CaptureMode.VIDEO_ONLY) + cameraSystem.setDynamicRange(DynamicRange.HLG10) + cameraSystem.setImageFormat(ImageOutputFormat.JPEG) + + cameraSystem.startCameraAndWaitUntilRunning() + + val settingsCheck = cameraSystem.getCurrentSettings() + .filterNotNull() + .produceIn(this) + + // Ensure we start in VIDEO_ONLY with HLG10 + var settings = settingsCheck.receive() + assertThat(settings.captureMode).isEqualTo(CaptureMode.VIDEO_ONLY) + assertThat(settings.dynamicRange).isEqualTo(DynamicRange.HLG10) + assertThat(settings.imageFormat).isEqualTo(ImageOutputFormat.JPEG) + + // Act. Switch to IMAGE_ONLY + cameraSystem.setCaptureMode(CaptureMode.IMAGE_ONLY) + + // Assert. Image format should be JPEG (SDR), but dynamicRange should still be HLG10 in settings + settings = settingsCheck.receive() + assertThat(settings.captureMode).isEqualTo(CaptureMode.IMAGE_ONLY) + assertThat(settings.imageFormat).isEqualTo(ImageOutputFormat.JPEG) + assertThat(settings.dynamicRange).isEqualTo(DynamicRange.HLG10) // Preserved! + + // Act. Switch back to VIDEO_ONLY + cameraSystem.setCaptureMode(CaptureMode.VIDEO_ONLY) + + // Assert. Should be back to VIDEO_ONLY with HLG10 + settings = settingsCheck.receive() + assertThat(settings.captureMode).isEqualTo(CaptureMode.VIDEO_ONLY) + assertThat(settings.dynamicRange).isEqualTo(DynamicRange.HLG10) + + // Clean-up. + settingsCheck.cancel() + } + + private suspend fun CoroutineScope.runSwitchCaptureMode_preservesImageHdr_test( + lensFacing: LensFacing + ) { + // Arrange. Initialize with default settings to query constraints safely. + val cameraSystem = createAndInitCameraXCameraSystem() + val systemConstraints = cameraSystem.getSystemConstraints().value + val cameraConstraints = systemConstraints?.perLensConstraints?.get(lensFacing) + + // This instrumented test runs on real hardware/emulator. Since we cannot mock the + // device's actual Ultra HDR capabilities, we use assume() to gracefully skip the test + // if the specified lens is not available or does not support Ultra HDR. + assume().withMessage("Ultra HDR not supported on $lensFacing, skip the test.") + .that( + cameraConstraints != null && + cameraConstraints.supportedImageFormatsMap[DEFAULT_CAMERA_APP_SETTINGS.streamConfig] + ?.contains(ImageOutputFormat.JPEG_ULTRA_HDR) == true + ).isTrue() + + // Configure the camera to use the target lens and enable Ultra HDR + cameraSystem.setLensFacing(lensFacing) + cameraSystem.setCaptureMode(CaptureMode.IMAGE_ONLY) + cameraSystem.setImageFormat(ImageOutputFormat.JPEG_ULTRA_HDR) + cameraSystem.setDynamicRange(DynamicRange.SDR) + + cameraSystem.startCameraAndWaitUntilRunning() + + val settingsCheck = cameraSystem.getCurrentSettings() + .filterNotNull() + .produceIn(this) + + // Ensure we start in IMAGE_ONLY with ULTRA_HDR + var settings = settingsCheck.receive() + assertThat(settings.captureMode).isEqualTo(CaptureMode.IMAGE_ONLY) + assertThat(settings.imageFormat).isEqualTo(ImageOutputFormat.JPEG_ULTRA_HDR) + assertThat(settings.dynamicRange).isEqualTo(DynamicRange.SDR) + + // Act. Switch to VIDEO_ONLY + cameraSystem.setCaptureMode(CaptureMode.VIDEO_ONLY) + + // Assert. Dynamic range should be SDR, but imageFormat should still be ULTRA_HDR in settings + settings = settingsCheck.receive() + assertThat(settings.captureMode).isEqualTo(CaptureMode.VIDEO_ONLY) + assertThat(settings.dynamicRange).isEqualTo(DynamicRange.SDR) + assertThat(settings.imageFormat).isEqualTo(ImageOutputFormat.JPEG_ULTRA_HDR) // Preserved! + + // Act. Switch back to IMAGE_ONLY + cameraSystem.setCaptureMode(CaptureMode.IMAGE_ONLY) + + // Assert. Should be back to IMAGE_ONLY with ULTRA_HDR + settings = settingsCheck.receive() + assertThat(settings.captureMode).isEqualTo(CaptureMode.IMAGE_ONLY) + assertThat(settings.imageFormat).isEqualTo(ImageOutputFormat.JPEG_ULTRA_HDR) + + // Clean-up. + settingsCheck.cancel() + } } object FakeImagePostProcessorFeatureKey : ImagePostProcessorFeatureKey diff --git a/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraSession.kt b/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraSession.kt index 2e23459c4..8e5bb9824 100644 --- a/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraSession.kt +++ b/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraSession.kt @@ -607,7 +607,7 @@ internal fun createUseCaseGroup( // only create image use case in image or standard val imageCaptureUseCase = if (captureMode != CaptureMode.VIDEO_ONLY) { - createImageUseCase(cameraInfo, aspectRatio, dynamicRange, imageFormat) + createImageUseCase(cameraInfo, aspectRatio, imageFormat) } else { null } @@ -666,15 +666,13 @@ private fun getHeightFromCropRect(cropRect: Rect?): Int { private fun createImageUseCase( cameraInfo: CameraInfo, aspectRatio: AspectRatio, - dynamicRange: DynamicRange, imageFormat: ImageOutputFormat ): ImageCapture { val builder = ImageCapture.Builder() builder.setResolutionSelector( getResolutionSelector(cameraInfo.sensorLandscapeRatio, aspectRatio) ) - if (dynamicRange != DynamicRange.SDR && imageFormat == ImageOutputFormat.JPEG_ULTRA_HDR - ) { + if (imageFormat == ImageOutputFormat.JPEG_ULTRA_HDR) { builder.setOutputFormat(ImageCapture.OUTPUT_FORMAT_JPEG_ULTRA_HDR) } return builder.build() diff --git a/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraXCameraSystem.kt b/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraXCameraSystem.kt index 134e3bcfb..e9e007b28 100644 --- a/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraXCameraSystem.kt +++ b/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraXCameraSystem.kt @@ -672,6 +672,9 @@ class CameraXCameraSystem( // Sets the camera to the designated lensFacing direction override suspend fun setLensFacing(lensFacing: LensFacing) { + // TODO: Handle lens flipping during recording when only one lens supports HDR. + // We should define the expected behavior (e.g., disable flip button, stop recording with error, + // or fallback to SDR mid-recording if supported by CameraX). currentSettings.update { old -> if (systemConstraints.availableLenses.contains(lensFacing)) { old?.copy(cameraLensFacing = lensFacing) @@ -706,29 +709,6 @@ class CameraXCameraSystem( // concurrent currently only supports VIDEO_ONLY if (concurrentCameraMode == ConcurrentCameraMode.DUAL) { CaptureMode.VIDEO_ONLY - } - - // if hdr is enabled... - else if (imageFormat == ImageOutputFormat.JPEG_ULTRA_HDR || - dynamicRange == DynamicRange.HLG10 - ) { - // if both hdr video and image capture are supported, default to VIDEO_ONLY - if (constraints.supportedDynamicRanges.contains(DynamicRange.HLG10) && - constraints.supportedImageFormatsMap[streamConfig] - ?.contains(ImageOutputFormat.JPEG_ULTRA_HDR) == true - ) { - if (captureMode == CaptureMode.STANDARD) { - CaptureMode.VIDEO_ONLY - } else { - return this - } - } - // return appropriate capture mode if only one is supported - else if (imageFormat == ImageOutputFormat.JPEG_ULTRA_HDR) { - CaptureMode.IMAGE_ONLY - } else { - CaptureMode.VIDEO_ONLY - } } else { defaultCaptureMode ?: return this } @@ -784,10 +764,13 @@ class CameraXCameraSystem( systemConstraints.perLensConstraints[cameraLensFacing]?.let { constraints -> with(constraints.supportedDynamicRanges) { val newDynamicRange = if (contains(dynamicRange) && - flashMode != FlashMode.LOW_LIGHT_BOOST + flashMode != FlashMode.LOW_LIGHT_BOOST && + captureMode != CaptureMode.STANDARD ) { dynamicRange } else { + // TODO: Consider preserving user preference for HDR instead of permanently + // resetting to SDR here when switching lenses. DynamicRange.SDR } @@ -811,9 +794,15 @@ class CameraXCameraSystem( private fun CameraAppSettings.tryApplyImageFormatConstraints(): CameraAppSettings = systemConstraints.perLensConstraints[cameraLensFacing]?.let { constraints -> with(constraints.supportedImageFormatsMap[streamConfig]) { - val newImageFormat = if (this != null && contains(imageFormat)) { + // Prioritize Low Light Boost over Ultra HDR to maintain consistency with + // Video HDR / Low Light Boost conflict resolution. + val newImageFormat = if (this != null && contains(imageFormat) && + captureMode != CaptureMode.STANDARD && + flashMode != FlashMode.LOW_LIGHT_BOOST + ) { imageFormat } else { + // TODO: Consider preserving user preference for HDR instead of permanently resetting to JPEG here when switching lenses. ImageOutputFormat.JPEG } @@ -927,6 +916,7 @@ class CameraXCameraSystem( currentSettings.update { old -> old?.copy(flashMode = flashMode) ?.tryApplyDynamicRangeConstraints() + ?.tryApplyImageFormatConstraints() ?.tryApplyConcurrentCameraModeConstraints() } } @@ -969,7 +959,7 @@ class CameraXCameraSystem( old?.copy(dynamicRange = dynamicRange) ?.tryApplyDynamicRangeConstraints() ?.tryApplyConcurrentCameraModeConstraints() - ?.tryApplyCaptureModeConstraints(CaptureMode.STANDARD) + ?.tryApplyCaptureModeConstraints() } } @@ -983,7 +973,7 @@ class CameraXCameraSystem( currentSettings.update { old -> old?.copy(concurrentCameraMode = concurrentCameraMode) ?.tryApplyConcurrentCameraModeConstraints() - ?.tryApplyCaptureModeConstraints(CaptureMode.STANDARD) + ?.tryApplyCaptureModeConstraints() } } @@ -991,7 +981,7 @@ class CameraXCameraSystem( currentSettings.update { old -> old?.copy(imageFormat = imageFormat) ?.tryApplyImageFormatConstraints() - ?.tryApplyCaptureModeConstraints(CaptureMode.STANDARD) + ?.tryApplyCaptureModeConstraints() } } @@ -1025,6 +1015,8 @@ class CameraXCameraSystem( override suspend fun setCaptureMode(captureMode: CaptureMode) { currentSettings.update { old -> old?.copy(captureMode = captureMode) + ?.tryApplyDynamicRangeConstraints() + ?.tryApplyImageFormatConstraints() } } diff --git a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/QuickSettingsScreen.kt b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/QuickSettingsScreen.kt index f7eff696e..44538d341 100644 --- a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/QuickSettingsScreen.kt +++ b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/QuickSettingsScreen.kt @@ -159,11 +159,26 @@ fun QuickSettingsBottomSheet( } add { + val captureMode = (quickSettingsUiState.captureModeUiState as? CaptureModeUiState.Available)?.selectedCaptureMode QuickSetHdr( modifier = Modifier.testTag(QUICK_SETTINGS_HDR_BUTTON), onClick = { d: DynamicRange, i: ImageOutputFormat -> - quickSettingsController.setDynamicRange(d) - quickSettingsController.setImageFormat(i) + when (captureMode) { + CaptureMode.IMAGE_ONLY -> { + quickSettingsController.setImageFormat(i) + } + CaptureMode.VIDEO_ONLY -> { + quickSettingsController.setDynamicRange(d) + } + CaptureMode.STANDARD -> { + quickSettingsController.setDynamicRange(d) + quickSettingsController.setImageFormat(i) + } + null -> { + quickSettingsController.setDynamicRange(d) + quickSettingsController.setImageFormat(i) + } + } }, hdrUiState = quickSettingsUiState.hdrUiState ) diff --git a/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/HdrUiStateAdapter.kt b/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/HdrUiStateAdapter.kt index e40480d00..cc84f931a 100644 --- a/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/HdrUiStateAdapter.kt +++ b/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/HdrUiStateAdapter.kt @@ -15,9 +15,11 @@ */ package com.google.jetpackcamera.ui.uistateadapter.capture +import com.google.jetpackcamera.model.CaptureMode import com.google.jetpackcamera.model.ConcurrentCameraMode import com.google.jetpackcamera.model.DynamicRange import com.google.jetpackcamera.model.ExternalCaptureMode +import com.google.jetpackcamera.model.ExternalCaptureMode.Companion.toCaptureMode import com.google.jetpackcamera.model.FlashMode import com.google.jetpackcamera.model.ImageOutputFormat import com.google.jetpackcamera.settings.model.CameraAppSettings @@ -58,45 +60,41 @@ fun HdrUiState.Companion.from( val cameraConstraints: CameraConstraints? = systemConstraints.forCurrentLens( cameraAppSettings ) - return when (externalCaptureMode) { - ExternalCaptureMode.ImageCapture, - ExternalCaptureMode.MultipleImageCapture -> if ( - cameraConstraints + + // Determine active capture mode, respecting external override + val activeCaptureMode = externalCaptureMode.toCaptureMode() ?: cameraAppSettings.captureMode + + return when (activeCaptureMode) { + CaptureMode.IMAGE_ONLY -> { + val supportsHdrImage = cameraConstraints ?.supportedImageFormatsMap?.get(cameraAppSettings.streamConfig) - ?.contains(ImageOutputFormat.JPEG_ULTRA_HDR) ?: false && - cameraAppSettings.flashMode != FlashMode.LOW_LIGHT_BOOST - ) { - HdrUiState.Available(cameraAppSettings.imageFormat, cameraAppSettings.dynamicRange) - } else { - HdrUiState.Unavailable + ?.contains(ImageOutputFormat.JPEG_ULTRA_HDR) ?: false + val isFlashHdrConflict = cameraAppSettings.flashMode == FlashMode.LOW_LIGHT_BOOST + + if (supportsHdrImage && !isFlashHdrConflict) { + HdrUiState.Available( + selectedImageFormat = cameraAppSettings.imageFormat, + selectedDynamicRange = DynamicRange.SDR // Force SDR in UI state for video + ) + } else { + HdrUiState.Unavailable + } } - - ExternalCaptureMode.VideoCapture -> if ( - cameraConstraints?.supportedDynamicRanges?.contains(DynamicRange.HLG10) == true && - cameraAppSettings.concurrentCameraMode != ConcurrentCameraMode.DUAL && - cameraAppSettings.flashMode != FlashMode.LOW_LIGHT_BOOST - ) { - HdrUiState.Available( - cameraAppSettings.imageFormat, - cameraAppSettings.dynamicRange - ) - } else { - HdrUiState.Unavailable + CaptureMode.VIDEO_ONLY -> { + val supportsHdrVideo = cameraConstraints?.supportedDynamicRanges?.contains(DynamicRange.HLG10) == true + val isFlashHdrConflict = cameraAppSettings.flashMode == FlashMode.LOW_LIGHT_BOOST + val isConcurrentConflict = cameraAppSettings.concurrentCameraMode == ConcurrentCameraMode.DUAL + + if (supportsHdrVideo && !isFlashHdrConflict && !isConcurrentConflict) { + HdrUiState.Available( + selectedImageFormat = ImageOutputFormat.JPEG, // Force SDR in UI state for image + selectedDynamicRange = cameraAppSettings.dynamicRange + ) + } else { + HdrUiState.Unavailable + } } - - ExternalCaptureMode.Standard -> if (( - cameraConstraints?.supportedDynamicRanges?.contains(DynamicRange.HLG10) == - true || - cameraConstraints?.supportedImageFormatsMap?.get( - cameraAppSettings.streamConfig - ) - ?.contains(ImageOutputFormat.JPEG_ULTRA_HDR) ?: false - ) && - cameraAppSettings.concurrentCameraMode != ConcurrentCameraMode.DUAL && - cameraAppSettings.flashMode != FlashMode.LOW_LIGHT_BOOST - ) { - HdrUiState.Available(cameraAppSettings.imageFormat, cameraAppSettings.dynamicRange) - } else { + CaptureMode.STANDARD -> { HdrUiState.Unavailable } } diff --git a/ui/uistateadapter/capture/src/test/java/com/google/jetpackcamera/ui/uistateadapter/capture/HdrUiStateAdapterTest.kt b/ui/uistateadapter/capture/src/test/java/com/google/jetpackcamera/ui/uistateadapter/capture/HdrUiStateAdapterTest.kt new file mode 100644 index 000000000..951ee2458 --- /dev/null +++ b/ui/uistateadapter/capture/src/test/java/com/google/jetpackcamera/ui/uistateadapter/capture/HdrUiStateAdapterTest.kt @@ -0,0 +1,216 @@ +/* + * Copyright (C) 2026 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 com.google.jetpackcamera.ui.uistateadapter.capture + +import com.google.common.truth.Truth.assertThat +import com.google.jetpackcamera.model.CaptureMode +import com.google.jetpackcamera.model.DynamicRange +import com.google.jetpackcamera.model.ExternalCaptureMode +import com.google.jetpackcamera.model.FlashMode +import com.google.jetpackcamera.model.ImageOutputFormat +import com.google.jetpackcamera.settings.model.CameraAppSettings +import com.google.jetpackcamera.settings.model.CameraConstraints +import com.google.jetpackcamera.settings.model.CameraSystemConstraints +import com.google.jetpackcamera.ui.uistate.capture.HdrUiState +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +@RunWith(JUnit4::class) +class HdrUiStateAdapterTest { + + private val emptyCameraConstraints = CameraConstraints( + supportedStabilizationModes = emptySet(), + supportedFixedFrameRates = emptySet(), + supportedDynamicRanges = emptySet(), + supportedVideoQualitiesMap = emptyMap(), + supportedImageFormatsMap = emptyMap(), + supportedIlluminants = emptySet(), + supportedFlashModes = emptySet(), + supportedZoomRange = null, + unsupportedStabilizationFpsMap = emptyMap(), + supportedTestPatterns = emptySet() + ) + + private val defaultCameraAppSettings = CameraAppSettings() + + @Test + fun from_standardMode_returnsUnavailable() { + // Given in STANDARD capture mode + val appSettings = defaultCameraAppSettings.copy(captureMode = CaptureMode.STANDARD) + val systemConstraints = CameraSystemConstraints( + perLensConstraints = mapOf( + appSettings.cameraLensFacing to emptyCameraConstraints.copy( + supportedDynamicRanges = setOf(DynamicRange.SDR, DynamicRange.HLG10), + supportedImageFormatsMap = mapOf( + appSettings.streamConfig to setOf(ImageOutputFormat.JPEG, ImageOutputFormat.JPEG_ULTRA_HDR) + ) + ) + ) + ) + + // When + val hdrUiState = HdrUiState.from(appSettings, systemConstraints, ExternalCaptureMode.Standard) + + // Then HDR is unavailable in Standard mode + assertThat(hdrUiState).isInstanceOf(HdrUiState.Unavailable::class.java) + } + + @Test + fun from_imageOnlyMode_hdrSupported_returnsAvailableWithRelevantSettings() { + // Given in IMAGE_ONLY capture mode, with HDR image supported + val appSettings = defaultCameraAppSettings.copy( + captureMode = CaptureMode.IMAGE_ONLY, + imageFormat = ImageOutputFormat.JPEG_ULTRA_HDR, + dynamicRange = DynamicRange.HLG10 + ) + val systemConstraints = CameraSystemConstraints( + perLensConstraints = mapOf( + appSettings.cameraLensFacing to emptyCameraConstraints.copy( + supportedImageFormatsMap = mapOf( + appSettings.streamConfig to setOf(ImageOutputFormat.JPEG, ImageOutputFormat.JPEG_ULTRA_HDR) + ) + ) + ) + ) + + // When + val hdrUiState = HdrUiState.from(appSettings, systemConstraints, ExternalCaptureMode.Standard) + + // Then HDR is available + assertThat(hdrUiState).isInstanceOf(HdrUiState.Available::class.java) + val availableState = hdrUiState as HdrUiState.Available + // Image format should be what is in settings + assertThat(availableState.selectedImageFormat).isEqualTo(ImageOutputFormat.JPEG_ULTRA_HDR) + // Dynamic range should be forced to SDR because we are in IMAGE_ONLY + assertThat(availableState.selectedDynamicRange).isEqualTo(DynamicRange.SDR) + } + + @Test + fun from_imageOnlyMode_hdrNotSupported_returnsUnavailable() { + // Given in IMAGE_ONLY capture mode, but HDR image NOT supported + val appSettings = defaultCameraAppSettings.copy(captureMode = CaptureMode.IMAGE_ONLY) + val systemConstraints = CameraSystemConstraints( + perLensConstraints = mapOf( + appSettings.cameraLensFacing to emptyCameraConstraints.copy( + supportedImageFormatsMap = mapOf( + appSettings.streamConfig to setOf(ImageOutputFormat.JPEG) + ) + ) + ) + ) + + // When + val hdrUiState = HdrUiState.from(appSettings, systemConstraints, ExternalCaptureMode.Standard) + + // Then HDR is unavailable + assertThat(hdrUiState).isInstanceOf(HdrUiState.Unavailable::class.java) + } + + @Test + fun from_imageOnlyMode_lowLightBoostOn_returnsUnavailable() { + // Given in IMAGE_ONLY capture mode with Low Light Boost ON, even though Ultra HDR is supported + val appSettings = defaultCameraAppSettings.copy( + captureMode = CaptureMode.IMAGE_ONLY, + flashMode = FlashMode.LOW_LIGHT_BOOST + ) + val systemConstraints = CameraSystemConstraints( + perLensConstraints = mapOf( + appSettings.cameraLensFacing to emptyCameraConstraints.copy( + supportedImageFormatsMap = mapOf( + appSettings.streamConfig to setOf(ImageOutputFormat.JPEG, ImageOutputFormat.JPEG_ULTRA_HDR) + ) + ) + ) + ) + + // When + val hdrUiState = HdrUiState.from(appSettings, systemConstraints, ExternalCaptureMode.Standard) + + // Then HDR is unavailable because of the flash mode conflict + assertThat(hdrUiState).isInstanceOf(HdrUiState.Unavailable::class.java) + } + + + @Test + fun from_videoOnlyMode_hdrSupported_returnsAvailableWithRelevantSettings() { + // Given in VIDEO_ONLY capture mode, with HDR video supported + val appSettings = defaultCameraAppSettings.copy( + captureMode = CaptureMode.VIDEO_ONLY, + dynamicRange = DynamicRange.HLG10, + imageFormat = ImageOutputFormat.JPEG_ULTRA_HDR + ) + val systemConstraints = CameraSystemConstraints( + perLensConstraints = mapOf( + appSettings.cameraLensFacing to emptyCameraConstraints.copy( + supportedDynamicRanges = setOf(DynamicRange.SDR, DynamicRange.HLG10) + ) + ) + ) + + // When + val hdrUiState = HdrUiState.from(appSettings, systemConstraints, ExternalCaptureMode.Standard) + + // Then HDR is available + assertThat(hdrUiState).isInstanceOf(HdrUiState.Available::class.java) + val availableState = hdrUiState as HdrUiState.Available + // Dynamic range should be what is in settings + assertThat(availableState.selectedDynamicRange).isEqualTo(DynamicRange.HLG10) + // Image format should be forced to JPEG because we are in VIDEO_ONLY + assertThat(availableState.selectedImageFormat).isEqualTo(ImageOutputFormat.JPEG) + } + + @Test + fun from_videoOnlyMode_hdrNotSupported_returnsUnavailable() { + // Given in VIDEO_ONLY capture mode, but HDR video NOT supported + val appSettings = defaultCameraAppSettings.copy(captureMode = CaptureMode.VIDEO_ONLY) + val systemConstraints = CameraSystemConstraints( + perLensConstraints = mapOf( + appSettings.cameraLensFacing to emptyCameraConstraints.copy( + supportedDynamicRanges = setOf(DynamicRange.SDR) + ) + ) + ) + + // When + val hdrUiState = HdrUiState.from(appSettings, systemConstraints, ExternalCaptureMode.Standard) + + // Then HDR is unavailable + assertThat(hdrUiState).isInstanceOf(HdrUiState.Unavailable::class.java) + } + + @Test + fun from_videoOnlyMode_lowLightBoostOn_returnsUnavailable() { + // Given in VIDEO_ONLY capture mode with Low Light Boost ON, even though HDR video is supported + val appSettings = defaultCameraAppSettings.copy( + captureMode = CaptureMode.VIDEO_ONLY, + flashMode = FlashMode.LOW_LIGHT_BOOST + ) + val systemConstraints = CameraSystemConstraints( + perLensConstraints = mapOf( + appSettings.cameraLensFacing to emptyCameraConstraints.copy( + supportedDynamicRanges = setOf(DynamicRange.SDR, DynamicRange.HLG10) + ) + ) + ) + + // When + val hdrUiState = HdrUiState.from(appSettings, systemConstraints, ExternalCaptureMode.Standard) + + // Then HDR is unavailable because of the flash mode conflict + assertThat(hdrUiState).isInstanceOf(HdrUiState.Unavailable::class.java) + } +} From d35aa646c213a4361810f9b3e7acf15ea71addff Mon Sep 17 00:00:00 2001 From: Kimberly Crevecoeur Date: Mon, 1 Jun 2026 18:26:38 -0700 Subject: [PATCH 2/7] spotless --- .../core/camera/CameraXCameraSystemTest.kt | 11 +++--- .../quicksettings/QuickSettingsScreen.kt | 4 +- .../capture/HdrUiStateAdapter.kt | 15 +++++--- .../capture/HdrUiStateAdapterTest.kt | 37 +++++++++++++------ 4 files changed, 44 insertions(+), 23 deletions(-) diff --git a/core/camera/src/androidTest/java/com/google/jetpackcamera/core/camera/CameraXCameraSystemTest.kt b/core/camera/src/androidTest/java/com/google/jetpackcamera/core/camera/CameraXCameraSystemTest.kt index df5ebd601..7be06add7 100644 --- a/core/camera/src/androidTest/java/com/google/jetpackcamera/core/camera/CameraXCameraSystemTest.kt +++ b/core/camera/src/androidTest/java/com/google/jetpackcamera/core/camera/CameraXCameraSystemTest.kt @@ -510,7 +510,7 @@ class CameraXCameraSystemTest { cameraSystem.setDynamicRange(DynamicRange.HLG10) cameraSystem.startCameraAndWaitUntilRunning() - + val dynamicRangeCheck = cameraSystem.getCurrentSettings() .filterNotNull() .map { it.dynamicRange } @@ -553,7 +553,7 @@ class CameraXCameraSystemTest { cameraSystem.setImageFormat(ImageOutputFormat.JPEG) cameraSystem.startCameraAndWaitUntilRunning() - + val settingsCheck = cameraSystem.getCurrentSettings() .filterNotNull() .produceIn(this) @@ -599,8 +599,9 @@ class CameraXCameraSystemTest { assume().withMessage("Ultra HDR not supported on $lensFacing, skip the test.") .that( cameraConstraints != null && - cameraConstraints.supportedImageFormatsMap[DEFAULT_CAMERA_APP_SETTINGS.streamConfig] - ?.contains(ImageOutputFormat.JPEG_ULTRA_HDR) == true + cameraConstraints.supportedImageFormatsMap[ + DEFAULT_CAMERA_APP_SETTINGS.streamConfig + ]?.contains(ImageOutputFormat.JPEG_ULTRA_HDR) == true ).isTrue() // Configure the camera to use the target lens and enable Ultra HDR @@ -610,7 +611,7 @@ class CameraXCameraSystemTest { cameraSystem.setDynamicRange(DynamicRange.SDR) cameraSystem.startCameraAndWaitUntilRunning() - + val settingsCheck = cameraSystem.getCurrentSettings() .filterNotNull() .produceIn(this) diff --git a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/QuickSettingsScreen.kt b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/QuickSettingsScreen.kt index 44538d341..72d98861b 100644 --- a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/QuickSettingsScreen.kt +++ b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/QuickSettingsScreen.kt @@ -159,7 +159,9 @@ fun QuickSettingsBottomSheet( } add { - val captureMode = (quickSettingsUiState.captureModeUiState as? CaptureModeUiState.Available)?.selectedCaptureMode + val captureModeUi = quickSettingsUiState.captureModeUiState + val captureMode = (captureModeUi as? CaptureModeUiState.Available) + ?.selectedCaptureMode QuickSetHdr( modifier = Modifier.testTag(QUICK_SETTINGS_HDR_BUTTON), onClick = { d: DynamicRange, i: ImageOutputFormat -> diff --git a/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/HdrUiStateAdapter.kt b/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/HdrUiStateAdapter.kt index cc84f931a..aff4aa3a3 100644 --- a/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/HdrUiStateAdapter.kt +++ b/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/HdrUiStateAdapter.kt @@ -60,9 +60,10 @@ fun HdrUiState.Companion.from( val cameraConstraints: CameraConstraints? = systemConstraints.forCurrentLens( cameraAppSettings ) - + // Determine active capture mode, respecting external override - val activeCaptureMode = externalCaptureMode.toCaptureMode() ?: cameraAppSettings.captureMode + val activeCaptureMode = + externalCaptureMode.toCaptureMode() ?: cameraAppSettings.captureMode return when (activeCaptureMode) { CaptureMode.IMAGE_ONLY -> { @@ -70,7 +71,7 @@ fun HdrUiState.Companion.from( ?.supportedImageFormatsMap?.get(cameraAppSettings.streamConfig) ?.contains(ImageOutputFormat.JPEG_ULTRA_HDR) ?: false val isFlashHdrConflict = cameraAppSettings.flashMode == FlashMode.LOW_LIGHT_BOOST - + if (supportsHdrImage && !isFlashHdrConflict) { HdrUiState.Available( selectedImageFormat = cameraAppSettings.imageFormat, @@ -81,10 +82,12 @@ fun HdrUiState.Companion.from( } } CaptureMode.VIDEO_ONLY -> { - val supportsHdrVideo = cameraConstraints?.supportedDynamicRanges?.contains(DynamicRange.HLG10) == true + val supportsHdrVideo = + cameraConstraints?.supportedDynamicRanges?.contains(DynamicRange.HLG10) == true val isFlashHdrConflict = cameraAppSettings.flashMode == FlashMode.LOW_LIGHT_BOOST - val isConcurrentConflict = cameraAppSettings.concurrentCameraMode == ConcurrentCameraMode.DUAL - + val isConcurrentConflict = + cameraAppSettings.concurrentCameraMode == ConcurrentCameraMode.DUAL + if (supportsHdrVideo && !isFlashHdrConflict && !isConcurrentConflict) { HdrUiState.Available( selectedImageFormat = ImageOutputFormat.JPEG, // Force SDR in UI state for image diff --git a/ui/uistateadapter/capture/src/test/java/com/google/jetpackcamera/ui/uistateadapter/capture/HdrUiStateAdapterTest.kt b/ui/uistateadapter/capture/src/test/java/com/google/jetpackcamera/ui/uistateadapter/capture/HdrUiStateAdapterTest.kt index 951ee2458..cf522bee1 100644 --- a/ui/uistateadapter/capture/src/test/java/com/google/jetpackcamera/ui/uistateadapter/capture/HdrUiStateAdapterTest.kt +++ b/ui/uistateadapter/capture/src/test/java/com/google/jetpackcamera/ui/uistateadapter/capture/HdrUiStateAdapterTest.kt @@ -56,14 +56,18 @@ class HdrUiStateAdapterTest { appSettings.cameraLensFacing to emptyCameraConstraints.copy( supportedDynamicRanges = setOf(DynamicRange.SDR, DynamicRange.HLG10), supportedImageFormatsMap = mapOf( - appSettings.streamConfig to setOf(ImageOutputFormat.JPEG, ImageOutputFormat.JPEG_ULTRA_HDR) + appSettings.streamConfig to setOf( + ImageOutputFormat.JPEG, + ImageOutputFormat.JPEG_ULTRA_HDR + ) ) ) ) ) // When - val hdrUiState = HdrUiState.from(appSettings, systemConstraints, ExternalCaptureMode.Standard) + val hdrUiState = + HdrUiState.from(appSettings, systemConstraints, ExternalCaptureMode.Standard) // Then HDR is unavailable in Standard mode assertThat(hdrUiState).isInstanceOf(HdrUiState.Unavailable::class.java) @@ -81,14 +85,18 @@ class HdrUiStateAdapterTest { perLensConstraints = mapOf( appSettings.cameraLensFacing to emptyCameraConstraints.copy( supportedImageFormatsMap = mapOf( - appSettings.streamConfig to setOf(ImageOutputFormat.JPEG, ImageOutputFormat.JPEG_ULTRA_HDR) + appSettings.streamConfig to setOf( + ImageOutputFormat.JPEG, + ImageOutputFormat.JPEG_ULTRA_HDR + ) ) ) ) ) // When - val hdrUiState = HdrUiState.from(appSettings, systemConstraints, ExternalCaptureMode.Standard) + val hdrUiState = + HdrUiState.from(appSettings, systemConstraints, ExternalCaptureMode.Standard) // Then HDR is available assertThat(hdrUiState).isInstanceOf(HdrUiState.Available::class.java) @@ -114,7 +122,8 @@ class HdrUiStateAdapterTest { ) // When - val hdrUiState = HdrUiState.from(appSettings, systemConstraints, ExternalCaptureMode.Standard) + val hdrUiState = + HdrUiState.from(appSettings, systemConstraints, ExternalCaptureMode.Standard) // Then HDR is unavailable assertThat(hdrUiState).isInstanceOf(HdrUiState.Unavailable::class.java) @@ -131,20 +140,23 @@ class HdrUiStateAdapterTest { perLensConstraints = mapOf( appSettings.cameraLensFacing to emptyCameraConstraints.copy( supportedImageFormatsMap = mapOf( - appSettings.streamConfig to setOf(ImageOutputFormat.JPEG, ImageOutputFormat.JPEG_ULTRA_HDR) + appSettings.streamConfig to setOf( + ImageOutputFormat.JPEG, + ImageOutputFormat.JPEG_ULTRA_HDR + ) ) ) ) ) // When - val hdrUiState = HdrUiState.from(appSettings, systemConstraints, ExternalCaptureMode.Standard) + val hdrUiState = + HdrUiState.from(appSettings, systemConstraints, ExternalCaptureMode.Standard) // Then HDR is unavailable because of the flash mode conflict assertThat(hdrUiState).isInstanceOf(HdrUiState.Unavailable::class.java) } - @Test fun from_videoOnlyMode_hdrSupported_returnsAvailableWithRelevantSettings() { // Given in VIDEO_ONLY capture mode, with HDR video supported @@ -162,7 +174,8 @@ class HdrUiStateAdapterTest { ) // When - val hdrUiState = HdrUiState.from(appSettings, systemConstraints, ExternalCaptureMode.Standard) + val hdrUiState = + HdrUiState.from(appSettings, systemConstraints, ExternalCaptureMode.Standard) // Then HDR is available assertThat(hdrUiState).isInstanceOf(HdrUiState.Available::class.java) @@ -186,7 +199,8 @@ class HdrUiStateAdapterTest { ) // When - val hdrUiState = HdrUiState.from(appSettings, systemConstraints, ExternalCaptureMode.Standard) + val hdrUiState = + HdrUiState.from(appSettings, systemConstraints, ExternalCaptureMode.Standard) // Then HDR is unavailable assertThat(hdrUiState).isInstanceOf(HdrUiState.Unavailable::class.java) @@ -208,7 +222,8 @@ class HdrUiStateAdapterTest { ) // When - val hdrUiState = HdrUiState.from(appSettings, systemConstraints, ExternalCaptureMode.Standard) + val hdrUiState = + HdrUiState.from(appSettings, systemConstraints, ExternalCaptureMode.Standard) // Then HDR is unavailable because of the flash mode conflict assertThat(hdrUiState).isInstanceOf(HdrUiState.Unavailable::class.java) From 59c6b8e929ec7aa6ca24b699a1f81c1845597d93 Mon Sep 17 00:00:00 2001 From: Kimberly Crevecoeur Date: Wed, 3 Jun 2026 11:30:11 -0700 Subject: [PATCH 3/7] address pr comments --- .../core/camera/CameraXCameraSystem.kt | 1 + .../capture/quicksettings/QuickSettingsScreen.kt | 14 +++----------- 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraXCameraSystem.kt b/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraXCameraSystem.kt index e9e007b28..cb67ce5f7 100644 --- a/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraXCameraSystem.kt +++ b/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraXCameraSystem.kt @@ -1017,6 +1017,7 @@ class CameraXCameraSystem( old?.copy(captureMode = captureMode) ?.tryApplyDynamicRangeConstraints() ?.tryApplyImageFormatConstraints() + ?.tryApplyConcurrentCameraModeConstraints() } } diff --git a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/QuickSettingsScreen.kt b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/QuickSettingsScreen.kt index 72d98861b..c2d1adb58 100644 --- a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/QuickSettingsScreen.kt +++ b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/QuickSettingsScreen.kt @@ -166,17 +166,9 @@ fun QuickSettingsBottomSheet( modifier = Modifier.testTag(QUICK_SETTINGS_HDR_BUTTON), onClick = { d: DynamicRange, i: ImageOutputFormat -> when (captureMode) { - CaptureMode.IMAGE_ONLY -> { - quickSettingsController.setImageFormat(i) - } - CaptureMode.VIDEO_ONLY -> { - quickSettingsController.setDynamicRange(d) - } - CaptureMode.STANDARD -> { - quickSettingsController.setDynamicRange(d) - quickSettingsController.setImageFormat(i) - } - null -> { + CaptureMode.IMAGE_ONLY -> quickSettingsController.setImageFormat(i) + CaptureMode.VIDEO_ONLY -> quickSettingsController.setDynamicRange(d) + else -> { quickSettingsController.setDynamicRange(d) quickSettingsController.setImageFormat(i) } From 54c2650a670912927274d962797d9b3a4fa0a91f Mon Sep 17 00:00:00 2001 From: Kimberly Crevecoeur Date: Mon, 8 Jun 2026 16:02:57 -0700 Subject: [PATCH 4/7] hdrUiStateAdapterTest visibility modifier --- .../ui/uistateadapter/capture/HdrUiStateAdapterTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/uistateadapter/capture/src/test/java/com/google/jetpackcamera/ui/uistateadapter/capture/HdrUiStateAdapterTest.kt b/ui/uistateadapter/capture/src/test/java/com/google/jetpackcamera/ui/uistateadapter/capture/HdrUiStateAdapterTest.kt index cf522bee1..4f172572a 100644 --- a/ui/uistateadapter/capture/src/test/java/com/google/jetpackcamera/ui/uistateadapter/capture/HdrUiStateAdapterTest.kt +++ b/ui/uistateadapter/capture/src/test/java/com/google/jetpackcamera/ui/uistateadapter/capture/HdrUiStateAdapterTest.kt @@ -30,7 +30,7 @@ import org.junit.runner.RunWith import org.junit.runners.JUnit4 @RunWith(JUnit4::class) -class HdrUiStateAdapterTest { +internal class HdrUiStateAdapterTest { private val emptyCameraConstraints = CameraConstraints( supportedStabilizationModes = emptySet(), From a0520f5c40d40283c914fac441356a8465e01eb3 Mon Sep 17 00:00:00 2001 From: Kimberly Crevecoeur Date: Mon, 8 Jun 2026 16:18:42 -0700 Subject: [PATCH 5/7] spotless --- .../components/capture/quicksettings/QuickSettingsScreen.kt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/QuickSettingsScreen.kt b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/QuickSettingsScreen.kt index 811236cb4..5361c8766 100644 --- a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/QuickSettingsScreen.kt +++ b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/QuickSettingsScreen.kt @@ -150,8 +150,10 @@ fun QuickSettingsBottomSheet( modifier = Modifier.testTag(QUICK_SETTINGS_HDR_BUTTON), onClick = { d: DynamicRange, i: ImageOutputFormat -> when (captureMode) { - CaptureMode.IMAGE_ONLY -> quickSettingsController.setImageFormat(i) - CaptureMode.VIDEO_ONLY -> quickSettingsController.setDynamicRange(d) + CaptureMode.IMAGE_ONLY -> + quickSettingsController.setImageFormat(i) + CaptureMode.VIDEO_ONLY -> + quickSettingsController.setDynamicRange(d) else -> { quickSettingsController.setDynamicRange(d) quickSettingsController.setImageFormat(i) From dadb77ce31139244b7af930fb348941ea94c3946 Mon Sep 17 00:00:00 2001 From: Kimberly Crevecoeur Date: Mon, 8 Jun 2026 17:16:56 -0700 Subject: [PATCH 6/7] cleanup --- .../com/google/jetpackcamera/core/camera/CameraXCameraSystem.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraXCameraSystem.kt b/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraXCameraSystem.kt index cb67ce5f7..afee91dcd 100644 --- a/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraXCameraSystem.kt +++ b/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraXCameraSystem.kt @@ -850,6 +850,7 @@ class CameraXCameraSystem( ConcurrentCameraMode.OFF -> this else -> if (systemConstraints.concurrentCamerasSupported && + captureMode == CaptureMode.VIDEO_ONLY && dynamicRange == DynamicRange.SDR && streamConfig == StreamConfig.MULTI_STREAM && flashMode != FlashMode.LOW_LIGHT_BOOST From 43997072a60176c256b61348645ff14216f7032a Mon Sep 17 00:00:00 2001 From: Kimberly Crevecoeur Date: Mon, 8 Jun 2026 17:31:20 -0700 Subject: [PATCH 7/7] update concurrent camera ui for constraints and add test for HDR setting interaction when switching to standard capture mode --- .../core/camera/CameraXCameraSystemTest.kt | 46 +++++++++++++++++++ .../capture/ConcurrentCameraUiStateAdapter.kt | 4 +- 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/core/camera/src/androidTest/java/com/google/jetpackcamera/core/camera/CameraXCameraSystemTest.kt b/core/camera/src/androidTest/java/com/google/jetpackcamera/core/camera/CameraXCameraSystemTest.kt index 7be06add7..44e635f24 100644 --- a/core/camera/src/androidTest/java/com/google/jetpackcamera/core/camera/CameraXCameraSystemTest.kt +++ b/core/camera/src/androidTest/java/com/google/jetpackcamera/core/camera/CameraXCameraSystemTest.kt @@ -467,6 +467,16 @@ class CameraXCameraSystemTest { runSwitchCaptureMode_toStandard_disablesHdr_test(LensFacing.FRONT) } + @Test + fun switchCaptureMode_toStandard_disablesImageHdr_back(): Unit = runBlocking { + runSwitchCaptureMode_toStandard_disablesImageHdr_test(LensFacing.BACK) + } + + @Test + fun switchCaptureMode_toStandard_disablesImageHdr_front(): Unit = runBlocking { + runSwitchCaptureMode_toStandard_disablesImageHdr_test(LensFacing.FRONT) + } + @Test fun switchCaptureMode_preservesVideoHdr_back(): Unit = runBlocking { runSwitchCaptureMode_preservesVideoHdr_test(LensFacing.BACK) @@ -529,6 +539,42 @@ class CameraXCameraSystemTest { dynamicRangeCheck.cancel() } + private suspend fun CoroutineScope.runSwitchCaptureMode_toStandard_disablesImageHdr_test( + lensFacing: LensFacing + ) { + val cameraSystem = createAndInitCameraXCameraSystem() + val systemConstraints = cameraSystem.getSystemConstraints().value + val cameraConstraints = systemConstraints?.perLensConstraints?.get(lensFacing) + + // Skip test if Ultra HDR is not supported on the target lens + assume().withMessage("Ultra HDR not supported on $lensFacing, skip the test.") + .that( + cameraConstraints != null && + cameraConstraints.supportedImageFormatsMap[ + DEFAULT_CAMERA_APP_SETTINGS.streamConfig + ]?.contains(ImageOutputFormat.JPEG_ULTRA_HDR) == true + ).isTrue() + + cameraSystem.setLensFacing(lensFacing) + cameraSystem.setCaptureMode(CaptureMode.IMAGE_ONLY) + cameraSystem.setImageFormat(ImageOutputFormat.JPEG_ULTRA_HDR) + + cameraSystem.startCameraAndWaitUntilRunning() + + val imageFormatCheck = cameraSystem.getCurrentSettings() + .filterNotNull() + .map { it.imageFormat } + .produceIn(this) + + imageFormatCheck.awaitValue(ImageOutputFormat.JPEG_ULTRA_HDR) + + cameraSystem.setCaptureMode(CaptureMode.STANDARD) + + imageFormatCheck.awaitValue(ImageOutputFormat.JPEG) + + imageFormatCheck.cancel() + } + private suspend fun CoroutineScope.runSwitchCaptureMode_preservesVideoHdr_test( lensFacing: LensFacing ) { diff --git a/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/ConcurrentCameraUiStateAdapter.kt b/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/ConcurrentCameraUiStateAdapter.kt index 15c5928a4..0dd781a14 100644 --- a/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/ConcurrentCameraUiStateAdapter.kt +++ b/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/ConcurrentCameraUiStateAdapter.kt @@ -61,8 +61,8 @@ fun ConcurrentCameraUiState.Companion.from( captureModeUiState as? CaptureModeUiState.Available ) - ?.selectedCaptureMode != - CaptureMode.IMAGE_ONLY + ?.selectedCaptureMode == + CaptureMode.VIDEO_ONLY ) && ( cameraAppSettings.dynamicRange != DEFAULT_HDR_DYNAMIC_RANGE &&