From a8b2766f856278404115132645fe02fdb5bb849e Mon Sep 17 00:00:00 2001 From: Kimberly Crevecoeur Date: Wed, 8 Apr 2026 19:12:16 +0000 Subject: [PATCH 01/51] wip setting row component --- .../ui/QuickSettingsComponents.kt | 88 +++++++++++++++++++ 1 file changed, 88 insertions(+) diff --git a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/ui/QuickSettingsComponents.kt b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/ui/QuickSettingsComponents.kt index 6b0eec752..a66dcf979 100644 --- a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/ui/QuickSettingsComponents.kt +++ b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/ui/QuickSettingsComponents.kt @@ -48,7 +48,9 @@ import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.SheetState +import androidx.compose.material3.Surface import androidx.compose.material3.Text +import androidx.compose.material3.darkColorScheme import androidx.compose.material3.minimumInteractiveComponentSize import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider @@ -762,6 +764,44 @@ private fun QuickSettingToggleButton( } } +@Composable +fun SettingRow( + title: String, + status: String, + modifier: Modifier = Modifier, + // Using vararg to accept multiple button components + vararg settingsButtons: @Composable () -> Unit +) { + Row( + modifier = modifier + .fillMaxWidth() + .padding(vertical = 12.dp, horizontal = 16.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Column(modifier = Modifier.weight(1f)) { + Text( + text = title, + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.onSurface + ) + Text( + text = status, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + settingsButtons.forEach { button -> + button() + } + } + } +} /** * Should you want to have an expanded view of a single quick setting */ @@ -947,3 +987,51 @@ private fun QuickSettingToggleButtonPreview() { } } } + +@Preview(name = "JCA Setting Row - Dark Mode", showBackground = true, backgroundColor = 0xFF000000) +@Composable +private fun PreviewSettingRowDark() { + MaterialTheme(colorScheme = darkColorScheme()) { + Surface( + modifier = Modifier.fillMaxWidth(), + color = Color.Black // Consistent with Camera UI + ) { + SettingRow( + title = "Video Resolution", + status = "Standard Definition", + settingsButtons = arrayOf( + { + // Off State (Highlighted per your screenshot) + QuickSettingToggleButton( + text = "SD", + accessibilityText = "Flash Off", + painter = painterResource(id = R.drawable.video_resolution_sd_icon), + isHighlighted = true, + onClick = {} + ) + }, + { + // On State + QuickSettingToggleButton( + text = "HD", + accessibilityText = "High Definition", + painter = painterResource(id = R.drawable.video_resolution_hd_icon), + isHighlighted = false, + onClick = {} + ) + }, + { + // Auto State + QuickSettingToggleButton( + text = "FHD", + accessibilityText = "Full High Definition", + painter = painterResource(id = R.drawable.video_resolution_fhd_icon), + isHighlighted = false, + onClick = {} + ) + } + ) + ) + } + } +} From 3e8d8a5a0ef01e95259327189c149c53f32761be Mon Sep 17 00:00:00 2001 From: Kimberly Crevecoeur Date: Thu, 9 Apr 2026 16:24:15 +0000 Subject: [PATCH 02/51] wip more quick setting row components --- .../quicksettings/QuickSettingsScreen.kt | 189 +++----- .../ui/QuickSettingsComponents.kt | 429 ++++++++---------- .../impl/QuickSettingsControllerImpl.kt | 9 +- .../quicksettings/QuickSettingsController.kt | 8 - .../uistate/capture/TrackedCaptureUiState.kt | 2 - .../capture/compound/QuickSettingsUiState.kt | 14 - .../capture/compound/CaptureUiStateAdapter.kt | 1 - .../compound/QuickSettingsUiStateAdapter.kt | 4 - 8 files changed, 252 insertions(+), 404 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 f7eff696e..a8a10b796 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 @@ -15,15 +15,13 @@ */ package com.google.jetpackcamera.ui.components.capture.quicksettings +import androidx.compose.foundation.layout.Column import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.platform.testTag -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.semantics.semantics -import androidx.compose.ui.semantics.stateDescription import androidx.compose.ui.tooling.preview.Preview import com.google.jetpackcamera.model.AspectRatio import com.google.jetpackcamera.model.CaptureMode @@ -33,26 +31,20 @@ import com.google.jetpackcamera.model.FlashMode import com.google.jetpackcamera.model.ImageOutputFormat import com.google.jetpackcamera.model.LensFacing import com.google.jetpackcamera.model.StreamConfig -import com.google.jetpackcamera.ui.components.capture.BTN_QUICK_SETTINGS_FOCUS_CAPTURE_MODE import com.google.jetpackcamera.ui.components.capture.QUICK_SETTINGS_CONCURRENT_CAMERA_MODE_BUTTON -import com.google.jetpackcamera.ui.components.capture.QUICK_SETTINGS_FLASH_BUTTON import com.google.jetpackcamera.ui.components.capture.QUICK_SETTINGS_FLIP_CAMERA_BUTTON import com.google.jetpackcamera.ui.components.capture.QUICK_SETTINGS_HDR_BUTTON -import com.google.jetpackcamera.ui.components.capture.QUICK_SETTINGS_RATIO_BUTTON import com.google.jetpackcamera.ui.components.capture.QUICK_SETTINGS_STREAM_CONFIG_BUTTON import com.google.jetpackcamera.ui.components.capture.R import com.google.jetpackcamera.ui.components.capture.SETTINGS_BUTTON +import com.google.jetpackcamera.ui.components.capture.quicksettings.ui.AspectRatioRow +import com.google.jetpackcamera.ui.components.capture.quicksettings.ui.CaptureModeRow +import com.google.jetpackcamera.ui.components.capture.quicksettings.ui.FlashRow import com.google.jetpackcamera.ui.components.capture.quicksettings.ui.QuickFlipCamera import com.google.jetpackcamera.ui.components.capture.quicksettings.ui.QuickNavSettings import com.google.jetpackcamera.ui.components.capture.quicksettings.ui.QuickSetConcurrentCamera -import com.google.jetpackcamera.ui.components.capture.quicksettings.ui.QuickSetFlash import com.google.jetpackcamera.ui.components.capture.quicksettings.ui.QuickSetHdr import com.google.jetpackcamera.ui.components.capture.quicksettings.ui.QuickSetStreamConfig -import com.google.jetpackcamera.ui.components.capture.quicksettings.ui.QuickSettingsBottomSheet as BottomSheetComponent -import com.google.jetpackcamera.ui.components.capture.quicksettings.ui.ToggleFocusedQuickSetCaptureMode -import com.google.jetpackcamera.ui.components.capture.quicksettings.ui.ToggleFocusedQuickSetRatio -import com.google.jetpackcamera.ui.components.capture.quicksettings.ui.focusedCaptureModeButtons -import com.google.jetpackcamera.ui.components.capture.quicksettings.ui.focusedRatioButtons import com.google.jetpackcamera.ui.controller.quicksettings.QuickSettingsController import com.google.jetpackcamera.ui.uistate.SingleSelectableUiState import com.google.jetpackcamera.ui.uistate.capture.AspectRatioUiState @@ -62,8 +54,8 @@ import com.google.jetpackcamera.ui.uistate.capture.FlashModeUiState import com.google.jetpackcamera.ui.uistate.capture.FlipLensUiState import com.google.jetpackcamera.ui.uistate.capture.HdrUiState import com.google.jetpackcamera.ui.uistate.capture.StreamConfigUiState -import com.google.jetpackcamera.ui.uistate.capture.compound.FocusedQuickSetting import com.google.jetpackcamera.ui.uistate.capture.compound.QuickSettingsUiState +import com.google.jetpackcamera.ui.components.capture.quicksettings.ui.QuickSettingsBottomSheet as BottomSheetComponent /** * The UI bottom sheet component for quick settings @@ -77,130 +69,95 @@ fun QuickSettingsBottomSheet( quickSettingsController: QuickSettingsController ) { if (quickSettingsUiState is QuickSettingsUiState.Available) { - val onUnFocus = { quickSettingsController.setFocusedSetting(FocusedQuickSetting.NONE) } + val displayedQuickSettings: List<@Composable () -> Unit> = - when (quickSettingsUiState.focusedQuickSetting) { - FocusedQuickSetting.ASPECT_RATIO -> focusedRatioButtons( - onUnFocus = onUnFocus, - onSetAspectRatio = quickSettingsController::setAspectRatio, - aspectRatioUiState = quickSettingsUiState.aspectRatioUiState - ) - FocusedQuickSetting.CAPTURE_MODE -> focusedCaptureModeButtons( - onUnFocus = onUnFocus, - onSetCaptureMode = quickSettingsController::setCaptureMode, - captureModeUiState = quickSettingsUiState.captureModeUiState - ) + buildList { - FocusedQuickSetting.NONE -> - buildList { - // todo(kc): change flash to expanded setting? - add { - QuickSetFlash( - modifier = Modifier.testTag(QUICK_SETTINGS_FLASH_BUTTON), - onClick = { f: FlashMode -> quickSettingsController.setFlash(f) }, - flashModeUiState = quickSettingsUiState.flashModeUiState - ) - } - add { - val description = - quickSettingsUiState.captureModeUiState.stateDescription() - ?.let { - stringResource(it) - } - ToggleFocusedQuickSetCaptureMode( - modifier = Modifier - .testTag(BTN_QUICK_SETTINGS_FOCUS_CAPTURE_MODE) - .semantics { description?.let { stateDescription = it } }, - setCaptureMode = { - quickSettingsController.setFocusedSetting( - FocusedQuickSetting.CAPTURE_MODE - ) - }, - captureModeUiState = quickSettingsUiState.captureModeUiState - ) - } + add { + QuickFlipCamera( + modifier = Modifier.testTag(QUICK_SETTINGS_FLIP_CAMERA_BUTTON), + setLensFacing = { l: LensFacing -> + quickSettingsController.setLensFacing(l) + }, + flipLensUiState = quickSettingsUiState.flipLensUiState + ) + } - add { - QuickFlipCamera( - modifier = Modifier.testTag(QUICK_SETTINGS_FLIP_CAMERA_BUTTON), - setLensFacing = { l: LensFacing -> - quickSettingsController.setLensFacing(l) - }, - flipLensUiState = quickSettingsUiState.flipLensUiState - ) - } - add { - ToggleFocusedQuickSetRatio( - modifier = Modifier.testTag(QUICK_SETTINGS_RATIO_BUTTON), - setRatio = { - quickSettingsController.setFocusedSetting( - FocusedQuickSetting.ASPECT_RATIO - ) - }, - isHighlightEnabled = false, - aspectRatioUiState = quickSettingsUiState.aspectRatioUiState - ) - } - add { - QuickSetStreamConfig( - modifier = Modifier.testTag( - QUICK_SETTINGS_STREAM_CONFIG_BUTTON - ), - setStreamConfig = { c: StreamConfig -> - quickSettingsController.setStreamConfig(c) - }, - streamConfigUiState = quickSettingsUiState.streamConfigUiState - ) - } + add { + QuickSetStreamConfig( + modifier = Modifier.testTag( + QUICK_SETTINGS_STREAM_CONFIG_BUTTON + ), + setStreamConfig = { c: StreamConfig -> + quickSettingsController.setStreamConfig(c) + }, + streamConfigUiState = quickSettingsUiState.streamConfigUiState + ) + } - add { - QuickSetHdr( - modifier = Modifier.testTag(QUICK_SETTINGS_HDR_BUTTON), - onClick = { d: DynamicRange, i: ImageOutputFormat -> - quickSettingsController.setDynamicRange(d) - quickSettingsController.setImageFormat(i) - }, - hdrUiState = quickSettingsUiState.hdrUiState - ) - } + add { + QuickSetHdr( + modifier = Modifier.testTag(QUICK_SETTINGS_HDR_BUTTON), + onClick = { d: DynamicRange, i: ImageOutputFormat -> + quickSettingsController.setDynamicRange(d) + quickSettingsController.setImageFormat(i) + }, + hdrUiState = quickSettingsUiState.hdrUiState + ) + } - add { - QuickSetConcurrentCamera( - modifier = - Modifier.testTag(QUICK_SETTINGS_CONCURRENT_CAMERA_MODE_BUTTON), - setConcurrentCameraMode = { c: ConcurrentCameraMode -> - quickSettingsController.setConcurrentCameraMode(c) - }, - concurrentCameraUiState = quickSettingsUiState - .concurrentCameraUiState - ) - } + add { + QuickSetConcurrentCamera( + modifier = + Modifier.testTag(QUICK_SETTINGS_CONCURRENT_CAMERA_MODE_BUTTON), + setConcurrentCameraMode = { c: ConcurrentCameraMode -> + quickSettingsController.setConcurrentCameraMode(c) + }, + concurrentCameraUiState = quickSettingsUiState + .concurrentCameraUiState + ) + } - add { - QuickNavSettings( - modifier = Modifier - .testTag(SETTINGS_BUTTON), - onNavigateToSettings = onNavigateToSettings - ) - } - } + add { + QuickNavSettings( + modifier = Modifier + .testTag(SETTINGS_BUTTON), + onNavigateToSettings = onNavigateToSettings + ) + } } + val sheetState = rememberModalBottomSheetState() if (quickSettingsUiState.quickSettingsIsOpen) { BottomSheetComponent( modifier = modifier, onDismiss = { - onUnFocus() quickSettingsController.toggleQuickSettings() }, sheetState = sheetState, + wipCapButton = { + Column { + FlashRow( + onSetFlashMode = quickSettingsController::setFlash, + flashModeUiState = quickSettingsUiState.flashModeUiState + ) + CaptureModeRow( + captureModeUiState = quickSettingsUiState.captureModeUiState, + onSetCaptureMode = quickSettingsController::setCaptureMode + ) + AspectRatioRow( + aspectRatioUiState = quickSettingsUiState.aspectRatioUiState, + onSetAspectRatio = quickSettingsController::setAspectRatio + ) + } + }, *displayedQuickSettings.toTypedArray() ) } @@ -342,8 +299,6 @@ fun ExpandedQuickSettingsUiPreview_WithHdr() { class NoOpQuickSettingsController : QuickSettingsController { override fun toggleQuickSettings() {} - override fun setFocusedSetting(focusedQuickSetting: FocusedQuickSetting) {} - override fun setLensFacing(lensFace: LensFacing) {} override fun setFlash(flashMode: FlashMode) {} diff --git a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/ui/QuickSettingsComponents.kt b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/ui/QuickSettingsComponents.kt index a66dcf979..c863a976f 100644 --- a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/ui/QuickSettingsComponents.kt +++ b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/ui/QuickSettingsComponents.kt @@ -135,22 +135,26 @@ fun QuickSetRatio( enum = enum, onClick = { onClick() }, isHighLighted = isHighlightEnabled && - (assignedRatio == aspectRatioUiState.selectedAspectRatio) + (assignedRatio == aspectRatioUiState.selectedAspectRatio) ) } } + @Composable -fun FocusedQuickSetCaptureMode( +fun CaptureModeRow( modifier: Modifier = Modifier, onSetCaptureMode: (CaptureMode) -> Unit, - captureModeUiState: CaptureModeUiState + captureModeUiState: CaptureModeUiState, ) { - val buttons: Array<@Composable () -> Unit> = - if (captureModeUiState is CaptureModeUiState.Available) { - arrayOf( + if (captureModeUiState is CaptureModeUiState.Available) { + SettingRow( + modifier = modifier, + title = "Capture Mode", + status = captureModeUiState.selectedCaptureMode.toString(), + settingsButtons = arrayOf( { - QuickSetCaptureMode( + CaptureModeToggleButton( modifier = Modifier .testTag(BTN_QUICK_SETTINGS_FOCUSED_CAPTURE_MODE_OPTION_STANDARD), onClick = { onSetCaptureMode(CaptureMode.STANDARD) }, @@ -160,7 +164,7 @@ fun FocusedQuickSetCaptureMode( ) }, { - QuickSetCaptureMode( + CaptureModeToggleButton( modifier = Modifier .testTag(BTN_QUICK_SETTINGS_FOCUSED_CAPTURE_MODE_IMAGE_ONLY), onClick = { onSetCaptureMode(CaptureMode.IMAGE_ONLY) }, @@ -170,7 +174,7 @@ fun FocusedQuickSetCaptureMode( ) }, { - QuickSetCaptureMode( + CaptureModeToggleButton( modifier = Modifier .testTag(BTN_QUICK_SETTINGS_FOCUSED_CAPTURE_MODE_VIDEO_ONLY), onClick = { onSetCaptureMode(CaptureMode.VIDEO_ONLY) }, @@ -180,53 +184,48 @@ fun FocusedQuickSetCaptureMode( ) } ) - } else { - emptyArray() - } - ExpandedQuickSetting(modifier = modifier, quickSettingButtons = buttons) + ) + } } @Composable -fun QuickSetCaptureMode( +fun CaptureModeToggleButton( modifier: Modifier = Modifier, onClick: () -> Unit, - captureModeUiState: CaptureModeUiState, - assignedCaptureMode: CaptureMode?, + captureModeUiState: CaptureModeUiState.Available, + assignedCaptureMode: CaptureMode, isHighlightEnabled: Boolean = false ) { - if (captureModeUiState is CaptureModeUiState.Available) { - val captureToUse = assignedCaptureMode ?: captureModeUiState.selectedCaptureMode - val enum = when (captureToUse) { - CaptureMode.STANDARD -> CameraCaptureMode.STANDARD - CaptureMode.VIDEO_ONLY -> CameraCaptureMode.VIDEO_ONLY - CaptureMode.IMAGE_ONLY -> CameraCaptureMode.IMAGE_ONLY - } + val enum = when (assignedCaptureMode) { + CaptureMode.STANDARD -> CameraCaptureMode.STANDARD + CaptureMode.VIDEO_ONLY -> CameraCaptureMode.VIDEO_ONLY + CaptureMode.IMAGE_ONLY -> CameraCaptureMode.IMAGE_ONLY + } - QuickSettingToggleButton( - modifier = modifier, - enum = enum, - onClick = { onClick() }, - enabled = when (assignedCaptureMode) { - null -> { - // only enabled if there are at least 2 supported capturemodes - captureModeUiState.availableCaptureModes.count { - it is SingleSelectableUiState.SelectableUi - } >= 2 - } + QuickSettingToggleButton( + modifier = modifier, + enum = enum, + onClick = { onClick() }, + enabled = when (assignedCaptureMode) { + null -> { + // only enabled if there are at least 2 supported capturemodes + captureModeUiState.availableCaptureModes.count { + it is SingleSelectableUiState.SelectableUi + } >= 2 + } - CaptureMode.STANDARD -> - captureModeUiState.isCaptureModeSelectable(CaptureMode.STANDARD) + CaptureMode.STANDARD -> + captureModeUiState.isCaptureModeSelectable(CaptureMode.STANDARD) - CaptureMode.VIDEO_ONLY -> - captureModeUiState.isCaptureModeSelectable(CaptureMode.VIDEO_ONLY) + CaptureMode.VIDEO_ONLY -> + captureModeUiState.isCaptureModeSelectable(CaptureMode.VIDEO_ONLY) - CaptureMode.IMAGE_ONLY -> - captureModeUiState.isCaptureModeSelectable(CaptureMode.IMAGE_ONLY) - }, - isHighLighted = + CaptureMode.IMAGE_ONLY -> + captureModeUiState.isCaptureModeSelectable(CaptureMode.IMAGE_ONLY) + }, + isHighLighted = isHighlightEnabled && (assignedCaptureMode == captureModeUiState.selectedCaptureMode) - ) - } + ) } /** @@ -243,39 +242,6 @@ fun QuickNavSettings(onNavigateToSettings: () -> Unit, modifier: Modifier = Modi ) } -@Composable -fun ToggleFocusedQuickSetCaptureMode( - setCaptureMode: (captureMode: CaptureMode) -> Unit, - captureModeUiState: CaptureModeUiState, - modifier: Modifier = Modifier, - isHighlightEnabled: Boolean = false -) { - if (captureModeUiState is CaptureModeUiState.Available) { - val enum = - when (captureModeUiState.selectedCaptureMode) { - CaptureMode.STANDARD -> CameraCaptureMode.STANDARD - CaptureMode.VIDEO_ONLY -> CameraCaptureMode.VIDEO_ONLY - CaptureMode.IMAGE_ONLY -> CameraCaptureMode.IMAGE_ONLY - } - - QuickSettingToggleButton( - modifier = modifier, - enum = enum, - isHighLighted = isHighlightEnabled, - enabled = captureModeUiState.availableCaptureModes.count { - it is SingleSelectableUiState.SelectableUi - } >= 2, - onClick = { - setCaptureMode( - captureModeUiState.availableCaptureModes.getNextSelectableItem( - captureModeUiState.selectedCaptureMode - ) - ) - } - - ) - } -} @Composable fun QuickSetHdr( @@ -286,9 +252,9 @@ fun QuickSetHdr( val enum = if (hdrUiState is HdrUiState.Available && ( - hdrUiState.selectedDynamicRange == DEFAULT_HDR_DYNAMIC_RANGE || - hdrUiState.selectedImageFormat == DEFAULT_HDR_IMAGE_OUTPUT - ) + hdrUiState.selectedDynamicRange == DEFAULT_HDR_DYNAMIC_RANGE || + hdrUiState.selectedImageFormat == DEFAULT_HDR_IMAGE_OUTPUT + ) ) { CameraDynamicRange.HDR } else { @@ -320,78 +286,131 @@ fun QuickSetHdr( onClick(newVideoDynamicRange, newImageOutputFormat) }, isHighLighted = ( - hdrUiState is HdrUiState.Available && - ( - hdrUiState.selectedDynamicRange == DEFAULT_HDR_DYNAMIC_RANGE || - hdrUiState.selectedImageFormat == DEFAULT_HDR_IMAGE_OUTPUT - ) - ), + hdrUiState is HdrUiState.Available && + ( + hdrUiState.selectedDynamicRange == DEFAULT_HDR_DYNAMIC_RANGE || + hdrUiState.selectedImageFormat == DEFAULT_HDR_IMAGE_OUTPUT + ) + ), enabled = hdrUiState is HdrUiState.Available ) } + @Composable -fun ToggleFocusedQuickSetRatio( - setRatio: (aspectRatio: AspectRatio) -> Unit, - aspectRatioUiState: AspectRatioUiState, +fun AspectRatioRow( modifier: Modifier = Modifier, - isHighlightEnabled: Boolean = false + onSetAspectRatio: (AspectRatio) -> Unit, + aspectRatioUiState: AspectRatioUiState, ) { if (aspectRatioUiState is AspectRatioUiState.Available) { - val enum = - when (aspectRatioUiState.selectedAspectRatio) { - AspectRatio.THREE_FOUR -> CameraAspectRatio.THREE_FOUR - AspectRatio.NINE_SIXTEEN -> CameraAspectRatio.NINE_SIXTEEN - AspectRatio.ONE_ONE -> CameraAspectRatio.ONE_ONE - } - QuickSettingToggleButton( + SettingRow( modifier = modifier, - enum = enum, - isHighLighted = isHighlightEnabled, - onClick = { - setRatio( - aspectRatioUiState.availableAspectRatios.getNextSelectableItem( - aspectRatioUiState.selectedAspectRatio + title = "Capture Mode", + status = aspectRatioUiState.selectedAspectRatio.toString(), + settingsButtons = arrayOf( + + { + QuickSetRatio( + modifier = Modifier.testTag(QUICK_SETTINGS_RATIO_3_4_BUTTON), + onClick = { onSetAspectRatio(AspectRatio.THREE_FOUR) }, + assignedRatio = AspectRatio.THREE_FOUR, + aspectRatioUiState = aspectRatioUiState, + isHighlightEnabled = true ) - ) - } - ) + }, + { + QuickSetRatio( + modifier = Modifier.testTag(QUICK_SETTINGS_RATIO_9_16_BUTTON), + onClick = { onSetAspectRatio(AspectRatio.NINE_SIXTEEN) }, + assignedRatio = AspectRatio.NINE_SIXTEEN, + aspectRatioUiState = aspectRatioUiState, + isHighlightEnabled = true + ) + }, + { + QuickSetRatio( + modifier = Modifier.testTag(QUICK_SETTINGS_RATIO_1_1_BUTTON), + onClick = { onSetAspectRatio(AspectRatio.ONE_ONE) }, + assignedRatio = AspectRatio.ONE_ONE, + aspectRatioUiState = aspectRatioUiState, + isHighlightEnabled = true + ) + } + )) } } + @Composable -fun QuickSetFlash( +fun FlashRow( modifier: Modifier = Modifier, - onClick: (FlashMode) -> Unit, - flashModeUiState: FlashModeUiState + onSetFlashMode: (FlashMode) -> Unit, + flashModeUiState: FlashModeUiState, ) { - when (flashModeUiState) { - is FlashModeUiState.Unavailable -> - QuickSettingToggleButton( - modifier = modifier, - enum = CameraFlashMode.OFF, - enabled = false, - onClick = {} - ) + if (flashModeUiState is FlashModeUiState.Available) { - is FlashModeUiState.Available -> - QuickSettingToggleButton( - modifier = modifier, - enum = flashModeUiState.selectedFlashMode.toCameraFlashMode( - flashModeUiState.isLowLightBoostActive - ), - isHighLighted = flashModeUiState.selectedFlashMode != FlashMode.OFF, - onClick = { - onClick( - flashModeUiState.availableFlashModes.getNextSelectableItem( - flashModeUiState.selectedFlashMode - ) + SettingRow( + modifier = modifier, + title = "Capture Mode", + status = flashModeUiState.selectedFlashMode.toString(), + settingsButtons = arrayOf( + + { + QuickSetFlash( + modifier = Modifier.testTag("flashOn"), + onClick = { onSetFlashMode(FlashMode.ON ) }, + assignedFlashMode = FlashMode.ON , + flashModeUiState = flashModeUiState, + ) + }, + { + QuickSetFlash( + modifier = Modifier.testTag("flashAuto"), + onClick = { onSetFlashMode(FlashMode.AUTO ) }, + assignedFlashMode = FlashMode.AUTO , + flashModeUiState = flashModeUiState, + ) + }, + { + QuickSetFlash( + modifier = Modifier.testTag("flashOff"), + onClick = { onSetFlashMode(FlashMode.OFF ) }, + assignedFlashMode = FlashMode.OFF , + flashModeUiState = flashModeUiState, ) } - ) + )) + } +} + + +@Composable +fun QuickSetFlash( + modifier: Modifier = Modifier, + onClick: (FlashMode) -> Unit, + assignedFlashMode: FlashMode, + flashModeUiState: FlashModeUiState.Available +) { + val enum = when (assignedFlashMode) { + FlashMode.OFF -> CameraFlashMode.OFF + FlashMode.ON -> CameraFlashMode.ON + FlashMode.AUTO -> CameraFlashMode.AUTO + FlashMode.LOW_LIGHT_BOOST -> TODO("not yet handled") } + QuickSettingToggleButton( + modifier = modifier, + enum = enum, + isHighLighted = flashModeUiState.selectedFlashMode == assignedFlashMode, + onClick = { + onClick( + assignedFlashMode + ) + } + ) } + @Composable fun QuickFlipCamera( setLensFacing: (LensFacing) -> Unit, @@ -460,7 +479,7 @@ fun QuickSetConcurrentCamera( } }, isHighLighted = concurrentCameraUiState.selectedConcurrentCameraMode == - ConcurrentCameraMode.DUAL, + ConcurrentCameraMode.DUAL, enabled = concurrentCameraUiState.isEnabled ) } @@ -548,6 +567,7 @@ fun QuickSettingsBottomSheet( modifier: Modifier, onDismiss: () -> Unit, sheetState: SheetState, + wipCapButton: @Composable () -> Unit, vararg quickSettingButtons: @Composable () -> Unit ) { val openDescription = stringResource(R.string.quick_settings_toggle_open_description) @@ -565,109 +585,17 @@ fun QuickSettingsBottomSheet( onDismissRequest = onDismiss, sheetState = sheetState ) { - QuickSettingsBottomSheetRow( - modifier = Modifier, - quickSettingButtons = quickSettingButtons - ) - } -} - -@OptIn(ExperimentalMaterial3ExpressiveApi::class) -@Composable -fun focusedRatioButtons( - onUnFocus: () -> Unit, - onSetAspectRatio: (AspectRatio) -> Unit, - aspectRatioUiState: AspectRatioUiState -): List<@Composable () -> Unit> = listOf( - { - CloseExpandedSettingsButton(onUnFocus) - }, - { - QuickSetRatio( - modifier = Modifier.testTag(QUICK_SETTINGS_RATIO_3_4_BUTTON), - onClick = { onSetAspectRatio(AspectRatio.THREE_FOUR) }, - assignedRatio = AspectRatio.THREE_FOUR, - aspectRatioUiState = aspectRatioUiState, - isHighlightEnabled = true - ) - }, - { - QuickSetRatio( - modifier = Modifier.testTag(QUICK_SETTINGS_RATIO_9_16_BUTTON), - onClick = { onSetAspectRatio(AspectRatio.NINE_SIXTEEN) }, - assignedRatio = AspectRatio.NINE_SIXTEEN, - aspectRatioUiState = aspectRatioUiState, - isHighlightEnabled = true - ) - }, - { - QuickSetRatio( - modifier = Modifier.testTag(QUICK_SETTINGS_RATIO_1_1_BUTTON), - onClick = { onSetAspectRatio(AspectRatio.ONE_ONE) }, - assignedRatio = AspectRatio.ONE_ONE, - aspectRatioUiState = aspectRatioUiState, - isHighlightEnabled = true - ) - } -) - -@OptIn(ExperimentalMaterial3ExpressiveApi::class) -@Composable -fun focusedCaptureModeButtons( - onUnFocus: () -> Unit, - onSetCaptureMode: (CaptureMode) -> Unit, - captureModeUiState: CaptureModeUiState -): List<@Composable () -> Unit> = listOf( - { - CloseExpandedSettingsButton(onUnFocus) - }, - { - QuickSetCaptureMode( - modifier = Modifier - .testTag(BTN_QUICK_SETTINGS_FOCUSED_CAPTURE_MODE_OPTION_STANDARD), - onClick = { onSetCaptureMode(CaptureMode.STANDARD) }, - assignedCaptureMode = CaptureMode.STANDARD, - captureModeUiState = captureModeUiState, - isHighlightEnabled = true - ) - }, - { - QuickSetCaptureMode( - modifier = Modifier - .testTag(BTN_QUICK_SETTINGS_FOCUSED_CAPTURE_MODE_IMAGE_ONLY), - onClick = { onSetCaptureMode(CaptureMode.IMAGE_ONLY) }, - assignedCaptureMode = CaptureMode.IMAGE_ONLY, - captureModeUiState = captureModeUiState, - isHighlightEnabled = true - ) - }, - { - QuickSetCaptureMode( - modifier = Modifier - .testTag(BTN_QUICK_SETTINGS_FOCUSED_CAPTURE_MODE_VIDEO_ONLY), - onClick = { onSetCaptureMode(CaptureMode.VIDEO_ONLY) }, - assignedCaptureMode = CaptureMode.VIDEO_ONLY, - captureModeUiState = captureModeUiState, - isHighlightEnabled = true - ) - } -) - -@Composable -private fun CloseExpandedSettingsButton(onUnFocus: () -> Unit, modifier: Modifier = Modifier) { - FilledIconButton( - modifier = modifier.testTag(QUICK_SETTINGS_CLOSE_EXPANDED_BUTTON), - onClick = onUnFocus - ) { - Icon( - imageVector = Icons.Default.Close, - contentDescription = stringResource( - R.string.quick_settings_btn_close_expanded_settings_description + wipCapButton() + Column { + QuickSettingsBottomSheetRow( + modifier = Modifier, + quickSettingButtons = quickSettingButtons ) - ) + } } } + /** * A horizontally scrollable row of quick setting buttons for a bottom sheet. * This row will only enable scrolling if its content overflows the screen width. @@ -802,6 +730,7 @@ fun SettingRow( } } } + /** * Should you want to have an expanded view of a single quick setting */ @@ -814,25 +743,25 @@ private fun ExpandedQuickSetting( min( quickSettingButtons.size, ( - ( - LocalConfiguration.current.screenWidthDp.dp - ( - dimensionResource( - id = R.dimen.quick_settings_ui_horizontal_padding - ) * 2 - ) - ) / ( - dimensionResource( - id = R.dimen.quick_settings_ui_item_icon_size - ) + + LocalConfiguration.current.screenWidthDp.dp - ( + dimensionResource( + id = R.dimen.quick_settings_ui_horizontal_padding + ) * 2 + ) + ) / ( - dimensionResource( - id = R.dimen.quick_settings_ui_item_padding - ) * - 2 - ) - ) - ).toInt() + dimensionResource( + id = R.dimen.quick_settings_ui_item_icon_size + ) + + ( + dimensionResource( + id = R.dimen.quick_settings_ui_item_padding + ) * + 2 + ) + ) + ).toInt() ) LazyVerticalGrid( modifier = modifier.fillMaxWidth(), @@ -850,9 +779,9 @@ fun HdrIndicator(hdrUiState: HdrUiState, modifier: Modifier = Modifier) { val enum = if (hdrUiState is HdrUiState.Available && ( - hdrUiState.selectedDynamicRange == DEFAULT_HDR_DYNAMIC_RANGE || - hdrUiState.selectedImageFormat == DEFAULT_HDR_IMAGE_OUTPUT - ) + hdrUiState.selectedDynamicRange == DEFAULT_HDR_DYNAMIC_RANGE || + hdrUiState.selectedImageFormat == DEFAULT_HDR_IMAGE_OUTPUT + ) ) { CameraDynamicRange.HDR } else { diff --git a/ui/controller/impl/src/main/java/com/google/jetpackcamera/ui/controller/impl/QuickSettingsControllerImpl.kt b/ui/controller/impl/src/main/java/com/google/jetpackcamera/ui/controller/impl/QuickSettingsControllerImpl.kt index 3bfac592c..c019db076 100644 --- a/ui/controller/impl/src/main/java/com/google/jetpackcamera/ui/controller/impl/QuickSettingsControllerImpl.kt +++ b/ui/controller/impl/src/main/java/com/google/jetpackcamera/ui/controller/impl/QuickSettingsControllerImpl.kt @@ -27,8 +27,6 @@ import com.google.jetpackcamera.model.LensFacing import com.google.jetpackcamera.model.StreamConfig import com.google.jetpackcamera.ui.controller.quicksettings.QuickSettingsController import com.google.jetpackcamera.ui.uistate.capture.TrackedCaptureUiState -import com.google.jetpackcamera.ui.uistate.capture.compound.FocusedQuickSetting -import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.cancel @@ -36,6 +34,7 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.job import kotlinx.coroutines.launch +import kotlin.coroutines.CoroutineContext /** * Implementation of [QuickSettingsController] that interacts with [CameraSystem] and updates @@ -61,12 +60,6 @@ class QuickSettingsControllerImpl( } } - override fun setFocusedSetting(focusedQuickSetting: FocusedQuickSetting) { - trackedCaptureUiState.update { old -> - old.copy(focusedQuickSetting = focusedQuickSetting) - } - } - override fun setLensFacing(lensFace: LensFacing) { scope.launch { // apply to cameraSystem diff --git a/ui/controller/src/main/java/com/google/jetpackcamera/ui/controller/quicksettings/QuickSettingsController.kt b/ui/controller/src/main/java/com/google/jetpackcamera/ui/controller/quicksettings/QuickSettingsController.kt index e0f40f4ac..026b23c42 100644 --- a/ui/controller/src/main/java/com/google/jetpackcamera/ui/controller/quicksettings/QuickSettingsController.kt +++ b/ui/controller/src/main/java/com/google/jetpackcamera/ui/controller/quicksettings/QuickSettingsController.kt @@ -23,7 +23,6 @@ import com.google.jetpackcamera.model.FlashMode import com.google.jetpackcamera.model.ImageOutputFormat import com.google.jetpackcamera.model.LensFacing import com.google.jetpackcamera.model.StreamConfig -import com.google.jetpackcamera.ui.uistate.capture.compound.FocusedQuickSetting /** * Interface for controlling quick settings. @@ -34,13 +33,6 @@ interface QuickSettingsController { */ fun toggleQuickSettings() - /** - * Sets the currently focused quick setting, used for expanding a setting's options. - * - * @param focusedQuickSetting The quick setting to focus. - */ - fun setFocusedSetting(focusedQuickSetting: FocusedQuickSetting) - /** * Sets the lens facing (e.g., front or back camera). * diff --git a/ui/uistate/capture/src/main/java/com/google/jetpackcamera/ui/uistate/capture/TrackedCaptureUiState.kt b/ui/uistate/capture/src/main/java/com/google/jetpackcamera/ui/uistate/capture/TrackedCaptureUiState.kt index 227b954d7..fa03ba9cf 100644 --- a/ui/uistate/capture/src/main/java/com/google/jetpackcamera/ui/uistate/capture/TrackedCaptureUiState.kt +++ b/ui/uistate/capture/src/main/java/com/google/jetpackcamera/ui/uistate/capture/TrackedCaptureUiState.kt @@ -16,7 +16,6 @@ package com.google.jetpackcamera.ui.uistate.capture import com.google.jetpackcamera.data.media.MediaDescriptor -import com.google.jetpackcamera.ui.uistate.capture.compound.FocusedQuickSetting /** * Data class to track UI-specific states within the PreviewViewModel. @@ -29,7 +28,6 @@ import com.google.jetpackcamera.ui.uistate.capture.compound.FocusedQuickSetting */ data class TrackedCaptureUiState( val isQuickSettingsOpen: Boolean = false, - val focusedQuickSetting: FocusedQuickSetting = FocusedQuickSetting.NONE, val isDebugOverlayOpen: Boolean = false, val isRecordingLocked: Boolean = false, val zoomAnimationTarget: Float? = null, diff --git a/ui/uistate/capture/src/main/java/com/google/jetpackcamera/ui/uistate/capture/compound/QuickSettingsUiState.kt b/ui/uistate/capture/src/main/java/com/google/jetpackcamera/ui/uistate/capture/compound/QuickSettingsUiState.kt index 49cab445f..27066891c 100644 --- a/ui/uistate/capture/src/main/java/com/google/jetpackcamera/ui/uistate/capture/compound/QuickSettingsUiState.kt +++ b/ui/uistate/capture/src/main/java/com/google/jetpackcamera/ui/uistate/capture/compound/QuickSettingsUiState.kt @@ -46,8 +46,6 @@ sealed interface QuickSettingsUiState { * @param hdrUiState The UI state for the HDR (High Dynamic Range) setting. * @param streamConfigUiState The UI state for stream configuration. * @param quickSettingsIsOpen Indicates whether the quick settings panel is currently open. - * @param focusedQuickSetting The specific quick setting that is currently focused by the user, - * allowing for more detailed interaction (e.g., showing a sub-menu). */ data class Available( val aspectRatioUiState: AspectRatioUiState, @@ -58,20 +56,8 @@ sealed interface QuickSettingsUiState { val hdrUiState: HdrUiState, val streamConfigUiState: StreamConfigUiState, val quickSettingsIsOpen: Boolean = false, - val focusedQuickSetting: FocusedQuickSetting = FocusedQuickSetting.NONE ) : QuickSettingsUiState companion object } -/** - * Represents which individual quick setting is currently focused by the user. - * - * When a quick setting is focused, the UI may highlight it or show a sub-panel with more options - * related to that setting. - */ -enum class FocusedQuickSetting { - NONE, - ASPECT_RATIO, - CAPTURE_MODE -} diff --git a/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/compound/CaptureUiStateAdapter.kt b/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/compound/CaptureUiStateAdapter.kt index eabc94353..df9175ac2 100644 --- a/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/compound/CaptureUiStateAdapter.kt +++ b/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/compound/CaptureUiStateAdapter.kt @@ -125,7 +125,6 @@ fun captureUiState( aspectRatioUiState, hdrUiState, trackedUiState.isQuickSettingsOpen, - trackedUiState.focusedQuickSetting, externalCaptureMode ), sessionFirstFrameTimestamp = cameraState.sessionFirstFrameTimestamp, diff --git a/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/compound/QuickSettingsUiStateAdapter.kt b/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/compound/QuickSettingsUiStateAdapter.kt index 2563adac4..cd80be11d 100644 --- a/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/compound/QuickSettingsUiStateAdapter.kt +++ b/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/compound/QuickSettingsUiStateAdapter.kt @@ -25,7 +25,6 @@ import com.google.jetpackcamera.ui.uistate.capture.FlashModeUiState import com.google.jetpackcamera.ui.uistate.capture.FlipLensUiState import com.google.jetpackcamera.ui.uistate.capture.HdrUiState import com.google.jetpackcamera.ui.uistate.capture.StreamConfigUiState -import com.google.jetpackcamera.ui.uistate.capture.compound.FocusedQuickSetting import com.google.jetpackcamera.ui.uistate.capture.compound.QuickSettingsUiState import com.google.jetpackcamera.ui.uistateadapter.capture.from @@ -43,7 +42,6 @@ import com.google.jetpackcamera.ui.uistateadapter.capture.from * @param aspectRatioUiState The UI state for the aspect ratio setting. * @param hdrUiState The UI state for the HDR setting. * @param quickSettingsIsOpen Indicates whether the quick settings panel is open. - * @param focusedQuickSetting The currently focused quick setting, if any. * @param externalCaptureMode The external capture mode, if any. * @return A [QuickSettingsUiState.Available] instance containing the consolidated states. */ @@ -56,7 +54,6 @@ fun QuickSettingsUiState.Companion.from( aspectRatioUiState: AspectRatioUiState, hdrUiState: HdrUiState, quickSettingsIsOpen: Boolean, - focusedQuickSetting: FocusedQuickSetting, externalCaptureMode: ExternalCaptureMode ): QuickSettingsUiState { val streamConfigUiState = StreamConfigUiState.from(cameraAppSettings) @@ -75,6 +72,5 @@ fun QuickSettingsUiState.Companion.from( hdrUiState = hdrUiState, streamConfigUiState = streamConfigUiState, quickSettingsIsOpen = quickSettingsIsOpen, - focusedQuickSetting = focusedQuickSetting ) } From 75c6f0da9b16b02bcec50a972339a51f34ed3251 Mon Sep 17 00:00:00 2001 From: Kimberly Crevecoeur Date: Thu, 9 Apr 2026 17:56:59 +0000 Subject: [PATCH 03/51] move setting button creation into auxiliary functions --- .../ui/QuickSettingsComponents.kt | 167 +++++++----------- 1 file changed, 68 insertions(+), 99 deletions(-) diff --git a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/ui/QuickSettingsComponents.kt b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/ui/QuickSettingsComponents.kt index c863a976f..ac45afc3e 100644 --- a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/ui/QuickSettingsComponents.kt +++ b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/ui/QuickSettingsComponents.kt @@ -35,11 +35,9 @@ import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.MoreHoriz import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi -import androidx.compose.material3.FilledIconButton import androidx.compose.material3.FilledIconToggleButton import androidx.compose.material3.Icon import androidx.compose.material3.IconButton @@ -82,15 +80,8 @@ import com.google.jetpackcamera.model.FlashMode import com.google.jetpackcamera.model.ImageOutputFormat import com.google.jetpackcamera.model.LensFacing import com.google.jetpackcamera.model.StreamConfig -import com.google.jetpackcamera.ui.components.capture.BTN_QUICK_SETTINGS_FOCUSED_CAPTURE_MODE_IMAGE_ONLY -import com.google.jetpackcamera.ui.components.capture.BTN_QUICK_SETTINGS_FOCUSED_CAPTURE_MODE_OPTION_STANDARD -import com.google.jetpackcamera.ui.components.capture.BTN_QUICK_SETTINGS_FOCUSED_CAPTURE_MODE_VIDEO_ONLY import com.google.jetpackcamera.ui.components.capture.QUICK_SETTINGS_BOTTOM_SHEET -import com.google.jetpackcamera.ui.components.capture.QUICK_SETTINGS_CLOSE_EXPANDED_BUTTON import com.google.jetpackcamera.ui.components.capture.QUICK_SETTINGS_DROP_DOWN -import com.google.jetpackcamera.ui.components.capture.QUICK_SETTINGS_RATIO_1_1_BUTTON -import com.google.jetpackcamera.ui.components.capture.QUICK_SETTINGS_RATIO_3_4_BUTTON -import com.google.jetpackcamera.ui.components.capture.QUICK_SETTINGS_RATIO_9_16_BUTTON import com.google.jetpackcamera.ui.components.capture.QUICK_SETTINGS_SCROLL_CONTAINER import com.google.jetpackcamera.ui.components.capture.R import com.google.jetpackcamera.ui.components.capture.SETTINGS_BUTTON @@ -152,42 +143,31 @@ fun CaptureModeRow( modifier = modifier, title = "Capture Mode", status = captureModeUiState.selectedCaptureMode.toString(), - settingsButtons = arrayOf( - { - CaptureModeToggleButton( - modifier = Modifier - .testTag(BTN_QUICK_SETTINGS_FOCUSED_CAPTURE_MODE_OPTION_STANDARD), - onClick = { onSetCaptureMode(CaptureMode.STANDARD) }, - assignedCaptureMode = CaptureMode.STANDARD, - captureModeUiState = captureModeUiState, - isHighlightEnabled = true - ) - }, - { - CaptureModeToggleButton( - modifier = Modifier - .testTag(BTN_QUICK_SETTINGS_FOCUSED_CAPTURE_MODE_IMAGE_ONLY), - onClick = { onSetCaptureMode(CaptureMode.IMAGE_ONLY) }, - assignedCaptureMode = CaptureMode.IMAGE_ONLY, - captureModeUiState = captureModeUiState, - isHighlightEnabled = true - ) - }, - { - CaptureModeToggleButton( - modifier = Modifier - .testTag(BTN_QUICK_SETTINGS_FOCUSED_CAPTURE_MODE_VIDEO_ONLY), - onClick = { onSetCaptureMode(CaptureMode.VIDEO_ONLY) }, - assignedCaptureMode = CaptureMode.VIDEO_ONLY, - captureModeUiState = captureModeUiState, - isHighlightEnabled = true - ) - } - ) + settingsButtons = createCaptureModeButtons(captureModeUiState, onSetCaptureMode) ) } } +@Composable +private fun createCaptureModeButtons( + captureModeUiState: CaptureModeUiState.Available, + onSetCaptureMode: (CaptureMode) -> Unit +): Array<@Composable () -> Unit> { + return captureModeUiState.availableCaptureModes + .map { selectableMode -> + @Composable { + CaptureModeToggleButton( + modifier = Modifier + .testTag("CaptureMode_${selectableMode.value.name}"), + onClick = { onSetCaptureMode(selectableMode.value) }, + assignedCaptureMode = selectableMode.value, + captureModeUiState = captureModeUiState, + isHighlightEnabled = true + ) + } + }.toTypedArray() +} + @Composable fun CaptureModeToggleButton( modifier: Modifier = Modifier, @@ -308,39 +288,30 @@ fun AspectRatioRow( modifier = modifier, title = "Capture Mode", status = aspectRatioUiState.selectedAspectRatio.toString(), - settingsButtons = arrayOf( - - { - QuickSetRatio( - modifier = Modifier.testTag(QUICK_SETTINGS_RATIO_3_4_BUTTON), - onClick = { onSetAspectRatio(AspectRatio.THREE_FOUR) }, - assignedRatio = AspectRatio.THREE_FOUR, - aspectRatioUiState = aspectRatioUiState, - isHighlightEnabled = true - ) - }, - { - QuickSetRatio( - modifier = Modifier.testTag(QUICK_SETTINGS_RATIO_9_16_BUTTON), - onClick = { onSetAspectRatio(AspectRatio.NINE_SIXTEEN) }, - assignedRatio = AspectRatio.NINE_SIXTEEN, - aspectRatioUiState = aspectRatioUiState, - isHighlightEnabled = true - ) - }, - { - QuickSetRatio( - modifier = Modifier.testTag(QUICK_SETTINGS_RATIO_1_1_BUTTON), - onClick = { onSetAspectRatio(AspectRatio.ONE_ONE) }, - assignedRatio = AspectRatio.ONE_ONE, - aspectRatioUiState = aspectRatioUiState, - isHighlightEnabled = true - ) - } - )) + settingsButtons = createAspectRatioButtons(aspectRatioUiState, onSetAspectRatio) + ) } } +@Composable +private fun createAspectRatioButtons( + aspectRatioUiState: AspectRatioUiState.Available, + onSetAspectRatio: (AspectRatio) -> Unit +): Array<@Composable () -> Unit> { + return aspectRatioUiState.availableAspectRatios + .map { selectableRatio -> + @Composable { + QuickSetRatio( + modifier = Modifier.testTag("AspectRatio_${selectableRatio.value.name}"), + onClick = { onSetAspectRatio(selectableRatio.value) }, + assignedRatio = selectableRatio.value, + aspectRatioUiState = aspectRatioUiState, + isHighlightEnabled = true + ) + } + }.toTypedArray() +} + @Composable fun FlashRow( @@ -354,36 +325,29 @@ fun FlashRow( modifier = modifier, title = "Capture Mode", status = flashModeUiState.selectedFlashMode.toString(), - settingsButtons = arrayOf( - - { - QuickSetFlash( - modifier = Modifier.testTag("flashOn"), - onClick = { onSetFlashMode(FlashMode.ON ) }, - assignedFlashMode = FlashMode.ON , - flashModeUiState = flashModeUiState, - ) - }, - { - QuickSetFlash( - modifier = Modifier.testTag("flashAuto"), - onClick = { onSetFlashMode(FlashMode.AUTO ) }, - assignedFlashMode = FlashMode.AUTO , - flashModeUiState = flashModeUiState, - ) - }, - { - QuickSetFlash( - modifier = Modifier.testTag("flashOff"), - onClick = { onSetFlashMode(FlashMode.OFF ) }, - assignedFlashMode = FlashMode.OFF , - flashModeUiState = flashModeUiState, - ) - } - )) + settingsButtons = createFlashModeButtons(flashModeUiState, onSetFlashMode) + ) } } +@Composable +private fun createFlashModeButtons( + flashModeUiState: FlashModeUiState.Available, + onSetFlashMode: (FlashMode) -> Unit +): Array<@Composable () -> Unit> { + return flashModeUiState.availableFlashModes + .map { selectableMode -> + @Composable { + QuickSetFlash( + modifier = Modifier.testTag("FlashMode_${selectableMode.value.name}"), + onClick = { onSetFlashMode(selectableMode.value) }, + assignedFlashMode = selectableMode.value, + flashModeUiState = flashModeUiState, + ) + } + }.toTypedArray() +} + @Composable fun QuickSetFlash( @@ -396,8 +360,13 @@ fun QuickSetFlash( FlashMode.OFF -> CameraFlashMode.OFF FlashMode.ON -> CameraFlashMode.ON FlashMode.AUTO -> CameraFlashMode.AUTO - FlashMode.LOW_LIGHT_BOOST -> TODO("not yet handled") + FlashMode.LOW_LIGHT_BOOST -> when (flashModeUiState.isLowLightBoostActive) { + true -> CameraFlashMode.LOW_LIGHT_BOOST_ACTIVE + false -> CameraFlashMode.LOW_LIGHT_BOOST_INACTIVE + } } + + QuickSettingToggleButton( modifier = modifier, enum = enum, From 9ce2206c59548b877d710095354ea6f31c013fc5 Mon Sep 17 00:00:00 2001 From: Kimberly Crevecoeur Date: Tue, 28 Apr 2026 17:15:55 +0000 Subject: [PATCH 04/51] refactor to quick setting structure WIP - remove flip camera, stream config, and concurrent camera from quick settings - all quick settings menu items adopt the button row ux - WIP unique selection of settings depending on current capture mode --- .../quicksettings/QuickSettingsScreen.kt | 345 +++++-------- .../ui/QuickSettingsComponents.kt | 478 +++++------------- .../capture/src/main/res/values/strings.xml | 3 + 3 files changed, 270 insertions(+), 556 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 a8a10b796..232154305 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 @@ -16,13 +16,16 @@ package com.google.jetpackcamera.ui.components.capture.quicksettings import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp import com.google.jetpackcamera.model.AspectRatio import com.google.jetpackcamera.model.CaptureMode import com.google.jetpackcamera.model.ConcurrentCameraMode @@ -31,20 +34,11 @@ import com.google.jetpackcamera.model.FlashMode import com.google.jetpackcamera.model.ImageOutputFormat import com.google.jetpackcamera.model.LensFacing import com.google.jetpackcamera.model.StreamConfig -import com.google.jetpackcamera.ui.components.capture.QUICK_SETTINGS_CONCURRENT_CAMERA_MODE_BUTTON -import com.google.jetpackcamera.ui.components.capture.QUICK_SETTINGS_FLIP_CAMERA_BUTTON -import com.google.jetpackcamera.ui.components.capture.QUICK_SETTINGS_HDR_BUTTON -import com.google.jetpackcamera.ui.components.capture.QUICK_SETTINGS_STREAM_CONFIG_BUTTON import com.google.jetpackcamera.ui.components.capture.R -import com.google.jetpackcamera.ui.components.capture.SETTINGS_BUTTON import com.google.jetpackcamera.ui.components.capture.quicksettings.ui.AspectRatioRow import com.google.jetpackcamera.ui.components.capture.quicksettings.ui.CaptureModeRow import com.google.jetpackcamera.ui.components.capture.quicksettings.ui.FlashRow -import com.google.jetpackcamera.ui.components.capture.quicksettings.ui.QuickFlipCamera -import com.google.jetpackcamera.ui.components.capture.quicksettings.ui.QuickNavSettings -import com.google.jetpackcamera.ui.components.capture.quicksettings.ui.QuickSetConcurrentCamera import com.google.jetpackcamera.ui.components.capture.quicksettings.ui.QuickSetHdr -import com.google.jetpackcamera.ui.components.capture.quicksettings.ui.QuickSetStreamConfig import com.google.jetpackcamera.ui.controller.quicksettings.QuickSettingsController import com.google.jetpackcamera.ui.uistate.SingleSelectableUiState import com.google.jetpackcamera.ui.uistate.capture.AspectRatioUiState @@ -58,241 +52,170 @@ import com.google.jetpackcamera.ui.uistate.capture.compound.QuickSettingsUiState import com.google.jetpackcamera.ui.components.capture.quicksettings.ui.QuickSettingsBottomSheet as BottomSheetComponent /** - * The UI bottom sheet component for quick settings + * The UI bottom sheet component for quick settings. */ -@Composable @OptIn(ExperimentalMaterial3Api::class) +@Composable fun QuickSettingsBottomSheet( - modifier: Modifier = Modifier, quickSettingsUiState: QuickSettingsUiState, + modifier: Modifier = Modifier, onNavigateToSettings: () -> Unit, quickSettingsController: QuickSettingsController ) { - if (quickSettingsUiState is QuickSettingsUiState.Available) { - - - val displayedQuickSettings: List<@Composable () -> Unit> = - - buildList { - - - add { - QuickFlipCamera( - modifier = Modifier.testTag(QUICK_SETTINGS_FLIP_CAMERA_BUTTON), - setLensFacing = { l: LensFacing -> - quickSettingsController.setLensFacing(l) - }, - flipLensUiState = quickSettingsUiState.flipLensUiState + if (quickSettingsUiState is QuickSettingsUiState.Available && + quickSettingsUiState.quickSettingsIsOpen + ) { + val sheetState = rememberModalBottomSheetState() + BottomSheetComponent( + modifier = modifier, + onDismiss = quickSettingsController::toggleQuickSettings, + sheetState = sheetState + ) { + (quickSettingsUiState.captureModeUiState as? CaptureModeUiState.Available)?.let { + when ( + it.selectedCaptureMode + ) { + CaptureMode.VIDEO_ONLY -> VideoQuickSettings( + quickSettingsUiState = quickSettingsUiState, + quickSettingsController = quickSettingsController ) - } - - - add { - QuickSetStreamConfig( - modifier = Modifier.testTag( - QUICK_SETTINGS_STREAM_CONFIG_BUTTON - ), - setStreamConfig = { c: StreamConfig -> - quickSettingsController.setStreamConfig(c) - }, - streamConfigUiState = quickSettingsUiState.streamConfigUiState + CaptureMode.IMAGE_ONLY -> ImageQuickSettings( + quickSettingsUiState = quickSettingsUiState, + quickSettingsController = quickSettingsController ) - } - add { - QuickSetHdr( - modifier = Modifier.testTag(QUICK_SETTINGS_HDR_BUTTON), - onClick = { d: DynamicRange, i: ImageOutputFormat -> - quickSettingsController.setDynamicRange(d) - quickSettingsController.setImageFormat(i) - }, - hdrUiState = quickSettingsUiState.hdrUiState + CaptureMode.STANDARD -> HybridQuickSettings( + quickSettingsUiState = quickSettingsUiState, + quickSettingsController = quickSettingsController ) } + } ?: ImageQuickSettings( + quickSettingsUiState = quickSettingsUiState, + quickSettingsController = quickSettingsController + ) + } + } +} - add { - QuickSetConcurrentCamera( - modifier = - Modifier.testTag(QUICK_SETTINGS_CONCURRENT_CAMERA_MODE_BUTTON), - setConcurrentCameraMode = { c: ConcurrentCameraMode -> - quickSettingsController.setConcurrentCameraMode(c) - }, - concurrentCameraUiState = quickSettingsUiState - .concurrentCameraUiState - ) - } +@Composable +fun HybridQuickSettings( + quickSettingsUiState: QuickSettingsUiState.Available, + quickSettingsController: QuickSettingsController +) { + Column { + Text( + modifier = Modifier.padding(16.dp), + text = stringResource(id = R.string.quick_settings_title_photo_and_video_settings), + style = MaterialTheme.typography.titleLarge + ) - add { - QuickNavSettings( - modifier = Modifier - .testTag(SETTINGS_BUTTON), - onNavigateToSettings = onNavigateToSettings - ) - } - } + // Flash Mode settings + if (quickSettingsUiState.flashModeUiState is FlashModeUiState.Available) { + FlashRow( + onSetFlashMode = quickSettingsController::setFlash, + flashModeUiState = quickSettingsUiState.flashModeUiState + ) + } + // Capture Mode settings + if (quickSettingsUiState.captureModeUiState is CaptureModeUiState.Available) { + CaptureModeRow( + onSetCaptureMode = quickSettingsController::setCaptureMode, + captureModeUiState = quickSettingsUiState.captureModeUiState + ) + } - val sheetState = rememberModalBottomSheetState() + // Aspect Ratio settings + if (quickSettingsUiState.aspectRatioUiState is AspectRatioUiState.Available) { + AspectRatioRow( + aspectRatioUiState = quickSettingsUiState.aspectRatioUiState, + onSetAspectRatio = quickSettingsController::setAspectRatio + ) + } - if (quickSettingsUiState.quickSettingsIsOpen) { - BottomSheetComponent( - modifier = modifier, - onDismiss = { - quickSettingsController.toggleQuickSettings() + // HDR settings + if (quickSettingsUiState.hdrUiState is HdrUiState.Available) { + QuickSetHdr( + onClick = { d: DynamicRange, i: ImageOutputFormat -> + quickSettingsController.setDynamicRange(d) + quickSettingsController.setImageFormat(i) }, - sheetState = sheetState, - wipCapButton = { - Column { - FlashRow( - onSetFlashMode = quickSettingsController::setFlash, - flashModeUiState = quickSettingsUiState.flashModeUiState - ) - CaptureModeRow( - captureModeUiState = quickSettingsUiState.captureModeUiState, - onSetCaptureMode = quickSettingsController::setCaptureMode - ) - AspectRatioRow( - aspectRatioUiState = quickSettingsUiState.aspectRatioUiState, - onSetAspectRatio = quickSettingsController::setAspectRatio - ) - } - }, - *displayedQuickSettings.toTypedArray() + hdrUiState = quickSettingsUiState.hdrUiState ) } } } -private fun CaptureModeUiState.stateDescription() = (this as? CaptureModeUiState.Available)?.let { - when (selectedCaptureMode) { - CaptureMode.STANDARD -> R.string.quick_settings_description_capture_mode_standard - CaptureMode.VIDEO_ONLY -> R.string.quick_settings_description_capture_mode_video_only - CaptureMode.IMAGE_ONLY -> R.string.quick_settings_description_capture_mode_image_only - } -} - -@Preview @Composable -fun ExpandedQuickSettingsUiPreview() { - MaterialTheme { - QuickSettingsBottomSheet( - quickSettingsUiState = QuickSettingsUiState.Available( - aspectRatioUiState = AspectRatioUiState.Available( - selectedAspectRatio = AspectRatio.NINE_SIXTEEN, - availableAspectRatios = listOf( - SingleSelectableUiState.SelectableUi(AspectRatio.NINE_SIXTEEN), - SingleSelectableUiState.SelectableUi(AspectRatio.THREE_FOUR), - SingleSelectableUiState.SelectableUi(AspectRatio.ONE_ONE) - ) - ), - captureModeUiState = CaptureModeUiState.Available( - selectedCaptureMode = CaptureMode.STANDARD, - availableCaptureModes = listOf( - SingleSelectableUiState.SelectableUi(CaptureMode.STANDARD), - SingleSelectableUiState.SelectableUi(CaptureMode.VIDEO_ONLY), - SingleSelectableUiState.SelectableUi(CaptureMode.IMAGE_ONLY) - ) - ), - concurrentCameraUiState = ConcurrentCameraUiState.Available( - selectedConcurrentCameraMode = ConcurrentCameraMode.OFF, - isEnabled = false - ), - flashModeUiState = FlashModeUiState.Available( - selectedFlashMode = FlashMode.OFF, - availableFlashModes = listOf( - SingleSelectableUiState.SelectableUi(FlashMode.OFF), - SingleSelectableUiState.SelectableUi(FlashMode.ON), - SingleSelectableUiState.SelectableUi(FlashMode.AUTO) - ), - isLowLightBoostActive = false - ), - flipLensUiState = FlipLensUiState.Available( - selectedLensFacing = LensFacing.BACK, - availableLensFacings = listOf( - SingleSelectableUiState.SelectableUi(LensFacing.BACK), - SingleSelectableUiState.SelectableUi(LensFacing.FRONT) - ) - ), - hdrUiState = HdrUiState.Unavailable, - streamConfigUiState = StreamConfigUiState.Available( - selectedStreamConfig = StreamConfig.MULTI_STREAM, - availableStreamConfigs = listOf( - SingleSelectableUiState.SelectableUi(StreamConfig.SINGLE_STREAM), - SingleSelectableUiState.SelectableUi(StreamConfig.MULTI_STREAM) - ), - isActive = false - ), - quickSettingsIsOpen = true - ), - onNavigateToSettings = {}, - quickSettingsController = NoOpQuickSettingsController() +fun VideoQuickSettings( + quickSettingsUiState: QuickSettingsUiState.Available, + quickSettingsController: QuickSettingsController +) { + Column { + Text( + modifier = Modifier.padding(16.dp), + text = stringResource(id = R.string.quick_settings_title_video_settings), + style = MaterialTheme.typography.titleLarge ) + + // Flash Mode settings + if (quickSettingsUiState.flashModeUiState is FlashModeUiState.Available) { + FlashRow( + onSetFlashMode = quickSettingsController::setFlash, + flashModeUiState = quickSettingsUiState.flashModeUiState + ) + } + + // HDR settings + if (quickSettingsUiState.hdrUiState is HdrUiState.Available) { + QuickSetHdr( + onClick = { d: DynamicRange, i: ImageOutputFormat -> + quickSettingsController.setDynamicRange(d) + quickSettingsController.setImageFormat(i) + }, + hdrUiState = quickSettingsUiState.hdrUiState + ) + } + // TODO: Add FPS setting + // TODO: Add Resolution setting + // TODO: Add Stabilization setting } } -@Preview @Composable -fun ExpandedQuickSettingsUiPreview_WithHdr() { - MaterialTheme { - QuickSettingsBottomSheet( - quickSettingsUiState = QuickSettingsUiState.Available( - aspectRatioUiState = AspectRatioUiState.Available( - selectedAspectRatio = AspectRatio.NINE_SIXTEEN, - availableAspectRatios = listOf( - SingleSelectableUiState.SelectableUi(AspectRatio.NINE_SIXTEEN), - SingleSelectableUiState.SelectableUi(AspectRatio.THREE_FOUR), - SingleSelectableUiState.SelectableUi(AspectRatio.ONE_ONE) - ) - ), - captureModeUiState = CaptureModeUiState.Available( - selectedCaptureMode = CaptureMode.STANDARD, - availableCaptureModes = listOf( - SingleSelectableUiState.SelectableUi(CaptureMode.STANDARD), - SingleSelectableUiState.SelectableUi(CaptureMode.VIDEO_ONLY), - SingleSelectableUiState.SelectableUi(CaptureMode.IMAGE_ONLY) - ) - ), - concurrentCameraUiState = ConcurrentCameraUiState.Available( - selectedConcurrentCameraMode = ConcurrentCameraMode.OFF, - isEnabled = false - ), - flashModeUiState = FlashModeUiState.Available( - selectedFlashMode = FlashMode.OFF, - availableFlashModes = listOf( - SingleSelectableUiState.SelectableUi(FlashMode.OFF), - SingleSelectableUiState.SelectableUi(FlashMode.ON), - SingleSelectableUiState.SelectableUi(FlashMode.AUTO) - ), - isLowLightBoostActive = false - ), - flipLensUiState = FlipLensUiState.Available( - selectedLensFacing = LensFacing.BACK, - availableLensFacings = listOf( - SingleSelectableUiState.SelectableUi(LensFacing.BACK), - SingleSelectableUiState.SelectableUi(LensFacing.FRONT) - ) - ), - hdrUiState = HdrUiState.Available( - selectedDynamicRange = DynamicRange.HLG10, - selectedImageFormat = ImageOutputFormat.JPEG_ULTRA_HDR - ), - streamConfigUiState = StreamConfigUiState.Available( - selectedStreamConfig = StreamConfig.MULTI_STREAM, - availableStreamConfigs = listOf( - SingleSelectableUiState.SelectableUi(StreamConfig.SINGLE_STREAM), - SingleSelectableUiState.SelectableUi(StreamConfig.MULTI_STREAM) - ), - isActive = false - ), - quickSettingsIsOpen = true - ), - onNavigateToSettings = { }, - quickSettingsController = NoOpQuickSettingsController() +fun ImageQuickSettings( + quickSettingsUiState: QuickSettingsUiState.Available, + quickSettingsController: QuickSettingsController +) { + Column { + Text( + modifier = Modifier.padding(16.dp), + text = stringResource(id = R.string.quick_settings_title_photo_settings), + style = MaterialTheme.typography.titleLarge ) + + // Flash Mode settings + if (quickSettingsUiState.flashModeUiState is FlashModeUiState.Available) { + FlashRow( + onSetFlashMode = quickSettingsController::setFlash, + flashModeUiState = quickSettingsUiState.flashModeUiState + ) + } + + // Aspect Ratio settings + if (quickSettingsUiState.aspectRatioUiState is AspectRatioUiState.Available) { + AspectRatioRow( + aspectRatioUiState = quickSettingsUiState.aspectRatioUiState, + onSetAspectRatio = quickSettingsController::setAspectRatio + ) + } + + // TODO: Add pre-capture timer setting } } + /** * A no-op implementation of [QuickSettingsController] for use in Compose previews and tests. */ diff --git a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/ui/QuickSettingsComponents.kt b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/ui/QuickSettingsComponents.kt index ac45afc3e..b3dd44c00 100644 --- a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/ui/QuickSettingsComponents.kt +++ b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/ui/QuickSettingsComponents.kt @@ -21,7 +21,6 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.IntrinsicSize -import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth @@ -30,12 +29,6 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.wrapContentWidth -import androidx.compose.foundation.lazy.LazyRow -import androidx.compose.foundation.lazy.grid.GridCells -import androidx.compose.foundation.lazy.grid.LazyVerticalGrid -import androidx.compose.foundation.lazy.itemsIndexed -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.MoreHoriz import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.FilledIconToggleButton @@ -48,6 +41,7 @@ import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.SheetState import androidx.compose.material3.Surface import androidx.compose.material3.Text +import androidx.compose.material3.TextButton import androidx.compose.material3.darkColorScheme import androidx.compose.material3.minimumInteractiveComponentSize import androidx.compose.runtime.Composable @@ -56,10 +50,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.painter.Painter -import androidx.compose.ui.graphics.vector.rememberVectorPainter -import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.testTag -import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.contentDescription @@ -78,59 +69,22 @@ import com.google.jetpackcamera.model.DEFAULT_HDR_IMAGE_OUTPUT import com.google.jetpackcamera.model.DynamicRange import com.google.jetpackcamera.model.FlashMode import com.google.jetpackcamera.model.ImageOutputFormat -import com.google.jetpackcamera.model.LensFacing -import com.google.jetpackcamera.model.StreamConfig import com.google.jetpackcamera.ui.components.capture.QUICK_SETTINGS_BOTTOM_SHEET import com.google.jetpackcamera.ui.components.capture.QUICK_SETTINGS_DROP_DOWN -import com.google.jetpackcamera.ui.components.capture.QUICK_SETTINGS_SCROLL_CONTAINER import com.google.jetpackcamera.ui.components.capture.R -import com.google.jetpackcamera.ui.components.capture.SETTINGS_BUTTON import com.google.jetpackcamera.ui.components.capture.quicksettings.CameraAspectRatio import com.google.jetpackcamera.ui.components.capture.quicksettings.CameraCaptureMode import com.google.jetpackcamera.ui.components.capture.quicksettings.CameraConcurrentCameraMode import com.google.jetpackcamera.ui.components.capture.quicksettings.CameraDynamicRange import com.google.jetpackcamera.ui.components.capture.quicksettings.CameraFlashMode -import com.google.jetpackcamera.ui.components.capture.quicksettings.CameraLensFace -import com.google.jetpackcamera.ui.components.capture.quicksettings.CameraStreamConfig import com.google.jetpackcamera.ui.components.capture.quicksettings.QuickSettingsEnum import com.google.jetpackcamera.ui.controller.quicksettings.QuickSettingsController -import com.google.jetpackcamera.ui.uistate.SingleSelectableUiState import com.google.jetpackcamera.ui.uistate.capture.AspectRatioUiState import com.google.jetpackcamera.ui.uistate.capture.CaptureModeUiState import com.google.jetpackcamera.ui.uistate.capture.CaptureModeUiState.Unavailable.isCaptureModeSelectable import com.google.jetpackcamera.ui.uistate.capture.ConcurrentCameraUiState import com.google.jetpackcamera.ui.uistate.capture.FlashModeUiState -import com.google.jetpackcamera.ui.uistate.capture.FlipLensUiState import com.google.jetpackcamera.ui.uistate.capture.HdrUiState -import com.google.jetpackcamera.ui.uistate.capture.StreamConfigUiState -import kotlin.math.min - -@Composable -fun QuickSetRatio( - onClick: () -> Unit, - assignedRatio: AspectRatio, - aspectRatioUiState: AspectRatioUiState, - modifier: Modifier = Modifier, - isHighlightEnabled: Boolean = false -) { - if (aspectRatioUiState is AspectRatioUiState.Available) { - val enum = - when (assignedRatio) { - AspectRatio.THREE_FOUR -> CameraAspectRatio.THREE_FOUR - AspectRatio.NINE_SIXTEEN -> CameraAspectRatio.NINE_SIXTEEN - AspectRatio.ONE_ONE -> CameraAspectRatio.ONE_ONE - else -> CameraAspectRatio.ONE_ONE - } - QuickSettingToggleButton( - modifier = modifier, - enum = enum, - onClick = { onClick() }, - isHighLighted = isHighlightEnabled && - (assignedRatio == aspectRatioUiState.selectedAspectRatio) - ) - } -} - @Composable fun CaptureModeRow( @@ -142,7 +96,7 @@ fun CaptureModeRow( SettingRow( modifier = modifier, title = "Capture Mode", - status = captureModeUiState.selectedCaptureMode.toString(), + stateString = captureModeUiState.selectedCaptureMode.toString(), settingsButtons = createCaptureModeButtons(captureModeUiState, onSetCaptureMode) ) } @@ -187,12 +141,6 @@ fun CaptureModeToggleButton( enum = enum, onClick = { onClick() }, enabled = when (assignedCaptureMode) { - null -> { - // only enabled if there are at least 2 supported capturemodes - captureModeUiState.availableCaptureModes.count { - it is SingleSelectableUiState.SelectableUi - } >= 2 - } CaptureMode.STANDARD -> captureModeUiState.isCaptureModeSelectable(CaptureMode.STANDARD) @@ -203,7 +151,7 @@ fun CaptureModeToggleButton( CaptureMode.IMAGE_ONLY -> captureModeUiState.isCaptureModeSelectable(CaptureMode.IMAGE_ONLY) }, - isHighLighted = + isSelected = isHighlightEnabled && (assignedCaptureMode == captureModeUiState.selectedCaptureMode) ) } @@ -211,14 +159,13 @@ fun CaptureModeToggleButton( /** * A button in the quick settings menu that will navigate to the default settings screen */ +@OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable fun QuickNavSettings(onNavigateToSettings: () -> Unit, modifier: Modifier = Modifier) { - QuickSettingToggleButton( + TextButton( + modifier = modifier, onClick = onNavigateToSettings, - text = stringResource(R.string.quick_settings_more_text), - accessibilityText = stringResource(R.string.quick_settings_more_description), - painter = rememberVectorPainter(Icons.Filled.MoreHoriz), - modifier = modifier.testTag(SETTINGS_BUTTON) + content = { Text(text = stringResource(R.string.quick_settings_more_text)) } ) } @@ -229,50 +176,38 @@ fun QuickSetHdr( onClick: (DynamicRange, ImageOutputFormat) -> Unit, hdrUiState: HdrUiState ) { - val enum = - if (hdrUiState is HdrUiState.Available && - ( - hdrUiState.selectedDynamicRange == DEFAULT_HDR_DYNAMIC_RANGE || - hdrUiState.selectedImageFormat == DEFAULT_HDR_IMAGE_OUTPUT - ) - ) { - CameraDynamicRange.HDR - } else { - CameraDynamicRange.SDR - } - - val newVideoDynamicRange = if ( - hdrUiState is HdrUiState.Available && - enum == CameraDynamicRange.SDR - ) { - DEFAULT_HDR_DYNAMIC_RANGE - } else { - DynamicRange.SDR - } - - val newImageOutputFormat = if ( - hdrUiState is HdrUiState.Available && - enum == CameraDynamicRange.SDR - ) { - DEFAULT_HDR_IMAGE_OUTPUT - } else { - ImageOutputFormat.JPEG - } + val isHdrOn = hdrUiState is HdrUiState.Available && + ( + hdrUiState.selectedDynamicRange == DEFAULT_HDR_DYNAMIC_RANGE || + hdrUiState.selectedImageFormat == DEFAULT_HDR_IMAGE_OUTPUT + ) - QuickSettingToggleButton( + SettingRow( modifier = modifier, - enum = enum, - onClick = { - onClick(newVideoDynamicRange, newImageOutputFormat) + title = "HDR", + stateString = if (isHdrOn) { + stringResource(R.string.quick_settings_dynamic_range_hdr) + } else { + stringResource(R.string.quick_settings_dynamic_range_sdr) }, - isHighLighted = ( - hdrUiState is HdrUiState.Available && - ( - hdrUiState.selectedDynamicRange == DEFAULT_HDR_DYNAMIC_RANGE || - hdrUiState.selectedImageFormat == DEFAULT_HDR_IMAGE_OUTPUT - ) - ), - enabled = hdrUiState is HdrUiState.Available + settingsButtons = arrayOf( + { + QuickSettingToggleButton( + enum = CameraDynamicRange.HDR, + onClick = { onClick(DEFAULT_HDR_DYNAMIC_RANGE, DEFAULT_HDR_IMAGE_OUTPUT) }, + isSelected = isHdrOn, + enabled = hdrUiState is HdrUiState.Available + ) + }, + { + QuickSettingToggleButton( + enum = CameraDynamicRange.SDR, + onClick = { onClick(DynamicRange.SDR, ImageOutputFormat.JPEG) }, + isSelected = !isHdrOn, + enabled = hdrUiState is HdrUiState.Available + ) + } + ) ) } @@ -284,34 +219,34 @@ fun AspectRatioRow( aspectRatioUiState: AspectRatioUiState, ) { if (aspectRatioUiState is AspectRatioUiState.Available) { + + val settingsButtons = aspectRatioUiState.availableAspectRatios + .map { selectableRatio -> + @Composable { + val enum = when (selectableRatio.value) { + AspectRatio.THREE_FOUR -> CameraAspectRatio.THREE_FOUR + AspectRatio.NINE_SIXTEEN -> CameraAspectRatio.NINE_SIXTEEN + AspectRatio.ONE_ONE -> CameraAspectRatio.ONE_ONE + else -> CameraAspectRatio.ONE_ONE + } + QuickSettingToggleButton( + modifier = Modifier.testTag("AspectRatio_${selectableRatio.value.name}"), + onClick = { onSetAspectRatio(selectableRatio.value) }, + enum = enum, + isSelected = selectableRatio.value == aspectRatioUiState.selectedAspectRatio + ) + } + }.toTypedArray() + SettingRow( modifier = modifier, - title = "Capture Mode", - status = aspectRatioUiState.selectedAspectRatio.toString(), - settingsButtons = createAspectRatioButtons(aspectRatioUiState, onSetAspectRatio) + title = "Aspect Ratio", + stateString = aspectRatioUiState.selectedAspectRatio.toString(), + settingsButtons = settingsButtons ) } } -@Composable -private fun createAspectRatioButtons( - aspectRatioUiState: AspectRatioUiState.Available, - onSetAspectRatio: (AspectRatio) -> Unit -): Array<@Composable () -> Unit> { - return aspectRatioUiState.availableAspectRatios - .map { selectableRatio -> - @Composable { - QuickSetRatio( - modifier = Modifier.testTag("AspectRatio_${selectableRatio.value.name}"), - onClick = { onSetAspectRatio(selectableRatio.value) }, - assignedRatio = selectableRatio.value, - aspectRatioUiState = aspectRatioUiState, - isHighlightEnabled = true - ) - } - }.toTypedArray() -} - @Composable fun FlashRow( @@ -324,30 +259,22 @@ fun FlashRow( SettingRow( modifier = modifier, title = "Capture Mode", - status = flashModeUiState.selectedFlashMode.toString(), - settingsButtons = createFlashModeButtons(flashModeUiState, onSetFlashMode) + stateString = flashModeUiState.selectedFlashMode.toString(), + settingsButtons = flashModeUiState.availableFlashModes + .map { selectableMode -> + @Composable { + QuickSetFlash( + modifier = Modifier.testTag("FlashMode_${selectableMode.value.name}"), + onClick = { onSetFlashMode(selectableMode.value) }, + assignedFlashMode = selectableMode.value, + flashModeUiState = flashModeUiState, + ) + } + }.toTypedArray() ) } } -@Composable -private fun createFlashModeButtons( - flashModeUiState: FlashModeUiState.Available, - onSetFlashMode: (FlashMode) -> Unit -): Array<@Composable () -> Unit> { - return flashModeUiState.availableFlashModes - .map { selectableMode -> - @Composable { - QuickSetFlash( - modifier = Modifier.testTag("FlashMode_${selectableMode.value.name}"), - onClick = { onSetFlashMode(selectableMode.value) }, - assignedFlashMode = selectableMode.value, - flashModeUiState = flashModeUiState, - ) - } - }.toTypedArray() -} - @Composable fun QuickSetFlash( @@ -365,12 +292,10 @@ fun QuickSetFlash( false -> CameraFlashMode.LOW_LIGHT_BOOST_INACTIVE } } - - QuickSettingToggleButton( modifier = modifier, enum = enum, - isHighLighted = flashModeUiState.selectedFlashMode == assignedFlashMode, + isSelected = flashModeUiState.selectedFlashMode == assignedFlashMode, onClick = { onClick( assignedFlashMode @@ -379,53 +304,6 @@ fun QuickSetFlash( ) } - -@Composable -fun QuickFlipCamera( - setLensFacing: (LensFacing) -> Unit, - flipLensUiState: FlipLensUiState, - modifier: Modifier = Modifier -) { - if (flipLensUiState is FlipLensUiState.Available) { - val enum = - when (flipLensUiState.selectedLensFacing) { - LensFacing.FRONT -> CameraLensFace.FRONT - LensFacing.BACK -> CameraLensFace.BACK - } - QuickSettingToggleButton( - modifier = modifier, - enum = enum, - onClick = { setLensFacing(flipLensUiState.selectedLensFacing.flip()) } - ) - } -} - -@Composable -fun QuickSetStreamConfig( - setStreamConfig: (StreamConfig) -> Unit, - streamConfigUiState: StreamConfigUiState, - modifier: Modifier = Modifier -) { - if (streamConfigUiState is StreamConfigUiState.Available) { - val enum: CameraStreamConfig = - when (streamConfigUiState.selectedStreamConfig) { - StreamConfig.MULTI_STREAM -> CameraStreamConfig.MULTI_STREAM - StreamConfig.SINGLE_STREAM -> CameraStreamConfig.SINGLE_STREAM - } - QuickSettingToggleButton( - modifier = modifier, - enum = enum, - onClick = { - when (streamConfigUiState.selectedStreamConfig) { - StreamConfig.MULTI_STREAM -> setStreamConfig(StreamConfig.SINGLE_STREAM) - StreamConfig.SINGLE_STREAM -> setStreamConfig(StreamConfig.MULTI_STREAM) - } - }, - enabled = streamConfigUiState.isActive - ) - } -} - @Composable fun QuickSetConcurrentCamera( setConcurrentCameraMode: (ConcurrentCameraMode) -> Unit, @@ -447,7 +325,7 @@ fun QuickSetConcurrentCamera( ConcurrentCameraMode.DUAL -> setConcurrentCameraMode(ConcurrentCameraMode.OFF) } }, - isHighLighted = concurrentCameraUiState.selectedConcurrentCameraMode == + isSelected = concurrentCameraUiState.selectedConcurrentCameraMode == ConcurrentCameraMode.DUAL, enabled = concurrentCameraUiState.isEnabled ) @@ -502,33 +380,12 @@ fun ToggleQuickSettingsButton( // // //////////////////////////////////////////////////// -@Composable -private fun QuickSettingToggleButton( - modifier: Modifier = Modifier, - enum: QuickSettingsEnum, - onClick: () -> Unit, - isHighLighted: Boolean = false, - enabled: Boolean = true -) { - QuickSettingToggleButton( - modifier = modifier, - text = stringResource(id = enum.getTextResId()), - accessibilityText = stringResource(id = enum.getDescriptionResId()), - onClick = { onClick() }, - isHighlighted = isHighLighted, - enabled = enabled, - painter = enum.getPainter() - ) -} - /** * A modal bottom sheet composable used to display a collection of quick setting buttons. * * @param modifier The [Modifier] to be applied to this composable. * @param onDismiss The lambda function to be invoked when the bottom sheet is dismissed. * @param sheetState The [SheetState] controlling the visibility and behavior of the bottom sheet. - * @param quickSettingButtons A variable number of composable functions that represent the buttons - * to be displayed within the quick settings bottom sheet. */ @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -536,8 +393,7 @@ fun QuickSettingsBottomSheet( modifier: Modifier, onDismiss: () -> Unit, sheetState: SheetState, - wipCapButton: @Composable () -> Unit, - vararg quickSettingButtons: @Composable () -> Unit + content: @Composable () -> Unit ) { val openDescription = stringResource(R.string.quick_settings_toggle_open_description) @@ -554,44 +410,68 @@ fun QuickSettingsBottomSheet( onDismissRequest = onDismiss, sheetState = sheetState ) { - wipCapButton() - Column { - QuickSettingsBottomSheetRow( - modifier = Modifier, - quickSettingButtons = quickSettingButtons - ) - } + content() } } - -/** - * A horizontally scrollable row of quick setting buttons for a bottom sheet. - * This row will only enable scrolling if its content overflows the screen width. - * - * @param quickSettingButtons A list of [QuickSettingCarouselButton]. - * @param modifier The Modifier to be applied to this composable. - */ @Composable -private fun QuickSettingsBottomSheetRow( +fun SettingRow( + title: String, + stateString: String, modifier: Modifier = Modifier, - vararg quickSettingButtons: @Composable () -> Unit + // Using vararg to accept multiple button components + vararg settingsButtons: @Composable () -> Unit ) { - // LazyRow is inherently scrollable if content exceeds bounds. - // It handles the "overflow to the right" behavior by default. - LazyRow( + Row( modifier = modifier - .testTag(QUICK_SETTINGS_SCROLL_CONTAINER) - .fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterHorizontally), - contentPadding = PaddingValues(horizontal = 16.dp) + .fillMaxWidth() + .padding(vertical = 12.dp, horizontal = 16.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween ) { - itemsIndexed(quickSettingButtons.toList()) { index, quickSetting -> - quickSetting() + Column(modifier = Modifier.weight(1f)) { + Text( + text = title, + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.onSurface + ) + Text( + text = stateString, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + settingsButtons.forEach { button -> + button() + } } } } +@Composable +private fun QuickSettingToggleButton( + modifier: Modifier = Modifier, + enum: QuickSettingsEnum, + onClick: () -> Unit, + isSelected: Boolean = false, + enabled: Boolean = true +) { + QuickSettingToggleButton( + modifier = modifier, + text = stringResource(id = enum.getTextResId()), + accessibilityText = stringResource(id = enum.getDescriptionResId()), + onClick = { onClick() }, + isSelected = isSelected, + enabled = enabled, + painter = enum.getPainter() + ) +} + /** * A customizable toggle button used within the quick settings menu. This button displays an icon * and a text label, and can be highlighted to indicate a selected state. It serves as a generic @@ -602,7 +482,7 @@ private fun QuickSettingsBottomSheetRow( * @param accessibilityText The content description for accessibility purposes. * @param painter The [Painter] for the icon displayed inside the button. * @param modifier The [Modifier] to be applied to this composable. - * @param isHighlighted A boolean indicating whether the button is currently in a highlighted (selected) state. + * @param isSelected A boolean indicating whether the button is currently in a highlighted (selected) state. * @param enabled A boolean indicating whether the button is interactive. */ @OptIn(ExperimentalMaterial3ExpressiveApi::class) @@ -613,7 +493,7 @@ private fun QuickSettingToggleButton( accessibilityText: String, painter: Painter, modifier: Modifier = Modifier, - isHighlighted: Boolean = false, + isSelected: Boolean = false, enabled: Boolean = true ) { val buttonSize = IconButtonDefaults.mediumContainerSize( @@ -629,7 +509,7 @@ private fun QuickSettingToggleButton( modifier = modifier .minimumInteractiveComponentSize() .size(buttonSize), - checked = isHighlighted, + checked = isSelected, enabled = enabled, onCheckedChange = { _ -> onClick() }, // 1. Size updated to width 48.dp and height 56.dp @@ -661,86 +541,6 @@ private fun QuickSettingToggleButton( } } -@Composable -fun SettingRow( - title: String, - status: String, - modifier: Modifier = Modifier, - // Using vararg to accept multiple button components - vararg settingsButtons: @Composable () -> Unit -) { - Row( - modifier = modifier - .fillMaxWidth() - .padding(vertical = 12.dp, horizontal = 16.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween - ) { - Column(modifier = Modifier.weight(1f)) { - Text( - text = title, - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.onSurface - ) - Text( - text = status, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - - Row( - horizontalArrangement = Arrangement.spacedBy(8.dp), - verticalAlignment = Alignment.CenterVertically - ) { - settingsButtons.forEach { button -> - button() - } - } - } -} - -/** - * Should you want to have an expanded view of a single quick setting - */ -@Composable -private fun ExpandedQuickSetting( - modifier: Modifier = Modifier, - vararg quickSettingButtons: @Composable () -> Unit -) { - val expandedNumOfColumns = - min( - quickSettingButtons.size, - ( - ( - LocalConfiguration.current.screenWidthDp.dp - ( - dimensionResource( - id = R.dimen.quick_settings_ui_horizontal_padding - ) * 2 - ) - ) / - ( - dimensionResource( - id = R.dimen.quick_settings_ui_item_icon_size - ) + - ( - dimensionResource( - id = R.dimen.quick_settings_ui_item_padding - ) * - 2 - ) - ) - ).toInt() - ) - LazyVerticalGrid( - modifier = modifier.fillMaxWidth(), - columns = GridCells.Fixed(count = expandedNumOfColumns) - ) { - items(quickSettingButtons.size) { i -> - quickSettingButtons[i]() - } - } -} @OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable @@ -813,18 +613,6 @@ private fun TopBarQuickSettingIcon( } } -private fun List>.getNextSelectableItem(selectedItem: T): T { - // Filter out only the selectable modes to cycle through them. - val selectableModes = this - .filterIsInstance>() - .map { it.value } // 'this' is already the list - - val currentIndex = selectableModes.indexOf(selectedItem) // selectedItem is passed directly - val nextIndex = (currentIndex + 1) % selectableModes.size - - return selectableModes[nextIndex] -} - private fun FlashMode.toCameraFlashMode(isActive: Boolean) = when (this) { FlashMode.OFF -> CameraFlashMode.OFF FlashMode.AUTO -> CameraFlashMode.AUTO @@ -859,7 +647,7 @@ private fun QuickSettingToggleButtonPreview() { text = "Flash Off", accessibilityText = "", painter = CameraFlashMode.OFF.getPainter(), - isHighlighted = false, + isSelected = false, enabled = true ) @@ -869,7 +657,7 @@ private fun QuickSettingToggleButtonPreview() { text = "Flash On", accessibilityText = "", painter = CameraFlashMode.ON.getPainter(), - isHighlighted = true, + isSelected = true, enabled = true ) @@ -879,7 +667,7 @@ private fun QuickSettingToggleButtonPreview() { text = "Flash Off", accessibilityText = "", painter = CameraFlashMode.OFF.getPainter(), - isHighlighted = false, + isSelected = false, enabled = false ) } @@ -896,7 +684,7 @@ private fun PreviewSettingRowDark() { ) { SettingRow( title = "Video Resolution", - status = "Standard Definition", + stateString = "Standard Definition", settingsButtons = arrayOf( { // Off State (Highlighted per your screenshot) @@ -904,7 +692,7 @@ private fun PreviewSettingRowDark() { text = "SD", accessibilityText = "Flash Off", painter = painterResource(id = R.drawable.video_resolution_sd_icon), - isHighlighted = true, + isSelected = true, onClick = {} ) }, @@ -914,7 +702,7 @@ private fun PreviewSettingRowDark() { text = "HD", accessibilityText = "High Definition", painter = painterResource(id = R.drawable.video_resolution_hd_icon), - isHighlighted = false, + isSelected = false, onClick = {} ) }, @@ -924,7 +712,7 @@ private fun PreviewSettingRowDark() { text = "FHD", accessibilityText = "Full High Definition", painter = painterResource(id = R.drawable.video_resolution_fhd_icon), - isHighlighted = false, + isSelected = false, onClick = {} ) } diff --git a/ui/components/capture/src/main/res/values/strings.xml b/ui/components/capture/src/main/res/values/strings.xml index b551cfec0..525f4da3b 100644 --- a/ui/components/capture/src/main/res/values/strings.xml +++ b/ui/components/capture/src/main/res/values/strings.xml @@ -141,4 +141,7 @@ View recently saved media + Video Settings + Photo Settings + Photo and Video Settings \ No newline at end of file From 957c9037278d8bef5f94ef57227584761440198c Mon Sep 17 00:00:00 2001 From: Kimberly Crevecoeur Date: Tue, 28 Apr 2026 17:20:18 +0000 Subject: [PATCH 05/51] spotless --- .../quicksettings/QuickSettingsScreen.kt | 8 +---- .../ui/QuickSettingsComponents.kt | 30 +++++++------------ .../impl/QuickSettingsControllerImpl.kt | 2 +- .../capture/compound/QuickSettingsUiState.kt | 3 +- .../compound/QuickSettingsUiStateAdapter.kt | 2 +- 5 files changed, 15 insertions(+), 30 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 232154305..c65cc7ebc 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 @@ -24,7 +24,6 @@ import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.google.jetpackcamera.model.AspectRatio import com.google.jetpackcamera.model.CaptureMode @@ -39,17 +38,13 @@ import com.google.jetpackcamera.ui.components.capture.quicksettings.ui.AspectRat import com.google.jetpackcamera.ui.components.capture.quicksettings.ui.CaptureModeRow import com.google.jetpackcamera.ui.components.capture.quicksettings.ui.FlashRow import com.google.jetpackcamera.ui.components.capture.quicksettings.ui.QuickSetHdr +import com.google.jetpackcamera.ui.components.capture.quicksettings.ui.QuickSettingsBottomSheet as BottomSheetComponent import com.google.jetpackcamera.ui.controller.quicksettings.QuickSettingsController -import com.google.jetpackcamera.ui.uistate.SingleSelectableUiState import com.google.jetpackcamera.ui.uistate.capture.AspectRatioUiState import com.google.jetpackcamera.ui.uistate.capture.CaptureModeUiState -import com.google.jetpackcamera.ui.uistate.capture.ConcurrentCameraUiState import com.google.jetpackcamera.ui.uistate.capture.FlashModeUiState -import com.google.jetpackcamera.ui.uistate.capture.FlipLensUiState import com.google.jetpackcamera.ui.uistate.capture.HdrUiState -import com.google.jetpackcamera.ui.uistate.capture.StreamConfigUiState import com.google.jetpackcamera.ui.uistate.capture.compound.QuickSettingsUiState -import com.google.jetpackcamera.ui.components.capture.quicksettings.ui.QuickSettingsBottomSheet as BottomSheetComponent /** * The UI bottom sheet component for quick settings. @@ -215,7 +210,6 @@ fun ImageQuickSettings( } } - /** * A no-op implementation of [QuickSettingsController] for use in Compose previews and tests. */ diff --git a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/ui/QuickSettingsComponents.kt b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/ui/QuickSettingsComponents.kt index b3dd44c00..6ef663690 100644 --- a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/ui/QuickSettingsComponents.kt +++ b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/ui/QuickSettingsComponents.kt @@ -90,7 +90,7 @@ import com.google.jetpackcamera.ui.uistate.capture.HdrUiState fun CaptureModeRow( modifier: Modifier = Modifier, onSetCaptureMode: (CaptureMode) -> Unit, - captureModeUiState: CaptureModeUiState, + captureModeUiState: CaptureModeUiState ) { if (captureModeUiState is CaptureModeUiState.Available) { SettingRow( @@ -141,7 +141,6 @@ fun CaptureModeToggleButton( enum = enum, onClick = { onClick() }, enabled = when (assignedCaptureMode) { - CaptureMode.STANDARD -> captureModeUiState.isCaptureModeSelectable(CaptureMode.STANDARD) @@ -152,7 +151,7 @@ fun CaptureModeToggleButton( captureModeUiState.isCaptureModeSelectable(CaptureMode.IMAGE_ONLY) }, isSelected = - isHighlightEnabled && (assignedCaptureMode == captureModeUiState.selectedCaptureMode) + isHighlightEnabled && (assignedCaptureMode == captureModeUiState.selectedCaptureMode) ) } @@ -169,7 +168,6 @@ fun QuickNavSettings(onNavigateToSettings: () -> Unit, modifier: Modifier = Modi ) } - @Composable fun QuickSetHdr( modifier: Modifier = Modifier, @@ -211,15 +209,13 @@ fun QuickSetHdr( ) } - @Composable fun AspectRatioRow( modifier: Modifier = Modifier, onSetAspectRatio: (AspectRatio) -> Unit, - aspectRatioUiState: AspectRatioUiState, + aspectRatioUiState: AspectRatioUiState ) { if (aspectRatioUiState is AspectRatioUiState.Available) { - val settingsButtons = aspectRatioUiState.availableAspectRatios .map { selectableRatio -> @Composable { @@ -247,15 +243,13 @@ fun AspectRatioRow( } } - @Composable fun FlashRow( modifier: Modifier = Modifier, onSetFlashMode: (FlashMode) -> Unit, - flashModeUiState: FlashModeUiState, + flashModeUiState: FlashModeUiState ) { if (flashModeUiState is FlashModeUiState.Available) { - SettingRow( modifier = modifier, title = "Capture Mode", @@ -267,7 +261,7 @@ fun FlashRow( modifier = Modifier.testTag("FlashMode_${selectableMode.value.name}"), onClick = { onSetFlashMode(selectableMode.value) }, assignedFlashMode = selectableMode.value, - flashModeUiState = flashModeUiState, + flashModeUiState = flashModeUiState ) } }.toTypedArray() @@ -275,7 +269,6 @@ fun FlashRow( } } - @Composable fun QuickSetFlash( modifier: Modifier = Modifier, @@ -326,7 +319,7 @@ fun QuickSetConcurrentCamera( } }, isSelected = concurrentCameraUiState.selectedConcurrentCameraMode == - ConcurrentCameraMode.DUAL, + ConcurrentCameraMode.DUAL, enabled = concurrentCameraUiState.isEnabled ) } @@ -393,7 +386,7 @@ fun QuickSettingsBottomSheet( modifier: Modifier, onDismiss: () -> Unit, sheetState: SheetState, - content: @Composable () -> Unit + content: @Composable () -> Unit ) { val openDescription = stringResource(R.string.quick_settings_toggle_open_description) @@ -410,7 +403,7 @@ fun QuickSettingsBottomSheet( onDismissRequest = onDismiss, sheetState = sheetState ) { - content() + content() } } @@ -541,16 +534,15 @@ private fun QuickSettingToggleButton( } } - @OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable fun HdrIndicator(hdrUiState: HdrUiState, modifier: Modifier = Modifier) { val enum = if (hdrUiState is HdrUiState.Available && ( - hdrUiState.selectedDynamicRange == DEFAULT_HDR_DYNAMIC_RANGE || - hdrUiState.selectedImageFormat == DEFAULT_HDR_IMAGE_OUTPUT - ) + hdrUiState.selectedDynamicRange == DEFAULT_HDR_DYNAMIC_RANGE || + hdrUiState.selectedImageFormat == DEFAULT_HDR_IMAGE_OUTPUT + ) ) { CameraDynamicRange.HDR } else { diff --git a/ui/controller/impl/src/main/java/com/google/jetpackcamera/ui/controller/impl/QuickSettingsControllerImpl.kt b/ui/controller/impl/src/main/java/com/google/jetpackcamera/ui/controller/impl/QuickSettingsControllerImpl.kt index c019db076..95440f311 100644 --- a/ui/controller/impl/src/main/java/com/google/jetpackcamera/ui/controller/impl/QuickSettingsControllerImpl.kt +++ b/ui/controller/impl/src/main/java/com/google/jetpackcamera/ui/controller/impl/QuickSettingsControllerImpl.kt @@ -27,6 +27,7 @@ import com.google.jetpackcamera.model.LensFacing import com.google.jetpackcamera.model.StreamConfig import com.google.jetpackcamera.ui.controller.quicksettings.QuickSettingsController import com.google.jetpackcamera.ui.uistate.capture.TrackedCaptureUiState +import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.cancel @@ -34,7 +35,6 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.job import kotlinx.coroutines.launch -import kotlin.coroutines.CoroutineContext /** * Implementation of [QuickSettingsController] that interacts with [CameraSystem] and updates diff --git a/ui/uistate/capture/src/main/java/com/google/jetpackcamera/ui/uistate/capture/compound/QuickSettingsUiState.kt b/ui/uistate/capture/src/main/java/com/google/jetpackcamera/ui/uistate/capture/compound/QuickSettingsUiState.kt index 27066891c..6f99f64c1 100644 --- a/ui/uistate/capture/src/main/java/com/google/jetpackcamera/ui/uistate/capture/compound/QuickSettingsUiState.kt +++ b/ui/uistate/capture/src/main/java/com/google/jetpackcamera/ui/uistate/capture/compound/QuickSettingsUiState.kt @@ -55,9 +55,8 @@ sealed interface QuickSettingsUiState { val flipLensUiState: FlipLensUiState, val hdrUiState: HdrUiState, val streamConfigUiState: StreamConfigUiState, - val quickSettingsIsOpen: Boolean = false, + val quickSettingsIsOpen: Boolean = false ) : QuickSettingsUiState companion object } - diff --git a/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/compound/QuickSettingsUiStateAdapter.kt b/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/compound/QuickSettingsUiStateAdapter.kt index cd80be11d..67adeb5d7 100644 --- a/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/compound/QuickSettingsUiStateAdapter.kt +++ b/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/compound/QuickSettingsUiStateAdapter.kt @@ -71,6 +71,6 @@ fun QuickSettingsUiState.Companion.from( flipLensUiState = flipLensUiState, hdrUiState = hdrUiState, streamConfigUiState = streamConfigUiState, - quickSettingsIsOpen = quickSettingsIsOpen, + quickSettingsIsOpen = quickSettingsIsOpen ) } From 713462dcf7d9abe2af8d88258106bd5bd87317da Mon Sep 17 00:00:00 2001 From: Kimberly Crevecoeur Date: Thu, 7 May 2026 10:57:12 -0700 Subject: [PATCH 06/51] Externalize quick settings strings and refactor flash mode logic. --- .../quicksettings/QuickSettingsScreen.kt | 10 +- .../ui/QuickSettingsComponents.kt | 163 +++++++--------- .../capture/src/main/res/values/strings.xml | 17 ++ .../capture/FlashModeUiStateAdapter.kt | 175 ++++++++++-------- 4 files changed, 189 insertions(+), 176 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 c65cc7ebc..debbb3954 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 @@ -37,7 +37,7 @@ import com.google.jetpackcamera.ui.components.capture.R import com.google.jetpackcamera.ui.components.capture.quicksettings.ui.AspectRatioRow import com.google.jetpackcamera.ui.components.capture.quicksettings.ui.CaptureModeRow import com.google.jetpackcamera.ui.components.capture.quicksettings.ui.FlashRow -import com.google.jetpackcamera.ui.components.capture.quicksettings.ui.QuickSetHdr +import com.google.jetpackcamera.ui.components.capture.quicksettings.ui.HdrRow import com.google.jetpackcamera.ui.components.capture.quicksettings.ui.QuickSettingsBottomSheet as BottomSheetComponent import com.google.jetpackcamera.ui.controller.quicksettings.QuickSettingsController import com.google.jetpackcamera.ui.uistate.capture.AspectRatioUiState @@ -53,9 +53,9 @@ import com.google.jetpackcamera.ui.uistate.capture.compound.QuickSettingsUiState @Composable fun QuickSettingsBottomSheet( quickSettingsUiState: QuickSettingsUiState, + quickSettingsController: QuickSettingsController, modifier: Modifier = Modifier, - onNavigateToSettings: () -> Unit, - quickSettingsController: QuickSettingsController + onNavigateToSettings: () -> Unit = {} ) { if (quickSettingsUiState is QuickSettingsUiState.Available && quickSettingsUiState.quickSettingsIsOpen @@ -131,7 +131,7 @@ fun HybridQuickSettings( // HDR settings if (quickSettingsUiState.hdrUiState is HdrUiState.Available) { - QuickSetHdr( + HdrRow( onClick = { d: DynamicRange, i: ImageOutputFormat -> quickSettingsController.setDynamicRange(d) quickSettingsController.setImageFormat(i) @@ -164,7 +164,7 @@ fun VideoQuickSettings( // HDR settings if (quickSettingsUiState.hdrUiState is HdrUiState.Available) { - QuickSetHdr( + HdrRow( onClick = { d: DynamicRange, i: ImageOutputFormat -> quickSettingsController.setDynamicRange(d) quickSettingsController.setImageFormat(i) diff --git a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/ui/QuickSettingsComponents.kt b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/ui/QuickSettingsComponents.kt index 6ef663690..fd73db29e 100644 --- a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/ui/QuickSettingsComponents.kt +++ b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/ui/QuickSettingsComponents.kt @@ -63,7 +63,6 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.google.jetpackcamera.model.AspectRatio import com.google.jetpackcamera.model.CaptureMode -import com.google.jetpackcamera.model.ConcurrentCameraMode import com.google.jetpackcamera.model.DEFAULT_HDR_DYNAMIC_RANGE import com.google.jetpackcamera.model.DEFAULT_HDR_IMAGE_OUTPUT import com.google.jetpackcamera.model.DynamicRange @@ -74,15 +73,14 @@ import com.google.jetpackcamera.ui.components.capture.QUICK_SETTINGS_DROP_DOWN import com.google.jetpackcamera.ui.components.capture.R import com.google.jetpackcamera.ui.components.capture.quicksettings.CameraAspectRatio import com.google.jetpackcamera.ui.components.capture.quicksettings.CameraCaptureMode -import com.google.jetpackcamera.ui.components.capture.quicksettings.CameraConcurrentCameraMode import com.google.jetpackcamera.ui.components.capture.quicksettings.CameraDynamicRange import com.google.jetpackcamera.ui.components.capture.quicksettings.CameraFlashMode import com.google.jetpackcamera.ui.components.capture.quicksettings.QuickSettingsEnum import com.google.jetpackcamera.ui.controller.quicksettings.QuickSettingsController +import com.google.jetpackcamera.ui.uistate.SingleSelectableUiState import com.google.jetpackcamera.ui.uistate.capture.AspectRatioUiState import com.google.jetpackcamera.ui.uistate.capture.CaptureModeUiState import com.google.jetpackcamera.ui.uistate.capture.CaptureModeUiState.Unavailable.isCaptureModeSelectable -import com.google.jetpackcamera.ui.uistate.capture.ConcurrentCameraUiState import com.google.jetpackcamera.ui.uistate.capture.FlashModeUiState import com.google.jetpackcamera.ui.uistate.capture.HdrUiState @@ -93,35 +91,33 @@ fun CaptureModeRow( captureModeUiState: CaptureModeUiState ) { if (captureModeUiState is CaptureModeUiState.Available) { + val enum = when (captureModeUiState.selectedCaptureMode){ + CaptureMode.STANDARD -> CameraCaptureMode.STANDARD + CaptureMode.IMAGE_ONLY -> CameraCaptureMode.IMAGE_ONLY + CaptureMode.VIDEO_ONLY -> CameraCaptureMode.VIDEO_ONLY + } + SettingRow( modifier = modifier, - title = "Capture Mode", - stateString = captureModeUiState.selectedCaptureMode.toString(), - settingsButtons = createCaptureModeButtons(captureModeUiState, onSetCaptureMode) + title = stringResource(id = R.string.quick_settings_title_capture_mode), + stateSubtitle = stringResource(enum.getTextResId()), + settingsButtons =captureModeUiState.availableCaptureModes + .map { selectableMode -> + @Composable { + CaptureModeToggleButton( + modifier = Modifier + .testTag("CaptureMode_${selectableMode.value.name}"), + onClick = { onSetCaptureMode(selectableMode.value) }, + assignedCaptureMode = selectableMode.value, + captureModeUiState = captureModeUiState, + isHighlightEnabled = true + ) + } + }.toTypedArray() ) } } -@Composable -private fun createCaptureModeButtons( - captureModeUiState: CaptureModeUiState.Available, - onSetCaptureMode: (CaptureMode) -> Unit -): Array<@Composable () -> Unit> { - return captureModeUiState.availableCaptureModes - .map { selectableMode -> - @Composable { - CaptureModeToggleButton( - modifier = Modifier - .testTag("CaptureMode_${selectableMode.value.name}"), - onClick = { onSetCaptureMode(selectableMode.value) }, - assignedCaptureMode = selectableMode.value, - captureModeUiState = captureModeUiState, - isHighlightEnabled = true - ) - } - }.toTypedArray() -} - @Composable fun CaptureModeToggleButton( modifier: Modifier = Modifier, @@ -169,7 +165,7 @@ fun QuickNavSettings(onNavigateToSettings: () -> Unit, modifier: Modifier = Modi } @Composable -fun QuickSetHdr( +fun HdrRow( modifier: Modifier = Modifier, onClick: (DynamicRange, ImageOutputFormat) -> Unit, hdrUiState: HdrUiState @@ -182,8 +178,8 @@ fun QuickSetHdr( SettingRow( modifier = modifier, - title = "HDR", - stateString = if (isHdrOn) { + title = stringResource(id = R.string.quick_settings_title_hdr), + stateSubtitle = if (isHdrOn) { stringResource(R.string.quick_settings_dynamic_range_hdr) } else { stringResource(R.string.quick_settings_dynamic_range_sdr) @@ -223,7 +219,6 @@ fun AspectRatioRow( AspectRatio.THREE_FOUR -> CameraAspectRatio.THREE_FOUR AspectRatio.NINE_SIXTEEN -> CameraAspectRatio.NINE_SIXTEEN AspectRatio.ONE_ONE -> CameraAspectRatio.ONE_ONE - else -> CameraAspectRatio.ONE_ONE } QuickSettingToggleButton( modifier = Modifier.testTag("AspectRatio_${selectableRatio.value.name}"), @@ -234,10 +229,18 @@ fun AspectRatioRow( } }.toTypedArray() + val stateSubtitle = when (aspectRatioUiState.selectedAspectRatio) { + AspectRatio.THREE_FOUR -> + stringResource(id = R.string.quick_settings_aspect_ratio_subtitle_three_four) + AspectRatio.NINE_SIXTEEN -> + stringResource(id = R.string.quick_settings_aspect_ratio_subtitle_nine_sixteen) + AspectRatio.ONE_ONE -> + stringResource(id = R.string.quick_settings_aspect_ratio_subtitle_one_one) + } SettingRow( modifier = modifier, - title = "Aspect Ratio", - stateString = aspectRatioUiState.selectedAspectRatio.toString(), + title = stringResource(id = R.string.quick_settings_title_aspect_ratio), + stateSubtitle = stateSubtitle, settingsButtons = settingsButtons ) } @@ -250,18 +253,38 @@ fun FlashRow( flashModeUiState: FlashModeUiState ) { if (flashModeUiState is FlashModeUiState.Available) { + val stateString = when (flashModeUiState.selectedFlashMode) { + FlashMode.OFF -> stringResource(id = R.string.quick_settings_flash_mode_subtitle_off) + FlashMode.AUTO -> stringResource(id = R.string.quick_settings_flash_mode_subtitle_auto) + FlashMode.ON -> stringResource(id = R.string.quick_settings_flash_mode_subtitle_on) + FlashMode.LOW_LIGHT_BOOST -> + stringResource(id = R.string.quick_settings_flash_mode_subtitle_low_light_boost) + } SettingRow( modifier = modifier, - title = "Capture Mode", - stateString = flashModeUiState.selectedFlashMode.toString(), + title = stringResource(id = R.string.quick_settings_title_flash_mode), + stateSubtitle = stateString, settingsButtons = flashModeUiState.availableFlashModes .map { selectableMode -> @Composable { - QuickSetFlash( - modifier = Modifier.testTag("FlashMode_${selectableMode.value.name}"), - onClick = { onSetFlashMode(selectableMode.value) }, - assignedFlashMode = selectableMode.value, - flashModeUiState = flashModeUiState + QuickSettingToggleButton( + modifier = modifier, + enabled = selectableMode is SingleSelectableUiState.SelectableUi, + enum = when (selectableMode.value) { + FlashMode.OFF -> CameraFlashMode.OFF + FlashMode.ON -> CameraFlashMode.ON + FlashMode.AUTO -> CameraFlashMode.AUTO + FlashMode.LOW_LIGHT_BOOST -> when (flashModeUiState.isLowLightBoostActive) { + true -> CameraFlashMode.LOW_LIGHT_BOOST_ACTIVE + false -> CameraFlashMode.LOW_LIGHT_BOOST_INACTIVE + } + }, + isSelected = flashModeUiState.selectedFlashMode == selectableMode.value, + onClick = { + onSetFlashMode( + selectableMode.value + ) + } ) } }.toTypedArray() @@ -269,62 +292,6 @@ fun FlashRow( } } -@Composable -fun QuickSetFlash( - modifier: Modifier = Modifier, - onClick: (FlashMode) -> Unit, - assignedFlashMode: FlashMode, - flashModeUiState: FlashModeUiState.Available -) { - val enum = when (assignedFlashMode) { - FlashMode.OFF -> CameraFlashMode.OFF - FlashMode.ON -> CameraFlashMode.ON - FlashMode.AUTO -> CameraFlashMode.AUTO - FlashMode.LOW_LIGHT_BOOST -> when (flashModeUiState.isLowLightBoostActive) { - true -> CameraFlashMode.LOW_LIGHT_BOOST_ACTIVE - false -> CameraFlashMode.LOW_LIGHT_BOOST_INACTIVE - } - } - QuickSettingToggleButton( - modifier = modifier, - enum = enum, - isSelected = flashModeUiState.selectedFlashMode == assignedFlashMode, - onClick = { - onClick( - assignedFlashMode - ) - } - ) -} - -@Composable -fun QuickSetConcurrentCamera( - setConcurrentCameraMode: (ConcurrentCameraMode) -> Unit, - concurrentCameraUiState: ConcurrentCameraUiState, - modifier: Modifier = Modifier -) { - if (concurrentCameraUiState is ConcurrentCameraUiState.Available) { - val enum: CameraConcurrentCameraMode = - when (concurrentCameraUiState.selectedConcurrentCameraMode) { - ConcurrentCameraMode.OFF -> CameraConcurrentCameraMode.OFF - ConcurrentCameraMode.DUAL -> CameraConcurrentCameraMode.DUAL - } - QuickSettingToggleButton( - modifier = modifier, - enum = enum, - onClick = { - when (concurrentCameraUiState.selectedConcurrentCameraMode) { - ConcurrentCameraMode.OFF -> setConcurrentCameraMode(ConcurrentCameraMode.DUAL) - ConcurrentCameraMode.DUAL -> setConcurrentCameraMode(ConcurrentCameraMode.OFF) - } - }, - isSelected = concurrentCameraUiState.selectedConcurrentCameraMode == - ConcurrentCameraMode.DUAL, - enabled = concurrentCameraUiState.isEnabled - ) - } -} - /** * Button to toggle quick settings */ @@ -410,7 +377,7 @@ fun QuickSettingsBottomSheet( @Composable fun SettingRow( title: String, - stateString: String, + stateSubtitle: String, modifier: Modifier = Modifier, // Using vararg to accept multiple button components vararg settingsButtons: @Composable () -> Unit @@ -429,7 +396,7 @@ fun SettingRow( color = MaterialTheme.colorScheme.onSurface ) Text( - text = stateString, + text = stateSubtitle, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant ) @@ -676,7 +643,7 @@ private fun PreviewSettingRowDark() { ) { SettingRow( title = "Video Resolution", - stateString = "Standard Definition", + stateSubtitle = "Standard Definition", settingsButtons = arrayOf( { // Off State (Highlighted per your screenshot) diff --git a/ui/components/capture/src/main/res/values/strings.xml b/ui/components/capture/src/main/res/values/strings.xml index 525f4da3b..f5f988328 100644 --- a/ui/components/capture/src/main/res/values/strings.xml +++ b/ui/components/capture/src/main/res/values/strings.xml @@ -144,4 +144,21 @@ Video Settings Photo Settings Photo and Video Settings + + + Capture Mode + HDR + Aspect Ratio + Flash Mode + + + 3:4 + 9:16 + 1:1 + + + Off + Auto + On + Low Light Boost \ No newline at end of file diff --git a/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/FlashModeUiStateAdapter.kt b/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/FlashModeUiStateAdapter.kt index fd14a385e..565a0bdaf 100644 --- a/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/FlashModeUiStateAdapter.kt +++ b/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/FlashModeUiStateAdapter.kt @@ -23,74 +23,105 @@ import com.google.jetpackcamera.model.LowLightBoostState import com.google.jetpackcamera.settings.model.CameraAppSettings import com.google.jetpackcamera.settings.model.CameraSystemConstraints import com.google.jetpackcamera.settings.model.forCurrentLens +import com.google.jetpackcamera.ui.components.capture.DisabledReason import com.google.jetpackcamera.ui.uistate.SingleSelectableUiState import com.google.jetpackcamera.ui.uistate.capture.FlashModeUiState import com.google.jetpackcamera.ui.uistate.capture.FlashModeUiState.Available import com.google.jetpackcamera.ui.uistate.capture.FlashModeUiState.Unavailable -import com.google.jetpackcamera.ui.uistateadapter.Utils +// Assuming Utils.getSelectableListFromValues is no longer needed with the new logic +// import com.google.jetpackcamera.ui.uistateadapter.Utils private val ORDERED_UI_SUPPORTED_FLASH_MODES = listOf( FlashMode.OFF, - FlashMode.ON, FlashMode.AUTO, + FlashMode.ON, FlashMode.LOW_LIGHT_BOOST ) /** - * Creates the initial [FlashModeUiState] from the given camera settings and system constraints. + * Creates the initial [FlashModeUiState] from the given camera settings, system constraints, + * and a set of flash modes designated to be visible. + * + * This factory function determines the set of displayable flash modes based on: + * 1. Overall device support. + * 2. Developer-defined visibility (via `visibleFlashModes`). + * 3. Support by the currently active lens. + * 4. Interactions with other settings (e.g., HDR, Concurrent Camera). * - * This factory function determines the set of available flash modes based on hardware support - * and current camera settings (like HDR or concurrent mode). If only [FlashMode.OFF] is available, - * or no modes are supported, it returns [FlashModeUiState.Unavailable]. + * Modes not supported by the device or not in `visibleFlashModes` are hidden. + * Modes not supported by the current lens are shown as disabled. + * Modes supported by the current lens are shown as enabled. * - * @param cameraAppSettings The current settings of the camera, used to determine which flash modes - * might be disabled due to other active settings (e.g., HDR). - * @param systemConstraints The hardware capabilities of the camera system, used to get the list - * of supported flash modes for the current lens. - * @return A [FlashModeUiState] which is either [Available] if multiple flash modes can be shown, - * or [Unavailable] if the flash controls should be hidden. + * @param cameraAppSettings The current settings of the camera. + * @param systemConstraints The hardware capabilities of the camera system. + * @return A [FlashModeUiState] which is either [Available] or [Unavailable]. */ fun FlashModeUiState.Companion.from( cameraAppSettings: CameraAppSettings, - systemConstraints: CameraSystemConstraints + systemConstraints: CameraSystemConstraints, + //visibleFlashModes: Set ): FlashModeUiState { val selectedFlashMode = cameraAppSettings.flashMode - val supportedFlashModes = ( - systemConstraints.forCurrentLens(cameraAppSettings) - ?.supportedFlashModes - ?: setOf(FlashMode.OFF) - ).toMutableSet() - - if (cameraAppSettings.dynamicRange != DynamicRange.SDR) { - supportedFlashModes.remove(FlashMode.LOW_LIGHT_BOOST) - } - if (cameraAppSettings.concurrentCameraMode == ConcurrentCameraMode.DUAL) { - supportedFlashModes.remove(FlashMode.LOW_LIGHT_BOOST) + // All modes potentially supported by the device + val allDeviceSupportedFlashModes = buildSet { + systemConstraints.perLensConstraints.let { + it.keys.forEach { key -> + it[key]?.supportedFlashModes?.let {flashModes -> addAll(flashModes) } + } + } } + println("low light boost supported by device?" + allDeviceSupportedFlashModes.contains(FlashMode.LOW_LIGHT_BOOST)) + + // Modes supported by the CURRENT lens + val currentLensSupportedFlashModes = systemConstraints.forCurrentLens(cameraAppSettings) + ?.supportedFlashModes ?: setOf(FlashMode.OFF) + + val displayableModes = mutableListOf>() + + for (mode in ORDERED_UI_SUPPORTED_FLASH_MODES) { + + // 1. Hide if not supported by the device at all. + if (!allDeviceSupportedFlashModes.contains(mode)) { + continue + } + + // 2. Hide if not designated as visible by the developer. + //todo(kc): supply visible flash modes from developer options + /*if (!visibleFlashModes.contains(mode)) { + continue + }*/ + + // 3. Special handling for LOW_LIGHT_BOOST based on other settings. + if (mode == FlashMode.LOW_LIGHT_BOOST) { + if (cameraAppSettings.dynamicRange != DynamicRange.SDR || + cameraAppSettings.concurrentCameraMode == ConcurrentCameraMode.DUAL) { + continue // Hide LLB if HDR or Dual Camera is active + } + } - // Ensure we at least support one flash mode - check(supportedFlashModes.isNotEmpty()) { - "No flash modes supported. Should at least support OFF." + // 4. Determine if Enabled or Disabled based on current lens support. + if (currentLensSupportedFlashModes.contains(mode)) { + displayableModes.add(SingleSelectableUiState.SelectableUi(mode)) // Enabled + } else { + //todo(kc): add actual disabledreason for flash mode + displayableModes.add(SingleSelectableUiState.Disabled(value = mode, disabledReason = DisabledReason.HDR_IMAGE_UNSUPPORTED_ON_LENS)) // Disabled + } } - // Convert available flash modes to list we support in the UI in our desired order - val availableModes = - Utils.getSelectableListFromValues( - supportedFlashModes, - ORDERED_UI_SUPPORTED_FLASH_MODES - ) + // UiState should be Unavailable if no modes are displayable, + // or if only OFF is displayable and it's selectable. + val onlyOffSelectable = displayableModes.size == 1 && + displayableModes[0].value == FlashMode.OFF && + displayableModes[0] is SingleSelectableUiState.SelectableUi - return if (availableModes.isEmpty() || - availableModes == listOf(SingleSelectableUiState.SelectableUi(FlashMode.OFF)) - ) { - // If we only support OFF, then return "Unavailable". + return if (displayableModes.isEmpty() || onlyOffSelectable) { Unavailable } else { Available( selectedFlashMode = selectedFlashMode, - availableFlashModes = availableModes, - isLowLightBoostActive = false + availableFlashModes = displayableModes, + isLowLightBoostActive = false // Initial state ) } } @@ -98,56 +129,54 @@ fun FlashModeUiState.Companion.from( /** * Updates an existing [FlashModeUiState] based on new camera settings and state. * - * This function efficiently updates the flash UI state without recreating it from scratch if - * possible. It checks for changes in supported modes, selected mode, and the real-time status - * of Low Light Boost. - * * @param cameraAppSettings The current application settings for the camera. * @param systemConstraints The hardware capabilities of the camera system. * @param cameraState The real-time state from the camera, used to check [LowLightBoostState]. - * @return An updated [FlashModeUiState]. This may be the same instance if no relevant - * state has changed, a copied instance with minor updates, or a completely new instance if - * supported flash modes have changed. + * @return An updated [FlashModeUiState]. */ fun FlashModeUiState.updateFrom( cameraAppSettings: CameraAppSettings, systemConstraints: CameraSystemConstraints, - cameraState: CameraState + cameraState: CameraState, ): FlashModeUiState { return when (this) { is Unavailable -> { // When previous state was "Unavailable", we'll try to create a new FlashModeUiState - FlashModeUiState.Companion.from(cameraAppSettings, systemConstraints) + FlashModeUiState.from(cameraAppSettings, systemConstraints) } is Available -> { - val supportedFlashModes = - systemConstraints.forCurrentLens(cameraAppSettings)?.supportedFlashModes - ?: setOf(FlashMode.OFF) - - // check if supported flash modes have changed without allocating a new list/set - val availableModesChanged = - this.availableFlashModes.size != supportedFlashModes.size || - this.availableFlashModes.any { !supportedFlashModes.contains(it.value) } - - if (availableModesChanged) { - // Supported flash modes have changed, generate a new FlashModeUiState - FlashModeUiState.Companion.from(cameraAppSettings, systemConstraints) - } else if (this.selectedFlashMode != cameraAppSettings.flashMode) { - // Only the selected flash mode has changed, just update the flash mode - copy(selectedFlashMode = cameraAppSettings.flashMode) + // Regenerate the potential new state based on the latest settings + val newUiState = FlashModeUiState.from(cameraAppSettings, systemConstraints) + + if (newUiState is Unavailable) { + newUiState // Switch to Unavailable } else { - if (cameraAppSettings.flashMode == FlashMode.LOW_LIGHT_BOOST) { - val strength = when (val llbState = cameraState.lowLightBoostState) { - is LowLightBoostState.Active -> llbState.strength - else -> LowLightBoostState.MINIMUM_STRENGTH - } - copy( - isLowLightBoostActive = strength > 0.5 - ) + newUiState as Available // Cast to Available + + // Check if the list of modes or their enabled/disabled states have changed. + // Data class list comparison works well here. + if (this.availableFlashModes != newUiState.availableFlashModes) { + newUiState + } else if (this.selectedFlashMode != cameraAppSettings.flashMode) { + // Only the selection changed + copy(selectedFlashMode = cameraAppSettings.flashMode) } else { - // Nothing has changed - this + // Check for Low Light Boost state changes if it's the selected mode + if (cameraAppSettings.flashMode == FlashMode.LOW_LIGHT_BOOST) { + val strength = when (val llbState = cameraState.lowLightBoostState) { + is LowLightBoostState.Active -> llbState.strength + else -> LowLightBoostState.MINIMUM_STRENGTH + } + val newIsLowLightBoostActive = strength > 0.5 + if (this.isLowLightBoostActive != newIsLowLightBoostActive) { + copy(isLowLightBoostActive = newIsLowLightBoostActive) + } else { + this // Nothing changed + } + } else { + this // Nothing changed + } } } } From 73a61d2c834ebb1dcbbae9f6065f9f4f5bf78c4e Mon Sep 17 00:00:00 2001 From: Kimberly Crevecoeur Date: Thu, 7 May 2026 11:52:51 -0700 Subject: [PATCH 07/51] rearrange some functions auxiliary function for settings subtitles --- .../ui/QuickSettingsComponents.kt | 301 +++++++++--------- 1 file changed, 159 insertions(+), 142 deletions(-) diff --git a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/ui/QuickSettingsComponents.kt b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/ui/QuickSettingsComponents.kt index fd73db29e..4d5853c30 100644 --- a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/ui/QuickSettingsComponents.kt +++ b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/ui/QuickSettingsComponents.kt @@ -15,6 +15,7 @@ */ package com.google.jetpackcamera.ui.components.capture.quicksettings.ui +import androidx.annotation.StringRes import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement @@ -84,40 +85,68 @@ import com.google.jetpackcamera.ui.uistate.capture.CaptureModeUiState.Unavailabl import com.google.jetpackcamera.ui.uistate.capture.FlashModeUiState import com.google.jetpackcamera.ui.uistate.capture.HdrUiState + +// //////////////////////////////////////////////////// +// +// quick settings navigation components +// +// //////////////////////////////////////////////////// +/** + * Button to toggle open quick settings + */ +@OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable -fun CaptureModeRow( +fun ToggleQuickSettingsButton( + isOpen: Boolean, modifier: Modifier = Modifier, - onSetCaptureMode: (CaptureMode) -> Unit, - captureModeUiState: CaptureModeUiState + quickSettingsController: QuickSettingsController ) { - if (captureModeUiState is CaptureModeUiState.Available) { - val enum = when (captureModeUiState.selectedCaptureMode){ - CaptureMode.STANDARD -> CameraCaptureMode.STANDARD - CaptureMode.IMAGE_ONLY -> CameraCaptureMode.IMAGE_ONLY - CaptureMode.VIDEO_ONLY -> CameraCaptureMode.VIDEO_ONLY - } - - SettingRow( - modifier = modifier, - title = stringResource(id = R.string.quick_settings_title_capture_mode), - stateSubtitle = stringResource(enum.getTextResId()), - settingsButtons =captureModeUiState.availableCaptureModes - .map { selectableMode -> - @Composable { - CaptureModeToggleButton( - modifier = Modifier - .testTag("CaptureMode_${selectableMode.value.name}"), - onClick = { onSetCaptureMode(selectableMode.value) }, - assignedCaptureMode = selectableMode.value, - captureModeUiState = captureModeUiState, - isHighlightEnabled = true - ) - } - }.toTypedArray() + val buttonSize = IconButtonDefaults.mediumContainerSize( + IconButtonDefaults.IconButtonWidthOption.Narrow + ) + val openDescription = stringResource(R.string.quick_settings_toggle_open_description) + val closedDescription = stringResource(R.string.quick_settings_toggle_closed_description) + IconButton( + modifier = modifier + .size(buttonSize) + .testTag(QUICK_SETTINGS_DROP_DOWN) + .semantics { + testTag = QUICK_SETTINGS_DROP_DOWN + contentDescription = if (isOpen) { + openDescription + } else { + closedDescription + } + }, + onClick = quickSettingsController::toggleQuickSettings, + colors = IconButtonDefaults.iconButtonColors( + // Set the background color of the button + containerColor = Color.White.copy(alpha = 0.08f), + // Set the color of the icon inside the button + contentColor = Color.White + ) + ) { + Icon( + painter = painterResource(R.drawable.settings_photo_camera_icon), + contentDescription = stringResource(R.string.quick_settings_toggle_icon_description) ) } } + +/** + * A button within the quick settings menu that will navigate to the default settings screen + */ +@OptIn(ExperimentalMaterial3ExpressiveApi::class) +@Composable +fun QuickNavSettings(onNavigateToSettings: () -> Unit, modifier: Modifier = Modifier) { + TextButton( + modifier = modifier, + onClick = onNavigateToSettings, + content = { Text(text = stringResource(R.string.quick_settings_more_text)) } + ) +} + @Composable fun CaptureModeToggleButton( modifier: Modifier = Modifier, @@ -132,7 +161,7 @@ fun CaptureModeToggleButton( CaptureMode.IMAGE_ONLY -> CameraCaptureMode.IMAGE_ONLY } - QuickSettingToggleButton( + QuickSettingToggleSelectorButton( modifier = modifier, enum = enum, onClick = { onClick() }, @@ -147,21 +176,83 @@ fun CaptureModeToggleButton( captureModeUiState.isCaptureModeSelectable(CaptureMode.IMAGE_ONLY) }, isSelected = - isHighlightEnabled && (assignedCaptureMode == captureModeUiState.selectedCaptureMode) + isHighlightEnabled && (assignedCaptureMode == captureModeUiState.selectedCaptureMode) ) } + +// //////////////////////////////////////////////////// +// +// complete quick settings screen components +// +// //////////////////////////////////////////////////// + + /** - * A button in the quick settings menu that will navigate to the default settings screen + * A modal bottom sheet composable used to display a collection of quick setting buttons. + * + * @param modifier The [Modifier] to be applied to this composable. + * @param onDismiss The lambda function to be invoked when the bottom sheet is dismissed. + * @param sheetState The [SheetState] controlling the visibility and behavior of the bottom sheet. */ -@OptIn(ExperimentalMaterial3ExpressiveApi::class) +@OptIn(ExperimentalMaterial3Api::class) @Composable -fun QuickNavSettings(onNavigateToSettings: () -> Unit, modifier: Modifier = Modifier) { - TextButton( - modifier = modifier, - onClick = onNavigateToSettings, - content = { Text(text = stringResource(R.string.quick_settings_more_text)) } - ) +fun QuickSettingsBottomSheet( + modifier: Modifier, + onDismiss: () -> Unit, + sheetState: SheetState, + content: @Composable () -> Unit +) { + val openDescription = stringResource(R.string.quick_settings_toggle_open_description) + + ModalBottomSheet( + modifier = modifier + .semantics { + // since Modal Bottom Sheet is placed above ALL other composables in the hierarchy, + // it doesn't inherit the "testTagsAsResourceId" property. + testTagsAsResourceId = true + testTag = QUICK_SETTINGS_BOTTOM_SHEET + contentDescription = openDescription + }, + + onDismissRequest = onDismiss, + sheetState = sheetState + ) { + content() + } +} +@Composable +fun CaptureModeRow( + modifier: Modifier = Modifier, + onSetCaptureMode: (CaptureMode) -> Unit, + captureModeUiState: CaptureModeUiState +) { + if (captureModeUiState is CaptureModeUiState.Available) { + val enum = when (captureModeUiState.selectedCaptureMode){ + CaptureMode.STANDARD -> CameraCaptureMode.STANDARD + CaptureMode.IMAGE_ONLY -> CameraCaptureMode.IMAGE_ONLY + CaptureMode.VIDEO_ONLY -> CameraCaptureMode.VIDEO_ONLY + } + + SettingRow( + modifier = modifier, + title = stringResource(id = R.string.quick_settings_title_capture_mode), + stateSubtitle = stringResource(enum.getTextResId()), + settingsButtons =captureModeUiState.availableCaptureModes + .map { selectableMode -> + @Composable { + CaptureModeToggleButton( + modifier = Modifier + .testTag("CaptureMode_${selectableMode.value.name}"), + onClick = { onSetCaptureMode(selectableMode.value) }, + assignedCaptureMode = selectableMode.value, + captureModeUiState = captureModeUiState, + isHighlightEnabled = true + ) + } + }.toTypedArray() + ) + } } @Composable @@ -186,7 +277,7 @@ fun HdrRow( }, settingsButtons = arrayOf( { - QuickSettingToggleButton( + QuickSettingToggleSelectorButton( enum = CameraDynamicRange.HDR, onClick = { onClick(DEFAULT_HDR_DYNAMIC_RANGE, DEFAULT_HDR_IMAGE_OUTPUT) }, isSelected = isHdrOn, @@ -194,7 +285,7 @@ fun HdrRow( ) }, { - QuickSettingToggleButton( + QuickSettingToggleSelectorButton( enum = CameraDynamicRange.SDR, onClick = { onClick(DynamicRange.SDR, ImageOutputFormat.JPEG) }, isSelected = !isHdrOn, @@ -220,7 +311,7 @@ fun AspectRatioRow( AspectRatio.NINE_SIXTEEN -> CameraAspectRatio.NINE_SIXTEEN AspectRatio.ONE_ONE -> CameraAspectRatio.ONE_ONE } - QuickSettingToggleButton( + QuickSettingToggleSelectorButton( modifier = Modifier.testTag("AspectRatio_${selectableRatio.value.name}"), onClick = { onSetAspectRatio(selectableRatio.value) }, enum = enum, @@ -229,18 +320,10 @@ fun AspectRatioRow( } }.toTypedArray() - val stateSubtitle = when (aspectRatioUiState.selectedAspectRatio) { - AspectRatio.THREE_FOUR -> - stringResource(id = R.string.quick_settings_aspect_ratio_subtitle_three_four) - AspectRatio.NINE_SIXTEEN -> - stringResource(id = R.string.quick_settings_aspect_ratio_subtitle_nine_sixteen) - AspectRatio.ONE_ONE -> - stringResource(id = R.string.quick_settings_aspect_ratio_subtitle_one_one) - } SettingRow( modifier = modifier, title = stringResource(id = R.string.quick_settings_title_aspect_ratio), - stateSubtitle = stateSubtitle, + stateSubtitle = stringResource(id = aspectRatioUiState.selectedAspectRatio.toSubtitleStringRes()), settingsButtons = settingsButtons ) } @@ -253,21 +336,14 @@ fun FlashRow( flashModeUiState: FlashModeUiState ) { if (flashModeUiState is FlashModeUiState.Available) { - val stateString = when (flashModeUiState.selectedFlashMode) { - FlashMode.OFF -> stringResource(id = R.string.quick_settings_flash_mode_subtitle_off) - FlashMode.AUTO -> stringResource(id = R.string.quick_settings_flash_mode_subtitle_auto) - FlashMode.ON -> stringResource(id = R.string.quick_settings_flash_mode_subtitle_on) - FlashMode.LOW_LIGHT_BOOST -> - stringResource(id = R.string.quick_settings_flash_mode_subtitle_low_light_boost) - } SettingRow( modifier = modifier, title = stringResource(id = R.string.quick_settings_title_flash_mode), - stateSubtitle = stateString, + stateSubtitle = stringResource(id = flashModeUiState.selectedFlashMode.toSubtitleStringRes()), settingsButtons = flashModeUiState.availableFlashModes .map { selectableMode -> @Composable { - QuickSettingToggleButton( + QuickSettingToggleSelectorButton( modifier = modifier, enabled = selectableMode is SingleSelectableUiState.SelectableUi, enum = when (selectableMode.value) { @@ -292,47 +368,7 @@ fun FlashRow( } } -/** - * Button to toggle quick settings - */ -@OptIn(ExperimentalMaterial3ExpressiveApi::class) -@Composable -fun ToggleQuickSettingsButton( - isOpen: Boolean, - modifier: Modifier = Modifier, - quickSettingsController: QuickSettingsController -) { - val buttonSize = IconButtonDefaults.mediumContainerSize( - IconButtonDefaults.IconButtonWidthOption.Narrow - ) - val openDescription = stringResource(R.string.quick_settings_toggle_open_description) - val closedDescription = stringResource(R.string.quick_settings_toggle_closed_description) - IconButton( - modifier = modifier - .size(buttonSize) - .testTag(QUICK_SETTINGS_DROP_DOWN) - .semantics { - testTag = QUICK_SETTINGS_DROP_DOWN - contentDescription = if (isOpen) { - openDescription - } else { - closedDescription - } - }, - onClick = quickSettingsController::toggleQuickSettings, - colors = IconButtonDefaults.iconButtonColors( - // Set the background color of the button - containerColor = Color.White.copy(alpha = 0.08f), - // Set the color of the icon inside the button - contentColor = Color.White - ) - ) { - Icon( - painter = painterResource(R.drawable.settings_photo_camera_icon), - contentDescription = stringResource(R.string.quick_settings_toggle_icon_description) - ) - } -} + // //////////////////////////////////////////////////// // @@ -340,40 +376,6 @@ fun ToggleQuickSettingsButton( // // //////////////////////////////////////////////////// -/** - * A modal bottom sheet composable used to display a collection of quick setting buttons. - * - * @param modifier The [Modifier] to be applied to this composable. - * @param onDismiss The lambda function to be invoked when the bottom sheet is dismissed. - * @param sheetState The [SheetState] controlling the visibility and behavior of the bottom sheet. - */ -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun QuickSettingsBottomSheet( - modifier: Modifier, - onDismiss: () -> Unit, - sheetState: SheetState, - content: @Composable () -> Unit -) { - val openDescription = stringResource(R.string.quick_settings_toggle_open_description) - - ModalBottomSheet( - modifier = modifier - .semantics { - // since Modal Bottom Sheet is placed above ALL other composables in the hierarchy, - // it doesn't inherit the "testTagsAsResourceId" property. - testTagsAsResourceId = true - testTag = QUICK_SETTINGS_BOTTOM_SHEET - contentDescription = openDescription - }, - - onDismissRequest = onDismiss, - sheetState = sheetState - ) { - content() - } -} - @Composable fun SettingRow( title: String, @@ -414,14 +416,14 @@ fun SettingRow( } @Composable -private fun QuickSettingToggleButton( +private fun QuickSettingToggleSelectorButton( modifier: Modifier = Modifier, enum: QuickSettingsEnum, onClick: () -> Unit, isSelected: Boolean = false, enabled: Boolean = true ) { - QuickSettingToggleButton( + QuickSettingToggleSelectorButton( modifier = modifier, text = stringResource(id = enum.getTextResId()), accessibilityText = stringResource(id = enum.getDescriptionResId()), @@ -447,7 +449,7 @@ private fun QuickSettingToggleButton( */ @OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable -private fun QuickSettingToggleButton( +private fun QuickSettingToggleSelectorButton( onClick: () -> Unit, text: String, accessibilityText: String, @@ -584,6 +586,21 @@ private fun FlashMode.toCameraFlashMode(isActive: Boolean) = when (this) { } } +@StringRes +private fun AspectRatio.toSubtitleStringRes(): Int = when (this) { + AspectRatio.THREE_FOUR -> R.string.quick_settings_aspect_ratio_subtitle_three_four + AspectRatio.NINE_SIXTEEN -> R.string.quick_settings_aspect_ratio_subtitle_nine_sixteen + AspectRatio.ONE_ONE -> R.string.quick_settings_aspect_ratio_subtitle_one_one +} + +@StringRes +private fun FlashMode.toSubtitleStringRes(): Int = when (this) { + FlashMode.OFF -> R.string.quick_settings_flash_mode_subtitle_off + FlashMode.AUTO -> R.string.quick_settings_flash_mode_subtitle_auto + FlashMode.ON -> R.string.quick_settings_flash_mode_subtitle_on + FlashMode.LOW_LIGHT_BOOST -> R.string.quick_settings_flash_mode_subtitle_low_light_boost +} + @Preview @Composable private fun QuickSettingToggleButtonPreview() { @@ -601,7 +618,7 @@ private fun QuickSettingToggleButtonPreview() { horizontalArrangement = Arrangement.SpaceBetween ) { // Instance 1: Unchecked state - QuickSettingToggleButton( + QuickSettingToggleSelectorButton( onClick = {}, text = "Flash Off", accessibilityText = "", @@ -611,7 +628,7 @@ private fun QuickSettingToggleButtonPreview() { ) // Instance 2: Checked state - QuickSettingToggleButton( + QuickSettingToggleSelectorButton( onClick = {}, text = "Flash On", accessibilityText = "", @@ -621,7 +638,7 @@ private fun QuickSettingToggleButtonPreview() { ) // Instance 3: Disabled state - QuickSettingToggleButton( + QuickSettingToggleSelectorButton( onClick = {}, text = "Flash Off", accessibilityText = "", @@ -647,7 +664,7 @@ private fun PreviewSettingRowDark() { settingsButtons = arrayOf( { // Off State (Highlighted per your screenshot) - QuickSettingToggleButton( + QuickSettingToggleSelectorButton( text = "SD", accessibilityText = "Flash Off", painter = painterResource(id = R.drawable.video_resolution_sd_icon), @@ -657,7 +674,7 @@ private fun PreviewSettingRowDark() { }, { // On State - QuickSettingToggleButton( + QuickSettingToggleSelectorButton( text = "HD", accessibilityText = "High Definition", painter = painterResource(id = R.drawable.video_resolution_hd_icon), @@ -667,7 +684,7 @@ private fun PreviewSettingRowDark() { }, { // Auto State - QuickSettingToggleButton( + QuickSettingToggleSelectorButton( text = "FHD", accessibilityText = "Full High Definition", painter = painterResource(id = R.drawable.video_resolution_fhd_icon), From 16936f46897a8520c61e04dd446799869661aa5f Mon Sep 17 00:00:00 2001 From: Kimberly Crevecoeur Date: Thu, 7 May 2026 12:39:36 -0700 Subject: [PATCH 08/51] update accessibility --- .../capture/quicksettings/ui/QuickSettingsComponents.kt | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/ui/QuickSettingsComponents.kt b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/ui/QuickSettingsComponents.kt index 4d5853c30..a39095d9b 100644 --- a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/ui/QuickSettingsComponents.kt +++ b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/ui/QuickSettingsComponents.kt @@ -55,6 +55,7 @@ import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.hideFromAccessibility import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.testTag import androidx.compose.ui.semantics.testTagsAsResourceId @@ -387,7 +388,8 @@ fun SettingRow( Row( modifier = modifier .fillMaxWidth() - .padding(vertical = 12.dp, horizontal = 16.dp), + .padding(vertical = 12.dp, horizontal = 16.dp) + .semantics(mergeDescendants = true) {}, verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween ) { @@ -491,7 +493,8 @@ private fun QuickSettingToggleSelectorButton( Text( modifier = Modifier .width(IntrinsicSize.Max) - .wrapContentWidth(), + .wrapContentWidth() + .semantics { hideFromAccessibility() }, text = text, style = MaterialTheme.typography.labelMedium, color = MaterialTheme.colorScheme.onSurface, From a2e0400a960cd48d8c0376e77b28fd5f5652242a Mon Sep 17 00:00:00 2001 From: Kimberly Crevecoeur Date: Thu, 7 May 2026 14:28:58 -0700 Subject: [PATCH 09/51] Add test tags for quick settings buttons and row components. --- .../ui/components/capture/TestTags.kt | 13 ++++ .../ui/QuickSettingsComponents.kt | 66 ++++++++++++++----- 2 files changed, 62 insertions(+), 17 deletions(-) diff --git a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/TestTags.kt b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/TestTags.kt index 9bf92ba3c..19aa670af 100644 --- a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/TestTags.kt +++ b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/TestTags.kt @@ -95,3 +95,16 @@ const val BTN_QUICK_SETTINGS_FOCUSED_CAPTURE_MODE_VIDEO_ONLY = "quick_settings_focused_capture_mode_btn_option_video_only" const val BTN_QUICK_SETTINGS_FOCUSED_CAPTURE_MODE_IMAGE_ONLY = "quick_settings_focused_capture_mode_btn_option_image_only" + +const val BTN_QUICK_SETTINGS_FLASH_OPTION_OFF = "btn_quick_settings_flash_option_off" +const val BTN_QUICK_SETTINGS_FLASH_OPTION_ON = "btn_quick_settings_flash_option_on" +const val BTN_QUICK_SETTINGS_FLASH_OPTION_AUTO = "btn_quick_settings_flash_option_auto" +const val BTN_QUICK_SETTINGS_FLASH_OPTION_LOW_LIGHT_BOOST = "btn_quick_settings_flash_option_low_light_boost" + +const val BTN_QUICK_SETTINGS_HDR_OPTION_ON = "btn_quick_settings_hdr_option_on" +const val BTN_QUICK_SETTINGS_HDR_OPTION_OFF = "btn_quick_settings_hdr_option_off" + +const val QUICK_SETTINGS_CAPTURE_MODE_ROW = "quick_settings_capture_mode_row" +const val QUICK_SETTINGS_HDR_ROW = "quick_settings_hdr_row" +const val QUICK_SETTINGS_ASPECT_RATIO_ROW = "quick_settings_aspect_ratio_row" +const val QUICK_SETTINGS_FLASH_ROW = "quick_settings_flash_row" diff --git a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/ui/QuickSettingsComponents.kt b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/ui/QuickSettingsComponents.kt index a39095d9b..f9559f230 100644 --- a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/ui/QuickSettingsComponents.kt +++ b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/ui/QuickSettingsComponents.kt @@ -73,6 +73,22 @@ import com.google.jetpackcamera.model.ImageOutputFormat import com.google.jetpackcamera.ui.components.capture.QUICK_SETTINGS_BOTTOM_SHEET import com.google.jetpackcamera.ui.components.capture.QUICK_SETTINGS_DROP_DOWN import com.google.jetpackcamera.ui.components.capture.R +import com.google.jetpackcamera.ui.components.capture.BTN_QUICK_SETTINGS_FOCUSED_CAPTURE_MODE_IMAGE_ONLY +import com.google.jetpackcamera.ui.components.capture.BTN_QUICK_SETTINGS_FOCUSED_CAPTURE_MODE_OPTION_STANDARD +import com.google.jetpackcamera.ui.components.capture.BTN_QUICK_SETTINGS_FOCUSED_CAPTURE_MODE_VIDEO_ONLY +import com.google.jetpackcamera.ui.components.capture.BTN_QUICK_SETTINGS_FLASH_OPTION_AUTO +import com.google.jetpackcamera.ui.components.capture.BTN_QUICK_SETTINGS_FLASH_OPTION_LOW_LIGHT_BOOST +import com.google.jetpackcamera.ui.components.capture.BTN_QUICK_SETTINGS_FLASH_OPTION_OFF +import com.google.jetpackcamera.ui.components.capture.BTN_QUICK_SETTINGS_FLASH_OPTION_ON +import com.google.jetpackcamera.ui.components.capture.BTN_QUICK_SETTINGS_HDR_OPTION_OFF +import com.google.jetpackcamera.ui.components.capture.BTN_QUICK_SETTINGS_HDR_OPTION_ON +import com.google.jetpackcamera.ui.components.capture.QUICK_SETTINGS_RATIO_1_1_BUTTON +import com.google.jetpackcamera.ui.components.capture.QUICK_SETTINGS_RATIO_3_4_BUTTON +import com.google.jetpackcamera.ui.components.capture.QUICK_SETTINGS_RATIO_9_16_BUTTON +import com.google.jetpackcamera.ui.components.capture.QUICK_SETTINGS_CAPTURE_MODE_ROW +import com.google.jetpackcamera.ui.components.capture.QUICK_SETTINGS_HDR_ROW +import com.google.jetpackcamera.ui.components.capture.QUICK_SETTINGS_ASPECT_RATIO_ROW +import com.google.jetpackcamera.ui.components.capture.QUICK_SETTINGS_FLASH_ROW import com.google.jetpackcamera.ui.components.capture.quicksettings.CameraAspectRatio import com.google.jetpackcamera.ui.components.capture.quicksettings.CameraCaptureMode import com.google.jetpackcamera.ui.components.capture.quicksettings.CameraDynamicRange @@ -236,21 +252,24 @@ fun CaptureModeRow( } SettingRow( - modifier = modifier, + modifier = modifier.testTag(QUICK_SETTINGS_CAPTURE_MODE_ROW), title = stringResource(id = R.string.quick_settings_title_capture_mode), stateSubtitle = stringResource(enum.getTextResId()), settingsButtons =captureModeUiState.availableCaptureModes - .map { selectableMode -> - @Composable { - CaptureModeToggleButton( - modifier = Modifier - .testTag("CaptureMode_${selectableMode.value.name}"), - onClick = { onSetCaptureMode(selectableMode.value) }, - assignedCaptureMode = selectableMode.value, - captureModeUiState = captureModeUiState, - isHighlightEnabled = true - ) - } + .map { selectableMode -> + @Composable { + val testTag = when (selectableMode.value) { + CaptureMode.STANDARD -> BTN_QUICK_SETTINGS_FOCUSED_CAPTURE_MODE_OPTION_STANDARD + CaptureMode.IMAGE_ONLY -> BTN_QUICK_SETTINGS_FOCUSED_CAPTURE_MODE_IMAGE_ONLY + CaptureMode.VIDEO_ONLY -> BTN_QUICK_SETTINGS_FOCUSED_CAPTURE_MODE_VIDEO_ONLY + } + CaptureModeToggleButton( + modifier = Modifier.testTag(testTag), + onClick = { onSetCaptureMode(selectableMode.value) }, + assignedCaptureMode = selectableMode.value, + captureModeUiState = captureModeUiState, + isHighlightEnabled = true + ) } }.toTypedArray() ) } @@ -269,7 +288,7 @@ fun HdrRow( ) SettingRow( - modifier = modifier, + modifier = modifier.testTag(QUICK_SETTINGS_HDR_ROW), title = stringResource(id = R.string.quick_settings_title_hdr), stateSubtitle = if (isHdrOn) { stringResource(R.string.quick_settings_dynamic_range_hdr) @@ -279,6 +298,7 @@ fun HdrRow( settingsButtons = arrayOf( { QuickSettingToggleSelectorButton( + modifier = Modifier.testTag(BTN_QUICK_SETTINGS_HDR_OPTION_ON), enum = CameraDynamicRange.HDR, onClick = { onClick(DEFAULT_HDR_DYNAMIC_RANGE, DEFAULT_HDR_IMAGE_OUTPUT) }, isSelected = isHdrOn, @@ -287,6 +307,7 @@ fun HdrRow( }, { QuickSettingToggleSelectorButton( + modifier = Modifier.testTag(BTN_QUICK_SETTINGS_HDR_OPTION_OFF), enum = CameraDynamicRange.SDR, onClick = { onClick(DynamicRange.SDR, ImageOutputFormat.JPEG) }, isSelected = !isHdrOn, @@ -312,8 +333,13 @@ fun AspectRatioRow( AspectRatio.NINE_SIXTEEN -> CameraAspectRatio.NINE_SIXTEEN AspectRatio.ONE_ONE -> CameraAspectRatio.ONE_ONE } + val testTag = when (selectableRatio.value) { + AspectRatio.THREE_FOUR -> QUICK_SETTINGS_RATIO_3_4_BUTTON + AspectRatio.NINE_SIXTEEN -> QUICK_SETTINGS_RATIO_9_16_BUTTON + AspectRatio.ONE_ONE -> QUICK_SETTINGS_RATIO_1_1_BUTTON + } QuickSettingToggleSelectorButton( - modifier = Modifier.testTag("AspectRatio_${selectableRatio.value.name}"), + modifier = Modifier.testTag(testTag), onClick = { onSetAspectRatio(selectableRatio.value) }, enum = enum, isSelected = selectableRatio.value == aspectRatioUiState.selectedAspectRatio @@ -322,7 +348,7 @@ fun AspectRatioRow( }.toTypedArray() SettingRow( - modifier = modifier, + modifier = modifier.testTag(QUICK_SETTINGS_ASPECT_RATIO_ROW), title = stringResource(id = R.string.quick_settings_title_aspect_ratio), stateSubtitle = stringResource(id = aspectRatioUiState.selectedAspectRatio.toSubtitleStringRes()), settingsButtons = settingsButtons @@ -338,14 +364,20 @@ fun FlashRow( ) { if (flashModeUiState is FlashModeUiState.Available) { SettingRow( - modifier = modifier, + modifier = modifier.testTag(QUICK_SETTINGS_FLASH_ROW), title = stringResource(id = R.string.quick_settings_title_flash_mode), stateSubtitle = stringResource(id = flashModeUiState.selectedFlashMode.toSubtitleStringRes()), settingsButtons = flashModeUiState.availableFlashModes .map { selectableMode -> @Composable { + val testTag = when (selectableMode.value) { + FlashMode.OFF -> BTN_QUICK_SETTINGS_FLASH_OPTION_OFF + FlashMode.ON -> BTN_QUICK_SETTINGS_FLASH_OPTION_ON + FlashMode.AUTO -> BTN_QUICK_SETTINGS_FLASH_OPTION_AUTO + FlashMode.LOW_LIGHT_BOOST -> BTN_QUICK_SETTINGS_FLASH_OPTION_LOW_LIGHT_BOOST + } QuickSettingToggleSelectorButton( - modifier = modifier, + modifier = Modifier.testTag(testTag), enabled = selectableMode is SingleSelectableUiState.SelectableUi, enum = when (selectableMode.value) { FlashMode.OFF -> CameraFlashMode.OFF From 9362ea6a91b143c54f6539ffcc4a49d2f512b03d Mon Sep 17 00:00:00 2001 From: Kimberly Crevecoeur Date: Thu, 7 May 2026 17:00:20 -0700 Subject: [PATCH 10/51] Re-add optional 'More Settings' button to Quick Settings and clean up state - Re-added the 'More Settings' button to the Quick Settings bottom sheet. - Introduced a 'showMoreSettingsButton' boolean parameter to control its visibility, defaulting to true. - Removed unused 'focusedQuickSetting' state from 'QuickSettingsUiState' and 'TrackedCaptureUiState'. - Cleaned up 'FlashModeUiStateAdapter.kt' by removing a debug 'println' and adding a 'todo(kc)' for 'visibleFlashModes'. --- .../feature/preview/PreviewScreen.kt | 3 +- .../quicksettings/QuickSettingsScreen.kt | 41 +++++++++++++++---- .../ui/QuickSettingsComponents.kt | 3 +- .../capture/FlashModeUiStateAdapter.kt | 4 +- 4 files changed, 39 insertions(+), 12 deletions(-) diff --git a/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewScreen.kt b/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewScreen.kt index 947873c5b..823c516ee 100644 --- a/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewScreen.kt +++ b/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewScreen.kt @@ -530,7 +530,8 @@ private fun ContentScreen( quickSettingsController.toggleQuickSettings() onNavigateToSettings() }, - quickSettingsController = quickSettingsController + quickSettingsController = quickSettingsController, + showMoreSettingsButton = true ) } }, 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 debbb3954..3356e8d09 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 @@ -38,6 +38,7 @@ import com.google.jetpackcamera.ui.components.capture.quicksettings.ui.AspectRat import com.google.jetpackcamera.ui.components.capture.quicksettings.ui.CaptureModeRow import com.google.jetpackcamera.ui.components.capture.quicksettings.ui.FlashRow import com.google.jetpackcamera.ui.components.capture.quicksettings.ui.HdrRow +import com.google.jetpackcamera.ui.components.capture.quicksettings.ui.QuickNavSettings import com.google.jetpackcamera.ui.components.capture.quicksettings.ui.QuickSettingsBottomSheet as BottomSheetComponent import com.google.jetpackcamera.ui.controller.quicksettings.QuickSettingsController import com.google.jetpackcamera.ui.uistate.capture.AspectRatioUiState @@ -55,7 +56,8 @@ fun QuickSettingsBottomSheet( quickSettingsUiState: QuickSettingsUiState, quickSettingsController: QuickSettingsController, modifier: Modifier = Modifier, - onNavigateToSettings: () -> Unit = {} + onNavigateToSettings: () -> Unit = {}, + showMoreSettingsButton: Boolean = true ) { if (quickSettingsUiState is QuickSettingsUiState.Available && quickSettingsUiState.quickSettingsIsOpen @@ -72,22 +74,30 @@ fun QuickSettingsBottomSheet( ) { CaptureMode.VIDEO_ONLY -> VideoQuickSettings( quickSettingsUiState = quickSettingsUiState, - quickSettingsController = quickSettingsController + quickSettingsController = quickSettingsController, + onNavigateToSettings = onNavigateToSettings, + showMoreSettingsButton = showMoreSettingsButton ) CaptureMode.IMAGE_ONLY -> ImageQuickSettings( quickSettingsUiState = quickSettingsUiState, - quickSettingsController = quickSettingsController + quickSettingsController = quickSettingsController, + onNavigateToSettings = onNavigateToSettings, + showMoreSettingsButton = showMoreSettingsButton ) CaptureMode.STANDARD -> HybridQuickSettings( quickSettingsUiState = quickSettingsUiState, - quickSettingsController = quickSettingsController + quickSettingsController = quickSettingsController, + onNavigateToSettings = onNavigateToSettings, + showMoreSettingsButton = showMoreSettingsButton ) } } ?: ImageQuickSettings( quickSettingsUiState = quickSettingsUiState, - quickSettingsController = quickSettingsController + quickSettingsController = quickSettingsController, + onNavigateToSettings = onNavigateToSettings, + showMoreSettingsButton = showMoreSettingsButton ) } } @@ -96,7 +106,9 @@ fun QuickSettingsBottomSheet( @Composable fun HybridQuickSettings( quickSettingsUiState: QuickSettingsUiState.Available, - quickSettingsController: QuickSettingsController + quickSettingsController: QuickSettingsController, + onNavigateToSettings: () -> Unit, + showMoreSettingsButton: Boolean ) { Column { Text( @@ -140,12 +152,17 @@ fun HybridQuickSettings( ) } } + if (showMoreSettingsButton) { + QuickNavSettings(onNavigateToSettings = onNavigateToSettings) + } } @Composable fun VideoQuickSettings( quickSettingsUiState: QuickSettingsUiState.Available, - quickSettingsController: QuickSettingsController + quickSettingsController: QuickSettingsController, + onNavigateToSettings: () -> Unit, + showMoreSettingsButton: Boolean ) { Column { Text( @@ -176,12 +193,17 @@ fun VideoQuickSettings( // TODO: Add Resolution setting // TODO: Add Stabilization setting } + if (showMoreSettingsButton) { + QuickNavSettings(onNavigateToSettings = onNavigateToSettings) + } } @Composable fun ImageQuickSettings( quickSettingsUiState: QuickSettingsUiState.Available, - quickSettingsController: QuickSettingsController + quickSettingsController: QuickSettingsController, + onNavigateToSettings: () -> Unit, + showMoreSettingsButton: Boolean ) { Column { Text( @@ -208,6 +230,9 @@ fun ImageQuickSettings( // TODO: Add pre-capture timer setting } + if (showMoreSettingsButton) { + QuickNavSettings(onNavigateToSettings = onNavigateToSettings) + } } /** diff --git a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/ui/QuickSettingsComponents.kt b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/ui/QuickSettingsComponents.kt index f9559f230..2dd2d63c0 100644 --- a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/ui/QuickSettingsComponents.kt +++ b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/ui/QuickSettingsComponents.kt @@ -73,6 +73,7 @@ import com.google.jetpackcamera.model.ImageOutputFormat import com.google.jetpackcamera.ui.components.capture.QUICK_SETTINGS_BOTTOM_SHEET import com.google.jetpackcamera.ui.components.capture.QUICK_SETTINGS_DROP_DOWN import com.google.jetpackcamera.ui.components.capture.R +import com.google.jetpackcamera.ui.components.capture.SETTINGS_BUTTON import com.google.jetpackcamera.ui.components.capture.BTN_QUICK_SETTINGS_FOCUSED_CAPTURE_MODE_IMAGE_ONLY import com.google.jetpackcamera.ui.components.capture.BTN_QUICK_SETTINGS_FOCUSED_CAPTURE_MODE_OPTION_STANDARD import com.google.jetpackcamera.ui.components.capture.BTN_QUICK_SETTINGS_FOCUSED_CAPTURE_MODE_VIDEO_ONLY @@ -158,7 +159,7 @@ fun ToggleQuickSettingsButton( @Composable fun QuickNavSettings(onNavigateToSettings: () -> Unit, modifier: Modifier = Modifier) { TextButton( - modifier = modifier, + modifier = modifier.testTag(SETTINGS_BUTTON), onClick = onNavigateToSettings, content = { Text(text = stringResource(R.string.quick_settings_more_text)) } ) diff --git a/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/FlashModeUiStateAdapter.kt b/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/FlashModeUiStateAdapter.kt index 565a0bdaf..6e8d8ff41 100644 --- a/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/FlashModeUiStateAdapter.kt +++ b/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/FlashModeUiStateAdapter.kt @@ -59,7 +59,8 @@ private val ORDERED_UI_SUPPORTED_FLASH_MODES = listOf( fun FlashModeUiState.Companion.from( cameraAppSettings: CameraAppSettings, systemConstraints: CameraSystemConstraints, - //visibleFlashModes: Set + // todo(kc): supply visible flash modes from developer options + // visibleFlashModes: Set = ORDERED_UI_SUPPORTED_FLASH_MODES.toSet() ): FlashModeUiState { val selectedFlashMode = cameraAppSettings.flashMode @@ -71,7 +72,6 @@ fun FlashModeUiState.Companion.from( } } } - println("low light boost supported by device?" + allDeviceSupportedFlashModes.contains(FlashMode.LOW_LIGHT_BOOST)) // Modes supported by the CURRENT lens val currentLensSupportedFlashModes = systemConstraints.forCurrentLens(cameraAppSettings) From a2ab97e55fdc215108e389a7b9fa5233215eb419 Mon Sep 17 00:00:00 2001 From: Kimberly Crevecoeur Date: Fri, 8 May 2026 17:38:22 -0700 Subject: [PATCH 11/51] spotless --- .../capture/FlashModeUiStateAdapter.kt | 25 +++++++++++-------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/FlashModeUiStateAdapter.kt b/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/FlashModeUiStateAdapter.kt index 6e8d8ff41..790133398 100644 --- a/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/FlashModeUiStateAdapter.kt +++ b/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/FlashModeUiStateAdapter.kt @@ -58,7 +58,7 @@ private val ORDERED_UI_SUPPORTED_FLASH_MODES = listOf( */ fun FlashModeUiState.Companion.from( cameraAppSettings: CameraAppSettings, - systemConstraints: CameraSystemConstraints, + systemConstraints: CameraSystemConstraints // todo(kc): supply visible flash modes from developer options // visibleFlashModes: Set = ORDERED_UI_SUPPORTED_FLASH_MODES.toSet() ): FlashModeUiState { @@ -68,7 +68,7 @@ fun FlashModeUiState.Companion.from( val allDeviceSupportedFlashModes = buildSet { systemConstraints.perLensConstraints.let { it.keys.forEach { key -> - it[key]?.supportedFlashModes?.let {flashModes -> addAll(flashModes) } + it[key]?.supportedFlashModes?.let { flashModes -> addAll(flashModes) } } } } @@ -80,14 +80,13 @@ fun FlashModeUiState.Companion.from( val displayableModes = mutableListOf>() for (mode in ORDERED_UI_SUPPORTED_FLASH_MODES) { - // 1. Hide if not supported by the device at all. if (!allDeviceSupportedFlashModes.contains(mode)) { continue } // 2. Hide if not designated as visible by the developer. - //todo(kc): supply visible flash modes from developer options + // todo(kc): supply visible flash modes from developer options /*if (!visibleFlashModes.contains(mode)) { continue }*/ @@ -95,7 +94,8 @@ fun FlashModeUiState.Companion.from( // 3. Special handling for LOW_LIGHT_BOOST based on other settings. if (mode == FlashMode.LOW_LIGHT_BOOST) { if (cameraAppSettings.dynamicRange != DynamicRange.SDR || - cameraAppSettings.concurrentCameraMode == ConcurrentCameraMode.DUAL) { + cameraAppSettings.concurrentCameraMode == ConcurrentCameraMode.DUAL + ) { continue // Hide LLB if HDR or Dual Camera is active } } @@ -104,16 +104,21 @@ fun FlashModeUiState.Companion.from( if (currentLensSupportedFlashModes.contains(mode)) { displayableModes.add(SingleSelectableUiState.SelectableUi(mode)) // Enabled } else { - //todo(kc): add actual disabledreason for flash mode - displayableModes.add(SingleSelectableUiState.Disabled(value = mode, disabledReason = DisabledReason.HDR_IMAGE_UNSUPPORTED_ON_LENS)) // Disabled + // todo(kc): add actual disabledreason for flash mode + displayableModes.add( + SingleSelectableUiState.Disabled( + value = mode, + disabledReason = DisabledReason.HDR_IMAGE_UNSUPPORTED_ON_LENS + ) + ) // Disabled } } // UiState should be Unavailable if no modes are displayable, // or if only OFF is displayable and it's selectable. val onlyOffSelectable = displayableModes.size == 1 && - displayableModes[0].value == FlashMode.OFF && - displayableModes[0] is SingleSelectableUiState.SelectableUi + displayableModes[0].value == FlashMode.OFF && + displayableModes[0] is SingleSelectableUiState.SelectableUi return if (displayableModes.isEmpty() || onlyOffSelectable) { Unavailable @@ -137,7 +142,7 @@ fun FlashModeUiState.Companion.from( fun FlashModeUiState.updateFrom( cameraAppSettings: CameraAppSettings, systemConstraints: CameraSystemConstraints, - cameraState: CameraState, + cameraState: CameraState ): FlashModeUiState { return when (this) { is Unavailable -> { From 5496148ad4dd2fbb4dd88005e8e371707e964d88 Mon Sep 17 00:00:00 2001 From: Kimberly Crevecoeur Date: Mon, 11 May 2026 15:42:56 -0700 Subject: [PATCH 12/51] Clean up unused quick settings resources and enums Remove unused drawables and their corresponding enum classes in QuickSettingsEnums that are no longer referenced after the quick settings refactoring. --- .../quicksettings/QuickSettingsEnums.kt | 29 ------------------- .../src/main/res/drawable/ic_more_horiz.xml | 26 ----------------- .../res/drawable/ic_picture_in_picture.xml | 26 ----------------- .../main/res/drawable/multi_stream_icon.xml | 21 -------------- .../drawable/picture_in_picture_off_icon.xml | 21 -------------- .../drawable/single_stream_capture_icon.xml | 21 -------------- 6 files changed, 144 deletions(-) delete mode 100644 ui/components/capture/src/main/res/drawable/ic_more_horiz.xml delete mode 100644 ui/components/capture/src/main/res/drawable/ic_picture_in_picture.xml delete mode 100644 ui/components/capture/src/main/res/drawable/multi_stream_icon.xml delete mode 100644 ui/components/capture/src/main/res/drawable/picture_in_picture_off_icon.xml delete mode 100644 ui/components/capture/src/main/res/drawable/single_stream_capture_icon.xml diff --git a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/QuickSettingsEnums.kt b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/QuickSettingsEnums.kt index fe886da50..70e220136 100644 --- a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/QuickSettingsEnums.kt +++ b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/QuickSettingsEnums.kt @@ -105,21 +105,7 @@ enum class CameraAspectRatio : QuickSettingsEnum { } } -enum class CameraStreamConfig : QuickSettingsEnum { - MULTI_STREAM { - override fun getDrawableResId() = R.drawable.multi_stream_icon - override fun getTextResId() = R.string.quick_settings_stream_config_multi - override fun getDescriptionResId() = R.string.quick_settings_stream_config_multi_description - }, - SINGLE_STREAM { - override fun getDrawableResId() = R.drawable.single_stream_capture_icon - - override fun getTextResId() = R.string.quick_settings_stream_config_single - override fun getDescriptionResId() = - R.string.quick_settings_stream_config_single_description - } -} enum class CameraDynamicRange : QuickSettingsEnum { SDR { @@ -165,19 +151,4 @@ enum class CameraCaptureMode : QuickSettingsEnum { R.string.quick_settings_description_capture_mode_image_only } } -enum class CameraConcurrentCameraMode : QuickSettingsEnum { - OFF { - override fun getDrawableResId() = R.drawable.picture_in_picture_off_icon - override fun getTextResId() = R.string.quick_settings_text_concurrent_camera_off - override fun getDescriptionResId() = - R.string.quick_settings_description_concurrent_camera_off - }, - DUAL { - - override fun getDrawableResId() = R.drawable.ic_picture_in_picture - override fun getTextResId() = R.string.quick_settings_text_concurrent_camera_dual - override fun getDescriptionResId() = - R.string.quick_settings_description_concurrent_camera_dual - } -} diff --git a/ui/components/capture/src/main/res/drawable/ic_more_horiz.xml b/ui/components/capture/src/main/res/drawable/ic_more_horiz.xml deleted file mode 100644 index cbf1b7dea..000000000 --- a/ui/components/capture/src/main/res/drawable/ic_more_horiz.xml +++ /dev/null @@ -1,26 +0,0 @@ - - - - - diff --git a/ui/components/capture/src/main/res/drawable/ic_picture_in_picture.xml b/ui/components/capture/src/main/res/drawable/ic_picture_in_picture.xml deleted file mode 100644 index 022dc4f40..000000000 --- a/ui/components/capture/src/main/res/drawable/ic_picture_in_picture.xml +++ /dev/null @@ -1,26 +0,0 @@ - - - - - diff --git a/ui/components/capture/src/main/res/drawable/multi_stream_icon.xml b/ui/components/capture/src/main/res/drawable/multi_stream_icon.xml deleted file mode 100644 index 834ee33a9..000000000 --- a/ui/components/capture/src/main/res/drawable/multi_stream_icon.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - diff --git a/ui/components/capture/src/main/res/drawable/picture_in_picture_off_icon.xml b/ui/components/capture/src/main/res/drawable/picture_in_picture_off_icon.xml deleted file mode 100644 index 3c394b176..000000000 --- a/ui/components/capture/src/main/res/drawable/picture_in_picture_off_icon.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - diff --git a/ui/components/capture/src/main/res/drawable/single_stream_capture_icon.xml b/ui/components/capture/src/main/res/drawable/single_stream_capture_icon.xml deleted file mode 100644 index cc818a10f..000000000 --- a/ui/components/capture/src/main/res/drawable/single_stream_capture_icon.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - From 0ebd3d7fce79d8aed2e46759725e4ab80e0fd4e6 Mon Sep 17 00:00:00 2001 From: Kimberly Crevecoeur Date: Mon, 11 May 2026 16:06:13 -0700 Subject: [PATCH 13/51] spotless --- .../ui/components/capture/TestTags.kt | 3 +- .../quicksettings/QuickSettingsEnums.kt | 3 - .../ui/QuickSettingsComponents.kt | 87 ++++++++++--------- 3 files changed, 48 insertions(+), 45 deletions(-) diff --git a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/TestTags.kt b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/TestTags.kt index 19aa670af..8f6d5046e 100644 --- a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/TestTags.kt +++ b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/TestTags.kt @@ -99,7 +99,8 @@ const val BTN_QUICK_SETTINGS_FOCUSED_CAPTURE_MODE_IMAGE_ONLY = const val BTN_QUICK_SETTINGS_FLASH_OPTION_OFF = "btn_quick_settings_flash_option_off" const val BTN_QUICK_SETTINGS_FLASH_OPTION_ON = "btn_quick_settings_flash_option_on" const val BTN_QUICK_SETTINGS_FLASH_OPTION_AUTO = "btn_quick_settings_flash_option_auto" -const val BTN_QUICK_SETTINGS_FLASH_OPTION_LOW_LIGHT_BOOST = "btn_quick_settings_flash_option_low_light_boost" +const val BTN_QUICK_SETTINGS_FLASH_OPTION_LOW_LIGHT_BOOST = + "btn_quick_settings_flash_option_low_light_boost" const val BTN_QUICK_SETTINGS_HDR_OPTION_ON = "btn_quick_settings_hdr_option_on" const val BTN_QUICK_SETTINGS_HDR_OPTION_OFF = "btn_quick_settings_hdr_option_off" diff --git a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/QuickSettingsEnums.kt b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/QuickSettingsEnums.kt index 70e220136..f064af0b7 100644 --- a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/QuickSettingsEnums.kt +++ b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/QuickSettingsEnums.kt @@ -105,8 +105,6 @@ enum class CameraAspectRatio : QuickSettingsEnum { } } - - enum class CameraDynamicRange : QuickSettingsEnum { SDR { @@ -151,4 +149,3 @@ enum class CameraCaptureMode : QuickSettingsEnum { R.string.quick_settings_description_capture_mode_image_only } } - diff --git a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/ui/QuickSettingsComponents.kt b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/ui/QuickSettingsComponents.kt index 2dd2d63c0..1d5112ad7 100644 --- a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/ui/QuickSettingsComponents.kt +++ b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/ui/QuickSettingsComponents.kt @@ -70,26 +70,26 @@ import com.google.jetpackcamera.model.DEFAULT_HDR_IMAGE_OUTPUT import com.google.jetpackcamera.model.DynamicRange import com.google.jetpackcamera.model.FlashMode import com.google.jetpackcamera.model.ImageOutputFormat -import com.google.jetpackcamera.ui.components.capture.QUICK_SETTINGS_BOTTOM_SHEET -import com.google.jetpackcamera.ui.components.capture.QUICK_SETTINGS_DROP_DOWN -import com.google.jetpackcamera.ui.components.capture.R -import com.google.jetpackcamera.ui.components.capture.SETTINGS_BUTTON -import com.google.jetpackcamera.ui.components.capture.BTN_QUICK_SETTINGS_FOCUSED_CAPTURE_MODE_IMAGE_ONLY -import com.google.jetpackcamera.ui.components.capture.BTN_QUICK_SETTINGS_FOCUSED_CAPTURE_MODE_OPTION_STANDARD -import com.google.jetpackcamera.ui.components.capture.BTN_QUICK_SETTINGS_FOCUSED_CAPTURE_MODE_VIDEO_ONLY import com.google.jetpackcamera.ui.components.capture.BTN_QUICK_SETTINGS_FLASH_OPTION_AUTO import com.google.jetpackcamera.ui.components.capture.BTN_QUICK_SETTINGS_FLASH_OPTION_LOW_LIGHT_BOOST import com.google.jetpackcamera.ui.components.capture.BTN_QUICK_SETTINGS_FLASH_OPTION_OFF import com.google.jetpackcamera.ui.components.capture.BTN_QUICK_SETTINGS_FLASH_OPTION_ON +import com.google.jetpackcamera.ui.components.capture.BTN_QUICK_SETTINGS_FOCUSED_CAPTURE_MODE_IMAGE_ONLY +import com.google.jetpackcamera.ui.components.capture.BTN_QUICK_SETTINGS_FOCUSED_CAPTURE_MODE_OPTION_STANDARD +import com.google.jetpackcamera.ui.components.capture.BTN_QUICK_SETTINGS_FOCUSED_CAPTURE_MODE_VIDEO_ONLY import com.google.jetpackcamera.ui.components.capture.BTN_QUICK_SETTINGS_HDR_OPTION_OFF import com.google.jetpackcamera.ui.components.capture.BTN_QUICK_SETTINGS_HDR_OPTION_ON +import com.google.jetpackcamera.ui.components.capture.QUICK_SETTINGS_ASPECT_RATIO_ROW +import com.google.jetpackcamera.ui.components.capture.QUICK_SETTINGS_BOTTOM_SHEET +import com.google.jetpackcamera.ui.components.capture.QUICK_SETTINGS_CAPTURE_MODE_ROW +import com.google.jetpackcamera.ui.components.capture.QUICK_SETTINGS_DROP_DOWN +import com.google.jetpackcamera.ui.components.capture.QUICK_SETTINGS_FLASH_ROW +import com.google.jetpackcamera.ui.components.capture.QUICK_SETTINGS_HDR_ROW import com.google.jetpackcamera.ui.components.capture.QUICK_SETTINGS_RATIO_1_1_BUTTON import com.google.jetpackcamera.ui.components.capture.QUICK_SETTINGS_RATIO_3_4_BUTTON import com.google.jetpackcamera.ui.components.capture.QUICK_SETTINGS_RATIO_9_16_BUTTON -import com.google.jetpackcamera.ui.components.capture.QUICK_SETTINGS_CAPTURE_MODE_ROW -import com.google.jetpackcamera.ui.components.capture.QUICK_SETTINGS_HDR_ROW -import com.google.jetpackcamera.ui.components.capture.QUICK_SETTINGS_ASPECT_RATIO_ROW -import com.google.jetpackcamera.ui.components.capture.QUICK_SETTINGS_FLASH_ROW +import com.google.jetpackcamera.ui.components.capture.R +import com.google.jetpackcamera.ui.components.capture.SETTINGS_BUTTON import com.google.jetpackcamera.ui.components.capture.quicksettings.CameraAspectRatio import com.google.jetpackcamera.ui.components.capture.quicksettings.CameraCaptureMode import com.google.jetpackcamera.ui.components.capture.quicksettings.CameraDynamicRange @@ -103,7 +103,6 @@ import com.google.jetpackcamera.ui.uistate.capture.CaptureModeUiState.Unavailabl import com.google.jetpackcamera.ui.uistate.capture.FlashModeUiState import com.google.jetpackcamera.ui.uistate.capture.HdrUiState - // //////////////////////////////////////////////////// // // quick settings navigation components @@ -151,7 +150,6 @@ fun ToggleQuickSettingsButton( } } - /** * A button within the quick settings menu that will navigate to the default settings screen */ @@ -194,18 +192,16 @@ fun CaptureModeToggleButton( captureModeUiState.isCaptureModeSelectable(CaptureMode.IMAGE_ONLY) }, isSelected = - isHighlightEnabled && (assignedCaptureMode == captureModeUiState.selectedCaptureMode) + isHighlightEnabled && (assignedCaptureMode == captureModeUiState.selectedCaptureMode) ) } - // //////////////////////////////////////////////////// // // complete quick settings screen components // // //////////////////////////////////////////////////// - /** * A modal bottom sheet composable used to display a collection of quick setting buttons. * @@ -239,6 +235,7 @@ fun QuickSettingsBottomSheet( content() } } + @Composable fun CaptureModeRow( modifier: Modifier = Modifier, @@ -246,7 +243,7 @@ fun CaptureModeRow( captureModeUiState: CaptureModeUiState ) { if (captureModeUiState is CaptureModeUiState.Available) { - val enum = when (captureModeUiState.selectedCaptureMode){ + val enum = when (captureModeUiState.selectedCaptureMode) { CaptureMode.STANDARD -> CameraCaptureMode.STANDARD CaptureMode.IMAGE_ONLY -> CameraCaptureMode.IMAGE_ONLY CaptureMode.VIDEO_ONLY -> CameraCaptureMode.VIDEO_ONLY @@ -256,21 +253,25 @@ fun CaptureModeRow( modifier = modifier.testTag(QUICK_SETTINGS_CAPTURE_MODE_ROW), title = stringResource(id = R.string.quick_settings_title_capture_mode), stateSubtitle = stringResource(enum.getTextResId()), - settingsButtons =captureModeUiState.availableCaptureModes - .map { selectableMode -> - @Composable { - val testTag = when (selectableMode.value) { - CaptureMode.STANDARD -> BTN_QUICK_SETTINGS_FOCUSED_CAPTURE_MODE_OPTION_STANDARD - CaptureMode.IMAGE_ONLY -> BTN_QUICK_SETTINGS_FOCUSED_CAPTURE_MODE_IMAGE_ONLY - CaptureMode.VIDEO_ONLY -> BTN_QUICK_SETTINGS_FOCUSED_CAPTURE_MODE_VIDEO_ONLY - } - CaptureModeToggleButton( - modifier = Modifier.testTag(testTag), - onClick = { onSetCaptureMode(selectableMode.value) }, - assignedCaptureMode = selectableMode.value, - captureModeUiState = captureModeUiState, - isHighlightEnabled = true - ) } + settingsButtons = captureModeUiState.availableCaptureModes + .map { selectableMode -> + @Composable { + val testTag = when (selectableMode.value) { + CaptureMode.STANDARD -> + BTN_QUICK_SETTINGS_FOCUSED_CAPTURE_MODE_OPTION_STANDARD + CaptureMode.IMAGE_ONLY -> + BTN_QUICK_SETTINGS_FOCUSED_CAPTURE_MODE_IMAGE_ONLY + CaptureMode.VIDEO_ONLY -> + BTN_QUICK_SETTINGS_FOCUSED_CAPTURE_MODE_VIDEO_ONLY + } + CaptureModeToggleButton( + modifier = Modifier.testTag(testTag), + onClick = { onSetCaptureMode(selectableMode.value) }, + assignedCaptureMode = selectableMode.value, + captureModeUiState = captureModeUiState, + isHighlightEnabled = true + ) + } }.toTypedArray() ) } @@ -351,7 +352,9 @@ fun AspectRatioRow( SettingRow( modifier = modifier.testTag(QUICK_SETTINGS_ASPECT_RATIO_ROW), title = stringResource(id = R.string.quick_settings_title_aspect_ratio), - stateSubtitle = stringResource(id = aspectRatioUiState.selectedAspectRatio.toSubtitleStringRes()), + stateSubtitle = stringResource( + id = aspectRatioUiState.selectedAspectRatio.toSubtitleStringRes() + ), settingsButtons = settingsButtons ) } @@ -367,7 +370,9 @@ fun FlashRow( SettingRow( modifier = modifier.testTag(QUICK_SETTINGS_FLASH_ROW), title = stringResource(id = R.string.quick_settings_title_flash_mode), - stateSubtitle = stringResource(id = flashModeUiState.selectedFlashMode.toSubtitleStringRes()), + stateSubtitle = stringResource( + id = flashModeUiState.selectedFlashMode.toSubtitleStringRes() + ), settingsButtons = flashModeUiState.availableFlashModes .map { selectableMode -> @Composable { @@ -375,7 +380,8 @@ fun FlashRow( FlashMode.OFF -> BTN_QUICK_SETTINGS_FLASH_OPTION_OFF FlashMode.ON -> BTN_QUICK_SETTINGS_FLASH_OPTION_ON FlashMode.AUTO -> BTN_QUICK_SETTINGS_FLASH_OPTION_AUTO - FlashMode.LOW_LIGHT_BOOST -> BTN_QUICK_SETTINGS_FLASH_OPTION_LOW_LIGHT_BOOST + FlashMode.LOW_LIGHT_BOOST -> + BTN_QUICK_SETTINGS_FLASH_OPTION_LOW_LIGHT_BOOST } QuickSettingToggleSelectorButton( modifier = Modifier.testTag(testTag), @@ -384,10 +390,11 @@ fun FlashRow( FlashMode.OFF -> CameraFlashMode.OFF FlashMode.ON -> CameraFlashMode.ON FlashMode.AUTO -> CameraFlashMode.AUTO - FlashMode.LOW_LIGHT_BOOST -> when (flashModeUiState.isLowLightBoostActive) { - true -> CameraFlashMode.LOW_LIGHT_BOOST_ACTIVE - false -> CameraFlashMode.LOW_LIGHT_BOOST_INACTIVE - } + FlashMode.LOW_LIGHT_BOOST -> + when (flashModeUiState.isLowLightBoostActive) { + true -> CameraFlashMode.LOW_LIGHT_BOOST_ACTIVE + false -> CameraFlashMode.LOW_LIGHT_BOOST_INACTIVE + } }, isSelected = flashModeUiState.selectedFlashMode == selectableMode.value, onClick = { @@ -402,8 +409,6 @@ fun FlashRow( } } - - // //////////////////////////////////////////////////// // // subcomponents used to build completed components From 1a3c0bb4103827ec9dd3484760c6bfdd318d34c3 Mon Sep 17 00:00:00 2001 From: Kimberly Crevecoeur Date: Tue, 26 May 2026 10:42:57 -0700 Subject: [PATCH 14/51] correct flash disabled reason for setting --- .../jetpackcamera/ui/components/capture/DisabledReason.kt | 4 ++++ .../google/jetpackcamera/ui/components/capture/TestTags.kt | 1 + ui/components/capture/src/main/res/values/strings.xml | 1 + .../ui/uistateadapter/capture/FlashModeUiStateAdapter.kt | 3 +-- 4 files changed, 7 insertions(+), 2 deletions(-) diff --git a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/DisabledReason.kt b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/DisabledReason.kt index 50c33e06b..ccc33140b 100644 --- a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/DisabledReason.kt +++ b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/DisabledReason.kt @@ -66,5 +66,9 @@ enum class DisabledReason( HDR_SIMULTANEOUS_IMAGE_VIDEO_UNSUPPORTED( HDR_SIMULTANEOUS_IMAGE_VIDEO_UNSUPPORTED_TAG, R.string.toast_hdr_simultaneous_image_video_unsupported + ), + FLASH_UNSUPPORTED_ON_LENS( + FLASH_UNSUPPORTED_ON_LENS_TAG, + R.string.toast_flash_unsupported_on_lens ) } diff --git a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/TestTags.kt b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/TestTags.kt index 8f6d5046e..315efa7e6 100644 --- a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/TestTags.kt +++ b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/TestTags.kt @@ -44,6 +44,7 @@ const val HDR_IMAGE_UNSUPPORTED_ON_MULTI_STREAM_TAG = "HdrImageUnsupportedOnMult const val HDR_VIDEO_UNSUPPORTED_ON_DEVICE_TAG = "HdrVideoUnsupportedOnDeviceTag" const val HDR_VIDEO_UNSUPPORTED_ON_LENS_TAG = "HdrVideoUnsupportedOnDeviceTag" const val HDR_SIMULTANEOUS_IMAGE_VIDEO_UNSUPPORTED_TAG = "HdrSimultaneousImageVideoUnsupportedTag" +const val FLASH_UNSUPPORTED_ON_LENS_TAG = "FlashUnsupportedOnLensTag" const val ZOOM_BUTTON_ROW_TAG = "ZoomButtonRowTag" const val ZOOM_BUTTON_MIN_TAG = "ZoomButtonMinTag" const val ZOOM_BUTTON_1_TAG = "ZoomButton1Tag" diff --git a/ui/components/capture/src/main/res/values/strings.xml b/ui/components/capture/src/main/res/values/strings.xml index f5f988328..da58afeec 100644 --- a/ui/components/capture/src/main/res/values/strings.xml +++ b/ui/components/capture/src/main/res/values/strings.xml @@ -64,6 +64,7 @@ HDR video not supported on this device HDR video not supported by current lens HDR video and image capture cannot be bound simultaneously + Flash not supported by current lens Quick settings open diff --git a/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/FlashModeUiStateAdapter.kt b/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/FlashModeUiStateAdapter.kt index 790133398..6b2a1e6f0 100644 --- a/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/FlashModeUiStateAdapter.kt +++ b/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/FlashModeUiStateAdapter.kt @@ -104,11 +104,10 @@ fun FlashModeUiState.Companion.from( if (currentLensSupportedFlashModes.contains(mode)) { displayableModes.add(SingleSelectableUiState.SelectableUi(mode)) // Enabled } else { - // todo(kc): add actual disabledreason for flash mode displayableModes.add( SingleSelectableUiState.Disabled( value = mode, - disabledReason = DisabledReason.HDR_IMAGE_UNSUPPORTED_ON_LENS + disabledReason = DisabledReason.FLASH_UNSUPPORTED_ON_LENS ) ) // Disabled } From f3e4396c1b95c0bb3a374d5065d5091772fc7492 Mon Sep 17 00:00:00 2001 From: Kimberly Crevecoeur Date: Tue, 26 May 2026 10:53:35 -0700 Subject: [PATCH 15/51] update more settings navigation button --- .../ui/QuickSettingsComponents.kt | 19 +++++++++++++------ .../capture/src/main/res/values/strings.xml | 2 +- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/ui/QuickSettingsComponents.kt b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/ui/QuickSettingsComponents.kt index 1d5112ad7..6e2677ff7 100644 --- a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/ui/QuickSettingsComponents.kt +++ b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/ui/QuickSettingsComponents.kt @@ -32,6 +32,7 @@ import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi +import androidx.compose.material3.Button import androidx.compose.material3.FilledIconToggleButton import androidx.compose.material3.Icon import androidx.compose.material3.IconButton @@ -153,14 +154,20 @@ fun ToggleQuickSettingsButton( /** * A button within the quick settings menu that will navigate to the default settings screen */ -@OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable fun QuickNavSettings(onNavigateToSettings: () -> Unit, modifier: Modifier = Modifier) { - TextButton( - modifier = modifier.testTag(SETTINGS_BUTTON), - onClick = onNavigateToSettings, - content = { Text(text = stringResource(R.string.quick_settings_more_text)) } - ) + Row( + modifier = modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 8.dp), + horizontalArrangement = Arrangement.End + ) { + Button( + modifier = Modifier.testTag(SETTINGS_BUTTON), + onClick = onNavigateToSettings, + content = { Text(text = stringResource(R.string.quick_settings_more_text)) } + ) + } } @Composable diff --git a/ui/components/capture/src/main/res/values/strings.xml b/ui/components/capture/src/main/res/values/strings.xml index da58afeec..7e84786d9 100644 --- a/ui/components/capture/src/main/res/values/strings.xml +++ b/ui/components/capture/src/main/res/values/strings.xml @@ -137,7 +137,7 @@ Image only capture - More + More Settings Settings icon From 28cff6c066a758e8548f57eb3cf86466e97979bd Mon Sep 17 00:00:00 2001 From: Kimberly Crevecoeur Date: Wed, 27 May 2026 10:09:38 -0700 Subject: [PATCH 16/51] delete unused resources and add statement to styleguide to check for such --- .gemini/styleguide.md | 1 + .../capture/src/main/res/values/strings.xml | 20 ------------------- 2 files changed, 1 insertion(+), 20 deletions(-) diff --git a/.gemini/styleguide.md b/.gemini/styleguide.md index dd76e4e19..e4eaad4cc 100644 --- a/.gemini/styleguide.md +++ b/.gemini/styleguide.md @@ -62,6 +62,7 @@ When reviewing a pull request, focus on the following key areas: 8. **Resource Management** * **No Hardcoded Strings:** Forbid hardcoded user-facing strings in composables. All text should be extracted into `strings.xml` to support localization and make updates easier. * **Prefer Vector Drawables:** For icons and simple graphics, vector drawables (SVGs) should be preferred over raster images (PNGs) to reduce APK size and ensure sharp rendering on all screen densities. + * **Delete Unused Resources:** When refactoring or removing components, ensure that any associated resources (such as strings in `strings.xml` or icons/drawables in the `res/drawable` directory) that are no longer used anywhere in the project are deleted to reduce the final file size and maintain codebase cleanliness. 9. **Readability, Logging, and Documentation** * **Code Clarity:** Is the code clear, concise, and easy to understand? Are function and variable names descriptive? diff --git a/ui/components/capture/src/main/res/values/strings.xml b/ui/components/capture/src/main/res/values/strings.xml index 7e84786d9..f9561d379 100644 --- a/ui/components/capture/src/main/res/values/strings.xml +++ b/ui/components/capture/src/main/res/values/strings.xml @@ -106,28 +106,8 @@ Flash on Low Light Boost on - - Single Stream - Multi Stream - Single-stream capture mode on - Multi-stream capture mode on - - - - Low light boost on - Low light boost off - Low light boost on - Low light boost off An error occurred when running low light boost. - - - - Single - Dual - Single camera mode - Concurrent dual camera mode - Hybrid Capture Video Only From 8453206dfdf26370d1029511bcfecc8b03f3f833 Mon Sep 17 00:00:00 2001 From: Kimberly Crevecoeur Date: Wed, 27 May 2026 10:12:01 -0700 Subject: [PATCH 17/51] restrict visibility modifiers --- .../quicksettings/QuickSettingsScreen.kt | 6 +++--- .../quicksettings/ui/QuickSettingsComponents.kt | 17 ++++++++--------- 2 files changed, 11 insertions(+), 12 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 3356e8d09..a5c339a55 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 @@ -104,7 +104,7 @@ fun QuickSettingsBottomSheet( } @Composable -fun HybridQuickSettings( +private fun HybridQuickSettings( quickSettingsUiState: QuickSettingsUiState.Available, quickSettingsController: QuickSettingsController, onNavigateToSettings: () -> Unit, @@ -158,7 +158,7 @@ fun HybridQuickSettings( } @Composable -fun VideoQuickSettings( +private fun VideoQuickSettings( quickSettingsUiState: QuickSettingsUiState.Available, quickSettingsController: QuickSettingsController, onNavigateToSettings: () -> Unit, @@ -199,7 +199,7 @@ fun VideoQuickSettings( } @Composable -fun ImageQuickSettings( +private fun ImageQuickSettings( quickSettingsUiState: QuickSettingsUiState.Available, quickSettingsController: QuickSettingsController, onNavigateToSettings: () -> Unit, diff --git a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/ui/QuickSettingsComponents.kt b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/ui/QuickSettingsComponents.kt index 6e2677ff7..398ac92a7 100644 --- a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/ui/QuickSettingsComponents.kt +++ b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/ui/QuickSettingsComponents.kt @@ -30,9 +30,9 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.wrapContentWidth +import androidx.compose.material3.Button import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi -import androidx.compose.material3.Button import androidx.compose.material3.FilledIconToggleButton import androidx.compose.material3.Icon import androidx.compose.material3.IconButton @@ -43,7 +43,6 @@ import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.SheetState import androidx.compose.material3.Surface import androidx.compose.material3.Text -import androidx.compose.material3.TextButton import androidx.compose.material3.darkColorScheme import androidx.compose.material3.minimumInteractiveComponentSize import androidx.compose.runtime.Composable @@ -155,7 +154,7 @@ fun ToggleQuickSettingsButton( * A button within the quick settings menu that will navigate to the default settings screen */ @Composable -fun QuickNavSettings(onNavigateToSettings: () -> Unit, modifier: Modifier = Modifier) { +internal fun QuickNavSettings(onNavigateToSettings: () -> Unit, modifier: Modifier = Modifier) { Row( modifier = modifier .fillMaxWidth() @@ -171,7 +170,7 @@ fun QuickNavSettings(onNavigateToSettings: () -> Unit, modifier: Modifier = Modi } @Composable -fun CaptureModeToggleButton( +private fun CaptureModeToggleButton( modifier: Modifier = Modifier, onClick: () -> Unit, captureModeUiState: CaptureModeUiState.Available, @@ -244,7 +243,7 @@ fun QuickSettingsBottomSheet( } @Composable -fun CaptureModeRow( +internal fun CaptureModeRow( modifier: Modifier = Modifier, onSetCaptureMode: (CaptureMode) -> Unit, captureModeUiState: CaptureModeUiState @@ -285,7 +284,7 @@ fun CaptureModeRow( } @Composable -fun HdrRow( +internal fun HdrRow( modifier: Modifier = Modifier, onClick: (DynamicRange, ImageOutputFormat) -> Unit, hdrUiState: HdrUiState @@ -328,7 +327,7 @@ fun HdrRow( } @Composable -fun AspectRatioRow( +internal fun AspectRatioRow( modifier: Modifier = Modifier, onSetAspectRatio: (AspectRatio) -> Unit, aspectRatioUiState: AspectRatioUiState @@ -368,7 +367,7 @@ fun AspectRatioRow( } @Composable -fun FlashRow( +internal fun FlashRow( modifier: Modifier = Modifier, onSetFlashMode: (FlashMode) -> Unit, flashModeUiState: FlashModeUiState @@ -423,7 +422,7 @@ fun FlashRow( // //////////////////////////////////////////////////// @Composable -fun SettingRow( +private fun SettingRow( title: String, stateSubtitle: String, modifier: Modifier = Modifier, From b222813c5830511af0403655db4f558e4764e583 Mon Sep 17 00:00:00 2001 From: Kimberly Crevecoeur Date: Mon, 1 Jun 2026 18:22:48 -0700 Subject: [PATCH 18/51] 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 19/51] 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 20/51] 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 21/51] 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 22/51] 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 23/51] 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 24/51] 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 && From 28bbbac187cc3c3c465f1df442fb8d2b4bfa19d7 Mon Sep 17 00:00:00 2001 From: Kimberly Crevecoeur Date: Thu, 11 Jun 2026 13:22:05 -0700 Subject: [PATCH 25/51] Support disabled HDR state when current lens doesn't support it but device does --- .../ui/QuickSettingsComponents.kt | 8 ++- .../ui/uistate/capture/HdrUiState.kt | 3 +- .../capture/HdrUiStateAdapter.kt | 57 ++++++++++----- .../capture/HdrUiStateAdapterTest.kt | 70 +++++++++++++++++++ 4 files changed, 117 insertions(+), 21 deletions(-) diff --git a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/ui/QuickSettingsComponents.kt b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/ui/QuickSettingsComponents.kt index 0e2b2857b..0d60cffd8 100644 --- a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/ui/QuickSettingsComponents.kt +++ b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/ui/QuickSettingsComponents.kt @@ -291,7 +291,8 @@ internal fun HdrRow( onClick: (DynamicRange, ImageOutputFormat) -> Unit, hdrUiState: HdrUiState ) { - val isHdrOn = hdrUiState is HdrUiState.Available && + val isSupported = hdrUiState is HdrUiState.Available && hdrUiState.isSupported + val isHdrOn = isSupported && ( hdrUiState.selectedDynamicRange == DEFAULT_HDR_DYNAMIC_RANGE || hdrUiState.selectedImageFormat == DEFAULT_HDR_IMAGE_OUTPUT @@ -312,7 +313,7 @@ internal fun HdrRow( enum = CameraDynamicRange.HDR, onClick = { onClick(DEFAULT_HDR_DYNAMIC_RANGE, DEFAULT_HDR_IMAGE_OUTPUT) }, isSelected = isHdrOn, - enabled = hdrUiState is HdrUiState.Available + enabled = isSupported ) }, { @@ -321,7 +322,7 @@ internal fun HdrRow( enum = CameraDynamicRange.SDR, onClick = { onClick(DynamicRange.SDR, ImageOutputFormat.JPEG) }, isSelected = !isHdrOn, - enabled = hdrUiState is HdrUiState.Available + enabled = isSupported ) } ) @@ -557,6 +558,7 @@ private fun QuickSettingToggleSelectorButton( fun HdrIndicator(hdrUiState: HdrUiState, modifier: Modifier = Modifier) { val enum = if (hdrUiState is HdrUiState.Available && + hdrUiState.isSupported && ( hdrUiState.selectedDynamicRange == DEFAULT_HDR_DYNAMIC_RANGE || hdrUiState.selectedImageFormat == DEFAULT_HDR_IMAGE_OUTPUT diff --git a/ui/uistate/capture/src/main/java/com/google/jetpackcamera/ui/uistate/capture/HdrUiState.kt b/ui/uistate/capture/src/main/java/com/google/jetpackcamera/ui/uistate/capture/HdrUiState.kt index da15b57ac..ff1875660 100644 --- a/ui/uistate/capture/src/main/java/com/google/jetpackcamera/ui/uistate/capture/HdrUiState.kt +++ b/ui/uistate/capture/src/main/java/com/google/jetpackcamera/ui/uistate/capture/HdrUiState.kt @@ -39,7 +39,8 @@ sealed interface HdrUiState { */ data class Available( val selectedImageFormat: ImageOutputFormat, - val selectedDynamicRange: DynamicRange + val selectedDynamicRange: DynamicRange, + val isSupported: Boolean = true ) : HdrUiState companion object 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 aff4aa3a3..ecad0e73e 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 @@ -67,36 +67,59 @@ fun HdrUiState.Companion.from( return when (activeCaptureMode) { CaptureMode.IMAGE_ONLY -> { - val supportsHdrImage = cameraConstraints - ?.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, - selectedDynamicRange = DynamicRange.SDR // Force SDR in UI state for video - ) - } else { + if (isFlashHdrConflict) { HdrUiState.Unavailable + } else { + val deviceSupportsHdrImage = systemConstraints.perLensConstraints.values.any { constraints -> + constraints.supportedImageFormatsMap[cameraAppSettings.streamConfig] + ?.contains(ImageOutputFormat.JPEG_ULTRA_HDR) ?: false + } + + if (deviceSupportsHdrImage) { + val supportsHdrImage = cameraConstraints + ?.supportedImageFormatsMap?.get(cameraAppSettings.streamConfig) + ?.contains(ImageOutputFormat.JPEG_ULTRA_HDR) ?: false + + HdrUiState.Available( + selectedImageFormat = cameraAppSettings.imageFormat, + selectedDynamicRange = DynamicRange.SDR, // Force SDR in UI state for video + isSupported = supportsHdrImage + ) + } 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 { + if (isFlashHdrConflict || isConcurrentConflict) { HdrUiState.Unavailable + } else { + val deviceSupportsHdrVideo = systemConstraints.perLensConstraints.values.any { constraints -> + constraints.supportedDynamicRanges.contains(DynamicRange.HLG10) + } + + if (deviceSupportsHdrVideo) { + val supportsHdrVideo = + cameraConstraints?.supportedDynamicRanges?.contains(DynamicRange.HLG10) == true + + HdrUiState.Available( + selectedImageFormat = ImageOutputFormat.JPEG, // Force SDR in UI state for image + selectedDynamicRange = cameraAppSettings.dynamicRange, + isSupported = supportsHdrVideo + ) + } else { + HdrUiState.Unavailable + } } } + 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 index 4f172572a..551dff836 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 @@ -21,6 +21,7 @@ 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.model.LensFacing import com.google.jetpackcamera.settings.model.CameraAppSettings import com.google.jetpackcamera.settings.model.CameraConstraints import com.google.jetpackcamera.settings.model.CameraSystemConstraints @@ -105,6 +106,7 @@ internal class HdrUiStateAdapterTest { 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) + assertThat(availableState.isSupported).isTrue() } @Test @@ -184,6 +186,7 @@ internal class HdrUiStateAdapterTest { 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) + assertThat(availableState.isSupported).isTrue() } @Test @@ -228,4 +231,71 @@ internal class HdrUiStateAdapterTest { // Then HDR is unavailable because of the flash mode conflict assertThat(hdrUiState).isInstanceOf(HdrUiState.Unavailable::class.java) } + + @Test + fun from_imageOnlyMode_hdrNotSupportedOnCurrentLens_butSupportedOnOtherLens_returnsAvailableWithIsSupportedFalse() { + // Given in IMAGE_ONLY capture mode + // Current lens (BACK) does NOT support HDR, but FRONT lens DOES support HDR + val appSettings = defaultCameraAppSettings.copy( + captureMode = CaptureMode.IMAGE_ONLY, + cameraLensFacing = LensFacing.BACK + ) + val systemConstraints = CameraSystemConstraints( + availableLenses = listOf(LensFacing.BACK, LensFacing.FRONT), + perLensConstraints = mapOf( + LensFacing.BACK to emptyCameraConstraints.copy( + supportedImageFormatsMap = mapOf( + appSettings.streamConfig to setOf(ImageOutputFormat.JPEG) + ) + ), + LensFacing.FRONT 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 but not supported (disabled) + assertThat(hdrUiState).isInstanceOf(HdrUiState.Available::class.java) + val availableState = hdrUiState as HdrUiState.Available + assertThat(availableState.isSupported).isFalse() + } + + @Test + fun from_videoOnlyMode_hdrNotSupportedOnCurrentLens_butSupportedOnOtherLens_returnsAvailableWithIsSupportedFalse() { + // Given in VIDEO_ONLY capture mode + // Current lens (BACK) does NOT support HDR, but FRONT lens DOES support HDR + val appSettings = defaultCameraAppSettings.copy( + captureMode = CaptureMode.VIDEO_ONLY, + cameraLensFacing = LensFacing.BACK + ) + val systemConstraints = CameraSystemConstraints( + availableLenses = listOf(LensFacing.BACK, LensFacing.FRONT), + perLensConstraints = mapOf( + LensFacing.BACK to emptyCameraConstraints.copy( + supportedDynamicRanges = setOf(DynamicRange.SDR) + ), + LensFacing.FRONT to emptyCameraConstraints.copy( + supportedDynamicRanges = setOf(DynamicRange.SDR, DynamicRange.HLG10) + ) + ) + ) + + // When + val hdrUiState = + HdrUiState.from(appSettings, systemConstraints, ExternalCaptureMode.Standard) + + // Then HDR is available but not supported (disabled) + assertThat(hdrUiState).isInstanceOf(HdrUiState.Available::class.java) + val availableState = hdrUiState as HdrUiState.Available + assertThat(availableState.isSupported).isFalse() + } } From 277d94e082fbe7012c2a26b420dabca632345071 Mon Sep 17 00:00:00 2001 From: Kimberly Crevecoeur Date: Thu, 11 Jun 2026 13:58:14 -0700 Subject: [PATCH 26/51] Disable LLB on HDR conflict and vice versa instead of hiding --- .../ui/components/capture/DisabledReason.kt | 4 + .../ui/components/capture/TestTags.kt | 1 + .../capture/src/main/res/values/strings.xml | 1 + .../quicksettings/QuickSettingsController.kt | 1 - .../capture/FlashModeUiStateAdapter.kt | 37 +++++++--- .../capture/HdrUiStateAdapter.kt | 74 ++++++++----------- .../capture/compound/CaptureUiStateAdapter.kt | 5 +- .../capture/FlashModeUiStateAdapterTest.kt | 61 +++++++++++++++ .../capture/HdrUiStateAdapterTest.kt | 42 +++++++++-- 9 files changed, 164 insertions(+), 62 deletions(-) diff --git a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/DisabledReason.kt b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/DisabledReason.kt index ccc33140b..99aa9ab62 100644 --- a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/DisabledReason.kt +++ b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/DisabledReason.kt @@ -70,5 +70,9 @@ enum class DisabledReason( FLASH_UNSUPPORTED_ON_LENS( FLASH_UNSUPPORTED_ON_LENS_TAG, R.string.toast_flash_unsupported_on_lens + ), + LLB_DISABLED_BY_HDR( + LLB_DISABLED_BY_HDR_TAG, + R.string.toast_llb_disabled_by_hdr ) } diff --git a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/TestTags.kt b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/TestTags.kt index 6bb2139fe..8dd862207 100644 --- a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/TestTags.kt +++ b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/TestTags.kt @@ -45,6 +45,7 @@ const val HDR_VIDEO_UNSUPPORTED_ON_DEVICE_TAG = "HdrVideoUnsupportedOnDeviceTag" const val HDR_VIDEO_UNSUPPORTED_ON_LENS_TAG = "HdrVideoUnsupportedOnDeviceTag" const val HDR_SIMULTANEOUS_IMAGE_VIDEO_UNSUPPORTED_TAG = "HdrSimultaneousImageVideoUnsupportedTag" const val FLASH_UNSUPPORTED_ON_LENS_TAG = "FlashUnsupportedOnLensTag" +const val LLB_DISABLED_BY_HDR_TAG = "LlbDisabledByHdrTag" const val ZOOM_BUTTON_ROW_TAG = "ZoomButtonRowTag" const val ZOOM_BUTTON_MIN_TAG = "ZoomButtonMinTag" const val ZOOM_BUTTON_1_TAG = "ZoomButton1Tag" diff --git a/ui/components/capture/src/main/res/values/strings.xml b/ui/components/capture/src/main/res/values/strings.xml index 4367e5c71..f9665aff7 100644 --- a/ui/components/capture/src/main/res/values/strings.xml +++ b/ui/components/capture/src/main/res/values/strings.xml @@ -59,6 +59,7 @@ HDR video not supported by current lens HDR video and image capture cannot be bound simultaneously Flash not supported by current lens + Low Light Boost is disabled when HDR is on Quick settings open diff --git a/ui/controller/src/main/java/com/google/jetpackcamera/ui/controller/quicksettings/QuickSettingsController.kt b/ui/controller/src/main/java/com/google/jetpackcamera/ui/controller/quicksettings/QuickSettingsController.kt index 405915947..49e97a2bd 100644 --- a/ui/controller/src/main/java/com/google/jetpackcamera/ui/controller/quicksettings/QuickSettingsController.kt +++ b/ui/controller/src/main/java/com/google/jetpackcamera/ui/controller/quicksettings/QuickSettingsController.kt @@ -22,7 +22,6 @@ import com.google.jetpackcamera.model.FlashMode import com.google.jetpackcamera.model.ImageOutputFormat import com.google.jetpackcamera.model.LensFacing - /** * Interface for controlling quick settings. */ diff --git a/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/FlashModeUiStateAdapter.kt b/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/FlashModeUiStateAdapter.kt index 6b2a1e6f0..77485afac 100644 --- a/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/FlashModeUiStateAdapter.kt +++ b/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/FlashModeUiStateAdapter.kt @@ -19,6 +19,8 @@ import com.google.jetpackcamera.core.camera.CameraState import com.google.jetpackcamera.model.ConcurrentCameraMode import com.google.jetpackcamera.model.DynamicRange import com.google.jetpackcamera.model.FlashMode +import com.google.jetpackcamera.model.ImageOutputFormat +import com.google.jetpackcamera.ui.uistate.capture.HdrUiState import com.google.jetpackcamera.model.LowLightBoostState import com.google.jetpackcamera.settings.model.CameraAppSettings import com.google.jetpackcamera.settings.model.CameraSystemConstraints @@ -58,7 +60,8 @@ private val ORDERED_UI_SUPPORTED_FLASH_MODES = listOf( */ fun FlashModeUiState.Companion.from( cameraAppSettings: CameraAppSettings, - systemConstraints: CameraSystemConstraints + systemConstraints: CameraSystemConstraints, + hdrUiState: HdrUiState = HdrUiState.Unavailable // todo(kc): supply visible flash modes from developer options // visibleFlashModes: Set = ORDERED_UI_SUPPORTED_FLASH_MODES.toSet() ): FlashModeUiState { @@ -77,6 +80,12 @@ fun FlashModeUiState.Companion.from( val currentLensSupportedFlashModes = systemConstraints.forCurrentLens(cameraAppSettings) ?.supportedFlashModes ?: setOf(FlashMode.OFF) + val isHdrOn = hdrUiState is HdrUiState.Available && + ( + hdrUiState.selectedDynamicRange == DynamicRange.HLG10 || + hdrUiState.selectedImageFormat == ImageOutputFormat.JPEG_ULTRA_HDR + ) + val displayableModes = mutableListOf>() for (mode in ORDERED_UI_SUPPORTED_FLASH_MODES) { @@ -93,21 +102,26 @@ fun FlashModeUiState.Companion.from( // 3. Special handling for LOW_LIGHT_BOOST based on other settings. if (mode == FlashMode.LOW_LIGHT_BOOST) { - if (cameraAppSettings.dynamicRange != DynamicRange.SDR || - cameraAppSettings.concurrentCameraMode == ConcurrentCameraMode.DUAL - ) { - continue // Hide LLB if HDR or Dual Camera is active + if (cameraAppSettings.concurrentCameraMode == ConcurrentCameraMode.DUAL) { + continue // Hide LLB if Dual Camera is active } } - // 4. Determine if Enabled or Disabled based on current lens support. - if (currentLensSupportedFlashModes.contains(mode)) { + // 4. Determine if Enabled or Disabled + val isLlbHdrConflict = mode == FlashMode.LOW_LIGHT_BOOST && isHdrOn + + if (currentLensSupportedFlashModes.contains(mode) && !isLlbHdrConflict) { displayableModes.add(SingleSelectableUiState.SelectableUi(mode)) // Enabled } else { + val reason = if (!currentLensSupportedFlashModes.contains(mode)) { + DisabledReason.FLASH_UNSUPPORTED_ON_LENS + } else { + DisabledReason.LLB_DISABLED_BY_HDR + } displayableModes.add( SingleSelectableUiState.Disabled( value = mode, - disabledReason = DisabledReason.FLASH_UNSUPPORTED_ON_LENS + disabledReason = reason ) ) // Disabled } @@ -141,17 +155,18 @@ fun FlashModeUiState.Companion.from( fun FlashModeUiState.updateFrom( cameraAppSettings: CameraAppSettings, systemConstraints: CameraSystemConstraints, - cameraState: CameraState + cameraState: CameraState, + hdrUiState: HdrUiState = HdrUiState.Unavailable ): FlashModeUiState { return when (this) { is Unavailable -> { // When previous state was "Unavailable", we'll try to create a new FlashModeUiState - FlashModeUiState.from(cameraAppSettings, systemConstraints) + FlashModeUiState.from(cameraAppSettings, systemConstraints, hdrUiState) } is Available -> { // Regenerate the potential new state based on the latest settings - val newUiState = FlashModeUiState.from(cameraAppSettings, systemConstraints) + val newUiState = FlashModeUiState.from(cameraAppSettings, systemConstraints, hdrUiState) if (newUiState is Unavailable) { newUiState // Switch to Unavailable 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 ecad0e73e..a5d47c5d3 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 @@ -67,56 +67,46 @@ fun HdrUiState.Companion.from( return when (activeCaptureMode) { CaptureMode.IMAGE_ONLY -> { - val isFlashHdrConflict = cameraAppSettings.flashMode == FlashMode.LOW_LIGHT_BOOST - - if (isFlashHdrConflict) { - HdrUiState.Unavailable - } else { - val deviceSupportsHdrImage = systemConstraints.perLensConstraints.values.any { constraints -> - constraints.supportedImageFormatsMap[cameraAppSettings.streamConfig] - ?.contains(ImageOutputFormat.JPEG_ULTRA_HDR) ?: false - } + val deviceSupportsHdrImage = systemConstraints.perLensConstraints.values.any { constraints -> + constraints.supportedImageFormatsMap[cameraAppSettings.streamConfig] + ?.contains(ImageOutputFormat.JPEG_ULTRA_HDR) ?: false + } - if (deviceSupportsHdrImage) { - val supportsHdrImage = cameraConstraints - ?.supportedImageFormatsMap?.get(cameraAppSettings.streamConfig) - ?.contains(ImageOutputFormat.JPEG_ULTRA_HDR) ?: false + if (deviceSupportsHdrImage) { + val supportsHdrImage = cameraConstraints + ?.supportedImageFormatsMap?.get(cameraAppSettings.streamConfig) + ?.contains(ImageOutputFormat.JPEG_ULTRA_HDR) ?: false + val isFlashHdrConflict = cameraAppSettings.flashMode == FlashMode.LOW_LIGHT_BOOST - HdrUiState.Available( - selectedImageFormat = cameraAppSettings.imageFormat, - selectedDynamicRange = DynamicRange.SDR, // Force SDR in UI state for video - isSupported = supportsHdrImage - ) - } else { - HdrUiState.Unavailable - } + HdrUiState.Available( + selectedImageFormat = cameraAppSettings.imageFormat, + selectedDynamicRange = DynamicRange.SDR, // Force SDR in UI state for video + isSupported = supportsHdrImage && !isFlashHdrConflict + ) + } else { + HdrUiState.Unavailable } } CaptureMode.VIDEO_ONLY -> { - val isFlashHdrConflict = cameraAppSettings.flashMode == FlashMode.LOW_LIGHT_BOOST - val isConcurrentConflict = - cameraAppSettings.concurrentCameraMode == ConcurrentCameraMode.DUAL - - if (isFlashHdrConflict || isConcurrentConflict) { - HdrUiState.Unavailable - } else { - val deviceSupportsHdrVideo = systemConstraints.perLensConstraints.values.any { constraints -> - constraints.supportedDynamicRanges.contains(DynamicRange.HLG10) - } + val deviceSupportsHdrVideo = systemConstraints.perLensConstraints.values.any { constraints -> + constraints.supportedDynamicRanges.contains(DynamicRange.HLG10) + } - if (deviceSupportsHdrVideo) { - val supportsHdrVideo = - cameraConstraints?.supportedDynamicRanges?.contains(DynamicRange.HLG10) == true + if (deviceSupportsHdrVideo) { + val supportsHdrVideo = + cameraConstraints?.supportedDynamicRanges?.contains(DynamicRange.HLG10) == true + val isFlashHdrConflict = cameraAppSettings.flashMode == FlashMode.LOW_LIGHT_BOOST + val isConcurrentConflict = + cameraAppSettings.concurrentCameraMode == ConcurrentCameraMode.DUAL - HdrUiState.Available( - selectedImageFormat = ImageOutputFormat.JPEG, // Force SDR in UI state for image - selectedDynamicRange = cameraAppSettings.dynamicRange, - isSupported = supportsHdrVideo - ) - } else { - HdrUiState.Unavailable - } + HdrUiState.Available( + selectedImageFormat = ImageOutputFormat.JPEG, // Force SDR in UI state for image + selectedDynamicRange = cameraAppSettings.dynamicRange, + isSupported = supportsHdrVideo && !isFlashHdrConflict && !isConcurrentConflict + ) + } else { + HdrUiState.Unavailable } } diff --git a/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/compound/CaptureUiStateAdapter.kt b/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/compound/CaptureUiStateAdapter.kt index adb1e34e0..d52b855c6 100644 --- a/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/compound/CaptureUiStateAdapter.kt +++ b/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/compound/CaptureUiStateAdapter.kt @@ -104,9 +104,10 @@ fun captureUiState( it?.updateFrom( cameraAppSettings = cameraAppSettings, systemConstraints = systemConstraints, - cameraState = roundedCameraState + cameraState = roundedCameraState, + hdrUiState = hdrUiState ) - ?: FlashModeUiState.from(cameraAppSettings, systemConstraints) + ?: FlashModeUiState.from(cameraAppSettings, systemConstraints, hdrUiState) } focusMeteringUiState = focusMeteringUiState.let { it?.updateFrom( diff --git a/ui/uistateadapter/capture/src/test/java/com/google/jetpackcamera/ui/uistateadapter/capture/FlashModeUiStateAdapterTest.kt b/ui/uistateadapter/capture/src/test/java/com/google/jetpackcamera/ui/uistateadapter/capture/FlashModeUiStateAdapterTest.kt index acc70425f..19b2be216 100644 --- a/ui/uistateadapter/capture/src/test/java/com/google/jetpackcamera/ui/uistateadapter/capture/FlashModeUiStateAdapterTest.kt +++ b/ui/uistateadapter/capture/src/test/java/com/google/jetpackcamera/ui/uistateadapter/capture/FlashModeUiStateAdapterTest.kt @@ -22,7 +22,12 @@ import com.google.jetpackcamera.model.LowLightBoostState 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.model.DynamicRange +import com.google.jetpackcamera.model.ImageOutputFormat +import com.google.jetpackcamera.ui.components.capture.DisabledReason +import com.google.jetpackcamera.ui.uistate.SingleSelectableUiState import com.google.jetpackcamera.ui.uistate.capture.FlashModeUiState +import com.google.jetpackcamera.ui.uistate.capture.HdrUiState import org.junit.Assert.assertThrows import org.junit.Test import org.junit.runner.RunWith @@ -259,4 +264,60 @@ class FlashModeUiStateAdapterTest { val availableUiState = updatedUiState as FlashModeUiState.Available assertThat(availableUiState.isLowLightBoostActive).isEqualTo(false) } + + @Test + fun from_hdrImageOn_lowLightBoostDisabled() { + // Given a system supporting LLB, but HDR image is ON + val appSettings = defaultCameraAppSettings.copy(flashMode = FlashMode.OFF) + val systemConstraints = CameraSystemConstraints( + perLensConstraints = mapOf( + appSettings.cameraLensFacing to emptyCameraConstraints.copy( + supportedFlashModes = setOf(FlashMode.OFF, FlashMode.LOW_LIGHT_BOOST) + ) + ) + ) + val hdrUiState = HdrUiState.Available( + selectedImageFormat = ImageOutputFormat.JPEG_ULTRA_HDR, + selectedDynamicRange = DynamicRange.SDR + ) + + // When + val flashModeUiState = FlashModeUiState.from(appSettings, systemConstraints, hdrUiState) + + // Then LLB is disabled because of HDR + assertThat(flashModeUiState).isInstanceOf(FlashModeUiState.Available::class.java) + val availableUiState = flashModeUiState as FlashModeUiState.Available + val llbState = availableUiState.availableFlashModes.find { it.value == FlashMode.LOW_LIGHT_BOOST } + assertThat(llbState).isInstanceOf(SingleSelectableUiState.Disabled::class.java) + val disabledLlb = llbState as SingleSelectableUiState.Disabled + assertThat(disabledLlb.disabledReason).isEqualTo(DisabledReason.LLB_DISABLED_BY_HDR) + } + + @Test + fun from_hdrVideoOn_lowLightBoostDisabled() { + // Given a system supporting LLB, but HDR video is ON + val appSettings = defaultCameraAppSettings.copy(flashMode = FlashMode.OFF) + val systemConstraints = CameraSystemConstraints( + perLensConstraints = mapOf( + appSettings.cameraLensFacing to emptyCameraConstraints.copy( + supportedFlashModes = setOf(FlashMode.OFF, FlashMode.LOW_LIGHT_BOOST) + ) + ) + ) + val hdrUiState = HdrUiState.Available( + selectedImageFormat = ImageOutputFormat.JPEG, + selectedDynamicRange = DynamicRange.HLG10 + ) + + // When + val flashModeUiState = FlashModeUiState.from(appSettings, systemConstraints, hdrUiState) + + // Then LLB is disabled because of HDR + assertThat(flashModeUiState).isInstanceOf(FlashModeUiState.Available::class.java) + val availableUiState = flashModeUiState as FlashModeUiState.Available + val llbState = availableUiState.availableFlashModes.find { it.value == FlashMode.LOW_LIGHT_BOOST } + assertThat(llbState).isInstanceOf(SingleSelectableUiState.Disabled::class.java) + val disabledLlb = llbState as SingleSelectableUiState.Disabled + assertThat(disabledLlb.disabledReason).isEqualTo(DisabledReason.LLB_DISABLED_BY_HDR) + } } 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 551dff836..6ffe8591b 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 @@ -17,6 +17,7 @@ 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.ConcurrentCameraMode import com.google.jetpackcamera.model.DynamicRange import com.google.jetpackcamera.model.ExternalCaptureMode import com.google.jetpackcamera.model.FlashMode @@ -132,7 +133,7 @@ internal class HdrUiStateAdapterTest { } @Test - fun from_imageOnlyMode_lowLightBoostOn_returnsUnavailable() { + fun from_imageOnlyMode_lowLightBoostOn_returnsAvailableWithIsSupportedFalse() { // 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, @@ -155,8 +156,10 @@ internal class HdrUiStateAdapterTest { 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) + // Then HDR is available but not supported because of the flash mode conflict + assertThat(hdrUiState).isInstanceOf(HdrUiState.Available::class.java) + val availableState = hdrUiState as HdrUiState.Available + assertThat(availableState.isSupported).isFalse() } @Test @@ -210,7 +213,7 @@ internal class HdrUiStateAdapterTest { } @Test - fun from_videoOnlyMode_lowLightBoostOn_returnsUnavailable() { + fun from_videoOnlyMode_lowLightBoostOn_returnsAvailableWithIsSupportedFalse() { // 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, @@ -228,8 +231,35 @@ internal class HdrUiStateAdapterTest { 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) + // Then HDR is available but not supported because of the flash mode conflict + assertThat(hdrUiState).isInstanceOf(HdrUiState.Available::class.java) + val availableState = hdrUiState as HdrUiState.Available + assertThat(availableState.isSupported).isFalse() + } + + @Test + fun from_videoOnlyMode_concurrentCameraOn_returnsAvailableWithIsSupportedFalse() { + // Given in VIDEO_ONLY capture mode with Concurrent Camera ON, even though HDR video is supported + val appSettings = defaultCameraAppSettings.copy( + captureMode = CaptureMode.VIDEO_ONLY, + concurrentCameraMode = ConcurrentCameraMode.DUAL + ) + 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 but not supported because of the concurrent camera conflict + assertThat(hdrUiState).isInstanceOf(HdrUiState.Available::class.java) + val availableState = hdrUiState as HdrUiState.Available + assertThat(availableState.isSupported).isFalse() } @Test From 35666efcc935fe21c376579c35db419a56a9706a Mon Sep 17 00:00:00 2001 From: Kimberly Crevecoeur Date: Fri, 12 Jun 2026 10:53:04 -0700 Subject: [PATCH 27/51] spotless --- .../capture/quicksettings/QuickSettingsScreen.kt | 3 --- .../quicksettings/ui/QuickSettingsComponents.kt | 2 -- .../capture/FlashModeUiStateAdapter.kt | 2 +- .../uistateadapter/capture/HdrUiStateAdapter.kt | 16 +++++++++------- .../compound/QuickSettingsUiStateAdapter.kt | 1 - .../capture/FlashModeUiStateAdapterTest.kt | 11 +++++++---- .../capture/HdrUiStateAdapterTest.kt | 4 ++-- 7 files changed, 19 insertions(+), 20 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 4d8ea267a..fe737ef64 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 @@ -31,21 +31,18 @@ import com.google.jetpackcamera.model.DynamicRange import com.google.jetpackcamera.model.FlashMode import com.google.jetpackcamera.model.ImageOutputFormat import com.google.jetpackcamera.model.LensFacing - import com.google.jetpackcamera.ui.components.capture.R import com.google.jetpackcamera.ui.components.capture.quicksettings.ui.AspectRatioRow import com.google.jetpackcamera.ui.components.capture.quicksettings.ui.CaptureModeRow import com.google.jetpackcamera.ui.components.capture.quicksettings.ui.FlashRow import com.google.jetpackcamera.ui.components.capture.quicksettings.ui.HdrRow import com.google.jetpackcamera.ui.components.capture.quicksettings.ui.QuickNavSettings - import com.google.jetpackcamera.ui.components.capture.quicksettings.ui.QuickSettingsBottomSheet as BottomSheetComponent import com.google.jetpackcamera.ui.controller.quicksettings.QuickSettingsController import com.google.jetpackcamera.ui.uistate.capture.AspectRatioUiState import com.google.jetpackcamera.ui.uistate.capture.CaptureModeUiState import com.google.jetpackcamera.ui.uistate.capture.FlashModeUiState import com.google.jetpackcamera.ui.uistate.capture.HdrUiState - import com.google.jetpackcamera.ui.uistate.capture.compound.QuickSettingsUiState /** diff --git a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/ui/QuickSettingsComponents.kt b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/ui/QuickSettingsComponents.kt index 0d60cffd8..bbcb728f2 100644 --- a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/ui/QuickSettingsComponents.kt +++ b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/ui/QuickSettingsComponents.kt @@ -94,7 +94,6 @@ import com.google.jetpackcamera.ui.components.capture.quicksettings.CameraAspect import com.google.jetpackcamera.ui.components.capture.quicksettings.CameraCaptureMode import com.google.jetpackcamera.ui.components.capture.quicksettings.CameraDynamicRange import com.google.jetpackcamera.ui.components.capture.quicksettings.CameraFlashMode - import com.google.jetpackcamera.ui.components.capture.quicksettings.QuickSettingsEnum import com.google.jetpackcamera.ui.controller.quicksettings.QuickSettingsController import com.google.jetpackcamera.ui.uistate.SingleSelectableUiState @@ -104,7 +103,6 @@ import com.google.jetpackcamera.ui.uistate.capture.CaptureModeUiState.Unavailabl import com.google.jetpackcamera.ui.uistate.capture.FlashModeUiState import com.google.jetpackcamera.ui.uistate.capture.HdrUiState - // //////////////////////////////////////////////////// // // quick settings navigation components diff --git a/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/FlashModeUiStateAdapter.kt b/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/FlashModeUiStateAdapter.kt index 77485afac..ae66b9269 100644 --- a/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/FlashModeUiStateAdapter.kt +++ b/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/FlashModeUiStateAdapter.kt @@ -20,7 +20,6 @@ import com.google.jetpackcamera.model.ConcurrentCameraMode import com.google.jetpackcamera.model.DynamicRange import com.google.jetpackcamera.model.FlashMode import com.google.jetpackcamera.model.ImageOutputFormat -import com.google.jetpackcamera.ui.uistate.capture.HdrUiState import com.google.jetpackcamera.model.LowLightBoostState import com.google.jetpackcamera.settings.model.CameraAppSettings import com.google.jetpackcamera.settings.model.CameraSystemConstraints @@ -30,6 +29,7 @@ import com.google.jetpackcamera.ui.uistate.SingleSelectableUiState import com.google.jetpackcamera.ui.uistate.capture.FlashModeUiState import com.google.jetpackcamera.ui.uistate.capture.FlashModeUiState.Available import com.google.jetpackcamera.ui.uistate.capture.FlashModeUiState.Unavailable +import com.google.jetpackcamera.ui.uistate.capture.HdrUiState // Assuming Utils.getSelectableListFromValues is no longer needed with the new logic // import com.google.jetpackcamera.ui.uistateadapter.Utils 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 a5d47c5d3..cd6b322d2 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 @@ -67,10 +67,11 @@ fun HdrUiState.Companion.from( return when (activeCaptureMode) { CaptureMode.IMAGE_ONLY -> { - val deviceSupportsHdrImage = systemConstraints.perLensConstraints.values.any { constraints -> - constraints.supportedImageFormatsMap[cameraAppSettings.streamConfig] - ?.contains(ImageOutputFormat.JPEG_ULTRA_HDR) ?: false - } + val deviceSupportsHdrImage = + systemConstraints.perLensConstraints.values.any { constraints -> + constraints.supportedImageFormatsMap[cameraAppSettings.streamConfig] + ?.contains(ImageOutputFormat.JPEG_ULTRA_HDR) ?: false + } if (deviceSupportsHdrImage) { val supportsHdrImage = cameraConstraints @@ -89,9 +90,10 @@ fun HdrUiState.Companion.from( } CaptureMode.VIDEO_ONLY -> { - val deviceSupportsHdrVideo = systemConstraints.perLensConstraints.values.any { constraints -> - constraints.supportedDynamicRanges.contains(DynamicRange.HLG10) - } + val deviceSupportsHdrVideo = + systemConstraints.perLensConstraints.values.any { constraints -> + constraints.supportedDynamicRanges.contains(DynamicRange.HLG10) + } if (deviceSupportsHdrVideo) { val supportsHdrVideo = diff --git a/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/compound/QuickSettingsUiStateAdapter.kt b/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/compound/QuickSettingsUiStateAdapter.kt index 305268190..2f6a56e0a 100644 --- a/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/compound/QuickSettingsUiStateAdapter.kt +++ b/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/compound/QuickSettingsUiStateAdapter.kt @@ -20,7 +20,6 @@ import com.google.jetpackcamera.ui.uistate.capture.CaptureModeUiState import com.google.jetpackcamera.ui.uistate.capture.FlashModeUiState import com.google.jetpackcamera.ui.uistate.capture.FlipLensUiState import com.google.jetpackcamera.ui.uistate.capture.HdrUiState - import com.google.jetpackcamera.ui.uistate.capture.compound.QuickSettingsUiState /** diff --git a/ui/uistateadapter/capture/src/test/java/com/google/jetpackcamera/ui/uistateadapter/capture/FlashModeUiStateAdapterTest.kt b/ui/uistateadapter/capture/src/test/java/com/google/jetpackcamera/ui/uistateadapter/capture/FlashModeUiStateAdapterTest.kt index 19b2be216..5e3de76ab 100644 --- a/ui/uistateadapter/capture/src/test/java/com/google/jetpackcamera/ui/uistateadapter/capture/FlashModeUiStateAdapterTest.kt +++ b/ui/uistateadapter/capture/src/test/java/com/google/jetpackcamera/ui/uistateadapter/capture/FlashModeUiStateAdapterTest.kt @@ -17,13 +17,13 @@ package com.google.jetpackcamera.ui.uistateadapter.capture import com.google.common.truth.Truth.assertThat import com.google.jetpackcamera.core.camera.CameraState +import com.google.jetpackcamera.model.DynamicRange import com.google.jetpackcamera.model.FlashMode +import com.google.jetpackcamera.model.ImageOutputFormat import com.google.jetpackcamera.model.LowLightBoostState 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.model.DynamicRange -import com.google.jetpackcamera.model.ImageOutputFormat import com.google.jetpackcamera.ui.components.capture.DisabledReason import com.google.jetpackcamera.ui.uistate.SingleSelectableUiState import com.google.jetpackcamera.ui.uistate.capture.FlashModeUiState @@ -287,7 +287,9 @@ class FlashModeUiStateAdapterTest { // Then LLB is disabled because of HDR assertThat(flashModeUiState).isInstanceOf(FlashModeUiState.Available::class.java) val availableUiState = flashModeUiState as FlashModeUiState.Available - val llbState = availableUiState.availableFlashModes.find { it.value == FlashMode.LOW_LIGHT_BOOST } + val llbState = availableUiState.availableFlashModes.find { + it.value == FlashMode.LOW_LIGHT_BOOST + } assertThat(llbState).isInstanceOf(SingleSelectableUiState.Disabled::class.java) val disabledLlb = llbState as SingleSelectableUiState.Disabled assertThat(disabledLlb.disabledReason).isEqualTo(DisabledReason.LLB_DISABLED_BY_HDR) @@ -315,7 +317,8 @@ class FlashModeUiStateAdapterTest { // Then LLB is disabled because of HDR assertThat(flashModeUiState).isInstanceOf(FlashModeUiState.Available::class.java) val availableUiState = flashModeUiState as FlashModeUiState.Available - val llbState = availableUiState.availableFlashModes.find { it.value == FlashMode.LOW_LIGHT_BOOST } + val llbState = + availableUiState.availableFlashModes.find { it.value == FlashMode.LOW_LIGHT_BOOST } assertThat(llbState).isInstanceOf(SingleSelectableUiState.Disabled::class.java) val disabledLlb = llbState as SingleSelectableUiState.Disabled assertThat(disabledLlb.disabledReason).isEqualTo(DisabledReason.LLB_DISABLED_BY_HDR) 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 6ffe8591b..61380838a 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 @@ -263,7 +263,7 @@ internal class HdrUiStateAdapterTest { } @Test - fun from_imageOnlyMode_hdrNotSupportedOnCurrentLens_butSupportedOnOtherLens_returnsAvailableWithIsSupportedFalse() { + fun from_imageOnlyMode_hdrUnsupportedOnCurrentLensOnly_returnsDisabled() { // Given in IMAGE_ONLY capture mode // Current lens (BACK) does NOT support HDR, but FRONT lens DOES support HDR val appSettings = defaultCameraAppSettings.copy( @@ -300,7 +300,7 @@ internal class HdrUiStateAdapterTest { } @Test - fun from_videoOnlyMode_hdrNotSupportedOnCurrentLens_butSupportedOnOtherLens_returnsAvailableWithIsSupportedFalse() { + fun from_videoOnlyMode_hdrUnsupportedOnCurrentLensOnly_returnsDisabled() { // Given in VIDEO_ONLY capture mode // Current lens (BACK) does NOT support HDR, but FRONT lens DOES support HDR val appSettings = defaultCameraAppSettings.copy( From cd4143e48b859718391328c27bcec50fd7a77561 Mon Sep 17 00:00:00 2001 From: Kimberly Crevecoeur Date: Mon, 15 Jun 2026 11:10:22 -0700 Subject: [PATCH 28/51] Address code review feedback on Quick Settings and Flash/HDR UI state - Fix accessibility issue in SettingRow by removing mergeDescendants from outer Row and applying it to title/subtitle Column. - Add missing KDocs for Quick Settings row composables. - Refactor FlashModeUiStateAdapter for idiomatic Kotlin (use flatMap, singleOrNull, when expression smart casting). - Rename FLASH_UNSUPPORTED_ON_LENS_TAG value to lower_snake_case. - Add unit test for FLASH_UNSUPPORTED_ON_LENS disabled reason in FlashModeUiStateAdapterTest. - Update HdrUiStateAdapter KDoc to reflect STANDARD mode support. - Update HdrUiState.Available KDoc to document isSupported parameter. --- .../ui/components/capture/TestTags.kt | 2 +- .../ui/QuickSettingsComponents.kt | 37 ++++++++++- .../ui/uistate/capture/HdrUiState.kt | 1 + .../capture/FlashModeUiStateAdapter.kt | 63 +++++++++---------- .../capture/HdrUiStateAdapter.kt | 2 +- .../capture/FlashModeUiStateAdapterTest.kt | 35 +++++++++++ 6 files changed, 101 insertions(+), 39 deletions(-) diff --git a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/TestTags.kt b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/TestTags.kt index 8dd862207..aefc5f349 100644 --- a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/TestTags.kt +++ b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/TestTags.kt @@ -44,7 +44,7 @@ const val HDR_IMAGE_UNSUPPORTED_ON_MULTI_STREAM_TAG = "HdrImageUnsupportedOnMult const val HDR_VIDEO_UNSUPPORTED_ON_DEVICE_TAG = "HdrVideoUnsupportedOnDeviceTag" const val HDR_VIDEO_UNSUPPORTED_ON_LENS_TAG = "HdrVideoUnsupportedOnDeviceTag" const val HDR_SIMULTANEOUS_IMAGE_VIDEO_UNSUPPORTED_TAG = "HdrSimultaneousImageVideoUnsupportedTag" -const val FLASH_UNSUPPORTED_ON_LENS_TAG = "FlashUnsupportedOnLensTag" +const val FLASH_UNSUPPORTED_ON_LENS_TAG = "toast_flash_unsupported_on_lens" const val LLB_DISABLED_BY_HDR_TAG = "LlbDisabledByHdrTag" const val ZOOM_BUTTON_ROW_TAG = "ZoomButtonRowTag" const val ZOOM_BUTTON_MIN_TAG = "ZoomButtonMinTag" diff --git a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/ui/QuickSettingsComponents.kt b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/ui/QuickSettingsComponents.kt index bbcb728f2..2e514c800 100644 --- a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/ui/QuickSettingsComponents.kt +++ b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/ui/QuickSettingsComponents.kt @@ -242,6 +242,13 @@ fun QuickSettingsBottomSheet( } } +/** + * A row component in the quick settings menu that allows the user to select the capture mode. + * + * @param modifier The [Modifier] to be applied to this row. + * @param onSetCaptureMode Callback invoked when a new capture mode is selected. + * @param captureModeUiState The current [CaptureModeUiState] representing available and selected modes. + */ @Composable internal fun CaptureModeRow( modifier: Modifier = Modifier, @@ -283,6 +290,13 @@ internal fun CaptureModeRow( } } +/** + * A row component in the quick settings menu that allows the user to toggle HDR (High Dynamic Range) settings. + * + * @param modifier The [Modifier] to be applied to this row. + * @param onClick Callback invoked when HDR is toggled, providing the selected [DynamicRange] and [ImageOutputFormat]. + * @param hdrUiState The current [HdrUiState] representing HDR availability and selection. + */ @Composable internal fun HdrRow( modifier: Modifier = Modifier, @@ -327,6 +341,13 @@ internal fun HdrRow( ) } +/** + * A row component in the quick settings menu that allows the user to select the aspect ratio. + * + * @param modifier The [Modifier] to be applied to this row. + * @param onSetAspectRatio Callback invoked when a new aspect ratio is selected. + * @param aspectRatioUiState The current [AspectRatioUiState] representing available and selected aspect ratios. + */ @Composable internal fun AspectRatioRow( modifier: Modifier = Modifier, @@ -367,6 +388,13 @@ internal fun AspectRatioRow( } } +/** + * A row component in the quick settings menu that allows the user to select the flash mode. + * + * @param modifier The [Modifier] to be applied to this row. + * @param onSetFlashMode Callback invoked when a new flash mode is selected. + * @param flashModeUiState The current [FlashModeUiState] representing available and selected flash modes. + */ @Composable internal fun FlashRow( modifier: Modifier = Modifier, @@ -433,12 +461,15 @@ private fun SettingRow( Row( modifier = modifier .fillMaxWidth() - .padding(vertical = 12.dp, horizontal = 16.dp) - .semantics(mergeDescendants = true) {}, + .padding(vertical = 12.dp, horizontal = 16.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween ) { - Column(modifier = Modifier.weight(1f)) { + Column( + modifier = Modifier + .weight(1f) + .semantics(mergeDescendants = true) {} + ) { Text( text = title, style = MaterialTheme.typography.titleMedium, diff --git a/ui/uistate/capture/src/main/java/com/google/jetpackcamera/ui/uistate/capture/HdrUiState.kt b/ui/uistate/capture/src/main/java/com/google/jetpackcamera/ui/uistate/capture/HdrUiState.kt index ff1875660..232718587 100644 --- a/ui/uistate/capture/src/main/java/com/google/jetpackcamera/ui/uistate/capture/HdrUiState.kt +++ b/ui/uistate/capture/src/main/java/com/google/jetpackcamera/ui/uistate/capture/HdrUiState.kt @@ -36,6 +36,7 @@ sealed interface HdrUiState { * * @param selectedImageFormat The currently selected image output format, which may be related to HDR. * @param selectedDynamicRange The currently selected [DynamicRange]. + * @param isSupported Whether HDR is supported in the current state/configuration. */ data class Available( val selectedImageFormat: ImageOutputFormat, diff --git a/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/FlashModeUiStateAdapter.kt b/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/FlashModeUiStateAdapter.kt index ae66b9269..2a50b35b2 100644 --- a/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/FlashModeUiStateAdapter.kt +++ b/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/FlashModeUiStateAdapter.kt @@ -68,13 +68,9 @@ fun FlashModeUiState.Companion.from( val selectedFlashMode = cameraAppSettings.flashMode // All modes potentially supported by the device - val allDeviceSupportedFlashModes = buildSet { - systemConstraints.perLensConstraints.let { - it.keys.forEach { key -> - it[key]?.supportedFlashModes?.let { flashModes -> addAll(flashModes) } - } - } - } + val allDeviceSupportedFlashModes = systemConstraints.perLensConstraints.values + .flatMap { it.supportedFlashModes } + .toSet() // Modes supported by the CURRENT lens val currentLensSupportedFlashModes = systemConstraints.forCurrentLens(cameraAppSettings) @@ -129,9 +125,9 @@ fun FlashModeUiState.Companion.from( // UiState should be Unavailable if no modes are displayable, // or if only OFF is displayable and it's selectable. - val onlyOffSelectable = displayableModes.size == 1 && - displayableModes[0].value == FlashMode.OFF && - displayableModes[0] is SingleSelectableUiState.SelectableUi + val onlyOffSelectable = displayableModes.singleOrNull()?.let { + it.value == FlashMode.OFF && it is SingleSelectableUiState.SelectableUi + } ?: false return if (displayableModes.isEmpty() || onlyOffSelectable) { Unavailable @@ -168,33 +164,32 @@ fun FlashModeUiState.updateFrom( // Regenerate the potential new state based on the latest settings val newUiState = FlashModeUiState.from(cameraAppSettings, systemConstraints, hdrUiState) - if (newUiState is Unavailable) { - newUiState // Switch to Unavailable - } else { - newUiState as Available // Cast to Available - - // Check if the list of modes or their enabled/disabled states have changed. - // Data class list comparison works well here. - if (this.availableFlashModes != newUiState.availableFlashModes) { - newUiState - } else if (this.selectedFlashMode != cameraAppSettings.flashMode) { - // Only the selection changed - copy(selectedFlashMode = cameraAppSettings.flashMode) - } else { - // Check for Low Light Boost state changes if it's the selected mode - if (cameraAppSettings.flashMode == FlashMode.LOW_LIGHT_BOOST) { - val strength = when (val llbState = cameraState.lowLightBoostState) { - is LowLightBoostState.Active -> llbState.strength - else -> LowLightBoostState.MINIMUM_STRENGTH - } - val newIsLowLightBoostActive = strength > 0.5 - if (this.isLowLightBoostActive != newIsLowLightBoostActive) { - copy(isLowLightBoostActive = newIsLowLightBoostActive) + when (newUiState) { + is Unavailable -> newUiState + is Available -> { + // Check if the list of modes or their enabled/disabled states have changed. + // Data class list comparison works well here. + if (this.availableFlashModes != newUiState.availableFlashModes) { + newUiState + } else if (this.selectedFlashMode != cameraAppSettings.flashMode) { + // Only the selection changed + copy(selectedFlashMode = cameraAppSettings.flashMode) + } else { + // Check for Low Light Boost state changes if it's the selected mode + if (cameraAppSettings.flashMode == FlashMode.LOW_LIGHT_BOOST) { + val strength = when (val llbState = cameraState.lowLightBoostState) { + is LowLightBoostState.Active -> llbState.strength + else -> LowLightBoostState.MINIMUM_STRENGTH + } + val newIsLowLightBoostActive = strength > 0.5 + if (this.isLowLightBoostActive != newIsLowLightBoostActive) { + copy(isLowLightBoostActive = newIsLowLightBoostActive) + } else { + this // Nothing changed + } } else { this // Nothing changed } - } else { - this // Nothing changed } } } 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 cd6b322d2..a967f4367 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 @@ -39,7 +39,7 @@ import com.google.jetpackcamera.ui.uistate.capture.HdrUiState * The logic is tailored to the [ExternalCaptureMode]: * - **ImageCapture / MultipleImageCapture**: Checks for `JPEG_ULTRA_HDR` support. * - **VideoCapture**: Checks for `HLG10` dynamic range support. - * - **Standard**: Checks for support for either `HLG10` or `JPEG_ULTRA_HDR`. + * - **Standard**: Currently not supported (returns [HdrUiState.Unavailable]). * * In all cases, HDR is disabled if `LOW_LIGHT_BOOST` flash mode is active. For video and standard * modes, it is also disabled if concurrent camera mode is active. diff --git a/ui/uistateadapter/capture/src/test/java/com/google/jetpackcamera/ui/uistateadapter/capture/FlashModeUiStateAdapterTest.kt b/ui/uistateadapter/capture/src/test/java/com/google/jetpackcamera/ui/uistateadapter/capture/FlashModeUiStateAdapterTest.kt index 5e3de76ab..339f1196a 100644 --- a/ui/uistateadapter/capture/src/test/java/com/google/jetpackcamera/ui/uistateadapter/capture/FlashModeUiStateAdapterTest.kt +++ b/ui/uistateadapter/capture/src/test/java/com/google/jetpackcamera/ui/uistateadapter/capture/FlashModeUiStateAdapterTest.kt @@ -20,6 +20,7 @@ import com.google.jetpackcamera.core.camera.CameraState import com.google.jetpackcamera.model.DynamicRange import com.google.jetpackcamera.model.FlashMode import com.google.jetpackcamera.model.ImageOutputFormat +import com.google.jetpackcamera.model.LensFacing import com.google.jetpackcamera.model.LowLightBoostState import com.google.jetpackcamera.settings.model.CameraAppSettings import com.google.jetpackcamera.settings.model.CameraConstraints @@ -323,4 +324,38 @@ class FlashModeUiStateAdapterTest { val disabledLlb = llbState as SingleSelectableUiState.Disabled assertThat(disabledLlb.disabledReason).isEqualTo(DisabledReason.LLB_DISABLED_BY_HDR) } + + @Test + fun from_flashUnsupportedOnCurrentLens_flashModeDisabled() { + // Given a device that supports FLASH_ON on some lens, but NOT on the lens + val appSettings = defaultCameraAppSettings.copy( + cameraLensFacing = LensFacing.BACK, + flashMode = FlashMode.OFF + ) + val systemConstraints = CameraSystemConstraints( + availableLenses = listOf(LensFacing.BACK, LensFacing.FRONT), + perLensConstraints = mapOf( + LensFacing.BACK to emptyCameraConstraints.copy( + // Current lens doesn't support ON + supportedFlashModes = setOf(FlashMode.OFF) + ), + LensFacing.FRONT to emptyCameraConstraints.copy( + // Other lens supports ON + supportedFlashModes = setOf(FlashMode.OFF, FlashMode.ON) + ) + ) + ) + + // When + val flashModeUiState = FlashModeUiState.from(appSettings, systemConstraints) + + // Then FLASH_ON is disabled because it is unsupported on the current lens + assertThat(flashModeUiState).isInstanceOf(FlashModeUiState.Available::class.java) + val availableUiState = flashModeUiState as FlashModeUiState.Available + val flashOnState = availableUiState.availableFlashModes.find { it.value == FlashMode.ON } + assertThat(flashOnState).isInstanceOf(SingleSelectableUiState.Disabled::class.java) + val disabledFlashOn = flashOnState as SingleSelectableUiState.Disabled + assertThat(disabledFlashOn.disabledReason) + .isEqualTo(DisabledReason.FLASH_UNSUPPORTED_ON_LENS) + } } From 6962be0ce6dcd9f75916e3b002b196bb0ca9daa3 Mon Sep 17 00:00:00 2001 From: Kimberly Crevecoeur Date: Wed, 17 Jun 2026 06:17:31 -0700 Subject: [PATCH 29/51] clean vestige dynamicrage param. restrictive hdruistateadapter --- .../java/com/google/jetpackcamera/core/camera/CameraSession.kt | 2 -- .../google/jetpackcamera/core/camera/ConcurrentCameraSession.kt | 1 - .../ui/uistateadapter/capture/HdrUiStateAdapter.kt | 2 +- 3 files changed, 1 insertion(+), 4 deletions(-) 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 8e5bb9824..af608df0e 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 @@ -224,7 +224,6 @@ internal suspend fun runSingleCameraSession( initialTransientSettings = currentTransientSettings, stabilizationMode = sessionSettings.stabilizationMode, aspectRatio = sessionSettings.aspectRatio, - dynamicRange = sessionSettings.dynamicRange, imageFormat = sessionSettings.imageFormat, captureMode = sessionSettings.captureMode, effect = cameraEffect, @@ -591,7 +590,6 @@ internal fun createUseCaseGroup( stabilizationMode: StabilizationMode, aspectRatio: AspectRatio, videoCaptureUseCase: VideoCapture?, - dynamicRange: DynamicRange, imageFormat: ImageOutputFormat, captureMode: CaptureMode, effect: CameraEffect? = null, diff --git a/core/camera/src/main/java/com/google/jetpackcamera/core/camera/ConcurrentCameraSession.kt b/core/camera/src/main/java/com/google/jetpackcamera/core/camera/ConcurrentCameraSession.kt index 8b3061895..f0e3e5ae9 100644 --- a/core/camera/src/main/java/com/google/jetpackcamera/core/camera/ConcurrentCameraSession.kt +++ b/core/camera/src/main/java/com/google/jetpackcamera/core/camera/ConcurrentCameraSession.kt @@ -79,7 +79,6 @@ internal suspend fun runConcurrentCameraSession( initialTransientSettings = initialTransientSettings, stabilizationMode = StabilizationMode.OFF, aspectRatio = sessionSettings.aspectRatio, - dynamicRange = DynamicRange.SDR, imageFormat = ImageOutputFormat.JPEG, captureMode = sessionSettings.captureMode, videoCaptureUseCase = videoCapture 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 aff4aa3a3..b9727ac43 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 @@ -52,7 +52,7 @@ import com.google.jetpackcamera.ui.uistate.capture.HdrUiState * @return [HdrUiState.Available] if the feature is supported and not blocked by other settings, * otherwise returns [HdrUiState.Unavailable]. */ -fun HdrUiState.Companion.from( +internal fun HdrUiState.Companion.from( cameraAppSettings: CameraAppSettings, systemConstraints: CameraSystemConstraints, externalCaptureMode: ExternalCaptureMode From e39d34523d2658bf09912e1bc96125e36784bb3e Mon Sep 17 00:00:00 2001 From: Kimberly Crevecoeur Date: Wed, 17 Jun 2026 14:54:36 -0700 Subject: [PATCH 30/51] Format code with Spotless --- .../com/google/jetpackcamera/ui/components/capture/TestTags.kt | 1 - .../ui/uistateadapter/capture/FlashModeUiStateAdapter.kt | 1 - .../ui/uistateadapter/capture/FlashModeUiStateAdapterTest.kt | 1 - 3 files changed, 3 deletions(-) diff --git a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/TestTags.kt b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/TestTags.kt index 93eea75ff..0db8bfad6 100644 --- a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/TestTags.kt +++ b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/TestTags.kt @@ -28,7 +28,6 @@ const val AMPLITUDE_NONE_TAG = "AmplitudeNoneTag" const val AMPLITUDE_HOT_TAG = "AmplitudeHotTag" const val FOCUS_METERING_INDICATOR_TAG = "FocusMeteringIndicatorTag" - const val ZOOM_BUTTON_ROW_TAG = "ZoomButtonRowTag" const val ZOOM_BUTTON_MIN_TAG = "ZoomButtonMinTag" const val ZOOM_BUTTON_1_TAG = "ZoomButton1Tag" diff --git a/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/FlashModeUiStateAdapter.kt b/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/FlashModeUiStateAdapter.kt index d0f6d9b1a..6f108e05c 100644 --- a/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/FlashModeUiStateAdapter.kt +++ b/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/FlashModeUiStateAdapter.kt @@ -24,7 +24,6 @@ import com.google.jetpackcamera.model.LowLightBoostState import com.google.jetpackcamera.settings.model.CameraAppSettings import com.google.jetpackcamera.settings.model.CameraSystemConstraints import com.google.jetpackcamera.settings.model.forCurrentLens - import com.google.jetpackcamera.ui.uistate.SingleSelectableUiState import com.google.jetpackcamera.ui.uistate.capture.FlashModeUiState import com.google.jetpackcamera.ui.uistate.capture.FlashModeUiState.Available diff --git a/ui/uistateadapter/capture/src/test/java/com/google/jetpackcamera/ui/uistateadapter/capture/FlashModeUiStateAdapterTest.kt b/ui/uistateadapter/capture/src/test/java/com/google/jetpackcamera/ui/uistateadapter/capture/FlashModeUiStateAdapterTest.kt index bc8f22b92..519bbd3e2 100644 --- a/ui/uistateadapter/capture/src/test/java/com/google/jetpackcamera/ui/uistateadapter/capture/FlashModeUiStateAdapterTest.kt +++ b/ui/uistateadapter/capture/src/test/java/com/google/jetpackcamera/ui/uistateadapter/capture/FlashModeUiStateAdapterTest.kt @@ -25,7 +25,6 @@ import com.google.jetpackcamera.model.LowLightBoostState 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.SingleSelectableUiState import com.google.jetpackcamera.ui.uistate.capture.FlashModeUiState import com.google.jetpackcamera.ui.uistate.capture.HdrUiState From c1d16a4d136d3adea3ce1866ba2316c948d32e86 Mon Sep 17 00:00:00 2001 From: Kimberly Crevecoeur Date: Fri, 19 Jun 2026 07:33:51 -0700 Subject: [PATCH 31/51] update naming conventions for component tags --- .../ui/components/capture/TestTags.kt | 8 ++++---- .../quicksettings/ui/QuickSettingsComponents.kt | 16 ++++++++-------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/TestTags.kt b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/TestTags.kt index 0db8bfad6..920b37c07 100644 --- a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/TestTags.kt +++ b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/TestTags.kt @@ -75,7 +75,7 @@ const val BTN_QUICK_SETTINGS_FLASH_OPTION_LOW_LIGHT_BOOST = const val BTN_QUICK_SETTINGS_HDR_OPTION_ON = "btn_quick_settings_hdr_option_on" const val BTN_QUICK_SETTINGS_HDR_OPTION_OFF = "btn_quick_settings_hdr_option_off" -const val QUICK_SETTINGS_CAPTURE_MODE_ROW = "quick_settings_capture_mode_row" -const val QUICK_SETTINGS_HDR_ROW = "quick_settings_hdr_row" -const val QUICK_SETTINGS_ASPECT_RATIO_ROW = "quick_settings_aspect_ratio_row" -const val QUICK_SETTINGS_FLASH_ROW = "quick_settings_flash_row" +const val ROW_QUICK_SETTINGS_CAPTURE_MODE = "row_quick_settings_capture_mode" +const val ROW_QUICK_SETTINGS_HDR = "row_quick_settings_hdr" +const val ROW_QUICK_SETTINGS_ASPECT_RATIO = "row_quick_settings_aspect_ratio" +const val ROW_QUICK_SETTINGS_FLASH = "row_quick_settings_flash" diff --git a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/ui/QuickSettingsComponents.kt b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/ui/QuickSettingsComponents.kt index 2e514c800..9b40dd4a3 100644 --- a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/ui/QuickSettingsComponents.kt +++ b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/ui/QuickSettingsComponents.kt @@ -79,12 +79,12 @@ import com.google.jetpackcamera.ui.components.capture.BTN_QUICK_SETTINGS_FOCUSED import com.google.jetpackcamera.ui.components.capture.BTN_QUICK_SETTINGS_FOCUSED_CAPTURE_MODE_VIDEO_ONLY import com.google.jetpackcamera.ui.components.capture.BTN_QUICK_SETTINGS_HDR_OPTION_OFF import com.google.jetpackcamera.ui.components.capture.BTN_QUICK_SETTINGS_HDR_OPTION_ON -import com.google.jetpackcamera.ui.components.capture.QUICK_SETTINGS_ASPECT_RATIO_ROW +import com.google.jetpackcamera.ui.components.capture.ROW_QUICK_SETTINGS_ASPECT_RATIO import com.google.jetpackcamera.ui.components.capture.QUICK_SETTINGS_BOTTOM_SHEET -import com.google.jetpackcamera.ui.components.capture.QUICK_SETTINGS_CAPTURE_MODE_ROW +import com.google.jetpackcamera.ui.components.capture.ROW_QUICK_SETTINGS_CAPTURE_MODE import com.google.jetpackcamera.ui.components.capture.QUICK_SETTINGS_DROP_DOWN -import com.google.jetpackcamera.ui.components.capture.QUICK_SETTINGS_FLASH_ROW -import com.google.jetpackcamera.ui.components.capture.QUICK_SETTINGS_HDR_ROW +import com.google.jetpackcamera.ui.components.capture.ROW_QUICK_SETTINGS_FLASH +import com.google.jetpackcamera.ui.components.capture.ROW_QUICK_SETTINGS_HDR import com.google.jetpackcamera.ui.components.capture.QUICK_SETTINGS_RATIO_1_1_BUTTON import com.google.jetpackcamera.ui.components.capture.QUICK_SETTINGS_RATIO_3_4_BUTTON import com.google.jetpackcamera.ui.components.capture.QUICK_SETTINGS_RATIO_9_16_BUTTON @@ -263,7 +263,7 @@ internal fun CaptureModeRow( } SettingRow( - modifier = modifier.testTag(QUICK_SETTINGS_CAPTURE_MODE_ROW), + modifier = modifier.testTag(ROW_QUICK_SETTINGS_CAPTURE_MODE), title = stringResource(id = R.string.quick_settings_title_capture_mode), stateSubtitle = stringResource(enum.getTextResId()), settingsButtons = captureModeUiState.availableCaptureModes @@ -311,7 +311,7 @@ internal fun HdrRow( ) SettingRow( - modifier = modifier.testTag(QUICK_SETTINGS_HDR_ROW), + modifier = modifier.testTag(ROW_QUICK_SETTINGS_HDR), title = stringResource(id = R.string.quick_settings_title_hdr), stateSubtitle = if (isHdrOn) { stringResource(R.string.quick_settings_dynamic_range_hdr) @@ -378,7 +378,7 @@ internal fun AspectRatioRow( }.toTypedArray() SettingRow( - modifier = modifier.testTag(QUICK_SETTINGS_ASPECT_RATIO_ROW), + modifier = modifier.testTag(ROW_QUICK_SETTINGS_ASPECT_RATIO), title = stringResource(id = R.string.quick_settings_title_aspect_ratio), stateSubtitle = stringResource( id = aspectRatioUiState.selectedAspectRatio.toSubtitleStringRes() @@ -403,7 +403,7 @@ internal fun FlashRow( ) { if (flashModeUiState is FlashModeUiState.Available) { SettingRow( - modifier = modifier.testTag(QUICK_SETTINGS_FLASH_ROW), + modifier = modifier.testTag(ROW_QUICK_SETTINGS_FLASH), title = stringResource(id = R.string.quick_settings_title_flash_mode), stateSubtitle = stringResource( id = flashModeUiState.selectedFlashMode.toSubtitleStringRes() From 8b428818996f8dee1947fb74ec667a87326d1a84 Mon Sep 17 00:00:00 2001 From: Kimberly Crevecoeur Date: Fri, 19 Jun 2026 08:27:37 -0700 Subject: [PATCH 32/51] polish Quick Settings state handling and clean up KDocs --- .../quicksettings/QuickSettingsScreen.kt | 50 ++++++++++--------- .../testing/FakeQuickSettingsController.kt | 1 - 2 files changed, 27 insertions(+), 24 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 fe737ef64..794638e21 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 @@ -66,37 +66,41 @@ fun QuickSettingsBottomSheet( onDismiss = quickSettingsController::toggleQuickSettings, sheetState = sheetState ) { - (quickSettingsUiState.captureModeUiState as? CaptureModeUiState.Available)?.let { - when ( - it.selectedCaptureMode - ) { - CaptureMode.VIDEO_ONLY -> VideoQuickSettings( - quickSettingsUiState = quickSettingsUiState, - quickSettingsController = quickSettingsController, - onNavigateToSettings = onNavigateToSettings, - showMoreSettingsButton = showMoreSettingsButton - ) + when (val captureModeUiState = quickSettingsUiState.captureModeUiState) { + is CaptureModeUiState.Available -> { + when (captureModeUiState.selectedCaptureMode) { + CaptureMode.VIDEO_ONLY -> VideoQuickSettings( + quickSettingsUiState = quickSettingsUiState, + quickSettingsController = quickSettingsController, + onNavigateToSettings = onNavigateToSettings, + showMoreSettingsButton = showMoreSettingsButton + ) - CaptureMode.IMAGE_ONLY -> ImageQuickSettings( - quickSettingsUiState = quickSettingsUiState, - quickSettingsController = quickSettingsController, - onNavigateToSettings = onNavigateToSettings, - showMoreSettingsButton = showMoreSettingsButton - ) + CaptureMode.IMAGE_ONLY -> ImageQuickSettings( + quickSettingsUiState = quickSettingsUiState, + quickSettingsController = quickSettingsController, + onNavigateToSettings = onNavigateToSettings, + showMoreSettingsButton = showMoreSettingsButton + ) - CaptureMode.STANDARD -> HybridQuickSettings( + CaptureMode.STANDARD -> HybridQuickSettings( + quickSettingsUiState = quickSettingsUiState, + quickSettingsController = quickSettingsController, + onNavigateToSettings = onNavigateToSettings, + showMoreSettingsButton = showMoreSettingsButton + ) + } + } + + is CaptureModeUiState.Unavailable -> { + ImageQuickSettings( quickSettingsUiState = quickSettingsUiState, quickSettingsController = quickSettingsController, onNavigateToSettings = onNavigateToSettings, showMoreSettingsButton = showMoreSettingsButton ) } - } ?: ImageQuickSettings( - quickSettingsUiState = quickSettingsUiState, - quickSettingsController = quickSettingsController, - onNavigateToSettings = onNavigateToSettings, - showMoreSettingsButton = showMoreSettingsButton - ) + } } } } diff --git a/ui/controller/testing/src/main/java/com/google/jetpackcamera/ui/controller/testing/FakeQuickSettingsController.kt b/ui/controller/testing/src/main/java/com/google/jetpackcamera/ui/controller/testing/FakeQuickSettingsController.kt index c3c7e5dbe..780b418ca 100644 --- a/ui/controller/testing/src/main/java/com/google/jetpackcamera/ui/controller/testing/FakeQuickSettingsController.kt +++ b/ui/controller/testing/src/main/java/com/google/jetpackcamera/ui/controller/testing/FakeQuickSettingsController.kt @@ -27,7 +27,6 @@ import com.google.jetpackcamera.ui.controller.quicksettings.QuickSettingsControl * A fake implementation of [QuickSettingsController] that allows for configuring actions for its methods. * * @param toggleQuickSettingsAction The action to perform when [toggleQuickSettings] is called. - * @param setFocusedSettingAction The action to perform when [setFocusedSetting] is called. * @param setLensFacingAction The action to perform when [setLensFacing] is called. * @param setFlashAction The action to perform when [setFlash] is called. * @param setAspectRatioAction The action to perform when [setAspectRatio] is called. From e2cf4d77032479116a9e1ac7b71c1f99f09b47d2 Mon Sep 17 00:00:00 2001 From: Kimberly Crevecoeur Date: Fri, 19 Jun 2026 08:28:33 -0700 Subject: [PATCH 33/51] spotless --- .../capture/quicksettings/ui/QuickSettingsComponents.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/ui/QuickSettingsComponents.kt b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/ui/QuickSettingsComponents.kt index 9b40dd4a3..3444ebdbf 100644 --- a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/ui/QuickSettingsComponents.kt +++ b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/ui/QuickSettingsComponents.kt @@ -79,16 +79,16 @@ import com.google.jetpackcamera.ui.components.capture.BTN_QUICK_SETTINGS_FOCUSED import com.google.jetpackcamera.ui.components.capture.BTN_QUICK_SETTINGS_FOCUSED_CAPTURE_MODE_VIDEO_ONLY import com.google.jetpackcamera.ui.components.capture.BTN_QUICK_SETTINGS_HDR_OPTION_OFF import com.google.jetpackcamera.ui.components.capture.BTN_QUICK_SETTINGS_HDR_OPTION_ON -import com.google.jetpackcamera.ui.components.capture.ROW_QUICK_SETTINGS_ASPECT_RATIO import com.google.jetpackcamera.ui.components.capture.QUICK_SETTINGS_BOTTOM_SHEET -import com.google.jetpackcamera.ui.components.capture.ROW_QUICK_SETTINGS_CAPTURE_MODE import com.google.jetpackcamera.ui.components.capture.QUICK_SETTINGS_DROP_DOWN -import com.google.jetpackcamera.ui.components.capture.ROW_QUICK_SETTINGS_FLASH -import com.google.jetpackcamera.ui.components.capture.ROW_QUICK_SETTINGS_HDR import com.google.jetpackcamera.ui.components.capture.QUICK_SETTINGS_RATIO_1_1_BUTTON import com.google.jetpackcamera.ui.components.capture.QUICK_SETTINGS_RATIO_3_4_BUTTON import com.google.jetpackcamera.ui.components.capture.QUICK_SETTINGS_RATIO_9_16_BUTTON import com.google.jetpackcamera.ui.components.capture.R +import com.google.jetpackcamera.ui.components.capture.ROW_QUICK_SETTINGS_ASPECT_RATIO +import com.google.jetpackcamera.ui.components.capture.ROW_QUICK_SETTINGS_CAPTURE_MODE +import com.google.jetpackcamera.ui.components.capture.ROW_QUICK_SETTINGS_FLASH +import com.google.jetpackcamera.ui.components.capture.ROW_QUICK_SETTINGS_HDR import com.google.jetpackcamera.ui.components.capture.SETTINGS_BUTTON import com.google.jetpackcamera.ui.components.capture.quicksettings.CameraAspectRatio import com.google.jetpackcamera.ui.components.capture.quicksettings.CameraCaptureMode From 9986ecd5bbbc373817459b9e97d9fbd6fe79ef4c Mon Sep 17 00:00:00 2001 From: Kimberly Crevecoeur Date: Wed, 24 Jun 2026 12:55:17 -0700 Subject: [PATCH 34/51] update comments --- .../ui/uistateadapter/capture/HdrUiStateAdapter.kt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) 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 b9727ac43..b892411b7 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 @@ -75,7 +75,8 @@ internal fun HdrUiState.Companion.from( if (supportsHdrImage && !isFlashHdrConflict) { HdrUiState.Available( selectedImageFormat = cameraAppSettings.imageFormat, - selectedDynamicRange = DynamicRange.SDR // Force SDR in UI state for video + // Force video dynamic range to SDR in UI state when in IMAGE_ONLY mode + selectedDynamicRange = DynamicRange.SDR ) } else { HdrUiState.Unavailable @@ -90,7 +91,8 @@ internal fun HdrUiState.Companion.from( if (supportsHdrVideo && !isFlashHdrConflict && !isConcurrentConflict) { HdrUiState.Available( - selectedImageFormat = ImageOutputFormat.JPEG, // Force SDR in UI state for image + // Force image format to SDR (JPEG) in UI state when in VIDEO_ONLY mode + selectedImageFormat = ImageOutputFormat.JPEG, selectedDynamicRange = cameraAppSettings.dynamicRange ) } else { From 0afe2f85dee187965ec9d0d4611c29122c80e8b5 Mon Sep 17 00:00:00 2001 From: Kimberly Crevecoeur Date: Fri, 26 Jun 2026 10:02:32 -0700 Subject: [PATCH 35/51] update hdruistate adapter docs --- .../uistateadapter/capture/HdrUiStateAdapter.kt | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) 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 b892411b7..7fecd0812 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 @@ -36,18 +36,19 @@ import com.google.jetpackcamera.ui.uistate.capture.HdrUiState * HDR formats ([DynamicRange.HLG10] for video, [ImageOutputFormat.JPEG_ULTRA_HDR] for images) and * various other settings that may conflict with HDR, such as flash mode or concurrent camera mode. * - * The logic is tailored to the [ExternalCaptureMode]: - * - **ImageCapture / MultipleImageCapture**: Checks for `JPEG_ULTRA_HDR` support. - * - **VideoCapture**: Checks for `HLG10` dynamic range support. - * - **Standard**: Checks for support for either `HLG10` or `JPEG_ULTRA_HDR`. + * The logic is tailored to the resolved [CaptureMode] (determined by the active settings and + * [ExternalCaptureMode] overrides): + * - **IMAGE_ONLY**: Checks for `JPEG_ULTRA_HDR` support. + * - **VIDEO_ONLY**: Checks for `HLG10` dynamic range support. + * - **STANDARD**: HDR is always unavailable. * - * In all cases, HDR is disabled if `LOW_LIGHT_BOOST` flash mode is active. For video and standard - * modes, it is also disabled if concurrent camera mode is active. + * In all cases, HDR is disabled if `LOW_LIGHT_BOOST` flash mode is active. For video mode, + * it is also disabled if concurrent camera mode is active. * * @param cameraAppSettings The current application and camera settings. * @param systemConstraints The capabilities and limitations of the device's camera hardware. * @param externalCaptureMode The mode indicating how the camera was launched (e.g., via an - * external intent), which influences which HDR formats are relevant. + * external intent), which influences the resolved capture mode. * * @return [HdrUiState.Available] if the feature is supported and not blocked by other settings, * otherwise returns [HdrUiState.Unavailable]. From 9b33f643f0e0ec3b4633cf2d227b403b828ac106 Mon Sep 17 00:00:00 2001 From: Kimberly Crevecoeur Date: Fri, 26 Jun 2026 10:16:29 -0700 Subject: [PATCH 36/51] refactor(ui): simplify HdrUiState resolution and add concurrent camera test - Simplify HdrUiState.from signature by removing redundant externalCaptureMode parameter. - Use cameraAppSettings.captureMode directly, as it already incorporates external overrides on startup. - Update CaptureUiStateAdapter to match the new signature. - Refactor HdrUiStateAdapterTest to use the new signature. - Add unit test to verify that concurrent camera (dual mode) disables HDR video. --- .../capture/HdrUiStateAdapter.kt | 14 ++----- .../capture/compound/CaptureUiStateAdapter.kt | 3 +- .../capture/HdrUiStateAdapterTest.kt | 38 +++++++++++++++---- 3 files changed, 35 insertions(+), 20 deletions(-) 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 7fecd0812..f8ee593b5 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 @@ -36,8 +36,7 @@ import com.google.jetpackcamera.ui.uistate.capture.HdrUiState * HDR formats ([DynamicRange.HLG10] for video, [ImageOutputFormat.JPEG_ULTRA_HDR] for images) and * various other settings that may conflict with HDR, such as flash mode or concurrent camera mode. * - * The logic is tailored to the resolved [CaptureMode] (determined by the active settings and - * [ExternalCaptureMode] overrides): + * The logic is tailored to the [CaptureMode]: * - **IMAGE_ONLY**: Checks for `JPEG_ULTRA_HDR` support. * - **VIDEO_ONLY**: Checks for `HLG10` dynamic range support. * - **STANDARD**: HDR is always unavailable. @@ -47,26 +46,19 @@ import com.google.jetpackcamera.ui.uistate.capture.HdrUiState * * @param cameraAppSettings The current application and camera settings. * @param systemConstraints The capabilities and limitations of the device's camera hardware. - * @param externalCaptureMode The mode indicating how the camera was launched (e.g., via an - * external intent), which influences the resolved capture mode. * * @return [HdrUiState.Available] if the feature is supported and not blocked by other settings, * otherwise returns [HdrUiState.Unavailable]. */ internal fun HdrUiState.Companion.from( cameraAppSettings: CameraAppSettings, - systemConstraints: CameraSystemConstraints, - externalCaptureMode: ExternalCaptureMode + systemConstraints: CameraSystemConstraints ): HdrUiState { val cameraConstraints: CameraConstraints? = systemConstraints.forCurrentLens( cameraAppSettings ) - // Determine active capture mode, respecting external override - val activeCaptureMode = - externalCaptureMode.toCaptureMode() ?: cameraAppSettings.captureMode - - return when (activeCaptureMode) { + return when (cameraAppSettings.captureMode) { CaptureMode.IMAGE_ONLY -> { val supportsHdrImage = cameraConstraints ?.supportedImageFormatsMap?.get(cameraAppSettings.streamConfig) diff --git a/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/compound/CaptureUiStateAdapter.kt b/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/compound/CaptureUiStateAdapter.kt index 3ba957975..860326ae7 100644 --- a/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/compound/CaptureUiStateAdapter.kt +++ b/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/compound/CaptureUiStateAdapter.kt @@ -96,8 +96,7 @@ fun captureUiState( val aspectRatioUiState = AspectRatioUiState.from(cameraAppSettings) val hdrUiState = HdrUiState.from( cameraAppSettings, - systemConstraints, - externalCaptureMode + systemConstraints ) flashModeUiState = flashModeUiState.let { 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 4f172572a..722565e87 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 @@ -17,6 +17,7 @@ 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.ConcurrentCameraMode import com.google.jetpackcamera.model.DynamicRange import com.google.jetpackcamera.model.ExternalCaptureMode import com.google.jetpackcamera.model.FlashMode @@ -67,7 +68,7 @@ internal class HdrUiStateAdapterTest { // When val hdrUiState = - HdrUiState.from(appSettings, systemConstraints, ExternalCaptureMode.Standard) + HdrUiState.from(appSettings, systemConstraints) // Then HDR is unavailable in Standard mode assertThat(hdrUiState).isInstanceOf(HdrUiState.Unavailable::class.java) @@ -96,7 +97,7 @@ internal class HdrUiStateAdapterTest { // When val hdrUiState = - HdrUiState.from(appSettings, systemConstraints, ExternalCaptureMode.Standard) + HdrUiState.from(appSettings, systemConstraints) // Then HDR is available assertThat(hdrUiState).isInstanceOf(HdrUiState.Available::class.java) @@ -123,7 +124,7 @@ internal class HdrUiStateAdapterTest { // When val hdrUiState = - HdrUiState.from(appSettings, systemConstraints, ExternalCaptureMode.Standard) + HdrUiState.from(appSettings, systemConstraints) // Then HDR is unavailable assertThat(hdrUiState).isInstanceOf(HdrUiState.Unavailable::class.java) @@ -151,7 +152,7 @@ internal class HdrUiStateAdapterTest { // When val hdrUiState = - HdrUiState.from(appSettings, systemConstraints, ExternalCaptureMode.Standard) + HdrUiState.from(appSettings, systemConstraints) // Then HDR is unavailable because of the flash mode conflict assertThat(hdrUiState).isInstanceOf(HdrUiState.Unavailable::class.java) @@ -175,7 +176,7 @@ internal class HdrUiStateAdapterTest { // When val hdrUiState = - HdrUiState.from(appSettings, systemConstraints, ExternalCaptureMode.Standard) + HdrUiState.from(appSettings, systemConstraints) // Then HDR is available assertThat(hdrUiState).isInstanceOf(HdrUiState.Available::class.java) @@ -200,7 +201,7 @@ internal class HdrUiStateAdapterTest { // When val hdrUiState = - HdrUiState.from(appSettings, systemConstraints, ExternalCaptureMode.Standard) + HdrUiState.from(appSettings, systemConstraints) // Then HDR is unavailable assertThat(hdrUiState).isInstanceOf(HdrUiState.Unavailable::class.java) @@ -223,9 +224,32 @@ internal class HdrUiStateAdapterTest { // When val hdrUiState = - HdrUiState.from(appSettings, systemConstraints, ExternalCaptureMode.Standard) + HdrUiState.from(appSettings, systemConstraints) // Then HDR is unavailable because of the flash mode conflict assertThat(hdrUiState).isInstanceOf(HdrUiState.Unavailable::class.java) } + + @Test + fun from_videoOnlyMode_concurrentCameraActive_returnsUnavailable() { + // Given in VIDEO_ONLY mode with HLG10 supported, but concurrent camera is DUAL + val appSettings = defaultCameraAppSettings.copy( + captureMode = CaptureMode.VIDEO_ONLY, + concurrentCameraMode = ConcurrentCameraMode.DUAL, + dynamicRange = DynamicRange.HLG10 + ) + val systemConstraints = CameraSystemConstraints( + perLensConstraints = mapOf( + appSettings.cameraLensFacing to emptyCameraConstraints.copy( + supportedDynamicRanges = setOf(DynamicRange.SDR, DynamicRange.HLG10) + ) + ) + ) + + // When + val hdrUiState = HdrUiState.from(appSettings, systemConstraints) + + // Then HDR is unavailable due to concurrent camera conflict + assertThat(hdrUiState).isInstanceOf(HdrUiState.Unavailable::class.java) + } } From 5b63267ebedbf8e95e096a3e8ed8e73a4a9f551e Mon Sep 17 00:00:00 2001 From: Kimberly Crevecoeur Date: Fri, 26 Jun 2026 11:01:35 -0700 Subject: [PATCH 37/51] spotless --- .../ui/uistateadapter/capture/HdrUiStateAdapter.kt | 2 -- .../ui/uistateadapter/capture/HdrUiStateAdapterTest.kt | 1 - 2 files changed, 3 deletions(-) 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 f8ee593b5..ff28e53cb 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 @@ -18,8 +18,6 @@ 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 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 722565e87..27e35196a 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 @@ -19,7 +19,6 @@ import com.google.common.truth.Truth.assertThat 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.FlashMode import com.google.jetpackcamera.model.ImageOutputFormat import com.google.jetpackcamera.settings.model.CameraAppSettings From 3f93f5d692defe6dd6769fd59a3c103228be4f68 Mon Sep 17 00:00:00 2001 From: Kimberly Crevecoeur Date: Fri, 26 Jun 2026 15:33:13 -0700 Subject: [PATCH 38/51] refactor(ui): Migrate SettingRow to Slot API for performance Refactors `SettingRow` in `QuickSettingsComponents.kt` to accept a `buttons: @Composable RowScope.() -> Unit` slot instead of a `vararg` array of Composables. This makes the Composable stable and skippable, preventing unnecessary recompositions, and eliminates `.toTypedArray()` allocations at call sites. Updates all callers (CaptureModeRow, HdrRow, AspectRatioRow, FlashRow, and previews) to use the new trailing lambda syntax. --- .../ui/QuickSettingsComponents.kt | 255 ++++++++---------- 1 file changed, 116 insertions(+), 139 deletions(-) diff --git a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/ui/QuickSettingsComponents.kt b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/ui/QuickSettingsComponents.kt index 3444ebdbf..7cb7b6a1e 100644 --- a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/ui/QuickSettingsComponents.kt +++ b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/ui/QuickSettingsComponents.kt @@ -23,6 +23,7 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height @@ -265,28 +266,26 @@ internal fun CaptureModeRow( SettingRow( modifier = modifier.testTag(ROW_QUICK_SETTINGS_CAPTURE_MODE), title = stringResource(id = R.string.quick_settings_title_capture_mode), - stateSubtitle = stringResource(enum.getTextResId()), - settingsButtons = captureModeUiState.availableCaptureModes - .map { selectableMode -> - @Composable { - val testTag = when (selectableMode.value) { - CaptureMode.STANDARD -> - BTN_QUICK_SETTINGS_FOCUSED_CAPTURE_MODE_OPTION_STANDARD - CaptureMode.IMAGE_ONLY -> - BTN_QUICK_SETTINGS_FOCUSED_CAPTURE_MODE_IMAGE_ONLY - CaptureMode.VIDEO_ONLY -> - BTN_QUICK_SETTINGS_FOCUSED_CAPTURE_MODE_VIDEO_ONLY - } - CaptureModeToggleButton( - modifier = Modifier.testTag(testTag), - onClick = { onSetCaptureMode(selectableMode.value) }, - assignedCaptureMode = selectableMode.value, - captureModeUiState = captureModeUiState, - isHighlightEnabled = true - ) - } - }.toTypedArray() - ) + stateSubtitle = stringResource(enum.getTextResId()) + ) { + captureModeUiState.availableCaptureModes.forEach { selectableMode -> + val testTag = when (selectableMode.value) { + CaptureMode.STANDARD -> + BTN_QUICK_SETTINGS_FOCUSED_CAPTURE_MODE_OPTION_STANDARD + CaptureMode.IMAGE_ONLY -> + BTN_QUICK_SETTINGS_FOCUSED_CAPTURE_MODE_IMAGE_ONLY + CaptureMode.VIDEO_ONLY -> + BTN_QUICK_SETTINGS_FOCUSED_CAPTURE_MODE_VIDEO_ONLY + } + CaptureModeToggleButton( + modifier = Modifier.testTag(testTag), + onClick = { onSetCaptureMode(selectableMode.value) }, + assignedCaptureMode = selectableMode.value, + captureModeUiState = captureModeUiState, + isHighlightEnabled = true + ) + } + } } } @@ -317,28 +316,23 @@ internal fun HdrRow( stringResource(R.string.quick_settings_dynamic_range_hdr) } else { stringResource(R.string.quick_settings_dynamic_range_sdr) - }, - settingsButtons = arrayOf( - { - QuickSettingToggleSelectorButton( - modifier = Modifier.testTag(BTN_QUICK_SETTINGS_HDR_OPTION_ON), - enum = CameraDynamicRange.HDR, - onClick = { onClick(DEFAULT_HDR_DYNAMIC_RANGE, DEFAULT_HDR_IMAGE_OUTPUT) }, - isSelected = isHdrOn, - enabled = isSupported - ) - }, - { - QuickSettingToggleSelectorButton( - modifier = Modifier.testTag(BTN_QUICK_SETTINGS_HDR_OPTION_OFF), - enum = CameraDynamicRange.SDR, - onClick = { onClick(DynamicRange.SDR, ImageOutputFormat.JPEG) }, - isSelected = !isHdrOn, - enabled = isSupported - ) - } + } + ) { + QuickSettingToggleSelectorButton( + modifier = Modifier.testTag(BTN_QUICK_SETTINGS_HDR_OPTION_ON), + enum = CameraDynamicRange.HDR, + onClick = { onClick(DEFAULT_HDR_DYNAMIC_RANGE, DEFAULT_HDR_IMAGE_OUTPUT) }, + isSelected = isHdrOn, + enabled = isSupported ) - ) + QuickSettingToggleSelectorButton( + modifier = Modifier.testTag(BTN_QUICK_SETTINGS_HDR_OPTION_OFF), + enum = CameraDynamicRange.SDR, + onClick = { onClick(DynamicRange.SDR, ImageOutputFormat.JPEG) }, + isSelected = !isHdrOn, + enabled = isSupported + ) + } } /** @@ -355,36 +349,32 @@ internal fun AspectRatioRow( aspectRatioUiState: AspectRatioUiState ) { if (aspectRatioUiState is AspectRatioUiState.Available) { - val settingsButtons = aspectRatioUiState.availableAspectRatios - .map { selectableRatio -> - @Composable { - val enum = when (selectableRatio.value) { - AspectRatio.THREE_FOUR -> CameraAspectRatio.THREE_FOUR - AspectRatio.NINE_SIXTEEN -> CameraAspectRatio.NINE_SIXTEEN - AspectRatio.ONE_ONE -> CameraAspectRatio.ONE_ONE - } - val testTag = when (selectableRatio.value) { - AspectRatio.THREE_FOUR -> QUICK_SETTINGS_RATIO_3_4_BUTTON - AspectRatio.NINE_SIXTEEN -> QUICK_SETTINGS_RATIO_9_16_BUTTON - AspectRatio.ONE_ONE -> QUICK_SETTINGS_RATIO_1_1_BUTTON - } - QuickSettingToggleSelectorButton( - modifier = Modifier.testTag(testTag), - onClick = { onSetAspectRatio(selectableRatio.value) }, - enum = enum, - isSelected = selectableRatio.value == aspectRatioUiState.selectedAspectRatio - ) - } - }.toTypedArray() - SettingRow( modifier = modifier.testTag(ROW_QUICK_SETTINGS_ASPECT_RATIO), title = stringResource(id = R.string.quick_settings_title_aspect_ratio), stateSubtitle = stringResource( id = aspectRatioUiState.selectedAspectRatio.toSubtitleStringRes() - ), - settingsButtons = settingsButtons - ) + ) + ) { + aspectRatioUiState.availableAspectRatios.forEach { selectableRatio -> + val enum = when (selectableRatio.value) { + AspectRatio.THREE_FOUR -> CameraAspectRatio.THREE_FOUR + AspectRatio.NINE_SIXTEEN -> CameraAspectRatio.NINE_SIXTEEN + AspectRatio.ONE_ONE -> CameraAspectRatio.ONE_ONE + } + val testTag = when (selectableRatio.value) { + AspectRatio.THREE_FOUR -> QUICK_SETTINGS_RATIO_3_4_BUTTON + AspectRatio.NINE_SIXTEEN -> QUICK_SETTINGS_RATIO_9_16_BUTTON + AspectRatio.ONE_ONE -> QUICK_SETTINGS_RATIO_1_1_BUTTON + } + QuickSettingToggleSelectorButton( + modifier = Modifier.testTag(testTag), + onClick = { onSetAspectRatio(selectableRatio.value) }, + enum = enum, + isSelected = selectableRatio.value == aspectRatioUiState.selectedAspectRatio + ) + } + } } } @@ -407,40 +397,38 @@ internal fun FlashRow( title = stringResource(id = R.string.quick_settings_title_flash_mode), stateSubtitle = stringResource( id = flashModeUiState.selectedFlashMode.toSubtitleStringRes() - ), - settingsButtons = flashModeUiState.availableFlashModes - .map { selectableMode -> - @Composable { - val testTag = when (selectableMode.value) { - FlashMode.OFF -> BTN_QUICK_SETTINGS_FLASH_OPTION_OFF - FlashMode.ON -> BTN_QUICK_SETTINGS_FLASH_OPTION_ON - FlashMode.AUTO -> BTN_QUICK_SETTINGS_FLASH_OPTION_AUTO - FlashMode.LOW_LIGHT_BOOST -> - BTN_QUICK_SETTINGS_FLASH_OPTION_LOW_LIGHT_BOOST - } - QuickSettingToggleSelectorButton( - modifier = Modifier.testTag(testTag), - enabled = selectableMode is SingleSelectableUiState.SelectableUi, - enum = when (selectableMode.value) { - FlashMode.OFF -> CameraFlashMode.OFF - FlashMode.ON -> CameraFlashMode.ON - FlashMode.AUTO -> CameraFlashMode.AUTO - FlashMode.LOW_LIGHT_BOOST -> - when (flashModeUiState.isLowLightBoostActive) { - true -> CameraFlashMode.LOW_LIGHT_BOOST_ACTIVE - false -> CameraFlashMode.LOW_LIGHT_BOOST_INACTIVE - } - }, - isSelected = flashModeUiState.selectedFlashMode == selectableMode.value, - onClick = { - onSetFlashMode( - selectableMode.value - ) + ) + ) { + flashModeUiState.availableFlashModes.forEach { selectableMode -> + val testTag = when (selectableMode.value) { + FlashMode.OFF -> BTN_QUICK_SETTINGS_FLASH_OPTION_OFF + FlashMode.ON -> BTN_QUICK_SETTINGS_FLASH_OPTION_ON + FlashMode.AUTO -> BTN_QUICK_SETTINGS_FLASH_OPTION_AUTO + FlashMode.LOW_LIGHT_BOOST -> + BTN_QUICK_SETTINGS_FLASH_OPTION_LOW_LIGHT_BOOST + } + QuickSettingToggleSelectorButton( + modifier = Modifier.testTag(testTag), + enabled = selectableMode is SingleSelectableUiState.SelectableUi, + enum = when (selectableMode.value) { + FlashMode.OFF -> CameraFlashMode.OFF + FlashMode.ON -> CameraFlashMode.ON + FlashMode.AUTO -> CameraFlashMode.AUTO + FlashMode.LOW_LIGHT_BOOST -> + when (flashModeUiState.isLowLightBoostActive) { + true -> CameraFlashMode.LOW_LIGHT_BOOST_ACTIVE + false -> CameraFlashMode.LOW_LIGHT_BOOST_INACTIVE } + }, + isSelected = flashModeUiState.selectedFlashMode == selectableMode.value, + onClick = { + onSetFlashMode( + selectableMode.value ) } - }.toTypedArray() - ) + ) + } + } } } @@ -455,8 +443,7 @@ private fun SettingRow( title: String, stateSubtitle: String, modifier: Modifier = Modifier, - // Using vararg to accept multiple button components - vararg settingsButtons: @Composable () -> Unit + buttons: @Composable RowScope.() -> Unit ) { Row( modifier = modifier @@ -484,12 +471,9 @@ private fun SettingRow( Row( horizontalArrangement = Arrangement.spacedBy(8.dp), - verticalAlignment = Alignment.CenterVertically - ) { - settingsButtons.forEach { button -> - button() - } - } + verticalAlignment = Alignment.CenterVertically, + content = buttons + ) } } @@ -741,39 +725,32 @@ private fun PreviewSettingRowDark() { SettingRow( title = "Video Resolution", stateSubtitle = "Standard Definition", - settingsButtons = arrayOf( - { - // Off State (Highlighted per your screenshot) - QuickSettingToggleSelectorButton( - text = "SD", - accessibilityText = "Flash Off", - painter = painterResource(id = R.drawable.video_resolution_sd_icon), - isSelected = true, - onClick = {} - ) - }, - { - // On State - QuickSettingToggleSelectorButton( - text = "HD", - accessibilityText = "High Definition", - painter = painterResource(id = R.drawable.video_resolution_hd_icon), - isSelected = false, - onClick = {} - ) - }, - { - // Auto State - QuickSettingToggleSelectorButton( - text = "FHD", - accessibilityText = "Full High Definition", - painter = painterResource(id = R.drawable.video_resolution_fhd_icon), - isSelected = false, - onClick = {} - ) - } + ) { + // Off State (Highlighted per your screenshot) + QuickSettingToggleSelectorButton( + text = "SD", + accessibilityText = "Flash Off", + painter = painterResource(id = R.drawable.video_resolution_sd_icon), + isSelected = true, + onClick = {} ) - ) + // On State + QuickSettingToggleSelectorButton( + text = "HD", + accessibilityText = "High Definition", + painter = painterResource(id = R.drawable.video_resolution_hd_icon), + isSelected = false, + onClick = {} + ) + // Auto State + QuickSettingToggleSelectorButton( + text = "FHD", + accessibilityText = "Full High Definition", + painter = painterResource(id = R.drawable.video_resolution_fhd_icon), + isSelected = false, + onClick = {} + ) + } } } } From f7cff4e71ff2054d08e6627846232d544a7c143e Mon Sep 17 00:00:00 2001 From: Kimberly Crevecoeur Date: Wed, 8 Jul 2026 04:33:15 -0700 Subject: [PATCH 39/51] Optimize FlashModeUiStateAdapter by removing redundant collection allocations and setting internal visibility --- .../capture/FlashModeUiStateAdapter.kt | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/FlashModeUiStateAdapter.kt b/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/FlashModeUiStateAdapter.kt index 6f108e05c..8601707b4 100644 --- a/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/FlashModeUiStateAdapter.kt +++ b/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/FlashModeUiStateAdapter.kt @@ -29,8 +29,6 @@ import com.google.jetpackcamera.ui.uistate.capture.FlashModeUiState import com.google.jetpackcamera.ui.uistate.capture.FlashModeUiState.Available import com.google.jetpackcamera.ui.uistate.capture.FlashModeUiState.Unavailable import com.google.jetpackcamera.ui.uistate.capture.HdrUiState -// Assuming Utils.getSelectableListFromValues is no longer needed with the new logic -// import com.google.jetpackcamera.ui.uistateadapter.Utils private val ORDERED_UI_SUPPORTED_FLASH_MODES = listOf( FlashMode.OFF, @@ -57,7 +55,7 @@ private val ORDERED_UI_SUPPORTED_FLASH_MODES = listOf( * @param systemConstraints The hardware capabilities of the camera system. * @return A [FlashModeUiState] which is either [Available] or [Unavailable]. */ -fun FlashModeUiState.Companion.from( +internal fun FlashModeUiState.Companion.from( cameraAppSettings: CameraAppSettings, systemConstraints: CameraSystemConstraints, hdrUiState: HdrUiState = HdrUiState.Unavailable @@ -67,9 +65,10 @@ fun FlashModeUiState.Companion.from( val selectedFlashMode = cameraAppSettings.flashMode // All modes potentially supported by the device - val allDeviceSupportedFlashModes = systemConstraints.perLensConstraints.values - .flatMap { it.supportedFlashModes } - .toSet() + val allDeviceSupportedFlashModes = mutableSetOf() + for (lensConstraint in systemConstraints.perLensConstraints.values) { + allDeviceSupportedFlashModes.addAll(lensConstraint.supportedFlashModes) + } // Modes supported by the CURRENT lens val currentLensSupportedFlashModes = systemConstraints.forCurrentLens(cameraAppSettings) @@ -147,7 +146,7 @@ fun FlashModeUiState.Companion.from( * @param cameraState The real-time state from the camera, used to check [LowLightBoostState]. * @return An updated [FlashModeUiState]. */ -fun FlashModeUiState.updateFrom( +internal fun FlashModeUiState.updateFrom( cameraAppSettings: CameraAppSettings, systemConstraints: CameraSystemConstraints, cameraState: CameraState, From 9f0e2af3f035f086e033aaa93178d1838d4dcc53 Mon Sep 17 00:00:00 2001 From: Kimberly Crevecoeur Date: Wed, 8 Jul 2026 07:26:00 -0700 Subject: [PATCH 40/51] Add unit test for Flash/HDR conflict happy path with LLB enabled --- .../capture/FlashModeUiStateAdapterTest.kt | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/ui/uistateadapter/capture/src/test/java/com/google/jetpackcamera/ui/uistateadapter/capture/FlashModeUiStateAdapterTest.kt b/ui/uistateadapter/capture/src/test/java/com/google/jetpackcamera/ui/uistateadapter/capture/FlashModeUiStateAdapterTest.kt index 519bbd3e2..b7b6ba969 100644 --- a/ui/uistateadapter/capture/src/test/java/com/google/jetpackcamera/ui/uistateadapter/capture/FlashModeUiStateAdapterTest.kt +++ b/ui/uistateadapter/capture/src/test/java/com/google/jetpackcamera/ui/uistateadapter/capture/FlashModeUiStateAdapterTest.kt @@ -29,6 +29,7 @@ import com.google.jetpackcamera.ui.uistate.SingleSelectableUiState import com.google.jetpackcamera.ui.uistate.capture.FlashModeUiState import com.google.jetpackcamera.ui.uistate.capture.HdrUiState import org.junit.Assert.assertThrows +import org.junit.Assume.assumeTrue import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.JUnit4 @@ -357,4 +358,38 @@ class FlashModeUiStateAdapterTest { assertThat(disabledFlashOn.disabledReason) .isEqualTo(DisabledReason.FLASH_UNSUPPORTED_ON_LENS) } + + @Test + fun from_hdrOff_lowLightBoostEnabled() { + // Given a system supporting LLB, and HDR is OFF (JPEG and SDR) + val appSettings = defaultCameraAppSettings.copy(flashMode = FlashMode.OFF) + val systemConstraints = CameraSystemConstraints( + perLensConstraints = mapOf( + appSettings.cameraLensFacing to emptyCameraConstraints.copy( + supportedFlashModes = setOf(FlashMode.OFF, FlashMode.LOW_LIGHT_BOOST) + ) + ) + ) + + // Skip the test if LOW_LIGHT_BOOST is not present in the lens constraints + val currentLensConstraints = systemConstraints.perLensConstraints[appSettings.cameraLensFacing] + val isLlbSupported = currentLensConstraints?.supportedFlashModes?.contains(FlashMode.LOW_LIGHT_BOOST) == true + assumeTrue("Skipping: Low Light Boost is not supported by constraints", isLlbSupported) + + val hdrUiState = HdrUiState.Available( + selectedImageFormat = ImageOutputFormat.JPEG, + selectedDynamicRange = DynamicRange.SDR + ) + + // When + val flashModeUiState = FlashModeUiState.from(appSettings, systemConstraints, hdrUiState) + + // Then LLB is selectable + assertThat(flashModeUiState).isInstanceOf(FlashModeUiState.Available::class.java) + val availableUiState = flashModeUiState as FlashModeUiState.Available + val llbState = availableUiState.availableFlashModes.find { + it.value == FlashMode.LOW_LIGHT_BOOST + } + assertThat(llbState).isInstanceOf(SingleSelectableUiState.SelectableUi::class.java) + } } From 64d1e0bfd0a578688e0c38ac025f65d910e0783d Mon Sep 17 00:00:00 2001 From: Kimberly Crevecoeur Date: Wed, 8 Jul 2026 08:20:23 -0700 Subject: [PATCH 41/51] Clean up unused imports, fix formatting, and sync KDocs --- .../quicksettings/ui/QuickSettingsComponents.kt | 4 +--- .../ui/controller/impl/QuickSettingsControllerImpl.kt | 1 - .../controller/testing/FakeQuickSettingsController.kt | 1 - .../uistate/capture/compound/QuickSettingsUiState.kt | 1 - .../uistateadapter/capture/FlashModeUiStateAdapter.kt | 2 ++ .../capture/compound/QuickSettingsUiStateAdapter.kt | 3 --- .../capture/FlashModeUiStateAdapterTest.kt | 11 ++++++++--- .../uistateadapter/capture/HdrUiStateAdapterTest.kt | 2 -- 8 files changed, 11 insertions(+), 14 deletions(-) diff --git a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/ui/QuickSettingsComponents.kt b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/ui/QuickSettingsComponents.kt index 477ae3050..06e773cfd 100644 --- a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/ui/QuickSettingsComponents.kt +++ b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/ui/QuickSettingsComponents.kt @@ -52,8 +52,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.painter.Painter -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.platform.LocalWindowInfo import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource @@ -735,7 +733,7 @@ private fun PreviewSettingRowDark() { ) { SettingRow( title = "Video Resolution", - stateSubtitle = "Standard Definition", + stateSubtitle = "Standard Definition" ) { // Off State (Highlighted per your screenshot) QuickSettingToggleSelectorButton( diff --git a/ui/controller/impl/src/main/java/com/google/jetpackcamera/ui/controller/impl/QuickSettingsControllerImpl.kt b/ui/controller/impl/src/main/java/com/google/jetpackcamera/ui/controller/impl/QuickSettingsControllerImpl.kt index e6328218a..ec95ede15 100644 --- a/ui/controller/impl/src/main/java/com/google/jetpackcamera/ui/controller/impl/QuickSettingsControllerImpl.kt +++ b/ui/controller/impl/src/main/java/com/google/jetpackcamera/ui/controller/impl/QuickSettingsControllerImpl.kt @@ -39,7 +39,6 @@ import kotlinx.coroutines.launch * [trackedCaptureUiState]. * * @param trackedCaptureUiState The state flow to update with quick settings information. - * @param scope The coroutine scope for launching camera operations. * @param cameraSystem The camera system to control. * @param externalCaptureMode The current external capture mode. * @param coroutineContext The [CoroutineContext] for launching coroutines. diff --git a/ui/controller/testing/src/main/java/com/google/jetpackcamera/ui/controller/testing/FakeQuickSettingsController.kt b/ui/controller/testing/src/main/java/com/google/jetpackcamera/ui/controller/testing/FakeQuickSettingsController.kt index 780b418ca..b7e2c5d7a 100644 --- a/ui/controller/testing/src/main/java/com/google/jetpackcamera/ui/controller/testing/FakeQuickSettingsController.kt +++ b/ui/controller/testing/src/main/java/com/google/jetpackcamera/ui/controller/testing/FakeQuickSettingsController.kt @@ -32,7 +32,6 @@ import com.google.jetpackcamera.ui.controller.quicksettings.QuickSettingsControl * @param setAspectRatioAction The action to perform when [setAspectRatio] is called. * @param setDynamicRangeAction The action to perform when [setDynamicRange] is called. * @param setImageFormatAction The action to perform when [setImageFormat] is called. - * @param setConcurrentCameraModeAction The action to perform when [setConcurrentCameraMode] is called. * @param setCaptureModeAction The action to perform when [setCaptureMode] is called. */ class FakeQuickSettingsController( diff --git a/ui/uistate/capture/src/main/java/com/google/jetpackcamera/ui/uistate/capture/compound/QuickSettingsUiState.kt b/ui/uistate/capture/src/main/java/com/google/jetpackcamera/ui/uistate/capture/compound/QuickSettingsUiState.kt index ff257eacc..2c2a61c44 100644 --- a/ui/uistate/capture/src/main/java/com/google/jetpackcamera/ui/uistate/capture/compound/QuickSettingsUiState.kt +++ b/ui/uistate/capture/src/main/java/com/google/jetpackcamera/ui/uistate/capture/compound/QuickSettingsUiState.kt @@ -38,7 +38,6 @@ sealed interface QuickSettingsUiState { * * @param aspectRatioUiState The UI state for the aspect ratio setting. * @param captureModeUiState The UI state for the capture mode (e.g., photo vs. video) setting. - * @param concurrentCameraUiState The UI state for concurrent camera mode. * @param flashModeUiState The UI state for the flash mode setting. * @param flipLensUiState The UI state for the flip lens (front/back camera) button. * @param hdrUiState The UI state for the HDR (High Dynamic Range) setting. diff --git a/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/FlashModeUiStateAdapter.kt b/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/FlashModeUiStateAdapter.kt index 8601707b4..c43848c71 100644 --- a/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/FlashModeUiStateAdapter.kt +++ b/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/FlashModeUiStateAdapter.kt @@ -53,6 +53,7 @@ private val ORDERED_UI_SUPPORTED_FLASH_MODES = listOf( * * @param cameraAppSettings The current settings of the camera. * @param systemConstraints The hardware capabilities of the camera system. + * @param hdrUiState The UI state for HDR, used to check Flash/HDR conflicts. * @return A [FlashModeUiState] which is either [Available] or [Unavailable]. */ internal fun FlashModeUiState.Companion.from( @@ -144,6 +145,7 @@ internal fun FlashModeUiState.Companion.from( * @param cameraAppSettings The current application settings for the camera. * @param systemConstraints The hardware capabilities of the camera system. * @param cameraState The real-time state from the camera, used to check [LowLightBoostState]. + * @param hdrUiState The UI state for HDR, used to check Flash/HDR conflicts. * @return An updated [FlashModeUiState]. */ internal fun FlashModeUiState.updateFrom( diff --git a/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/compound/QuickSettingsUiStateAdapter.kt b/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/compound/QuickSettingsUiStateAdapter.kt index 2f6a56e0a..008c2289d 100644 --- a/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/compound/QuickSettingsUiStateAdapter.kt +++ b/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/compound/QuickSettingsUiStateAdapter.kt @@ -31,12 +31,9 @@ import com.google.jetpackcamera.ui.uistate.capture.compound.QuickSettingsUiState * @param captureModeUiState The UI state for the capture mode (e.g., photo, video). * @param flashModeUiState The UI state for the flash mode. * @param flipLensUiState The UI state for the flip lens button. - * @param cameraAppSettings The current application settings for the camera. - * @param systemConstraints The constraints of the camera system. * @param aspectRatioUiState The UI state for the aspect ratio setting. * @param hdrUiState The UI state for the HDR setting. * @param quickSettingsIsOpen Indicates whether the quick settings panel is open. - * @param externalCaptureMode The external capture mode, if any. * @return A [QuickSettingsUiState.Available] instance containing the consolidated states. */ fun QuickSettingsUiState.Companion.from( diff --git a/ui/uistateadapter/capture/src/test/java/com/google/jetpackcamera/ui/uistateadapter/capture/FlashModeUiStateAdapterTest.kt b/ui/uistateadapter/capture/src/test/java/com/google/jetpackcamera/ui/uistateadapter/capture/FlashModeUiStateAdapterTest.kt index b7b6ba969..4b9742b75 100644 --- a/ui/uistateadapter/capture/src/test/java/com/google/jetpackcamera/ui/uistateadapter/capture/FlashModeUiStateAdapterTest.kt +++ b/ui/uistateadapter/capture/src/test/java/com/google/jetpackcamera/ui/uistateadapter/capture/FlashModeUiStateAdapterTest.kt @@ -372,9 +372,14 @@ class FlashModeUiStateAdapterTest { ) // Skip the test if LOW_LIGHT_BOOST is not present in the lens constraints - val currentLensConstraints = systemConstraints.perLensConstraints[appSettings.cameraLensFacing] - val isLlbSupported = currentLensConstraints?.supportedFlashModes?.contains(FlashMode.LOW_LIGHT_BOOST) == true - assumeTrue("Skipping: Low Light Boost is not supported by constraints", isLlbSupported) + val currentLensConstraints = + systemConstraints.perLensConstraints[appSettings.cameraLensFacing] + val isLlbSupported = currentLensConstraints?.supportedFlashModes + ?.contains(FlashMode.LOW_LIGHT_BOOST) == true + assumeTrue( + "Skipping: Low Light Boost is not supported by constraints", + isLlbSupported + ) val hdrUiState = HdrUiState.Available( selectedImageFormat = ImageOutputFormat.JPEG, 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 e9b073fac..a004d7735 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 @@ -327,6 +327,4 @@ internal class HdrUiStateAdapterTest { val availableState = hdrUiState as HdrUiState.Available assertThat(availableState.isSupported).isFalse() } - } - From 687f0a192e4cba39630d06ae1abb7d0ddfa06633 Mon Sep 17 00:00:00 2001 From: Kimberly Crevecoeur Date: Fri, 17 Jul 2026 11:36:03 -0700 Subject: [PATCH 42/51] Address PR comments: refactor Quick Settings layouts, decouple HDR state, and hide unsupported flash modes --- .../feature/preview/PreviewScreen.kt | 4 +- .../feature/preview/PreviewViewModel.kt | 2 +- gradle.properties | 4 +- .../quicksettings/QuickSettingsScreen.kt | 67 ++++++------ .../ui/QuickSettingsComponents.kt | 100 +++++++----------- .../capture/src/main/res/values/strings.xml | 13 ++- .../impl/QuickSettingsControllerImpl.kt | 4 +- .../capture/FlashModeUiStateAdapter.kt | 60 +++++------ .../capture/compound/CaptureUiStateAdapter.kt | 5 +- .../capture/FlashModeUiStateAdapterTest.kt | 96 +++++++++++------ .../capture/HdrUiStateAdapterTest.kt | 4 +- 11 files changed, 188 insertions(+), 171 deletions(-) diff --git a/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewScreen.kt b/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewScreen.kt index 9fbe2bd70..5ddab542b 100644 --- a/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewScreen.kt +++ b/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewScreen.kt @@ -361,8 +361,8 @@ private fun ContentScreen( val flashModeIndicatorLambda = remember { @Composable { modifier: Modifier -> FlashModeIndicator( - modifier = modifier, - flashModeUiStateProvider = { flashModeState.value } + flashModeUiState = flashModeState.value, + modifier = modifier ) } } diff --git a/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewViewModel.kt b/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewViewModel.kt index a54fe3988..a694995f7 100644 --- a/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewViewModel.kt +++ b/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewViewModel.kt @@ -163,8 +163,8 @@ class PreviewViewModel @Inject constructor( val quickSettingsController: QuickSettingsController = QuickSettingsControllerImpl( trackedCaptureUiState = trackedCaptureUiState, - cameraSystem = cameraSystemRepository.cameraSystem, externalCaptureMode = externalCaptureMode, + cameraSystem = cameraSystemRepository.cameraSystem, coroutineContext = viewModelScope.coroutineContext ) diff --git a/gradle.properties b/gradle.properties index 15ab33aba..cc44676e5 100644 --- a/gradle.properties +++ b/gradle.properties @@ -44,4 +44,6 @@ android.nonFinalResIds=false android.experimental.testOptions.managedDevices.maxConcurrentDevices=1 android.experimental.testOptions.managedDevices.setupTimeoutMinutes=180 # Ensure we can run managed devices on servers that don't support hardware rendering -android.testoptions.manageddevices.emulator.gpu=swiftshader_indirect \ No newline at end of file +android.testoptions.manageddevices.emulator.gpu=swiftshader_indirect +# Enabled parallel sync for Gradle 9.4+ +org.gradle.tooling.parallel=true 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 794638e21..4e0ec71d3 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 @@ -15,7 +15,9 @@ */ package com.google.jetpackcamera.ui.components.capture.quicksettings +import androidx.annotation.StringRes import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.padding import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme @@ -60,7 +62,7 @@ fun QuickSettingsBottomSheet( if (quickSettingsUiState is QuickSettingsUiState.Available && quickSettingsUiState.quickSettingsIsOpen ) { - val sheetState = rememberModalBottomSheetState() + val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) BottomSheetComponent( modifier = modifier, onDismiss = quickSettingsController::toggleQuickSettings, @@ -106,19 +108,37 @@ fun QuickSettingsBottomSheet( } @Composable -private fun HybridQuickSettings( - quickSettingsUiState: QuickSettingsUiState.Available, - quickSettingsController: QuickSettingsController, +private fun QuickSettingsLayout( + @StringRes titleRes: Int, + showMoreSettingsButton: Boolean, onNavigateToSettings: () -> Unit, - showMoreSettingsButton: Boolean + content: @Composable ColumnScope.() -> Unit ) { Column { Text( - modifier = Modifier.padding(16.dp), - text = stringResource(id = R.string.quick_settings_title_photo_and_video_settings), + modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom = 8.dp), + text = stringResource(id = titleRes), style = MaterialTheme.typography.titleLarge ) + content() + } + if (showMoreSettingsButton) { + QuickNavSettings(onNavigateToSettings = onNavigateToSettings) + } +} +@Composable +private fun HybridQuickSettings( + quickSettingsUiState: QuickSettingsUiState.Available, + quickSettingsController: QuickSettingsController, + onNavigateToSettings: () -> Unit, + showMoreSettingsButton: Boolean +) { + QuickSettingsLayout( + titleRes = R.string.quick_settings_title_photo_and_video_settings, + showMoreSettingsButton = showMoreSettingsButton, + onNavigateToSettings = onNavigateToSettings + ) { // Flash Mode settings if (quickSettingsUiState.flashModeUiState is FlashModeUiState.Available) { FlashRow( @@ -154,9 +174,6 @@ private fun HybridQuickSettings( ) } } - if (showMoreSettingsButton) { - QuickNavSettings(onNavigateToSettings = onNavigateToSettings) - } } @Composable @@ -166,13 +183,11 @@ private fun VideoQuickSettings( onNavigateToSettings: () -> Unit, showMoreSettingsButton: Boolean ) { - Column { - Text( - modifier = Modifier.padding(16.dp), - text = stringResource(id = R.string.quick_settings_title_video_settings), - style = MaterialTheme.typography.titleLarge - ) - + QuickSettingsLayout( + titleRes = R.string.quick_settings_title_video_settings, + showMoreSettingsButton = showMoreSettingsButton, + onNavigateToSettings = onNavigateToSettings + ) { // Flash Mode settings if (quickSettingsUiState.flashModeUiState is FlashModeUiState.Available) { FlashRow( @@ -194,9 +209,6 @@ private fun VideoQuickSettings( // TODO: Add Resolution setting // TODO: Add Stabilization setting } - if (showMoreSettingsButton) { - QuickNavSettings(onNavigateToSettings = onNavigateToSettings) - } } @Composable @@ -206,13 +218,11 @@ private fun ImageQuickSettings( onNavigateToSettings: () -> Unit, showMoreSettingsButton: Boolean ) { - Column { - Text( - modifier = Modifier.padding(16.dp), - text = stringResource(id = R.string.quick_settings_title_photo_settings), - style = MaterialTheme.typography.titleLarge - ) - + QuickSettingsLayout( + titleRes = R.string.quick_settings_title_photo_settings, + showMoreSettingsButton = showMoreSettingsButton, + onNavigateToSettings = onNavigateToSettings + ) { // Flash Mode settings if (quickSettingsUiState.flashModeUiState is FlashModeUiState.Available) { FlashRow( @@ -241,9 +251,6 @@ private fun ImageQuickSettings( // TODO: Add pre-capture timer setting } - if (showMoreSettingsButton) { - QuickNavSettings(onNavigateToSettings = onNavigateToSettings) - } } /** diff --git a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/ui/QuickSettingsComponents.kt b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/ui/QuickSettingsComponents.kt index 32a9d3af6..a2a407fd9 100644 --- a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/ui/QuickSettingsComponents.kt +++ b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/ui/QuickSettingsComponents.kt @@ -18,6 +18,8 @@ package com.google.jetpackcamera.ui.components.capture.quicksettings.ui import androidx.annotation.StringRes import androidx.compose.foundation.background import androidx.compose.foundation.clickable +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -217,13 +219,14 @@ private fun CaptureModeToggleButton( * @param modifier The [Modifier] to be applied to this composable. * @param onDismiss The lambda function to be invoked when the bottom sheet is dismissed. * @param sheetState The [SheetState] controlling the visibility and behavior of the bottom sheet. + * @param content The composable content to display inside the bottom sheet. */ @OptIn(ExperimentalMaterial3Api::class) @Composable fun QuickSettingsBottomSheet( - modifier: Modifier, onDismiss: () -> Unit, sheetState: SheetState, + modifier: Modifier = Modifier, content: @Composable () -> Unit ) { val openDescription = stringResource(R.string.quick_settings_toggle_open_description) @@ -450,7 +453,7 @@ private fun SettingRow( Row( modifier = modifier .fillMaxWidth() - .padding(vertical = 12.dp, horizontal = 16.dp), + .padding(vertical = 4.dp, horizontal = 16.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween ) { @@ -461,12 +464,12 @@ private fun SettingRow( ) { Text( text = title, - style = MaterialTheme.typography.titleMedium, + style = MaterialTheme.typography.labelMedium, color = MaterialTheme.colorScheme.onSurface ) Text( text = stateSubtitle, - style = MaterialTheme.typography.bodyMedium, + style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.onSurfaceVariant ) } @@ -489,7 +492,6 @@ private fun QuickSettingToggleSelectorButton( ) { QuickSettingToggleSelectorButton( modifier = modifier, - text = stringResource(id = enum.getTextResId()), accessibilityText = stringResource(id = enum.getDescriptionResId()), onClick = { onClick() }, isSelected = isSelected, @@ -499,12 +501,9 @@ private fun QuickSettingToggleSelectorButton( } /** - * A customizable toggle button used within the quick settings menu. This button displays an icon - * and a text label, and can be highlighted to indicate a selected state. It serves as a generic - * component for various quick settings options. + * A customizable toggle button used within the quick settings menu. * * @param onClick The lambda function to be invoked when the button is clicked. - * @param text The text label displayed below the icon. * @param accessibilityText The content description for accessibility purposes. * @param painter The [Painter] for the icon displayed inside the button. * @param modifier The [Modifier] to be applied to this composable. @@ -515,7 +514,6 @@ private fun QuickSettingToggleSelectorButton( @Composable private fun QuickSettingToggleSelectorButton( onClick: () -> Unit, - text: String, accessibilityText: String, painter: Painter, modifier: Modifier = Modifier, @@ -526,44 +524,28 @@ private fun QuickSettingToggleSelectorButton( IconButtonDefaults.IconButtonWidthOption.Narrow ) - Column( - modifier = Modifier.width(width = buttonSize.width), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center + FilledIconToggleButton( + modifier = modifier + .minimumInteractiveComponentSize() + .size(buttonSize), + checked = isSelected, + enabled = enabled, + onCheckedChange = { _ -> onClick() }, + shapes = IconButtonDefaults.toggleableShapes( + shape = CircleShape, + checkedShape = RoundedCornerShape(12.dp) + ), + colors = IconButtonDefaults.filledIconToggleButtonColors( + containerColor = Color.White.copy(alpha = 0.20f), + checkedContainerColor = MaterialTheme.colorScheme.primary, + contentColor = MaterialTheme.colorScheme.onSurface, + checkedContentColor = MaterialTheme.colorScheme.onPrimary + ) ) { - FilledIconToggleButton( - modifier = modifier - .minimumInteractiveComponentSize() - .size(buttonSize), - checked = isSelected, - enabled = enabled, - onCheckedChange = { _ -> onClick() }, - // 1. Size updated to width 48.dp and height 56.dp - - shapes = IconButtonDefaults.toggleableShapes(), - colors = IconButtonDefaults.filledIconToggleButtonColors() - .copy(containerColor = Color.White.copy(alpha = .17f)) - ) { - Icon( - modifier = Modifier.size(IconButtonDefaults.mediumIconSize), - painter = painter, - contentDescription = accessibilityText - ) - } - - Spacer(Modifier.height(4.dp)) - Text( - modifier = Modifier - .width(IntrinsicSize.Max) - .wrapContentWidth() - .semantics { hideFromAccessibility() }, - text = text, - style = MaterialTheme.typography.labelMedium, - color = MaterialTheme.colorScheme.onSurface, - textAlign = TextAlign.Center, - minLines = 2, - maxLines = 2, - overflow = TextOverflow.Ellipsis + Icon( + modifier = Modifier.size(IconButtonDefaults.mediumIconSize), + painter = painter, + contentDescription = accessibilityText ) } } @@ -594,14 +576,14 @@ fun HdrIndicator(hdrUiState: HdrUiState, modifier: Modifier = Modifier) { * A composable that displays an icon indicating the current flash mode. * * @param modifier the modifier for this component. - * @param flashModeUiStateProvider the provider for [FlashModeUiState] for this component. + * @param flashModeUiState the [FlashModeUiState] for this component. */ @Composable fun FlashModeIndicator( - modifier: Modifier = Modifier, - flashModeUiStateProvider: () -> FlashModeUiState + flashModeUiState: FlashModeUiState, + modifier: Modifier = Modifier ) { - when (val flashModeUiState = flashModeUiStateProvider()) { + when (flashModeUiState) { is FlashModeUiState.Unavailable -> TopBarQuickSettingIcon( modifier = modifier, @@ -695,7 +677,6 @@ private fun QuickSettingToggleButtonPreview() { // Instance 1: Unchecked state QuickSettingToggleSelectorButton( onClick = {}, - text = "Flash Off", accessibilityText = "", painter = CameraFlashMode.OFF.getPainter(), isSelected = false, @@ -705,7 +686,6 @@ private fun QuickSettingToggleButtonPreview() { // Instance 2: Checked state QuickSettingToggleSelectorButton( onClick = {}, - text = "Flash On", accessibilityText = "", painter = CameraFlashMode.ON.getPainter(), isSelected = true, @@ -715,7 +695,6 @@ private fun QuickSettingToggleButtonPreview() { // Instance 3: Disabled state QuickSettingToggleSelectorButton( onClick = {}, - text = "Flash Off", accessibilityText = "", painter = CameraFlashMode.OFF.getPainter(), isSelected = false, @@ -734,29 +713,26 @@ private fun PreviewSettingRowDark() { color = Color.Black // Consistent with Camera UI ) { SettingRow( - title = "Video Resolution", - stateSubtitle = "Standard Definition" + title = stringResource(R.string.quick_settings_preview_video_resolution), + stateSubtitle = stringResource(R.string.quick_settings_preview_standard_definition) ) { // Off State (Highlighted per your screenshot) QuickSettingToggleSelectorButton( - text = "SD", - accessibilityText = "Flash Off", + accessibilityText = stringResource(R.string.quick_settings_preview_sd_description), painter = painterResource(id = R.drawable.video_resolution_sd_icon), isSelected = true, onClick = {} ) // On State QuickSettingToggleSelectorButton( - text = "HD", - accessibilityText = "High Definition", + accessibilityText = stringResource(R.string.quick_settings_preview_hd_description), painter = painterResource(id = R.drawable.video_resolution_hd_icon), isSelected = false, onClick = {} ) // Auto State QuickSettingToggleSelectorButton( - text = "FHD", - accessibilityText = "Full High Definition", + accessibilityText = stringResource(R.string.quick_settings_preview_fhd_description), painter = painterResource(id = R.drawable.video_resolution_fhd_icon), isSelected = false, onClick = {} diff --git a/ui/components/capture/src/main/res/values/strings.xml b/ui/components/capture/src/main/res/values/strings.xml index 3819d069f..637539c55 100644 --- a/ui/components/capture/src/main/res/values/strings.xml +++ b/ui/components/capture/src/main/res/values/strings.xml @@ -115,9 +115,9 @@ View recently saved media - Video Settings - Photo Settings - Photo and Video Settings + Video settings + Photo settings + Photo and video settings Capture Mode @@ -135,4 +135,11 @@ Auto On Low Light Boost + + + Video Resolution + Standard Definition + Standard Definition + High Definition + Full High Definition \ No newline at end of file diff --git a/ui/controller/impl/src/main/java/com/google/jetpackcamera/ui/controller/impl/QuickSettingsControllerImpl.kt b/ui/controller/impl/src/main/java/com/google/jetpackcamera/ui/controller/impl/QuickSettingsControllerImpl.kt index ec95ede15..8eb00ebcc 100644 --- a/ui/controller/impl/src/main/java/com/google/jetpackcamera/ui/controller/impl/QuickSettingsControllerImpl.kt +++ b/ui/controller/impl/src/main/java/com/google/jetpackcamera/ui/controller/impl/QuickSettingsControllerImpl.kt @@ -39,14 +39,14 @@ import kotlinx.coroutines.launch * [trackedCaptureUiState]. * * @param trackedCaptureUiState The state flow to update with quick settings information. + * @param externalCaptureMode The external capture mode active. * @param cameraSystem The camera system to control. - * @param externalCaptureMode The current external capture mode. * @param coroutineContext The [CoroutineContext] for launching coroutines. */ class QuickSettingsControllerImpl( private val trackedCaptureUiState: MutableStateFlow, - private val cameraSystem: CameraSystem, private val externalCaptureMode: ExternalCaptureMode, + private val cameraSystem: CameraSystem, coroutineContext: CoroutineContext ) : QuickSettingsController { private val job = Job(parent = coroutineContext[Job.Key]) diff --git a/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/FlashModeUiStateAdapter.kt b/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/FlashModeUiStateAdapter.kt index c43848c71..7ab1dab94 100644 --- a/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/FlashModeUiStateAdapter.kt +++ b/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/FlashModeUiStateAdapter.kt @@ -17,6 +17,7 @@ package com.google.jetpackcamera.ui.uistateadapter.capture import com.google.jetpackcamera.core.camera.CameraState import com.google.jetpackcamera.model.ConcurrentCameraMode +import com.google.jetpackcamera.model.CaptureMode import com.google.jetpackcamera.model.DynamicRange import com.google.jetpackcamera.model.FlashMode import com.google.jetpackcamera.model.ImageOutputFormat @@ -28,7 +29,6 @@ import com.google.jetpackcamera.ui.uistate.SingleSelectableUiState import com.google.jetpackcamera.ui.uistate.capture.FlashModeUiState import com.google.jetpackcamera.ui.uistate.capture.FlashModeUiState.Available import com.google.jetpackcamera.ui.uistate.capture.FlashModeUiState.Unavailable -import com.google.jetpackcamera.ui.uistate.capture.HdrUiState private val ORDERED_UI_SUPPORTED_FLASH_MODES = listOf( FlashMode.OFF, @@ -47,19 +47,17 @@ private val ORDERED_UI_SUPPORTED_FLASH_MODES = listOf( * 3. Support by the currently active lens. * 4. Interactions with other settings (e.g., HDR, Concurrent Camera). * - * Modes not supported by the device or not in `visibleFlashModes` are hidden. - * Modes not supported by the current lens are shown as disabled. - * Modes supported by the current lens are shown as enabled. - * - * @param cameraAppSettings The current settings of the camera. - * @param systemConstraints The hardware capabilities of the camera system. - * @param hdrUiState The UI state for HDR, used to check Flash/HDR conflicts. - * @return A [FlashModeUiState] which is either [Available] or [Unavailable]. - */ + * Modes not supported by the device or not in `visibleFlashModes` are hidden. + * Modes not supported by the current lens are hidden. + * Modes supported by the current lens are shown as enabled, or disabled if in conflict. + * + * @param cameraAppSettings The current settings of the camera. + * @param systemConstraints The hardware capabilities of the camera system. + * @return A [FlashModeUiState] which is either [Available] or [Unavailable]. + */ internal fun FlashModeUiState.Companion.from( cameraAppSettings: CameraAppSettings, - systemConstraints: CameraSystemConstraints, - hdrUiState: HdrUiState = HdrUiState.Unavailable + systemConstraints: CameraSystemConstraints // todo(kc): supply visible flash modes from developer options // visibleFlashModes: Set = ORDERED_UI_SUPPORTED_FLASH_MODES.toSet() ): FlashModeUiState { @@ -75,11 +73,13 @@ internal fun FlashModeUiState.Companion.from( val currentLensSupportedFlashModes = systemConstraints.forCurrentLens(cameraAppSettings) ?.supportedFlashModes ?: setOf(FlashMode.OFF) - val isHdrOn = hdrUiState is HdrUiState.Available && - ( - hdrUiState.selectedDynamicRange == DynamicRange.HLG10 || - hdrUiState.selectedImageFormat == ImageOutputFormat.JPEG_ULTRA_HDR - ) + val isHdrOn = ( + cameraAppSettings.captureMode == CaptureMode.IMAGE_ONLY && + cameraAppSettings.imageFormat == ImageOutputFormat.JPEG_ULTRA_HDR + ) || ( + cameraAppSettings.captureMode == CaptureMode.VIDEO_ONLY && + cameraAppSettings.dynamicRange == DynamicRange.HLG10 + ) val displayableModes = mutableListOf>() @@ -95,28 +95,28 @@ internal fun FlashModeUiState.Companion.from( continue }*/ - // 3. Special handling for LOW_LIGHT_BOOST based on other settings. + // 3. Hide if not supported on the current lens. + if (!currentLensSupportedFlashModes.contains(mode)) { + continue + } + + // 4. Special handling for LOW_LIGHT_BOOST based on other settings. if (mode == FlashMode.LOW_LIGHT_BOOST) { if (cameraAppSettings.concurrentCameraMode == ConcurrentCameraMode.DUAL) { continue // Hide LLB if Dual Camera is active } } - // 4. Determine if Enabled or Disabled + // 5. Determine if Enabled or Disabled val isLlbHdrConflict = mode == FlashMode.LOW_LIGHT_BOOST && isHdrOn - if (currentLensSupportedFlashModes.contains(mode) && !isLlbHdrConflict) { + if (!isLlbHdrConflict) { displayableModes.add(SingleSelectableUiState.SelectableUi(mode)) // Enabled } else { - val reason = if (!currentLensSupportedFlashModes.contains(mode)) { - DisabledReason.FLASH_UNSUPPORTED_ON_LENS - } else { - DisabledReason.LLB_DISABLED_BY_HDR - } displayableModes.add( SingleSelectableUiState.Disabled( value = mode, - disabledReason = reason + disabledReason = DisabledReason.LLB_DISABLED_BY_HDR ) ) // Disabled } @@ -145,24 +145,22 @@ internal fun FlashModeUiState.Companion.from( * @param cameraAppSettings The current application settings for the camera. * @param systemConstraints The hardware capabilities of the camera system. * @param cameraState The real-time state from the camera, used to check [LowLightBoostState]. - * @param hdrUiState The UI state for HDR, used to check Flash/HDR conflicts. * @return An updated [FlashModeUiState]. */ internal fun FlashModeUiState.updateFrom( cameraAppSettings: CameraAppSettings, systemConstraints: CameraSystemConstraints, - cameraState: CameraState, - hdrUiState: HdrUiState = HdrUiState.Unavailable + cameraState: CameraState ): FlashModeUiState { return when (this) { is Unavailable -> { // When previous state was "Unavailable", we'll try to create a new FlashModeUiState - FlashModeUiState.from(cameraAppSettings, systemConstraints, hdrUiState) + FlashModeUiState.from(cameraAppSettings, systemConstraints) } is Available -> { // Regenerate the potential new state based on the latest settings - val newUiState = FlashModeUiState.from(cameraAppSettings, systemConstraints, hdrUiState) + val newUiState = FlashModeUiState.from(cameraAppSettings, systemConstraints) when (newUiState) { is Unavailable -> newUiState diff --git a/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/compound/CaptureUiStateAdapter.kt b/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/compound/CaptureUiStateAdapter.kt index a56b6b975..2afabe86f 100644 --- a/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/compound/CaptureUiStateAdapter.kt +++ b/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/compound/CaptureUiStateAdapter.kt @@ -103,10 +103,9 @@ fun captureUiState( it?.updateFrom( cameraAppSettings = cameraAppSettings, systemConstraints = systemConstraints, - cameraState = roundedCameraState, - hdrUiState = hdrUiState + cameraState = roundedCameraState ) - ?: FlashModeUiState.from(cameraAppSettings, systemConstraints, hdrUiState) + ?: FlashModeUiState.from(cameraAppSettings, systemConstraints) } focusMeteringUiState = focusMeteringUiState.let { it?.updateFrom( diff --git a/ui/uistateadapter/capture/src/test/java/com/google/jetpackcamera/ui/uistateadapter/capture/FlashModeUiStateAdapterTest.kt b/ui/uistateadapter/capture/src/test/java/com/google/jetpackcamera/ui/uistateadapter/capture/FlashModeUiStateAdapterTest.kt index 4b9742b75..ed847bd0e 100644 --- a/ui/uistateadapter/capture/src/test/java/com/google/jetpackcamera/ui/uistateadapter/capture/FlashModeUiStateAdapterTest.kt +++ b/ui/uistateadapter/capture/src/test/java/com/google/jetpackcamera/ui/uistateadapter/capture/FlashModeUiStateAdapterTest.kt @@ -17,6 +17,8 @@ package com.google.jetpackcamera.ui.uistateadapter.capture import com.google.common.truth.Truth.assertThat import com.google.jetpackcamera.core.camera.CameraState +import com.google.jetpackcamera.model.CaptureMode +import com.google.jetpackcamera.model.ConcurrentCameraMode import com.google.jetpackcamera.model.DynamicRange import com.google.jetpackcamera.model.FlashMode import com.google.jetpackcamera.model.ImageOutputFormat @@ -268,8 +270,12 @@ class FlashModeUiStateAdapterTest { @Test fun from_hdrImageOn_lowLightBoostDisabled() { - // Given a system supporting LLB, but HDR image is ON - val appSettings = defaultCameraAppSettings.copy(flashMode = FlashMode.OFF) + // Given a system supporting LLB, but HDR image is ON in settings + val appSettings = defaultCameraAppSettings.copy( + flashMode = FlashMode.OFF, + captureMode = CaptureMode.IMAGE_ONLY, + imageFormat = ImageOutputFormat.JPEG_ULTRA_HDR + ) val systemConstraints = CameraSystemConstraints( perLensConstraints = mapOf( appSettings.cameraLensFacing to emptyCameraConstraints.copy( @@ -277,13 +283,9 @@ class FlashModeUiStateAdapterTest { ) ) ) - val hdrUiState = HdrUiState.Available( - selectedImageFormat = ImageOutputFormat.JPEG_ULTRA_HDR, - selectedDynamicRange = DynamicRange.SDR - ) // When - val flashModeUiState = FlashModeUiState.from(appSettings, systemConstraints, hdrUiState) + val flashModeUiState = FlashModeUiState.from(appSettings, systemConstraints) // Then LLB is disabled because of HDR assertThat(flashModeUiState).isInstanceOf(FlashModeUiState.Available::class.java) @@ -298,8 +300,12 @@ class FlashModeUiStateAdapterTest { @Test fun from_hdrVideoOn_lowLightBoostDisabled() { - // Given a system supporting LLB, but HDR video is ON - val appSettings = defaultCameraAppSettings.copy(flashMode = FlashMode.OFF) + // Given a system supporting LLB, but HDR video is ON in settings + val appSettings = defaultCameraAppSettings.copy( + flashMode = FlashMode.OFF, + captureMode = CaptureMode.VIDEO_ONLY, + dynamicRange = DynamicRange.HLG10 + ) val systemConstraints = CameraSystemConstraints( perLensConstraints = mapOf( appSettings.cameraLensFacing to emptyCameraConstraints.copy( @@ -307,13 +313,9 @@ class FlashModeUiStateAdapterTest { ) ) ) - val hdrUiState = HdrUiState.Available( - selectedImageFormat = ImageOutputFormat.JPEG, - selectedDynamicRange = DynamicRange.HLG10 - ) // When - val flashModeUiState = FlashModeUiState.from(appSettings, systemConstraints, hdrUiState) + val flashModeUiState = FlashModeUiState.from(appSettings, systemConstraints) // Then LLB is disabled because of HDR assertThat(flashModeUiState).isInstanceOf(FlashModeUiState.Available::class.java) @@ -326,8 +328,8 @@ class FlashModeUiStateAdapterTest { } @Test - fun from_flashUnsupportedOnCurrentLens_flashModeDisabled() { - // Given a device that supports FLASH_ON on some lens, but NOT on the lens + fun from_flashUnsupportedOnCurrentLens_flashModeHidden() { + // Given a device that supports FLASH_ON on some lens, but NOT on the current lens (which supports OFF and AUTO) val appSettings = defaultCameraAppSettings.copy( cameraLensFacing = LensFacing.BACK, flashMode = FlashMode.OFF @@ -336,12 +338,12 @@ class FlashModeUiStateAdapterTest { availableLenses = listOf(LensFacing.BACK, LensFacing.FRONT), perLensConstraints = mapOf( LensFacing.BACK to emptyCameraConstraints.copy( - // Current lens doesn't support ON - supportedFlashModes = setOf(FlashMode.OFF) + // Current lens supports OFF and AUTO + supportedFlashModes = setOf(FlashMode.OFF, FlashMode.AUTO) ), LensFacing.FRONT to emptyCameraConstraints.copy( - // Other lens supports ON - supportedFlashModes = setOf(FlashMode.OFF, FlashMode.ON) + // Other lens supports ON as well + supportedFlashModes = setOf(FlashMode.OFF, FlashMode.AUTO, FlashMode.ON) ) ) ) @@ -349,20 +351,24 @@ class FlashModeUiStateAdapterTest { // When val flashModeUiState = FlashModeUiState.from(appSettings, systemConstraints) - // Then FLASH_ON is disabled because it is unsupported on the current lens + // Then FLASH_ON is hidden because it is unsupported on the current lens assertThat(flashModeUiState).isInstanceOf(FlashModeUiState.Available::class.java) val availableUiState = flashModeUiState as FlashModeUiState.Available - val flashOnState = availableUiState.availableFlashModes.find { it.value == FlashMode.ON } - assertThat(flashOnState).isInstanceOf(SingleSelectableUiState.Disabled::class.java) - val disabledFlashOn = flashOnState as SingleSelectableUiState.Disabled - assertThat(disabledFlashOn.disabledReason) - .isEqualTo(DisabledReason.FLASH_UNSUPPORTED_ON_LENS) + val hasFlashOn = availableUiState.availableFlashModes.any { it.value == FlashMode.ON } + assertThat(hasFlashOn).isFalse() + // Ensure other modes are present + val hasFlashAuto = availableUiState.availableFlashModes.any { it.value == FlashMode.AUTO } + assertThat(hasFlashAuto).isTrue() } @Test fun from_hdrOff_lowLightBoostEnabled() { - // Given a system supporting LLB, and HDR is OFF (JPEG and SDR) - val appSettings = defaultCameraAppSettings.copy(flashMode = FlashMode.OFF) + // Given a system supporting LLB, and HDR is OFF in settings + val appSettings = defaultCameraAppSettings.copy( + flashMode = FlashMode.OFF, + captureMode = CaptureMode.IMAGE_ONLY, + imageFormat = ImageOutputFormat.JPEG + ) val systemConstraints = CameraSystemConstraints( perLensConstraints = mapOf( appSettings.cameraLensFacing to emptyCameraConstraints.copy( @@ -381,13 +387,8 @@ class FlashModeUiStateAdapterTest { isLlbSupported ) - val hdrUiState = HdrUiState.Available( - selectedImageFormat = ImageOutputFormat.JPEG, - selectedDynamicRange = DynamicRange.SDR - ) - // When - val flashModeUiState = FlashModeUiState.from(appSettings, systemConstraints, hdrUiState) + val flashModeUiState = FlashModeUiState.from(appSettings, systemConstraints) // Then LLB is selectable assertThat(flashModeUiState).isInstanceOf(FlashModeUiState.Available::class.java) @@ -397,4 +398,31 @@ class FlashModeUiStateAdapterTest { } assertThat(llbState).isInstanceOf(SingleSelectableUiState.SelectableUi::class.java) } + + @Test + fun from_concurrentCameraDual_lowLightBoostHidden() { + // Given a system supporting ON and LLB, but concurrent camera is DUAL + val appSettings = defaultCameraAppSettings.copy( + flashMode = FlashMode.OFF, + concurrentCameraMode = ConcurrentCameraMode.DUAL + ) + val systemConstraints = CameraSystemConstraints( + perLensConstraints = mapOf( + appSettings.cameraLensFacing to emptyCameraConstraints.copy( + supportedFlashModes = setOf(FlashMode.OFF, FlashMode.ON, FlashMode.LOW_LIGHT_BOOST) + ) + ) + ) + + // When + val flashModeUiState = FlashModeUiState.from(appSettings, systemConstraints) + + // Then LLB is not present in available flash modes, but ON is present (keeping state Available) + assertThat(flashModeUiState).isInstanceOf(FlashModeUiState.Available::class.java) + val availableUiState = flashModeUiState as FlashModeUiState.Available + val hasLlb = availableUiState.availableFlashModes.any { it.value == FlashMode.LOW_LIGHT_BOOST } + assertThat(hasLlb).isFalse() + val hasFlashOn = availableUiState.availableFlashModes.any { it.value == FlashMode.ON } + assertThat(hasFlashOn).isTrue() + } } 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 d87217b97..fdfe87c56 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 @@ -274,12 +274,12 @@ internal class HdrUiStateAdapterTest { perLensConstraints = mapOf( LensFacing.BACK to emptyCameraConstraints.copy( supportedImageFormatsMap = mapOf( - appSettings.streamConfig to setOf(ImageOutputFormat.JPEG) + false to setOf(ImageOutputFormat.JPEG) ) ), LensFacing.FRONT to emptyCameraConstraints.copy( supportedImageFormatsMap = mapOf( - appSettings.streamConfig to setOf( + false to setOf( ImageOutputFormat.JPEG, ImageOutputFormat.JPEG_ULTRA_HDR ) From 67997838eababdf1942c4f76a6f390db7b52ebb5 Mon Sep 17 00:00:00 2001 From: Kimberly Crevecoeur Date: Fri, 17 Jul 2026 11:50:34 -0700 Subject: [PATCH 43/51] spotless --- .../ui/QuickSettingsComponents.kt | 27 +++++++++---------- .../capture/FlashModeUiStateAdapter.kt | 22 +++++++-------- .../capture/FlashModeUiStateAdapterTest.kt | 11 +++++--- 3 files changed, 31 insertions(+), 29 deletions(-) diff --git a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/ui/QuickSettingsComponents.kt b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/ui/QuickSettingsComponents.kt index a2a407fd9..5fa05d463 100644 --- a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/ui/QuickSettingsComponents.kt +++ b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/ui/QuickSettingsComponents.kt @@ -18,22 +18,19 @@ package com.google.jetpackcamera.ui.components.capture.quicksettings.ui import androidx.annotation.StringRes import androidx.compose.foundation.background import androidx.compose.foundation.clickable -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.RowScope -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width -import androidx.compose.foundation.layout.wrapContentWidth +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Button import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @@ -60,12 +57,9 @@ import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.contentDescription -import androidx.compose.ui.semantics.hideFromAccessibility import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.testTag import androidx.compose.ui.semantics.testTagsAsResourceId -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.google.jetpackcamera.model.AspectRatio @@ -579,10 +573,7 @@ fun HdrIndicator(hdrUiState: HdrUiState, modifier: Modifier = Modifier) { * @param flashModeUiState the [FlashModeUiState] for this component. */ @Composable -fun FlashModeIndicator( - flashModeUiState: FlashModeUiState, - modifier: Modifier = Modifier -) { +fun FlashModeIndicator(flashModeUiState: FlashModeUiState, modifier: Modifier = Modifier) { when (flashModeUiState) { is FlashModeUiState.Unavailable -> TopBarQuickSettingIcon( @@ -718,21 +709,27 @@ private fun PreviewSettingRowDark() { ) { // Off State (Highlighted per your screenshot) QuickSettingToggleSelectorButton( - accessibilityText = stringResource(R.string.quick_settings_preview_sd_description), + accessibilityText = stringResource( + R.string.quick_settings_preview_sd_description + ), painter = painterResource(id = R.drawable.video_resolution_sd_icon), isSelected = true, onClick = {} ) // On State QuickSettingToggleSelectorButton( - accessibilityText = stringResource(R.string.quick_settings_preview_hd_description), + accessibilityText = stringResource( + R.string.quick_settings_preview_hd_description + ), painter = painterResource(id = R.drawable.video_resolution_hd_icon), isSelected = false, onClick = {} ) // Auto State QuickSettingToggleSelectorButton( - accessibilityText = stringResource(R.string.quick_settings_preview_fhd_description), + accessibilityText = stringResource( + R.string.quick_settings_preview_fhd_description + ), painter = painterResource(id = R.drawable.video_resolution_fhd_icon), isSelected = false, onClick = {} diff --git a/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/FlashModeUiStateAdapter.kt b/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/FlashModeUiStateAdapter.kt index 7ab1dab94..d9583facf 100644 --- a/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/FlashModeUiStateAdapter.kt +++ b/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/FlashModeUiStateAdapter.kt @@ -16,8 +16,8 @@ package com.google.jetpackcamera.ui.uistateadapter.capture import com.google.jetpackcamera.core.camera.CameraState -import com.google.jetpackcamera.model.ConcurrentCameraMode import com.google.jetpackcamera.model.CaptureMode +import com.google.jetpackcamera.model.ConcurrentCameraMode import com.google.jetpackcamera.model.DynamicRange import com.google.jetpackcamera.model.FlashMode import com.google.jetpackcamera.model.ImageOutputFormat @@ -47,14 +47,14 @@ private val ORDERED_UI_SUPPORTED_FLASH_MODES = listOf( * 3. Support by the currently active lens. * 4. Interactions with other settings (e.g., HDR, Concurrent Camera). * - * Modes not supported by the device or not in `visibleFlashModes` are hidden. - * Modes not supported by the current lens are hidden. - * Modes supported by the current lens are shown as enabled, or disabled if in conflict. - * - * @param cameraAppSettings The current settings of the camera. - * @param systemConstraints The hardware capabilities of the camera system. - * @return A [FlashModeUiState] which is either [Available] or [Unavailable]. - */ + * Modes not supported by the device or not in `visibleFlashModes` are hidden. + * Modes not supported by the current lens are hidden. + * Modes supported by the current lens are shown as enabled, or disabled if in conflict. + * + * @param cameraAppSettings The current settings of the camera. + * @param systemConstraints The hardware capabilities of the camera system. + * @return A [FlashModeUiState] which is either [Available] or [Unavailable]. + */ internal fun FlashModeUiState.Companion.from( cameraAppSettings: CameraAppSettings, systemConstraints: CameraSystemConstraints @@ -76,10 +76,10 @@ internal fun FlashModeUiState.Companion.from( val isHdrOn = ( cameraAppSettings.captureMode == CaptureMode.IMAGE_ONLY && cameraAppSettings.imageFormat == ImageOutputFormat.JPEG_ULTRA_HDR - ) || ( + ) || ( cameraAppSettings.captureMode == CaptureMode.VIDEO_ONLY && cameraAppSettings.dynamicRange == DynamicRange.HLG10 - ) + ) val displayableModes = mutableListOf>() diff --git a/ui/uistateadapter/capture/src/test/java/com/google/jetpackcamera/ui/uistateadapter/capture/FlashModeUiStateAdapterTest.kt b/ui/uistateadapter/capture/src/test/java/com/google/jetpackcamera/ui/uistateadapter/capture/FlashModeUiStateAdapterTest.kt index ed847bd0e..634d564a1 100644 --- a/ui/uistateadapter/capture/src/test/java/com/google/jetpackcamera/ui/uistateadapter/capture/FlashModeUiStateAdapterTest.kt +++ b/ui/uistateadapter/capture/src/test/java/com/google/jetpackcamera/ui/uistateadapter/capture/FlashModeUiStateAdapterTest.kt @@ -29,7 +29,6 @@ import com.google.jetpackcamera.settings.model.CameraConstraints import com.google.jetpackcamera.settings.model.CameraSystemConstraints import com.google.jetpackcamera.ui.uistate.SingleSelectableUiState import com.google.jetpackcamera.ui.uistate.capture.FlashModeUiState -import com.google.jetpackcamera.ui.uistate.capture.HdrUiState import org.junit.Assert.assertThrows import org.junit.Assume.assumeTrue import org.junit.Test @@ -409,7 +408,11 @@ class FlashModeUiStateAdapterTest { val systemConstraints = CameraSystemConstraints( perLensConstraints = mapOf( appSettings.cameraLensFacing to emptyCameraConstraints.copy( - supportedFlashModes = setOf(FlashMode.OFF, FlashMode.ON, FlashMode.LOW_LIGHT_BOOST) + supportedFlashModes = setOf( + FlashMode.OFF, + FlashMode.ON, + FlashMode.LOW_LIGHT_BOOST + ) ) ) ) @@ -420,7 +423,9 @@ class FlashModeUiStateAdapterTest { // Then LLB is not present in available flash modes, but ON is present (keeping state Available) assertThat(flashModeUiState).isInstanceOf(FlashModeUiState.Available::class.java) val availableUiState = flashModeUiState as FlashModeUiState.Available - val hasLlb = availableUiState.availableFlashModes.any { it.value == FlashMode.LOW_LIGHT_BOOST } + val hasLlb = availableUiState.availableFlashModes.any { + it.value == FlashMode.LOW_LIGHT_BOOST + } assertThat(hasLlb).isFalse() val hasFlashOn = availableUiState.availableFlashModes.any { it.value == FlashMode.ON } assertThat(hasFlashOn).isTrue() From c3e221100477bac0baf8a61cbaad5fb462f2342f Mon Sep 17 00:00:00 2001 From: Kimberly Crevecoeur Date: Tue, 28 Jul 2026 09:36:06 -0700 Subject: [PATCH 44/51] Refactor QuickSettingsController and align HDR constraints - Remove redundant `externalCaptureMode` from `QuickSettingsControllerImpl` constructor and streamline selection guards. - Align `CameraXCameraSystem` HDR format constraints check with `HdrUiStateAdapter` by checking if the active effect targets image capture instead of checking for any active effect. - Address PR styling comments by converting trailing comments to dedicated lines. - Fix UI accessibility nit (remove duplicate announcements) and add missing KDoc to `HdrIndicator`. - Create unit tests for `QuickSettingsControllerImpl` and `QuickSettingsUiStateAdapter`, and add test coverage for effect interactions in `HdrUiStateAdapterTest`. --- .../core/camera/CameraXCameraSystem.kt | 9 +- .../feature/preview/PreviewViewModel.kt | 1 - .../ui/QuickSettingsComponents.kt | 11 +- .../impl/QuickSettingsControllerImpl.kt | 16 +-- .../impl/QuickSettingsControllerImplTest.kt | 122 ++++++++++++++++++ .../capture/FlashModeUiStateAdapter.kt | 17 ++- .../capture/HdrUiStateAdapter.kt | 4 +- .../capture/HdrUiStateAdapterTest.kt | 58 +++++++++ .../QuickSettingsUiStateAdapterTest.kt | 59 +++++++++ 9 files changed, 274 insertions(+), 23 deletions(-) create mode 100644 ui/controller/impl/src/test/java/com/google/jetpackcamera/ui/controller/impl/QuickSettingsControllerImplTest.kt create mode 100644 ui/uistateadapter/capture/src/test/java/com/google/jetpackcamera/ui/uistateadapter/capture/compound/QuickSettingsUiStateAdapterTest.kt 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 5aa7e465c..99556c0ee 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 @@ -52,6 +52,7 @@ import com.google.jetpackcamera.core.common.FilePathGenerator import com.google.jetpackcamera.core.common.IODispatcher import com.google.jetpackcamera.model.AspectRatio import com.google.jetpackcamera.model.CameraEffectId +import com.google.jetpackcamera.model.CameraEffectTarget import com.google.jetpackcamera.model.CameraZoomRatio import com.google.jetpackcamera.model.CaptureMode import com.google.jetpackcamera.model.ConcurrentCameraMode @@ -807,9 +808,15 @@ class CameraXCameraSystem( this.copy(aspectRatio = AspectRatio.NINE_SIXTEEN) } + private fun CameraAppSettings.affectsImageCapture(): Boolean { + val activeEffectTargets = systemConstraints.perLensConstraints[cameraLensFacing] + ?.effectTargetsMap?.get(selectedCameraEffect) ?: emptySet() + return activeEffectTargets.contains(CameraEffectTarget.IMAGE_CAPTURE) + } + private fun CameraAppSettings.tryApplyImageFormatConstraints(): CameraAppSettings = systemConstraints.perLensConstraints[cameraLensFacing]?.let { constraints -> - with(constraints.supportedImageFormatsMap[isSingleStreamLayout()]) { + with(constraints.supportedImageFormatsMap[affectsImageCapture()]) { // 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) && diff --git a/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewViewModel.kt b/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewViewModel.kt index a694995f7..5efb1a6cf 100644 --- a/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewViewModel.kt +++ b/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewViewModel.kt @@ -163,7 +163,6 @@ class PreviewViewModel @Inject constructor( val quickSettingsController: QuickSettingsController = QuickSettingsControllerImpl( trackedCaptureUiState = trackedCaptureUiState, - externalCaptureMode = externalCaptureMode, cameraSystem = cameraSystemRepository.cameraSystem, coroutineContext = viewModelScope.coroutineContext ) diff --git a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/ui/QuickSettingsComponents.kt b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/ui/QuickSettingsComponents.kt index 5fa05d463..c949305b2 100644 --- a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/ui/QuickSettingsComponents.kt +++ b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/ui/QuickSettingsComponents.kt @@ -144,7 +144,7 @@ fun ToggleQuickSettingsButton( ) { Icon( painter = painterResource(R.drawable.settings_photo_camera_icon), - contentDescription = stringResource(R.string.quick_settings_toggle_icon_description) + contentDescription = null ) } } @@ -544,6 +544,12 @@ private fun QuickSettingToggleSelectorButton( } } +/** + * An indicator showing the current HDR status (SDR vs HDR). + * + * @param hdrUiState The current [HdrUiState] containing the selected dynamic range and format. + * @param modifier The [Modifier] to be applied to this indicator. + */ @OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable fun HdrIndicator(hdrUiState: HdrUiState, modifier: Modifier = Modifier) { @@ -701,7 +707,8 @@ private fun PreviewSettingRowDark() { MaterialTheme(colorScheme = darkColorScheme()) { Surface( modifier = Modifier.fillMaxWidth(), - color = Color.Black // Consistent with Camera UI + // Consistent with Camera UI + color = Color.Black ) { SettingRow( title = stringResource(R.string.quick_settings_preview_video_resolution), diff --git a/ui/controller/impl/src/main/java/com/google/jetpackcamera/ui/controller/impl/QuickSettingsControllerImpl.kt b/ui/controller/impl/src/main/java/com/google/jetpackcamera/ui/controller/impl/QuickSettingsControllerImpl.kt index 8eb00ebcc..677919d5f 100644 --- a/ui/controller/impl/src/main/java/com/google/jetpackcamera/ui/controller/impl/QuickSettingsControllerImpl.kt +++ b/ui/controller/impl/src/main/java/com/google/jetpackcamera/ui/controller/impl/QuickSettingsControllerImpl.kt @@ -19,7 +19,6 @@ import com.google.jetpackcamera.core.camera.CameraSystem import com.google.jetpackcamera.model.AspectRatio 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.model.LensFacing @@ -45,7 +44,6 @@ import kotlinx.coroutines.launch */ class QuickSettingsControllerImpl( private val trackedCaptureUiState: MutableStateFlow, - private val externalCaptureMode: ExternalCaptureMode, private val cameraSystem: CameraSystem, coroutineContext: CoroutineContext ) : QuickSettingsController { @@ -78,20 +76,14 @@ class QuickSettingsControllerImpl( } override fun setDynamicRange(dynamicRange: DynamicRange) { - if (externalCaptureMode != ExternalCaptureMode.ImageCapture && - externalCaptureMode != ExternalCaptureMode.MultipleImageCapture - ) { - scope.launch { - cameraSystem.setDynamicRange(dynamicRange) - } + scope.launch { + cameraSystem.setDynamicRange(dynamicRange) } } override fun setImageFormat(imageOutputFormat: ImageOutputFormat) { - if (externalCaptureMode != ExternalCaptureMode.VideoCapture) { - scope.launch { - cameraSystem.setImageFormat(imageOutputFormat) - } + scope.launch { + cameraSystem.setImageFormat(imageOutputFormat) } } diff --git a/ui/controller/impl/src/test/java/com/google/jetpackcamera/ui/controller/impl/QuickSettingsControllerImplTest.kt b/ui/controller/impl/src/test/java/com/google/jetpackcamera/ui/controller/impl/QuickSettingsControllerImplTest.kt new file mode 100644 index 000000000..3acbdef1a --- /dev/null +++ b/ui/controller/impl/src/test/java/com/google/jetpackcamera/ui/controller/impl/QuickSettingsControllerImplTest.kt @@ -0,0 +1,122 @@ +/* + * 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.controller.impl + +import com.google.common.truth.Truth.assertThat +import com.google.jetpackcamera.core.camera.testing.FakeCameraSystem +import com.google.jetpackcamera.model.AspectRatio +import com.google.jetpackcamera.model.CaptureMode +import com.google.jetpackcamera.model.DynamicRange +import com.google.jetpackcamera.model.FlashMode +import com.google.jetpackcamera.model.ImageOutputFormat +import com.google.jetpackcamera.model.LensFacing +import com.google.jetpackcamera.ui.uistate.capture.TrackedCaptureUiState +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +@OptIn(ExperimentalCoroutinesApi::class) +@RunWith(JUnit4::class) +internal class QuickSettingsControllerImplTest { + private val testScope = TestScope() + private val testDispatcher = StandardTestDispatcher(testScope.testScheduler) + + private val cameraSystem = FakeCameraSystem() + private val trackedCaptureUiState = MutableStateFlow(TrackedCaptureUiState()) + private lateinit var controller: QuickSettingsControllerImpl + + @Before + fun setup() { + controller = QuickSettingsControllerImpl( + trackedCaptureUiState = trackedCaptureUiState, + cameraSystem = cameraSystem, + coroutineContext = testDispatcher + ) + } + + @Test + fun toggleQuickSettings_mutatesUiState() = testScope.runTest { + val initialValue = trackedCaptureUiState.value.isQuickSettingsOpen + controller.toggleQuickSettings() + assertThat(trackedCaptureUiState.value.isQuickSettingsOpen).isEqualTo(!initialValue) + } + + @Test + fun setLensFacing_mutatesCameraSystem() = testScope.runTest { + controller.setLensFacing(LensFacing.FRONT) + advanceUntilIdle() + assertThat( + cameraSystem.getCurrentSettings().value?.cameraLensFacing + ).isEqualTo(LensFacing.FRONT) + } + + @Test + fun setFlash_mutatesCameraSystem() = testScope.runTest { + controller.setFlash(FlashMode.ON) + advanceUntilIdle() + assertThat(cameraSystem.getCurrentSettings().value?.flashMode).isEqualTo(FlashMode.ON) + } + + @Test + fun setAspectRatio_mutatesCameraSystem() = testScope.runTest { + controller.setAspectRatio(AspectRatio.ONE_ONE) + advanceUntilIdle() + assertThat( + cameraSystem.getCurrentSettings().value?.aspectRatio + ).isEqualTo(AspectRatio.ONE_ONE) + } + + @Test + fun setDynamicRange_mutatesCameraSystem() = testScope.runTest { + controller.setDynamicRange(DynamicRange.HLG10) + advanceUntilIdle() + assertThat( + cameraSystem.getCurrentSettings().value?.dynamicRange + ).isEqualTo(DynamicRange.HLG10) + } + + @Test + fun setImageFormat_mutatesCameraSystem() = testScope.runTest { + controller.setImageFormat(ImageOutputFormat.JPEG_ULTRA_HDR) + advanceUntilIdle() + assertThat( + cameraSystem.getCurrentSettings().value?.imageFormat + ).isEqualTo(ImageOutputFormat.JPEG_ULTRA_HDR) + } + + @Test + fun setCaptureMode_mutatesCameraSystem() = testScope.runTest { + controller.setCaptureMode(CaptureMode.VIDEO_ONLY) + advanceUntilIdle() + assertThat( + cameraSystem.getCurrentSettings().value?.captureMode + ).isEqualTo(CaptureMode.VIDEO_ONLY) + } + + @Test + fun cancelScope_cancelsCoroutineScope() = testScope.runTest { + val job = controller.cancelScope() + job.join() + assertThat(job.isCancelled).isTrue() + } +} diff --git a/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/FlashModeUiStateAdapter.kt b/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/FlashModeUiStateAdapter.kt index d9583facf..c5fda2024 100644 --- a/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/FlashModeUiStateAdapter.kt +++ b/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/FlashModeUiStateAdapter.kt @@ -103,7 +103,8 @@ internal fun FlashModeUiState.Companion.from( // 4. Special handling for LOW_LIGHT_BOOST based on other settings. if (mode == FlashMode.LOW_LIGHT_BOOST) { if (cameraAppSettings.concurrentCameraMode == ConcurrentCameraMode.DUAL) { - continue // Hide LLB if Dual Camera is active + // Hide LLB if Dual Camera is active + continue } } @@ -111,14 +112,15 @@ internal fun FlashModeUiState.Companion.from( val isLlbHdrConflict = mode == FlashMode.LOW_LIGHT_BOOST && isHdrOn if (!isLlbHdrConflict) { - displayableModes.add(SingleSelectableUiState.SelectableUi(mode)) // Enabled + // Enabled + displayableModes.add(SingleSelectableUiState.SelectableUi(mode)) } else { displayableModes.add( SingleSelectableUiState.Disabled( value = mode, disabledReason = DisabledReason.LLB_DISABLED_BY_HDR ) - ) // Disabled + ) } } @@ -134,7 +136,8 @@ internal fun FlashModeUiState.Companion.from( Available( selectedFlashMode = selectedFlashMode, availableFlashModes = displayableModes, - isLowLightBoostActive = false // Initial state + // Initial state + isLowLightBoostActive = false ) } } @@ -183,10 +186,12 @@ internal fun FlashModeUiState.updateFrom( if (this.isLowLightBoostActive != newIsLowLightBoostActive) { copy(isLowLightBoostActive = newIsLowLightBoostActive) } else { - this // Nothing changed + // Nothing changed + this } } else { - this // Nothing changed + // Nothing changed + this } } } 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 8103c316b..cfb77af7d 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 @@ -96,7 +96,9 @@ internal fun HdrUiState.Companion.from( if (deviceSupportsHdrVideo) { val supportsHdrVideo = - cameraConstraints?.supportedDynamicRanges?.contains(DynamicRange.HLG10) == true + cameraConstraints + ?.supportedDynamicRanges + ?.contains(DynamicRange.HLG10) == true val isFlashHdrConflict = cameraAppSettings.flashMode == FlashMode.LOW_LIGHT_BOOST val isConcurrentConflict = cameraAppSettings.concurrentCameraMode == ConcurrentCameraMode.DUAL 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 fdfe87c56..7f8bc5f54 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 @@ -16,6 +16,8 @@ package com.google.jetpackcamera.ui.uistateadapter.capture import com.google.common.truth.Truth.assertThat +import com.google.jetpackcamera.model.CameraEffectId +import com.google.jetpackcamera.model.CameraEffectTarget import com.google.jetpackcamera.model.CaptureMode import com.google.jetpackcamera.model.ConcurrentCameraMode import com.google.jetpackcamera.model.DynamicRange @@ -327,4 +329,60 @@ internal class HdrUiStateAdapterTest { val availableState = hdrUiState as HdrUiState.Available assertThat(availableState.isSupported).isFalse() } + + @Test + fun from_imageOnlyMode_effectNotAffectingImage_hdrSupported() { + val appSettings = defaultCameraAppSettings.copy( + captureMode = CaptureMode.IMAGE_ONLY, + imageFormat = ImageOutputFormat.JPEG_ULTRA_HDR, + selectedCameraEffect = CameraEffectId("post_processing") + ) + val systemConstraints = CameraSystemConstraints( + perLensConstraints = mapOf( + appSettings.cameraLensFacing to emptyCameraConstraints.copy( + effectTargetsMap = mapOf( + CameraEffectId("post_processing") to setOf(CameraEffectTarget.PREVIEW) + ), + supportedImageFormatsMap = mapOf( + false to setOf(ImageOutputFormat.JPEG, ImageOutputFormat.JPEG_ULTRA_HDR), + true to setOf(ImageOutputFormat.JPEG) + ) + ) + ) + ) + + val hdrUiState = HdrUiState.from(appSettings, systemConstraints) + + assertThat(hdrUiState).isInstanceOf(HdrUiState.Available::class.java) + val availableState = hdrUiState as HdrUiState.Available + assertThat(availableState.isSupported).isTrue() + } + + @Test + fun from_imageOnlyMode_effectAffectingImage_hdrUnsupported() { + val appSettings = defaultCameraAppSettings.copy( + captureMode = CaptureMode.IMAGE_ONLY, + imageFormat = ImageOutputFormat.JPEG_ULTRA_HDR, + selectedCameraEffect = CameraEffectId("post_processing") + ) + val systemConstraints = CameraSystemConstraints( + perLensConstraints = mapOf( + appSettings.cameraLensFacing to emptyCameraConstraints.copy( + effectTargetsMap = mapOf( + CameraEffectId("post_processing") to setOf(CameraEffectTarget.IMAGE_CAPTURE) + ), + supportedImageFormatsMap = mapOf( + false to setOf(ImageOutputFormat.JPEG, ImageOutputFormat.JPEG_ULTRA_HDR), + true to setOf(ImageOutputFormat.JPEG) + ) + ) + ) + ) + + val hdrUiState = HdrUiState.from(appSettings, systemConstraints) + + assertThat(hdrUiState).isInstanceOf(HdrUiState.Available::class.java) + val availableState = hdrUiState as HdrUiState.Available + assertThat(availableState.isSupported).isFalse() + } } diff --git a/ui/uistateadapter/capture/src/test/java/com/google/jetpackcamera/ui/uistateadapter/capture/compound/QuickSettingsUiStateAdapterTest.kt b/ui/uistateadapter/capture/src/test/java/com/google/jetpackcamera/ui/uistateadapter/capture/compound/QuickSettingsUiStateAdapterTest.kt new file mode 100644 index 000000000..4b1175c96 --- /dev/null +++ b/ui/uistateadapter/capture/src/test/java/com/google/jetpackcamera/ui/uistateadapter/capture/compound/QuickSettingsUiStateAdapterTest.kt @@ -0,0 +1,59 @@ +/* + * 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.compound + +import com.google.common.truth.Truth.assertThat +import com.google.jetpackcamera.ui.uistate.capture.AspectRatioUiState +import com.google.jetpackcamera.ui.uistate.capture.CaptureModeUiState +import com.google.jetpackcamera.ui.uistate.capture.FlashModeUiState +import com.google.jetpackcamera.ui.uistate.capture.FlipLensUiState +import com.google.jetpackcamera.ui.uistate.capture.HdrUiState +import com.google.jetpackcamera.ui.uistate.capture.compound.QuickSettingsUiState +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +@RunWith(JUnit4::class) +internal class QuickSettingsUiStateAdapterTest { + + @Test + fun from_correctlyBundlesStates() { + val captureModeUiState = CaptureModeUiState.Unavailable + val flashModeUiState = FlashModeUiState.Unavailable + val flipLensUiState = FlipLensUiState.Unavailable + val aspectRatioUiState = AspectRatioUiState.Unavailable + val hdrUiState = HdrUiState.Unavailable + val quickSettingsIsOpen = true + + val quickSettingsUiState = QuickSettingsUiState.from( + captureModeUiState = captureModeUiState, + flashModeUiState = flashModeUiState, + flipLensUiState = flipLensUiState, + aspectRatioUiState = aspectRatioUiState, + hdrUiState = hdrUiState, + quickSettingsIsOpen = quickSettingsIsOpen + ) + + assertThat(quickSettingsUiState).isInstanceOf(QuickSettingsUiState.Available::class.java) + val availableState = quickSettingsUiState as QuickSettingsUiState.Available + assertThat(availableState.captureModeUiState).isEqualTo(captureModeUiState) + assertThat(availableState.flashModeUiState).isEqualTo(flashModeUiState) + assertThat(availableState.flipLensUiState).isEqualTo(flipLensUiState) + assertThat(availableState.aspectRatioUiState).isEqualTo(aspectRatioUiState) + assertThat(availableState.hdrUiState).isEqualTo(hdrUiState) + assertThat(availableState.quickSettingsIsOpen).isEqualTo(quickSettingsIsOpen) + } +} From 54dfcf22066604849d2e09d243c7f7e5c96f620b Mon Sep 17 00:00:00 2001 From: Kimberly Crevecoeur Date: Tue, 28 Jul 2026 09:52:13 -0700 Subject: [PATCH 45/51] Refactor Quick Settings to unified layout and apply code review cleanups - Consolidate HybridQuickSettings, VideoQuickSettings, and ImageQuickSettings in QuickSettingsScreen.kt into a single unified QuickSettingsContent layout that adapts dynamically based on the active CaptureMode. - Rename QuickSettingsBottomSheet in QuickSettingsComponents.kt to QuickSettingsModalBottomSheet to avoid composable name duplication. - Remove redundant remember keys for indicator slot lambdas in PreviewScreen.kt. - Remove unused FLASH_UNSUPPORTED_ON_LENS enum value in DisabledReason.kt. - Change quick settings group title string resources in strings.xml to Title Case for visual consistency. - Format preview annotation and wrap lines exceeding 100 characters in QuickSettingsComponents.kt. - Replace JUnit assertThrows usage with try-catch in FlashModeUiStateAdapterTest.kt to avoid JUnit imports. - Add test coverage for simple selection changes in FlashModeUiStateAdapter.updateFrom. --- .../feature/preview/PreviewScreen.kt | 6 +- .../quicksettings/QuickSettingsScreen.kt | 157 ++++-------------- .../ui/QuickSettingsComponents.kt | 11 +- .../capture/src/main/res/values/strings.xml | 6 +- .../uistateadapter/capture/DisabledReason.kt | 3 - .../capture/FlashModeUiStateAdapterTest.kt | 37 ++++- 6 files changed, 84 insertions(+), 136 deletions(-) diff --git a/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewScreen.kt b/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewScreen.kt index 5ddab542b..7ff12fafe 100644 --- a/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewScreen.kt +++ b/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewScreen.kt @@ -351,7 +351,7 @@ private fun ContentScreen( // Slot lambdas are wrapped in remember blocks to isolate recompositions. val hdrState = remember { derivedStateOf { currentCaptureUiStateProvider().hdrUiState } } - val hdrIndicatorLambda = remember(hdrState) { + val hdrIndicatorLambda = remember { @Composable { modifier: Modifier -> HdrIndicator(modifier = modifier, hdrUiState = hdrState.value) } @@ -368,7 +368,7 @@ private fun ContentScreen( } val videoQualityState = remember { derivedStateOf { currentCaptureUiStateProvider().videoQuality } } - val videoQualityIndicatorLambda = remember(videoQualityState) { + val videoQualityIndicatorLambda = remember { @Composable { modifier: Modifier -> VideoQualityIcon( videoQualityState.value, @@ -381,7 +381,7 @@ private fun ContentScreen( currentCaptureUiStateProvider().stabilizationUiState } } - val stabilizationIndicatorLambda = remember(stabilizationState) { + val stabilizationIndicatorLambda = remember { @Composable { modifier: Modifier -> StabilizationIcon( modifier = modifier, 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 4e0ec71d3..aba109044 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 @@ -39,7 +39,7 @@ import com.google.jetpackcamera.ui.components.capture.quicksettings.ui.CaptureMo import com.google.jetpackcamera.ui.components.capture.quicksettings.ui.FlashRow import com.google.jetpackcamera.ui.components.capture.quicksettings.ui.HdrRow import com.google.jetpackcamera.ui.components.capture.quicksettings.ui.QuickNavSettings -import com.google.jetpackcamera.ui.components.capture.quicksettings.ui.QuickSettingsBottomSheet as BottomSheetComponent +import com.google.jetpackcamera.ui.components.capture.quicksettings.ui.QuickSettingsModalBottomSheet import com.google.jetpackcamera.ui.controller.quicksettings.QuickSettingsController import com.google.jetpackcamera.ui.uistate.capture.AspectRatioUiState import com.google.jetpackcamera.ui.uistate.capture.CaptureModeUiState @@ -63,46 +63,17 @@ fun QuickSettingsBottomSheet( quickSettingsUiState.quickSettingsIsOpen ) { val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) - BottomSheetComponent( + QuickSettingsModalBottomSheet( modifier = modifier, onDismiss = quickSettingsController::toggleQuickSettings, sheetState = sheetState ) { - when (val captureModeUiState = quickSettingsUiState.captureModeUiState) { - is CaptureModeUiState.Available -> { - when (captureModeUiState.selectedCaptureMode) { - CaptureMode.VIDEO_ONLY -> VideoQuickSettings( - quickSettingsUiState = quickSettingsUiState, - quickSettingsController = quickSettingsController, - onNavigateToSettings = onNavigateToSettings, - showMoreSettingsButton = showMoreSettingsButton - ) - - CaptureMode.IMAGE_ONLY -> ImageQuickSettings( - quickSettingsUiState = quickSettingsUiState, - quickSettingsController = quickSettingsController, - onNavigateToSettings = onNavigateToSettings, - showMoreSettingsButton = showMoreSettingsButton - ) - - CaptureMode.STANDARD -> HybridQuickSettings( - quickSettingsUiState = quickSettingsUiState, - quickSettingsController = quickSettingsController, - onNavigateToSettings = onNavigateToSettings, - showMoreSettingsButton = showMoreSettingsButton - ) - } - } - - is CaptureModeUiState.Unavailable -> { - ImageQuickSettings( - quickSettingsUiState = quickSettingsUiState, - quickSettingsController = quickSettingsController, - onNavigateToSettings = onNavigateToSettings, - showMoreSettingsButton = showMoreSettingsButton - ) - } - } + QuickSettingsContent( + quickSettingsUiState = quickSettingsUiState, + quickSettingsController = quickSettingsController, + onNavigateToSettings = onNavigateToSettings, + showMoreSettingsButton = showMoreSettingsButton + ) } } } @@ -128,14 +99,23 @@ private fun QuickSettingsLayout( } @Composable -private fun HybridQuickSettings( +private fun QuickSettingsContent( quickSettingsUiState: QuickSettingsUiState.Available, quickSettingsController: QuickSettingsController, onNavigateToSettings: () -> Unit, showMoreSettingsButton: Boolean ) { + val captureMode = (quickSettingsUiState.captureModeUiState as? CaptureModeUiState.Available) + ?.selectedCaptureMode ?: CaptureMode.IMAGE_ONLY + + val titleRes = when (captureMode) { + CaptureMode.VIDEO_ONLY -> R.string.quick_settings_title_video_settings + CaptureMode.IMAGE_ONLY -> R.string.quick_settings_title_photo_settings + CaptureMode.STANDARD -> R.string.quick_settings_title_photo_and_video_settings + } + QuickSettingsLayout( - titleRes = R.string.quick_settings_title_photo_and_video_settings, + titleRes = titleRes, showMoreSettingsButton = showMoreSettingsButton, onNavigateToSettings = onNavigateToSettings ) { @@ -147,16 +127,20 @@ private fun HybridQuickSettings( ) } - // Capture Mode settings - if (quickSettingsUiState.captureModeUiState is CaptureModeUiState.Available) { + // Capture Mode settings (Standard only) + if (captureMode == CaptureMode.STANDARD && + quickSettingsUiState.captureModeUiState is CaptureModeUiState.Available + ) { CaptureModeRow( onSetCaptureMode = quickSettingsController::setCaptureMode, captureModeUiState = quickSettingsUiState.captureModeUiState ) } - // Aspect Ratio settings - if (quickSettingsUiState.aspectRatioUiState is AspectRatioUiState.Available) { + // Aspect Ratio settings (Standard and Image only) + if ((captureMode == CaptureMode.STANDARD || captureMode == CaptureMode.IMAGE_ONLY) && + quickSettingsUiState.aspectRatioUiState is AspectRatioUiState.Available + ) { AspectRatioRow( aspectRatioUiState = quickSettingsUiState.aspectRatioUiState, onSetAspectRatio = quickSettingsController::setAspectRatio @@ -167,89 +151,18 @@ private fun HybridQuickSettings( if (quickSettingsUiState.hdrUiState is HdrUiState.Available) { HdrRow( onClick = { d: DynamicRange, i: ImageOutputFormat -> - quickSettingsController.setDynamicRange(d) - quickSettingsController.setImageFormat(i) - }, - hdrUiState = quickSettingsUiState.hdrUiState - ) - } - } -} - -@Composable -private fun VideoQuickSettings( - quickSettingsUiState: QuickSettingsUiState.Available, - quickSettingsController: QuickSettingsController, - onNavigateToSettings: () -> Unit, - showMoreSettingsButton: Boolean -) { - QuickSettingsLayout( - titleRes = R.string.quick_settings_title_video_settings, - showMoreSettingsButton = showMoreSettingsButton, - onNavigateToSettings = onNavigateToSettings - ) { - // Flash Mode settings - if (quickSettingsUiState.flashModeUiState is FlashModeUiState.Available) { - FlashRow( - onSetFlashMode = quickSettingsController::setFlash, - flashModeUiState = quickSettingsUiState.flashModeUiState - ) - } - - // HDR settings - if (quickSettingsUiState.hdrUiState is HdrUiState.Available) { - HdrRow( - onClick = { d: DynamicRange, _ -> - quickSettingsController.setDynamicRange(d) - }, - hdrUiState = quickSettingsUiState.hdrUiState - ) - } - // TODO: Add FPS setting - // TODO: Add Resolution setting - // TODO: Add Stabilization setting - } -} - -@Composable -private fun ImageQuickSettings( - quickSettingsUiState: QuickSettingsUiState.Available, - quickSettingsController: QuickSettingsController, - onNavigateToSettings: () -> Unit, - showMoreSettingsButton: Boolean -) { - QuickSettingsLayout( - titleRes = R.string.quick_settings_title_photo_settings, - showMoreSettingsButton = showMoreSettingsButton, - onNavigateToSettings = onNavigateToSettings - ) { - // Flash Mode settings - if (quickSettingsUiState.flashModeUiState is FlashModeUiState.Available) { - FlashRow( - onSetFlashMode = quickSettingsController::setFlash, - flashModeUiState = quickSettingsUiState.flashModeUiState - ) - } - - // Aspect Ratio settings - if (quickSettingsUiState.aspectRatioUiState is AspectRatioUiState.Available) { - AspectRatioRow( - aspectRatioUiState = quickSettingsUiState.aspectRatioUiState, - onSetAspectRatio = quickSettingsController::setAspectRatio - ) - } - - // HDR settings - if (quickSettingsUiState.hdrUiState is HdrUiState.Available) { - HdrRow( - onClick = { _, i: ImageOutputFormat -> - quickSettingsController.setImageFormat(i) + when (captureMode) { + CaptureMode.STANDARD -> { + quickSettingsController.setDynamicRange(d) + quickSettingsController.setImageFormat(i) + } + CaptureMode.VIDEO_ONLY -> quickSettingsController.setDynamicRange(d) + CaptureMode.IMAGE_ONLY -> quickSettingsController.setImageFormat(i) + } }, hdrUiState = quickSettingsUiState.hdrUiState ) } - - // TODO: Add pre-capture timer setting } } diff --git a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/ui/QuickSettingsComponents.kt b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/ui/QuickSettingsComponents.kt index c949305b2..180a53c78 100644 --- a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/ui/QuickSettingsComponents.kt +++ b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/ui/QuickSettingsComponents.kt @@ -197,7 +197,8 @@ private fun CaptureModeToggleButton( captureModeUiState.isCaptureModeSelectable(CaptureMode.IMAGE_ONLY) }, isSelected = - isHighlightEnabled && (assignedCaptureMode == captureModeUiState.selectedCaptureMode) + isHighlightEnabled && + (assignedCaptureMode == captureModeUiState.selectedCaptureMode) ) } @@ -217,7 +218,7 @@ private fun CaptureModeToggleButton( */ @OptIn(ExperimentalMaterial3Api::class) @Composable -fun QuickSettingsBottomSheet( +internal fun QuickSettingsModalBottomSheet( onDismiss: () -> Unit, sheetState: SheetState, modifier: Modifier = Modifier, @@ -701,7 +702,11 @@ private fun QuickSettingToggleButtonPreview() { } } -@Preview(name = "JCA Setting Row - Dark Mode", showBackground = true, backgroundColor = 0xFF000000) +@Preview( + name = "JCA Setting Row - Dark Mode", + showBackground = true, + backgroundColor = 0xFF000000 +) @Composable private fun PreviewSettingRowDark() { MaterialTheme(colorScheme = darkColorScheme()) { diff --git a/ui/components/capture/src/main/res/values/strings.xml b/ui/components/capture/src/main/res/values/strings.xml index 637539c55..4be557d81 100644 --- a/ui/components/capture/src/main/res/values/strings.xml +++ b/ui/components/capture/src/main/res/values/strings.xml @@ -115,9 +115,9 @@ View recently saved media - Video settings - Photo settings - Photo and video settings + Video Settings + Photo Settings + Photo and Video Settings Capture Mode diff --git a/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/DisabledReason.kt b/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/DisabledReason.kt index b58ea2cda..ad983c587 100644 --- a/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/DisabledReason.kt +++ b/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/DisabledReason.kt @@ -54,9 +54,6 @@ enum class DisabledReason( HDR_SIMULTANEOUS_IMAGE_VIDEO_UNSUPPORTED( R.string.toast_hdr_simultaneous_image_video_unsupported ), - FLASH_UNSUPPORTED_ON_LENS( - R.string.toast_flash_unsupported_on_lens - ), LLB_DISABLED_BY_HDR( R.string.toast_llb_disabled_by_hdr ) diff --git a/ui/uistateadapter/capture/src/test/java/com/google/jetpackcamera/ui/uistateadapter/capture/FlashModeUiStateAdapterTest.kt b/ui/uistateadapter/capture/src/test/java/com/google/jetpackcamera/ui/uistateadapter/capture/FlashModeUiStateAdapterTest.kt index 634d564a1..eda3edc4f 100644 --- a/ui/uistateadapter/capture/src/test/java/com/google/jetpackcamera/ui/uistateadapter/capture/FlashModeUiStateAdapterTest.kt +++ b/ui/uistateadapter/capture/src/test/java/com/google/jetpackcamera/ui/uistateadapter/capture/FlashModeUiStateAdapterTest.kt @@ -29,7 +29,6 @@ import com.google.jetpackcamera.settings.model.CameraConstraints import com.google.jetpackcamera.settings.model.CameraSystemConstraints import com.google.jetpackcamera.ui.uistate.SingleSelectableUiState import com.google.jetpackcamera.ui.uistate.capture.FlashModeUiState -import org.junit.Assert.assertThrows import org.junit.Assume.assumeTrue import org.junit.Test import org.junit.runner.RunWith @@ -154,13 +153,17 @@ class FlashModeUiStateAdapterTest { ) // Then an IllegalStateException is thrown - assertThrows(IllegalStateException::class.java) { + var exceptionThrown = false + try { uiState.updateFrom( initialAppSettings, newSystemConstraints, defaultCameraState ) + } catch (e: IllegalStateException) { + exceptionThrown = true } + assertThat(exceptionThrown).isTrue() } @Test @@ -430,4 +433,34 @@ class FlashModeUiStateAdapterTest { val hasFlashOn = availableUiState.availableFlashModes.any { it.value == FlashMode.ON } assertThat(hasFlashOn).isTrue() } + + @Test + fun updateFrom_selectedFlashModeChanged_updatesSelection() { + // Given an initial UI state with OFF selected + val initialAppSettings = defaultCameraAppSettings.copy(flashMode = FlashMode.OFF) + val systemConstraints = CameraSystemConstraints( + perLensConstraints = mapOf( + initialAppSettings.cameraLensFacing to emptyCameraConstraints.copy( + supportedFlashModes = setOf(FlashMode.OFF, FlashMode.ON) + ) + ) + ) + val uiState = FlashModeUiState.from(initialAppSettings, systemConstraints) + assertThat((uiState as FlashModeUiState.Available).selectedFlashMode).isEqualTo(FlashMode.OFF) + + // When the selected flash mode changes to ON in settings, but constraints are unchanged + val newAppSettings = initialAppSettings.copy(flashMode = FlashMode.ON) + val updatedUiState = uiState.updateFrom( + newAppSettings, + systemConstraints, + defaultCameraState + ) + + // Then the selected flash mode in UI state is updated + assertThat(updatedUiState).isInstanceOf(FlashModeUiState.Available::class.java) + val availableUiState = updatedUiState as FlashModeUiState.Available + assertThat(availableUiState.selectedFlashMode).isEqualTo(FlashMode.ON) + // And the available modes did not change + assertThat(availableUiState.availableFlashModes).isEqualTo(uiState.availableFlashModes) + } } From c682cbc27c955448115806e4f4178cb7c06c6b33 Mon Sep 17 00:00:00 2001 From: Kimberly Crevecoeur Date: Wed, 29 Jul 2026 08:48:43 -0700 Subject: [PATCH 46/51] Refactor QuickSettings row components to use a generic list-based row Introduces `QuickSettingsListRow`, a generic helper Composable that handles the boilerplate of iterating over selectable UI states (`SingleSelectableUiState`), mapping values to quick settings icons/labels via `QuickSettingsEnum`, and rendering the buttons. Refactors `FlashRow`, `AspectRatioRow`, and `CaptureModeRow` to utilize this generic helper, which: - Deduplicates list iteration, button configuration, and selection logic. - Eliminates the custom `CaptureModeToggleButton` helper. - Simplifies code maintenance and future addition of quick settings options. --- .../ui/QuickSettingsComponents.kt | 183 +++++++++--------- 1 file changed, 88 insertions(+), 95 deletions(-) diff --git a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/ui/QuickSettingsComponents.kt b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/ui/QuickSettingsComponents.kt index 180a53c78..94f3ad6a4 100644 --- a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/ui/QuickSettingsComponents.kt +++ b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/ui/QuickSettingsComponents.kt @@ -168,39 +168,7 @@ internal fun QuickNavSettings(onNavigateToSettings: () -> Unit, modifier: Modifi } } -@Composable -private fun CaptureModeToggleButton( - modifier: Modifier = Modifier, - onClick: () -> Unit, - captureModeUiState: CaptureModeUiState.Available, - assignedCaptureMode: CaptureMode, - isHighlightEnabled: Boolean = false -) { - val enum = when (assignedCaptureMode) { - CaptureMode.STANDARD -> CameraCaptureMode.STANDARD - CaptureMode.VIDEO_ONLY -> CameraCaptureMode.VIDEO_ONLY - CaptureMode.IMAGE_ONLY -> CameraCaptureMode.IMAGE_ONLY - } - QuickSettingToggleSelectorButton( - modifier = modifier, - enum = enum, - onClick = { onClick() }, - enabled = when (assignedCaptureMode) { - CaptureMode.STANDARD -> - captureModeUiState.isCaptureModeSelectable(CaptureMode.STANDARD) - - CaptureMode.VIDEO_ONLY -> - captureModeUiState.isCaptureModeSelectable(CaptureMode.VIDEO_ONLY) - - CaptureMode.IMAGE_ONLY -> - captureModeUiState.isCaptureModeSelectable(CaptureMode.IMAGE_ONLY) - }, - isSelected = - isHighlightEnabled && - (assignedCaptureMode == captureModeUiState.selectedCaptureMode) - ) -} // //////////////////////////////////////////////////// // @@ -263,29 +231,28 @@ internal fun CaptureModeRow( CaptureMode.VIDEO_ONLY -> CameraCaptureMode.VIDEO_ONLY } - SettingRow( + QuickSettingsListRow( modifier = modifier.testTag(ROW_QUICK_SETTINGS_CAPTURE_MODE), title = stringResource(id = R.string.quick_settings_title_capture_mode), - stateSubtitle = stringResource(enum.getTextResId()) - ) { - captureModeUiState.availableCaptureModes.forEach { selectableMode -> - val testTag = when (selectableMode.value) { - CaptureMode.STANDARD -> - BTN_QUICK_SETTINGS_FOCUSED_CAPTURE_MODE_OPTION_STANDARD - CaptureMode.IMAGE_ONLY -> - BTN_QUICK_SETTINGS_FOCUSED_CAPTURE_MODE_IMAGE_ONLY - CaptureMode.VIDEO_ONLY -> - BTN_QUICK_SETTINGS_FOCUSED_CAPTURE_MODE_VIDEO_ONLY + stateSubtitle = stringResource(enum.getTextResId()), + items = captureModeUiState.availableCaptureModes, + selectedItem = captureModeUiState.selectedCaptureMode, + onItemClick = onSetCaptureMode, + testTagMapper = { mode -> + when (mode) { + CaptureMode.STANDARD -> BTN_QUICK_SETTINGS_FOCUSED_CAPTURE_MODE_OPTION_STANDARD + CaptureMode.IMAGE_ONLY -> BTN_QUICK_SETTINGS_FOCUSED_CAPTURE_MODE_IMAGE_ONLY + CaptureMode.VIDEO_ONLY -> BTN_QUICK_SETTINGS_FOCUSED_CAPTURE_MODE_VIDEO_ONLY + } + }, + enumMapper = { mode -> + when (mode) { + CaptureMode.STANDARD -> CameraCaptureMode.STANDARD + CaptureMode.IMAGE_ONLY -> CameraCaptureMode.IMAGE_ONLY + CaptureMode.VIDEO_ONLY -> CameraCaptureMode.VIDEO_ONLY } - CaptureModeToggleButton( - modifier = Modifier.testTag(testTag), - onClick = { onSetCaptureMode(selectableMode.value) }, - assignedCaptureMode = selectableMode.value, - captureModeUiState = captureModeUiState, - isHighlightEnabled = true - ) } - } + ) } } @@ -349,32 +316,30 @@ internal fun AspectRatioRow( aspectRatioUiState: AspectRatioUiState ) { if (aspectRatioUiState is AspectRatioUiState.Available) { - SettingRow( + QuickSettingsListRow( modifier = modifier.testTag(ROW_QUICK_SETTINGS_ASPECT_RATIO), title = stringResource(id = R.string.quick_settings_title_aspect_ratio), stateSubtitle = stringResource( id = aspectRatioUiState.selectedAspectRatio.toSubtitleStringRes() - ) - ) { - aspectRatioUiState.availableAspectRatios.forEach { selectableRatio -> - val enum = when (selectableRatio.value) { - AspectRatio.THREE_FOUR -> CameraAspectRatio.THREE_FOUR - AspectRatio.NINE_SIXTEEN -> CameraAspectRatio.NINE_SIXTEEN - AspectRatio.ONE_ONE -> CameraAspectRatio.ONE_ONE - } - val testTag = when (selectableRatio.value) { + ), + items = aspectRatioUiState.availableAspectRatios, + selectedItem = aspectRatioUiState.selectedAspectRatio, + onItemClick = onSetAspectRatio, + testTagMapper = { ratio -> + when (ratio) { AspectRatio.THREE_FOUR -> QUICK_SETTINGS_RATIO_3_4_BUTTON AspectRatio.NINE_SIXTEEN -> QUICK_SETTINGS_RATIO_9_16_BUTTON AspectRatio.ONE_ONE -> QUICK_SETTINGS_RATIO_1_1_BUTTON } - QuickSettingToggleSelectorButton( - modifier = Modifier.testTag(testTag), - onClick = { onSetAspectRatio(selectableRatio.value) }, - enum = enum, - isSelected = selectableRatio.value == aspectRatioUiState.selectedAspectRatio - ) + }, + enumMapper = { ratio -> + when (ratio) { + AspectRatio.THREE_FOUR -> CameraAspectRatio.THREE_FOUR + AspectRatio.NINE_SIXTEEN -> CameraAspectRatio.NINE_SIXTEEN + AspectRatio.ONE_ONE -> CameraAspectRatio.ONE_ONE + } } - } + ) } } @@ -392,43 +357,37 @@ internal fun FlashRow( flashModeUiState: FlashModeUiState ) { if (flashModeUiState is FlashModeUiState.Available) { - SettingRow( + QuickSettingsListRow( modifier = modifier.testTag(ROW_QUICK_SETTINGS_FLASH), title = stringResource(id = R.string.quick_settings_title_flash_mode), stateSubtitle = stringResource( id = flashModeUiState.selectedFlashMode.toSubtitleStringRes() - ) - ) { - flashModeUiState.availableFlashModes.forEach { selectableMode -> - val testTag = when (selectableMode.value) { + ), + items = flashModeUiState.availableFlashModes, + selectedItem = flashModeUiState.selectedFlashMode, + onItemClick = onSetFlashMode, + testTagMapper = { mode -> + when (mode) { FlashMode.OFF -> BTN_QUICK_SETTINGS_FLASH_OPTION_OFF FlashMode.ON -> BTN_QUICK_SETTINGS_FLASH_OPTION_ON FlashMode.AUTO -> BTN_QUICK_SETTINGS_FLASH_OPTION_AUTO + FlashMode.LOW_LIGHT_BOOST -> BTN_QUICK_SETTINGS_FLASH_OPTION_LOW_LIGHT_BOOST + } + }, + enumMapper = { mode -> + when (mode) { + FlashMode.OFF -> CameraFlashMode.OFF + FlashMode.ON -> CameraFlashMode.ON + FlashMode.AUTO -> CameraFlashMode.AUTO FlashMode.LOW_LIGHT_BOOST -> - BTN_QUICK_SETTINGS_FLASH_OPTION_LOW_LIGHT_BOOST + if (flashModeUiState.isLowLightBoostActive) { + CameraFlashMode.LOW_LIGHT_BOOST_ACTIVE + } else { + CameraFlashMode.LOW_LIGHT_BOOST_INACTIVE + } } - QuickSettingToggleSelectorButton( - modifier = Modifier.testTag(testTag), - enabled = selectableMode is SingleSelectableUiState.SelectableUi, - enum = when (selectableMode.value) { - FlashMode.OFF -> CameraFlashMode.OFF - FlashMode.ON -> CameraFlashMode.ON - FlashMode.AUTO -> CameraFlashMode.AUTO - FlashMode.LOW_LIGHT_BOOST -> - when (flashModeUiState.isLowLightBoostActive) { - true -> CameraFlashMode.LOW_LIGHT_BOOST_ACTIVE - false -> CameraFlashMode.LOW_LIGHT_BOOST_INACTIVE - } - }, - isSelected = flashModeUiState.selectedFlashMode == selectableMode.value, - onClick = { - onSetFlashMode( - selectableMode.value - ) - } - ) } - } + ) } } @@ -438,6 +397,40 @@ internal fun FlashRow( // // //////////////////////////////////////////////////// +@Composable +private fun QuickSettingsListRow( + title: String, + stateSubtitle: String, + items: List>, + selectedItem: T, + onItemClick: (T) -> Unit, + enumMapper: (T) -> QuickSettingsEnum, + testTagMapper: (T) -> String, + modifier: Modifier = Modifier +) { + SettingRow( + modifier = modifier, + title = title, + stateSubtitle = stateSubtitle + ) { + items.forEach { selectableItem -> + val value = selectableItem.value + val isSelected = value == selectedItem + val enumVal = enumMapper(value) + val testTag = testTagMapper(value) + val enabled = selectableItem is SingleSelectableUiState.SelectableUi + + QuickSettingToggleSelectorButton( + modifier = Modifier.testTag(testTag), + enum = enumVal, + onClick = { onItemClick(value) }, + isSelected = isSelected, + enabled = enabled + ) + } + } +} + @Composable private fun SettingRow( title: String, From fa32a0cbe184b779a35fc171b6f13d2032772754 Mon Sep 17 00:00:00 2001 From: Kimberly Crevecoeur Date: Wed, 29 Jul 2026 08:55:20 -0700 Subject: [PATCH 47/51] use connected button shapes --- .../ui/QuickSettingsComponents.kt | 54 +++++++++++++------ 1 file changed, 38 insertions(+), 16 deletions(-) diff --git a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/ui/QuickSettingsComponents.kt b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/ui/QuickSettingsComponents.kt index 94f3ad6a4..f1f264c0d 100644 --- a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/ui/QuickSettingsComponents.kt +++ b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/ui/QuickSettingsComponents.kt @@ -32,10 +32,14 @@ import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Button +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.material3.ButtonGroupDefaults import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi -import androidx.compose.material3.FilledIconToggleButton import androidx.compose.material3.Icon +import androidx.compose.material3.ToggleButton +import androidx.compose.material3.ToggleButtonDefaults +import androidx.compose.material3.ToggleButtonShapes import androidx.compose.material3.IconButton import androidx.compose.material3.IconButtonDefaults import androidx.compose.material3.LocalContentColor @@ -263,6 +267,7 @@ internal fun CaptureModeRow( * @param onClick Callback invoked when HDR is toggled, providing the selected [DynamicRange] and [ImageOutputFormat]. * @param hdrUiState The current [HdrUiState] representing HDR availability and selection. */ +@OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable internal fun HdrRow( modifier: Modifier = Modifier, @@ -290,14 +295,16 @@ internal fun HdrRow( enum = CameraDynamicRange.HDR, onClick = { onClick(DEFAULT_HDR_DYNAMIC_RANGE, DEFAULT_HDR_IMAGE_OUTPUT) }, isSelected = isHdrOn, - enabled = isSupported + enabled = isSupported, + shapes = ButtonGroupDefaults.connectedLeadingButtonShapes() ) QuickSettingToggleSelectorButton( modifier = Modifier.testTag(BTN_QUICK_SETTINGS_HDR_OPTION_OFF), enum = CameraDynamicRange.SDR, onClick = { onClick(DynamicRange.SDR, ImageOutputFormat.JPEG) }, isSelected = !isHdrOn, - enabled = isSupported + enabled = isSupported, + shapes = ButtonGroupDefaults.connectedTrailingButtonShapes() ) } } @@ -397,6 +404,7 @@ internal fun FlashRow( // // //////////////////////////////////////////////////// +@OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable private fun QuickSettingsListRow( title: String, @@ -413,29 +421,39 @@ private fun QuickSettingsListRow( title = title, stateSubtitle = stateSubtitle ) { - items.forEach { selectableItem -> + items.forEachIndexed { index, selectableItem -> val value = selectableItem.value val isSelected = value == selectedItem val enumVal = enumMapper(value) val testTag = testTagMapper(value) val enabled = selectableItem is SingleSelectableUiState.SelectableUi + val shapes = when { + items.size == 1 -> ToggleButtonDefaults.shapes() + index == 0 -> ButtonGroupDefaults.connectedLeadingButtonShapes() + index == items.lastIndex -> ButtonGroupDefaults.connectedTrailingButtonShapes() + else -> ButtonGroupDefaults.connectedMiddleButtonShapes() + } + QuickSettingToggleSelectorButton( modifier = Modifier.testTag(testTag), enum = enumVal, onClick = { onItemClick(value) }, isSelected = isSelected, - enabled = enabled + enabled = enabled, + shapes = shapes ) } } } +@OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable private fun SettingRow( title: String, stateSubtitle: String, modifier: Modifier = Modifier, + horizontalArrangement: Arrangement.Horizontal = Arrangement.spacedBy(ButtonGroupDefaults.ConnectedSpaceBetween), buttons: @Composable RowScope.() -> Unit ) { Row( @@ -463,20 +481,22 @@ private fun SettingRow( } Row( - horizontalArrangement = Arrangement.spacedBy(8.dp), + horizontalArrangement = horizontalArrangement, verticalAlignment = Alignment.CenterVertically, content = buttons ) } } +@OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable private fun QuickSettingToggleSelectorButton( modifier: Modifier = Modifier, enum: QuickSettingsEnum, onClick: () -> Unit, isSelected: Boolean = false, - enabled: Boolean = true + enabled: Boolean = true, + shapes: ToggleButtonShapes = ToggleButtonDefaults.shapes() ) { QuickSettingToggleSelectorButton( modifier = modifier, @@ -484,7 +504,8 @@ private fun QuickSettingToggleSelectorButton( onClick = { onClick() }, isSelected = isSelected, enabled = enabled, - painter = enum.getPainter() + painter = enum.getPainter(), + shapes = shapes ) } @@ -506,29 +527,28 @@ private fun QuickSettingToggleSelectorButton( painter: Painter, modifier: Modifier = Modifier, isSelected: Boolean = false, - enabled: Boolean = true + enabled: Boolean = true, + shapes: ToggleButtonShapes = ToggleButtonDefaults.shapes() ) { val buttonSize = IconButtonDefaults.mediumContainerSize( IconButtonDefaults.IconButtonWidthOption.Narrow ) - FilledIconToggleButton( + ToggleButton( modifier = modifier .minimumInteractiveComponentSize() .size(buttonSize), checked = isSelected, enabled = enabled, onCheckedChange = { _ -> onClick() }, - shapes = IconButtonDefaults.toggleableShapes( - shape = CircleShape, - checkedShape = RoundedCornerShape(12.dp) - ), - colors = IconButtonDefaults.filledIconToggleButtonColors( + shapes = shapes, + colors = ToggleButtonDefaults.toggleButtonColors( containerColor = Color.White.copy(alpha = 0.20f), checkedContainerColor = MaterialTheme.colorScheme.primary, contentColor = MaterialTheme.colorScheme.onSurface, checkedContentColor = MaterialTheme.colorScheme.onPrimary - ) + ), + contentPadding = PaddingValues(0.dp) ) { Icon( modifier = Modifier.size(IconButtonDefaults.mediumIconSize), @@ -649,6 +669,7 @@ private fun FlashMode.toSubtitleStringRes(): Int = when (this) { FlashMode.LOW_LIGHT_BOOST -> R.string.quick_settings_flash_mode_subtitle_low_light_boost } +@OptIn(ExperimentalMaterial3ExpressiveApi::class) @Preview @Composable private fun QuickSettingToggleButtonPreview() { @@ -695,6 +716,7 @@ private fun QuickSettingToggleButtonPreview() { } } +@OptIn(ExperimentalMaterial3ExpressiveApi::class) @Preview( name = "JCA Setting Row - Dark Mode", showBackground = true, From cb36de97215d8cbb8bdbcee85e3e71d792453282 Mon Sep 17 00:00:00 2001 From: Kimberly Crevecoeur Date: Wed, 29 Jul 2026 09:18:01 -0700 Subject: [PATCH 48/51] adjust button dimensions --- .../ui/QuickSettingsComponents.kt | 72 +++++++++---------- .../uistateadapter/capture/DisabledReason.kt | 6 ++ .../capture/FlashModeUiStateAdapter.kt | 22 ++++-- .../capture/src/main/res/values/strings.xml | 1 + 4 files changed, 57 insertions(+), 44 deletions(-) diff --git a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/ui/QuickSettingsComponents.kt b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/ui/QuickSettingsComponents.kt index f1f264c0d..3a680be29 100644 --- a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/ui/QuickSettingsComponents.kt +++ b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/ui/QuickSettingsComponents.kt @@ -22,6 +22,7 @@ import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.fillMaxWidth @@ -29,17 +30,12 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width -import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Button -import androidx.compose.foundation.layout.PaddingValues import androidx.compose.material3.ButtonGroupDefaults import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.Icon -import androidx.compose.material3.ToggleButton -import androidx.compose.material3.ToggleButtonDefaults -import androidx.compose.material3.ToggleButtonShapes import androidx.compose.material3.IconButton import androidx.compose.material3.IconButtonDefaults import androidx.compose.material3.LocalContentColor @@ -48,6 +44,9 @@ import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.SheetState import androidx.compose.material3.Surface import androidx.compose.material3.Text +import androidx.compose.material3.ToggleButton +import androidx.compose.material3.ToggleButtonDefaults +import androidx.compose.material3.ToggleButtonShapes import androidx.compose.material3.darkColorScheme import androidx.compose.material3.minimumInteractiveComponentSize import androidx.compose.runtime.Composable @@ -102,7 +101,6 @@ import com.google.jetpackcamera.ui.controller.quicksettings.QuickSettingsControl import com.google.jetpackcamera.ui.uistate.SingleSelectableUiState import com.google.jetpackcamera.ui.uistate.capture.AspectRatioUiState import com.google.jetpackcamera.ui.uistate.capture.CaptureModeUiState -import com.google.jetpackcamera.ui.uistate.capture.CaptureModeUiState.Unavailable.isCaptureModeSelectable import com.google.jetpackcamera.ui.uistate.capture.FlashModeUiState import com.google.jetpackcamera.ui.uistate.capture.HdrUiState @@ -281,32 +279,34 @@ internal fun HdrRow( hdrUiState.selectedImageFormat == DEFAULT_HDR_IMAGE_OUTPUT ) - SettingRow( + QuickSettingsListRow( modifier = modifier.testTag(ROW_QUICK_SETTINGS_HDR), title = stringResource(id = R.string.quick_settings_title_hdr), stateSubtitle = if (isHdrOn) { stringResource(R.string.quick_settings_dynamic_range_hdr) } else { stringResource(R.string.quick_settings_dynamic_range_sdr) - } - ) { - QuickSettingToggleSelectorButton( - modifier = Modifier.testTag(BTN_QUICK_SETTINGS_HDR_OPTION_ON), - enum = CameraDynamicRange.HDR, - onClick = { onClick(DEFAULT_HDR_DYNAMIC_RANGE, DEFAULT_HDR_IMAGE_OUTPUT) }, - isSelected = isHdrOn, - enabled = isSupported, - shapes = ButtonGroupDefaults.connectedLeadingButtonShapes() - ) - QuickSettingToggleSelectorButton( - modifier = Modifier.testTag(BTN_QUICK_SETTINGS_HDR_OPTION_OFF), - enum = CameraDynamicRange.SDR, - onClick = { onClick(DynamicRange.SDR, ImageOutputFormat.JPEG) }, - isSelected = !isHdrOn, - enabled = isSupported, - shapes = ButtonGroupDefaults.connectedTrailingButtonShapes() - ) - } + }, + items = listOf( + SingleSelectableUiState.SelectableUi(true), + SingleSelectableUiState.SelectableUi(false) + ), + selectedItem = isHdrOn, + onItemClick = { hdrEnabled -> + if (hdrEnabled) { + onClick(DEFAULT_HDR_DYNAMIC_RANGE, DEFAULT_HDR_IMAGE_OUTPUT) + } else { + onClick(DynamicRange.SDR, ImageOutputFormat.JPEG) + } + }, + testTagMapper = { hdrEnabled -> + if (hdrEnabled) BTN_QUICK_SETTINGS_HDR_OPTION_ON else BTN_QUICK_SETTINGS_HDR_OPTION_OFF + }, + enumMapper = { hdrEnabled -> + if (hdrEnabled) CameraDynamicRange.HDR else CameraDynamicRange.SDR + }, + isItemEnabled = { isSupported } + ) } /** @@ -414,7 +414,8 @@ private fun QuickSettingsListRow( onItemClick: (T) -> Unit, enumMapper: (T) -> QuickSettingsEnum, testTagMapper: (T) -> String, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, + isItemEnabled: (SingleSelectableUiState) -> Boolean = { it is SingleSelectableUiState.SelectableUi } ) { SettingRow( modifier = modifier, @@ -426,7 +427,7 @@ private fun QuickSettingsListRow( val isSelected = value == selectedItem val enumVal = enumMapper(value) val testTag = testTagMapper(value) - val enabled = selectableItem is SingleSelectableUiState.SelectableUi + val enabled = isItemEnabled(selectableItem) val shapes = when { items.size == 1 -> ToggleButtonDefaults.shapes() @@ -453,7 +454,7 @@ private fun SettingRow( title: String, stateSubtitle: String, modifier: Modifier = Modifier, - horizontalArrangement: Arrangement.Horizontal = Arrangement.spacedBy(ButtonGroupDefaults.ConnectedSpaceBetween), + horizontalArrangement: Arrangement.Horizontal = Arrangement.spacedBy(2.dp), buttons: @Composable RowScope.() -> Unit ) { Row( @@ -476,7 +477,7 @@ private fun SettingRow( Text( text = stateSubtitle, style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant + color = MaterialTheme.colorScheme.primaryFixed ) } @@ -530,25 +531,20 @@ private fun QuickSettingToggleSelectorButton( enabled: Boolean = true, shapes: ToggleButtonShapes = ToggleButtonDefaults.shapes() ) { - val buttonSize = IconButtonDefaults.mediumContainerSize( - IconButtonDefaults.IconButtonWidthOption.Narrow - ) - ToggleButton( modifier = modifier - .minimumInteractiveComponentSize() - .size(buttonSize), + .minimumInteractiveComponentSize(), checked = isSelected, enabled = enabled, onCheckedChange = { _ -> onClick() }, shapes = shapes, colors = ToggleButtonDefaults.toggleButtonColors( - containerColor = Color.White.copy(alpha = 0.20f), + containerColor = MaterialTheme.colorScheme.onSecondaryFixedVariant, checkedContainerColor = MaterialTheme.colorScheme.primary, contentColor = MaterialTheme.colorScheme.onSurface, checkedContentColor = MaterialTheme.colorScheme.onPrimary ), - contentPadding = PaddingValues(0.dp) + contentPadding = PaddingValues(horizontal = 12.dp, vertical = 6.dp) ) { Icon( modifier = Modifier.size(IconButtonDefaults.mediumIconSize), diff --git a/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/DisabledReason.kt b/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/DisabledReason.kt index ad983c587..f49875e7a 100644 --- a/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/DisabledReason.kt +++ b/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/DisabledReason.kt @@ -56,5 +56,11 @@ enum class DisabledReason( ), LLB_DISABLED_BY_HDR( R.string.toast_llb_disabled_by_hdr + ), + FLASH_UNSUPPORTED_ON_LENS( + R.string.toast_flash_unsupported_on_lens + ), + LLB_UNSUPPORTED_ON_LENS( + R.string.toast_llb_unsupported_on_lens ) } diff --git a/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/FlashModeUiStateAdapter.kt b/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/FlashModeUiStateAdapter.kt index c5fda2024..aad4233ca 100644 --- a/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/FlashModeUiStateAdapter.kt +++ b/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/FlashModeUiStateAdapter.kt @@ -95,10 +95,8 @@ internal fun FlashModeUiState.Companion.from( continue }*/ - // 3. Hide if not supported on the current lens. - if (!currentLensSupportedFlashModes.contains(mode)) { - continue - } + // 3. Check if supported on the current lens + val isSupportedOnCurrentLens = currentLensSupportedFlashModes.contains(mode) // 4. Special handling for LOW_LIGHT_BOOST based on other settings. if (mode == FlashMode.LOW_LIGHT_BOOST) { @@ -111,14 +109,26 @@ internal fun FlashModeUiState.Companion.from( // 5. Determine if Enabled or Disabled val isLlbHdrConflict = mode == FlashMode.LOW_LIGHT_BOOST && isHdrOn - if (!isLlbHdrConflict) { + if (isSupportedOnCurrentLens && !isLlbHdrConflict) { // Enabled displayableModes.add(SingleSelectableUiState.SelectableUi(mode)) } else { + // Disabled + val reason = when { + !isSupportedOnCurrentLens -> { + if (mode == FlashMode.LOW_LIGHT_BOOST) { + DisabledReason.LLB_UNSUPPORTED_ON_LENS + } else { + DisabledReason.FLASH_UNSUPPORTED_ON_LENS + } + } + isLlbHdrConflict -> DisabledReason.LLB_DISABLED_BY_HDR + else -> throw IllegalStateException("Unexpected disabled state") + } displayableModes.add( SingleSelectableUiState.Disabled( value = mode, - disabledReason = DisabledReason.LLB_DISABLED_BY_HDR + disabledReason = reason ) ) } diff --git a/ui/uistateadapter/capture/src/main/res/values/strings.xml b/ui/uistateadapter/capture/src/main/res/values/strings.xml index 3019efa87..21121596d 100644 --- a/ui/uistateadapter/capture/src/main/res/values/strings.xml +++ b/ui/uistateadapter/capture/src/main/res/values/strings.xml @@ -31,4 +31,5 @@ HDR video and image capture cannot be bound simultaneously Flash not supported by current lens Low Light Boost is disabled when HDR is on + Low light boost not supported by current lens From b58627f478325b437e819fad42cc4f4910020c24 Mon Sep 17 00:00:00 2001 From: Kimberly Crevecoeur Date: Wed, 29 Jul 2026 10:10:43 -0700 Subject: [PATCH 49/51] adjustpreview --- .../capture/quicksettings/ui/QuickSettingsComponents.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/ui/QuickSettingsComponents.kt b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/ui/QuickSettingsComponents.kt index 3a680be29..0d28b0002 100644 --- a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/ui/QuickSettingsComponents.kt +++ b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/ui/QuickSettingsComponents.kt @@ -737,6 +737,7 @@ private fun PreviewSettingRowDark() { ), painter = painterResource(id = R.drawable.video_resolution_sd_icon), isSelected = true, + shapes = ButtonGroupDefaults.connectedLeadingButtonShapes(), onClick = {} ) // On State @@ -746,6 +747,7 @@ private fun PreviewSettingRowDark() { ), painter = painterResource(id = R.drawable.video_resolution_hd_icon), isSelected = false, + shapes = ButtonGroupDefaults.connectedMiddleButtonShapes(), onClick = {} ) // Auto State @@ -755,6 +757,7 @@ private fun PreviewSettingRowDark() { ), painter = painterResource(id = R.drawable.video_resolution_fhd_icon), isSelected = false, + shapes = ButtonGroupDefaults.connectedTrailingButtonShapes(), onClick = {} ) } From e7f36318e4ed911c04bedb3da8bee6beb672cf0b Mon Sep 17 00:00:00 2001 From: Kimberly Crevecoeur Date: Wed, 29 Jul 2026 10:13:32 -0700 Subject: [PATCH 50/51] spotless --- .../capture/quicksettings/ui/QuickSettingsComponents.kt | 7 +++---- .../uistateadapter/capture/FlashModeUiStateAdapterTest.kt | 4 +++- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/ui/QuickSettingsComponents.kt b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/ui/QuickSettingsComponents.kt index 0d28b0002..0347ba54c 100644 --- a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/ui/QuickSettingsComponents.kt +++ b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/ui/QuickSettingsComponents.kt @@ -30,7 +30,6 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Button import androidx.compose.material3.ButtonGroupDefaults import androidx.compose.material3.ExperimentalMaterial3Api @@ -170,8 +169,6 @@ internal fun QuickNavSettings(onNavigateToSettings: () -> Unit, modifier: Modifi } } - - // //////////////////////////////////////////////////// // // complete quick settings screen components @@ -415,7 +412,9 @@ private fun QuickSettingsListRow( enumMapper: (T) -> QuickSettingsEnum, testTagMapper: (T) -> String, modifier: Modifier = Modifier, - isItemEnabled: (SingleSelectableUiState) -> Boolean = { it is SingleSelectableUiState.SelectableUi } + isItemEnabled: ( + SingleSelectableUiState + ) -> Boolean = { it is SingleSelectableUiState.SelectableUi } ) { SettingRow( modifier = modifier, diff --git a/ui/uistateadapter/capture/src/test/java/com/google/jetpackcamera/ui/uistateadapter/capture/FlashModeUiStateAdapterTest.kt b/ui/uistateadapter/capture/src/test/java/com/google/jetpackcamera/ui/uistateadapter/capture/FlashModeUiStateAdapterTest.kt index eda3edc4f..f628b4be4 100644 --- a/ui/uistateadapter/capture/src/test/java/com/google/jetpackcamera/ui/uistateadapter/capture/FlashModeUiStateAdapterTest.kt +++ b/ui/uistateadapter/capture/src/test/java/com/google/jetpackcamera/ui/uistateadapter/capture/FlashModeUiStateAdapterTest.kt @@ -446,7 +446,9 @@ class FlashModeUiStateAdapterTest { ) ) val uiState = FlashModeUiState.from(initialAppSettings, systemConstraints) - assertThat((uiState as FlashModeUiState.Available).selectedFlashMode).isEqualTo(FlashMode.OFF) + assertThat( + (uiState as FlashModeUiState.Available).selectedFlashMode + ).isEqualTo(FlashMode.OFF) // When the selected flash mode changes to ON in settings, but constraints are unchanged val newAppSettings = initialAppSettings.copy(flashMode = FlashMode.ON) From bfd867e62e218acdf0f62a9f98561009a5244522 Mon Sep 17 00:00:00 2001 From: Kimberly Crevecoeur Date: Wed, 29 Jul 2026 10:30:51 -0700 Subject: [PATCH 51/51] update test behavior and minsdk --- gradle.properties | 2 -- gradle/libs.versions.toml | 2 +- .../capture/FlashModeUiStateAdapterTest.kt | 13 +++++++++---- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/gradle.properties b/gradle.properties index cc44676e5..a890a1341 100644 --- a/gradle.properties +++ b/gradle.properties @@ -45,5 +45,3 @@ android.experimental.testOptions.managedDevices.maxConcurrentDevices=1 android.experimental.testOptions.managedDevices.setupTimeoutMinutes=180 # Ensure we can run managed devices on servers that don't support hardware rendering android.testoptions.manageddevices.emulator.gpu=swiftshader_indirect -# Enabled parallel sync for Gradle 9.4+ -org.gradle.tooling.parallel=true diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 69f1d6c6d..a2317d598 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,7 +3,7 @@ compileSdk = "36" desugar_jdk_libs = "2.1.5" orchestrator = "1.6.1" -minSdk = "23" +minSdk = "24" targetSdk = "35" # Used below in dependency definitions diff --git a/ui/uistateadapter/capture/src/test/java/com/google/jetpackcamera/ui/uistateadapter/capture/FlashModeUiStateAdapterTest.kt b/ui/uistateadapter/capture/src/test/java/com/google/jetpackcamera/ui/uistateadapter/capture/FlashModeUiStateAdapterTest.kt index f628b4be4..781f6a692 100644 --- a/ui/uistateadapter/capture/src/test/java/com/google/jetpackcamera/ui/uistateadapter/capture/FlashModeUiStateAdapterTest.kt +++ b/ui/uistateadapter/capture/src/test/java/com/google/jetpackcamera/ui/uistateadapter/capture/FlashModeUiStateAdapterTest.kt @@ -330,7 +330,7 @@ class FlashModeUiStateAdapterTest { } @Test - fun from_flashUnsupportedOnCurrentLens_flashModeHidden() { + fun from_flashUnsupportedOnCurrentLens_flashModeDisabled() { // Given a device that supports FLASH_ON on some lens, but NOT on the current lens (which supports OFF and AUTO) val appSettings = defaultCameraAppSettings.copy( cameraLensFacing = LensFacing.BACK, @@ -353,11 +353,16 @@ class FlashModeUiStateAdapterTest { // When val flashModeUiState = FlashModeUiState.from(appSettings, systemConstraints) - // Then FLASH_ON is hidden because it is unsupported on the current lens + // Then FLASH_ON is disabled because it is unsupported on the current lens assertThat(flashModeUiState).isInstanceOf(FlashModeUiState.Available::class.java) val availableUiState = flashModeUiState as FlashModeUiState.Available - val hasFlashOn = availableUiState.availableFlashModes.any { it.value == FlashMode.ON } - assertThat(hasFlashOn).isFalse() + val flashOnState = + availableUiState.availableFlashModes.find { it.value == FlashMode.ON } + assertThat(flashOnState).isInstanceOf(SingleSelectableUiState.Disabled::class.java) + val disabledFlashOn = flashOnState as SingleSelectableUiState.Disabled + assertThat( + disabledFlashOn.disabledReason + ).isEqualTo(DisabledReason.FLASH_UNSUPPORTED_ON_LENS) // Ensure other modes are present val hasFlashAuto = availableUiState.availableFlashModes.any { it.value == FlashMode.AUTO } assertThat(hasFlashAuto).isTrue()