Skip to content

Refactor Quick Settings UI#530

Draft
Kimblebee wants to merge 23 commits into
kim/hdr/decouple-capture-modesfrom
kim/refactor/quickSettings/button-rows
Draft

Refactor Quick Settings UI#530
Kimblebee wants to merge 23 commits into
kim/hdr/decouple-capture-modesfrom
kim/refactor/quickSettings/button-rows

Conversation

@Kimblebee

Copy link
Copy Markdown
Collaborator

No description provided.

Kimblebee added 19 commits April 8, 2026 19:12
- 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
auxiliary function for settings subtitles
… 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'.
    Remove unused drawables and their corresponding enum classes in
    QuickSettingsEnums that are no longer referenced after the quick
    settings refactoring.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request refactors the quick settings UI to use a row-based layout (SettingRow) for settings like Capture Mode, HDR, Aspect Ratio, and Flash Mode, while removing the individual setting expansion logic. It also updates FlashModeUiStateAdapter to handle disabled states for unsupported flash modes on the current lens. The review feedback highlights a critical accessibility issue where mergeDescendants = true on SettingRow blocks screen readers from accessing individual buttons. Additionally, suggestions were made to make the Kotlin code in FlashModeUiStateAdapter more idiomatic, and several style guide violations were identified, including missing KDoc comments on internal composables and an incorrectly formatted test tag string.

Comment on lines +432 to +436
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)
.semantics(mergeDescendants = true) {},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Setting mergeDescendants = true on the outer Row of SettingRow will merge the entire row, including all interactive buttons, into a single accessibility node. This makes it impossible for screen reader users to focus and click the individual setting buttons (like Off, On, Auto). Please remove mergeDescendants = true from the outer Row so that the interactive buttons remain individually accessible.

    Row(
        modifier = modifier
            .fillMaxWidth()
            .padding(vertical = 12.dp, horizontal = 16.dp),

Comment on lines +68 to 74
val allDeviceSupportedFlashModes = buildSet<FlashMode> {
systemConstraints.perLensConstraints.let {
it.keys.forEach { key ->
it[key]?.supportedFlashModes?.let { flashModes -> addAll(flashModes) }
}
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The logic to collect all supported flash modes across all lenses can be simplified and made more idiomatic by using Kotlin's standard library functions like flatMap and toSet() on the map's values.

    val allDeviceSupportedFlashModes = systemConstraints.perLensConstraints.values
        .flatMap { it.supportedFlashModes }
        .toSet()

Comment on lines +118 to +120
val onlyOffSelectable = displayableModes.size == 1 &&
displayableModes[0].value == FlashMode.OFF &&
displayableModes[0] is SingleSelectableUiState.SelectableUi

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Instead of checking the size and accessing the first element by index, we can use singleOrNull() which is safer and more idiomatic in Kotlin.

Suggested change
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

Comment on lines +156 to 185
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
}
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using when to handle newUiState allows us to leverage Kotlin's smart casting and avoid the explicit cast newUiState as Available.

            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
                        }
                    }
                }
            }

Comment on lines 245 to +246
@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
internal fun CaptureModeRow(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

According to the repository style guide (Section 11), all non-private members (including internal composables) must have KDoc documentation. Please add a descriptive KDoc for CaptureModeRow explaining its behavior, features, and parameters.

References
  1. All non-private classes, functions, and composables must have KDoc documentation. (link)

Comment on lines 286 to +287
@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
internal fun HdrRow(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

According to the repository style guide (Section 11), all non-private members (including internal composables) must have KDoc documentation. Please add a descriptive KDoc for HdrRow explaining its behavior, features, and parameters.

References
  1. All non-private classes, functions, and composables must have KDoc documentation. (link)

Comment on lines +329 to +330
@Composable
internal fun AspectRatioRow(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

According to the repository style guide (Section 11), all non-private members (including internal composables) must have KDoc documentation. Please add a descriptive KDoc for AspectRatioRow explaining its behavior, features, and parameters.

References
  1. All non-private classes, functions, and composables must have KDoc documentation. (link)

Comment on lines 369 to +370
@Composable
private fun CloseExpandedSettingsButton(onUnFocus: () -> Unit, modifier: Modifier = Modifier) {
FilledIconButton(
modifier = modifier.testTag(QUICK_SETTINGS_CLOSE_EXPANDED_BUTTON),
onClick = onUnFocus
) {
Icon(
painter = painterResource(R.drawable.ic_close),
contentDescription = stringResource(
R.string.quick_settings_btn_close_expanded_settings_description
)
internal fun FlashRow(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

According to the repository style guide (Section 11), all non-private members (including internal composables) must have KDoc documentation. Please add a descriptive KDoc for FlashRow explaining its behavior, features, and parameters.

References
  1. All non-private classes, functions, and composables must have KDoc documentation. (link)

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"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

According to the repository style guide (Section 10), when creating a new test tag, the string value must be a lower_snake_case string following the schema element_purpose_value. Please update the string value of FLASH_UNSUPPORTED_ON_LENS_TAG to follow this convention (e.g., "toast_flash_unsupported_on_lens").

References
  1. When creating a new test tag, the constant name must be in UPPER_SNAKE_CASE, and the string value must be a lower_snake_case string following a specific schema: element_purpose_value. (link)

@Kimblebee Kimblebee changed the base branch from main to kim/hdr/decouple-capture-modes June 12, 2026 18:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant