diff options
14 files changed, 1029 insertions, 219 deletions
diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/LocationProviderDialogScreen.kt b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/LocationProviderDialogScreen.kt index 6af62e01f..510d19706 100644 --- a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/LocationProviderDialogScreen.kt +++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/LocationProviderDialogScreen.kt @@ -27,8 +27,9 @@ import androidx.wear.compose.material.ChipDefaults import androidx.wear.compose.material.MaterialTheme import androidx.wear.compose.material.SwipeToDismissBox import com.android.permissioncontroller.permission.ui.wear.elements.Chip -import com.android.permissioncontroller.permission.ui.wear.elements.Scaffold +import com.android.permissioncontroller.permission.ui.wear.elements.material3.WearPermissionScaffold import com.android.permissioncontroller.permission.ui.wear.model.LocationProviderInterceptDialogArgs +import com.android.permissioncontroller.permission.ui.wear.theme.WearPermissionMaterialUIVersion @Composable fun LocationProviderDialogScreen(args: LocationProviderInterceptDialogArgs?) { @@ -41,7 +42,8 @@ fun LocationProviderDialogScreen(args: LocationProviderInterceptDialogArgs?) { } } SwipeToDismissBox(state = state) { isBackground -> - Scaffold( + WearPermissionScaffold( + materialUIVersion = WearPermissionMaterialUIVersion.MATERIAL2_5, showTimeText = false, image = iconId, title = stringResource(titleId), @@ -54,7 +56,7 @@ fun LocationProviderDialogScreen(args: LocationProviderInterceptDialogArgs?) { onClick = onLocationSettingsClick, modifier = Modifier.fillMaxWidth(), textColor = MaterialTheme.colors.surface, - colors = ChipDefaults.primaryChipColors() + colors = ChipDefaults.primaryChipColors(), ) } item { @@ -64,7 +66,7 @@ fun LocationProviderDialogScreen(args: LocationProviderInterceptDialogArgs?) { modifier = Modifier.fillMaxWidth(), ) } - } + }, ) } } diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/WearGrantPermissionsScreen.kt b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/WearGrantPermissionsScreen.kt index 950353f52..50a19e571 100644 --- a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/WearGrantPermissionsScreen.kt +++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/WearGrantPermissionsScreen.kt @@ -22,6 +22,7 @@ import androidx.compose.runtime.livedata.observeAsState import androidx.compose.ui.Modifier import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.res.stringResource +import com.android.permission.flags.Flags import com.android.permissioncontroller.R import com.android.permissioncontroller.permission.ui.GrantPermissionsActivity.ALLOW_ALWAYS_BUTTON import com.android.permissioncontroller.permission.ui.GrantPermissionsActivity.ALLOW_BUTTON @@ -37,17 +38,19 @@ import com.android.permissioncontroller.permission.ui.GrantPermissionsActivity.N import com.android.permissioncontroller.permission.ui.GrantPermissionsActivity.NO_UPGRADE_OT_AND_DONT_ASK_AGAIN_BUTTON import com.android.permissioncontroller.permission.ui.GrantPermissionsActivity.NO_UPGRADE_OT_BUTTON import com.android.permissioncontroller.permission.ui.wear.GrantPermissionsWearViewHandler.BUTTON_RES_ID_TO_NUM -import com.android.permissioncontroller.permission.ui.wear.elements.Chip import com.android.permissioncontroller.permission.ui.wear.elements.ScrollableScreen -import com.android.permissioncontroller.permission.ui.wear.elements.ToggleChip import com.android.permissioncontroller.permission.ui.wear.elements.ToggleChipToggleControl +import com.android.permissioncontroller.permission.ui.wear.elements.material3.WearPermissionButton +import com.android.permissioncontroller.permission.ui.wear.elements.material3.WearPermissionToggleControl import com.android.permissioncontroller.permission.ui.wear.model.WearGrantPermissionsViewModel +import com.android.permissioncontroller.permission.ui.wear.theme.WearPermissionMaterialUIVersion.MATERIAL2_5 +import com.android.permissioncontroller.permission.ui.wear.theme.WearPermissionMaterialUIVersion.MATERIAL3 @Composable fun WearGrantPermissionsScreen( viewModel: WearGrantPermissionsViewModel, onButtonClicked: (Int) -> Unit, - onLocationSwitchChanged: (Boolean) -> Unit + onLocationSwitchChanged: (Boolean) -> Unit, ) { val groupMessage = viewModel.groupMessageLiveData.observeAsState("") val icon = viewModel.iconLiveData.observeAsState(null) @@ -55,8 +58,16 @@ fun WearGrantPermissionsScreen( val locationVisibilities = viewModel.locationVisibilitiesLiveData.observeAsState(emptyList()) val preciseLocationChecked = viewModel.preciseLocationCheckedLiveData.observeAsState(false) val buttonVisibilities = viewModel.buttonVisibilitiesLiveData.observeAsState(emptyList()) + val useMaterial3Controls = Flags.wearComposeMaterial3() + val materialUIVersion = + if (useMaterial3Controls) { + MATERIAL3 + } else { + MATERIAL2_5 + } ScrollableScreen( + materialUIVersion = materialUIVersion, showTimeText = false, image = icon.value, title = groupMessage.value, @@ -69,13 +80,14 @@ fun WearGrantPermissionsScreen( locationVisibilities.value.getOrElse(DIALOG_WITH_BOTH_LOCATIONS) { false } ) { item { - ToggleChip( + WearPermissionToggleControl( checked = preciseLocationChecked.value, - onCheckedChanged = { onLocationSwitchChanged(it) }, + onCheckedChanged = onLocationSwitchChanged, label = stringResource(R.string.app_permission_location_accuracy), toggleControl = ToggleChipToggleControl.Switch, modifier = Modifier.fillMaxWidth(), - labelMaxLine = Integer.MAX_VALUE + labelMaxLines = Integer.MAX_VALUE, + materialUIVersion = materialUIVersion, ) } } @@ -87,16 +99,17 @@ fun WearGrantPermissionsScreen( } if (buttonVisibilities.value[pos]) { item { - Chip( + WearPermissionButton( label = getPrimaryText( - pos, - locationVisibilities.value, - labelsByButton(BUTTON_RES_ID_TO_NUM.valueAt(i)) + pos = pos, + locationVisibilities = locationVisibilities.value, + default = labelsByButton(BUTTON_RES_ID_TO_NUM.valueAt(i)), ), onClick = { onButtonClicked(BUTTON_RES_ID_TO_NUM.keyAt(i)) }, modifier = Modifier.fillMaxWidth(), - labelMaxLines = Integer.MAX_VALUE + labelMaxLines = Integer.MAX_VALUE, + materialUIVersion = materialUIVersion, ) } } @@ -108,7 +121,7 @@ fun setContent( composeView: ComposeView, viewModel: WearGrantPermissionsViewModel, onButtonClicked: (Int) -> Unit, - onLocationSwitchChanged: (Boolean) -> Unit + onLocationSwitchChanged: (Boolean) -> Unit, ) { composeView.setContent { WearGrantPermissionsScreen(viewModel, onButtonClicked, onLocationSwitchChanged) diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/AnnotatedText.kt b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/AnnotatedText.kt index 34c7cee9a..07bb88e80 100644 --- a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/AnnotatedText.kt +++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/AnnotatedText.kt @@ -41,7 +41,7 @@ fun AnnotatedText( text: CharSequence, style: TextStyle, modifier: Modifier = Modifier, - shouldCapitalize: Boolean + shouldCapitalize: Boolean, ) { val onClickCallbacks = mutableMapOf<String, (View) -> Unit>() val context = LocalContext.current @@ -56,7 +56,7 @@ fun AnnotatedText( text, shouldCapitalize, onClickCallbacks, - listener = listener + listener = listener, ) BasicText(text = annotatedString, style = style, modifier = modifier) } @@ -67,7 +67,7 @@ private fun spannableStringToAnnotatedString( shouldCapitalize: Boolean, onClickCallbacks: MutableMap<String, (View) -> Unit>, spanColor: Color = MaterialTheme.colors.primary, - listener: LinkInteractionListener + listener: LinkInteractionListener, ): AnnotatedString { val finalString = if (shouldCapitalize) text.toString().capitalize() else text.toString() val annotatedString = @@ -85,14 +85,14 @@ private fun spannableStringToAnnotatedString( start, end, onClickCallbacks, - listener + listener, ) else -> addStyle(SpanStyle(), start, end) } } } } else { - AnnotatedString(text.toString()) + AnnotatedString(finalString) } return annotatedString } @@ -103,14 +103,10 @@ private fun AnnotatedString.Builder.addClickableSpan( start: Int, end: Int, onClickCallbacks: MutableMap<String, (View) -> Unit>, - listener: LinkInteractionListener + listener: LinkInteractionListener, ) { val key = "${CLICKABLE_SPAN_TAG}:$start:$end" onClickCallbacks[key] = span::onClick addLink(LinkAnnotation.Clickable(key, linkInteractionListener = listener), start, end) - addStyle( - SpanStyle(color = spanColor, textDecoration = TextDecoration.Underline), - start, - end, - ) + addStyle(SpanStyle(color = spanColor, textDecoration = TextDecoration.Underline), start, end) } diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/Button.kt b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/Button.kt deleted file mode 100644 index 1394c56ea..000000000 --- a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/Button.kt +++ /dev/null @@ -1,133 +0,0 @@ -/* - * Copyright (C) 2023 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.android.permissioncontroller.permission.ui.wear.elements - -import androidx.annotation.DrawableRes -import androidx.compose.foundation.layout.size -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.unit.Dp -import androidx.wear.compose.material.Button -import androidx.wear.compose.material.ButtonColors -import androidx.wear.compose.material.ButtonDefaults -import androidx.wear.compose.material.ButtonDefaults.DefaultButtonSize -import androidx.wear.compose.material.ButtonDefaults.DefaultIconSize -import androidx.wear.compose.material.ButtonDefaults.LargeButtonSize -import androidx.wear.compose.material.ButtonDefaults.LargeIconSize -import androidx.wear.compose.material.ButtonDefaults.SmallButtonSize -import androidx.wear.compose.material.ButtonDefaults.SmallIconSize - -/** - * This component is an alternative to [Button], providing the following: - * - a convenient way of providing an icon and choosing its size from a range of sizes recommended - * by the Wear guidelines; - */ -@Composable -public fun Button( - imageVector: ImageVector, - contentDescription: String, - onClick: () -> Unit, - modifier: Modifier = Modifier, - colors: ButtonColors = ButtonDefaults.primaryButtonColors(), - buttonSize: ButtonSize = ButtonSize.Default, - iconRtlMode: IconRtlMode = IconRtlMode.Default, - enabled: Boolean = true -) { - Button( - icon = imageVector, - contentDescription = contentDescription, - onClick = onClick, - modifier = modifier, - colors = colors, - buttonSize = buttonSize, - iconRtlMode = iconRtlMode, - enabled = enabled - ) -} - -/** - * This component is an alternative to [Button], providing the following: - * - a convenient way of providing an icon and choosing its size from a range of sizes recommended - * by the Wear guidelines; - */ -@Composable -public fun Button( - @DrawableRes id: Int, - contentDescription: String, - onClick: () -> Unit, - modifier: Modifier = Modifier, - colors: ButtonColors = ButtonDefaults.primaryButtonColors(), - buttonSize: ButtonSize = ButtonSize.Default, - iconRtlMode: IconRtlMode = IconRtlMode.Default, - enabled: Boolean = true -) { - Button( - icon = id, - contentDescription = contentDescription, - onClick = onClick, - modifier = modifier, - colors = colors, - buttonSize = buttonSize, - iconRtlMode = iconRtlMode, - enabled = enabled - ) -} - -@Composable -internal fun Button( - icon: Any, - contentDescription: String, - onClick: () -> Unit, - modifier: Modifier = Modifier, - colors: ButtonColors = ButtonDefaults.primaryButtonColors(), - buttonSize: ButtonSize = ButtonSize.Default, - iconRtlMode: IconRtlMode = IconRtlMode.Default, - enabled: Boolean = true -) { - Button( - onClick = onClick, - modifier = modifier.size(buttonSize.tapTargetSize), - enabled = enabled, - colors = colors - ) { - val iconModifier = Modifier.size(buttonSize.iconSize).align(Alignment.Center) - - Icon( - icon = icon, - contentDescription = contentDescription, - modifier = iconModifier, - rtlMode = iconRtlMode - ) - } -} - -public sealed class ButtonSize(public val iconSize: Dp, public val tapTargetSize: Dp) { - public object Default : - ButtonSize(iconSize = DefaultIconSize, tapTargetSize = DefaultButtonSize) - - public object Large : ButtonSize(iconSize = LargeIconSize, tapTargetSize = LargeButtonSize) - public object Small : ButtonSize(iconSize = SmallIconSize, tapTargetSize = SmallButtonSize) - - /** - * Custom sizes should follow the - * [accessibility principles and guidance for touch targets](https://developer.android.com/training/wearables/accessibility#set-minimum). - */ - public data class Custom(val customIconSize: Dp, val customTapTargetSize: Dp) : - ButtonSize(iconSize = customIconSize, tapTargetSize = customTapTargetSize) -} diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/ScrollableScreen.kt b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/ScrollableScreen.kt index d8f340a7b..d1b7e899b 100644 --- a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/ScrollableScreen.kt +++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/ScrollableScreen.kt @@ -63,7 +63,10 @@ import androidx.wear.compose.material.TimeText import androidx.wear.compose.material.Vignette import androidx.wear.compose.material.VignettePosition import androidx.wear.compose.material.scrollAway +import com.android.permissioncontroller.permission.ui.wear.elements.material3.WearPermissionScaffold import com.android.permissioncontroller.permission.ui.wear.elements.rotaryinput.rotaryWithScroll +import com.android.permissioncontroller.permission.ui.wear.theme.WearPermissionMaterialUIVersion +import com.android.permissioncontroller.permission.ui.wear.theme.WearPermissionMaterialUIVersion.MATERIAL2_5 import com.android.permissioncontroller.permission.ui.wear.theme.WearPermissionTheme /** @@ -74,6 +77,7 @@ import com.android.permissioncontroller.permission.ui.wear.theme.WearPermissionT */ @Composable fun ScrollableScreen( + materialUIVersion: WearPermissionMaterialUIVersion = MATERIAL2_5, showTimeText: Boolean = true, title: String? = null, subtitle: CharSequence? = null, @@ -103,7 +107,8 @@ fun ScrollableScreen( if (getBackStackEntryCount(activity) > 0) { SwipeToDismissBox(state = state) { isBackground -> - Scaffold( + WearPermissionScaffold( + materialUIVersion, showTimeText, title, subtitle, @@ -111,11 +116,12 @@ fun ScrollableScreen( isLoading = isLoading || isBackground || dismissed, content, titleTestTag, - subtitleTestTag + subtitleTestTag, ) } } else { - Scaffold( + WearPermissionScaffold( + materialUIVersion, showTimeText, title, subtitle, @@ -123,13 +129,13 @@ fun ScrollableScreen( isLoading, content, titleTestTag, - subtitleTestTag + subtitleTestTag, ) } } @Composable -internal fun Scaffold( +internal fun Wear2Scaffold( showTimeText: Boolean, title: String?, subtitle: CharSequence?, @@ -165,14 +171,14 @@ internal fun Scaffold( start = titleHorizontalPadding, top = 4.dp, bottom = titleBottomPadding, - end = titleHorizontalPadding + end = titleHorizontalPadding, ) val subTitlePaddingValues = PaddingValues( start = subtitleHorizontalPadding, top = 4.dp, bottom = subtitleBottomPadding, - end = subtitleHorizontalPadding + end = subtitleHorizontalPadding, ) val initialCenterIndex = 0 val centerHeightDp = Dp(LocalConfiguration.current.screenHeightDp / 2.0f) @@ -191,14 +197,14 @@ internal fun Scaffold( modifier = Modifier.rotaryWithScroll( scrollableState = listState, - focusRequester = focusRequester + focusRequester = focusRequester, ), timeText = { if (showTimeText && !isLoading) { TimeText( modifier = Modifier.scrollAway(listState, initialCenterIndex, scrollAwayOffset) - .padding(top = timeTextTopPadding), + .padding(top = timeTextTopPadding) ) } }, @@ -208,7 +214,7 @@ internal fun Scaffold( { PositionIndicator(scalingLazyListState = listState) } } else { null - } + }, ) { Box(modifier = Modifier.fillMaxSize()) { if (isLoading) { @@ -225,8 +231,8 @@ internal fun Scaffold( start = scrollContentHorizontalPadding, end = scrollContentHorizontalPadding, top = scrollContentTopPadding, - bottom = scrollContentBottomPadding - ) + bottom = scrollContentBottomPadding, + ), ) { staticItem() image?.let { @@ -238,7 +244,7 @@ internal fun Scaffold( painter = painterResource(id = image), contentDescription = null, contentScale = ContentScale.Crop, - modifier = imageModifier + modifier = imageModifier, ) } is Drawable -> @@ -247,7 +253,7 @@ internal fun Scaffold( painter = rememberDrawablePainter(image), contentDescription = null, contentScale = ContentScale.Crop, - modifier = imageModifier + modifier = imageModifier, ) } else -> {} @@ -263,7 +269,7 @@ internal fun Scaffold( Text( text = title, textAlign = TextAlign.Center, - modifier = modifier + modifier = modifier, ) } } @@ -282,7 +288,7 @@ internal fun Scaffold( color = MaterialTheme.colors.onSurfaceVariant ), modifier = modifier, - shouldCapitalize = true + shouldCapitalize = true, ) } } diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/ToggleChip.kt b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/ToggleChip.kt index a21a9d015..4f4201748 100644 --- a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/ToggleChip.kt +++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/ToggleChip.kt @@ -29,11 +29,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.compositeOver -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.semantics.Role -import androidx.compose.ui.semantics.role -import androidx.compose.ui.semantics.semantics -import androidx.compose.ui.semantics.stateDescription import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.wear.compose.material.ChipDefaults @@ -44,7 +39,6 @@ import androidx.wear.compose.material.ToggleChip import androidx.wear.compose.material.ToggleChipColors import androidx.wear.compose.material.ToggleChipDefaults import androidx.wear.compose.material.contentColorFor -import com.android.permissioncontroller.R /** * This component is an alternative to [ToggleChip], providing the following: @@ -67,7 +61,7 @@ fun ToggleChip( secondaryLabelMaxLine: Int? = null, colors: ToggleChipColors = ToggleChipDefaults.toggleChipColors(), enabled: Boolean = true, - interactionSource: MutableInteractionSource = remember { MutableInteractionSource() } + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, ) { val hasSecondaryLabel = secondaryLabel != null @@ -78,7 +72,7 @@ fun ToggleChip( textAlign = TextAlign.Start, overflow = TextOverflow.Ellipsis, maxLines = labelMaxLine ?: if (hasSecondaryLabel) 1 else 2, - style = MaterialTheme.typography.button + style = MaterialTheme.typography.button, ) } @@ -89,7 +83,7 @@ fun ToggleChip( text = secondaryLabel, overflow = TextOverflow.Ellipsis, maxLines = secondaryLabelMaxLine ?: 1, - style = MaterialTheme.typography.caption2 + style = MaterialTheme.typography.caption2, ) } } @@ -110,7 +104,7 @@ fun ToggleChip( IconRtlMode.Mirrored } else { IconRtlMode.Default - } + }, ) } @@ -123,42 +117,23 @@ fun ToggleChip( tint = iconColor, contentDescription = null, modifier = Modifier.size(ChipDefaults.IconSize).clip(CircleShape), - rtlMode = iconRtlMode + rtlMode = iconRtlMode, ) } } } - val semanticsRole = - when (toggleControl) { - ToggleChipToggleControl.Switch -> Role.Switch - ToggleChipToggleControl.Radio -> Role.RadioButton - ToggleChipToggleControl.Checkbox -> Role.Checkbox - } - - val stateDescriptionSemantics = - stringResource( - if (checked) { - R.string.on - } else { - R.string.off - } - ) ToggleChip( checked = checked, onCheckedChange = onCheckedChanged, label = labelParam, toggleControl = toggleControlParam, - modifier = - modifier.fillMaxWidth().semantics { - role = semanticsRole - stateDescription = stateDescriptionSemantics - }, + modifier = modifier.fillMaxWidth().toggleControlSemantics(toggleControl, checked), appIcon = iconParam, secondaryLabel = secondaryLabelParam, colors = colors, enabled = enabled, - interactionSource = interactionSource + interactionSource = interactionSource, ) } @@ -198,7 +173,7 @@ fun toggleChipDisabledColors(): ToggleChipColors { uncheckedSecondaryContentColor = uncheckedSecondaryContentColor.copy(alpha = ContentAlpha.disabled), uncheckedToggleControlColor = - uncheckedToggleControlColor.copy(alpha = ContentAlpha.disabled) + uncheckedToggleControlColor.copy(alpha = ContentAlpha.disabled), ) } @@ -236,6 +211,6 @@ fun toggleChipBackgroundColors(): ToggleChipColors { uncheckedEndBackgroundColor = uncheckedEndBackgroundColor, uncheckedContentColor = uncheckedContentColor, uncheckedSecondaryContentColor = uncheckedSecondaryContentColor, - uncheckedToggleControlColor = uncheckedToggleControlColor + uncheckedToggleControlColor = uncheckedToggleControlColor, ) } diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/ToggleChipToggleControl.kt b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/ToggleChipToggleControl.kt index a4ce4e764..b6f6db4d3 100644 --- a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/ToggleChipToggleControl.kt +++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/ToggleChipToggleControl.kt @@ -16,8 +16,43 @@ package com.android.permissioncontroller.permission.ui.wear.elements -public enum class ToggleChipToggleControl { +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.semantics.role +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.semantics.stateDescription +import com.android.permissioncontroller.R + +enum class ToggleChipToggleControl { Switch, Radio, - Checkbox + Checkbox, +} + +@Composable +fun Modifier.toggleControlSemantics( + toggleControl: ToggleChipToggleControl, + checked: Boolean, +): Modifier { + val semanticsRole = + when (toggleControl) { + ToggleChipToggleControl.Switch -> Role.Switch + ToggleChipToggleControl.Radio -> Role.RadioButton + ToggleChipToggleControl.Checkbox -> Role.Checkbox + } + val stateDescriptionSemantics = + stringResource( + if (checked) { + R.string.on + } else { + R.string.off + } + ) + + return semantics { + role = semanticsRole + stateDescription = stateDescriptionSemantics + } } diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/material3/WearPermissionButton.kt b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/material3/WearPermissionButton.kt new file mode 100644 index 000000000..4ed9e92b9 --- /dev/null +++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/material3/WearPermissionButton.kt @@ -0,0 +1,120 @@ +/* + * Copyright 2024 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 + * + * https://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.android.permissioncontroller.permission.ui.wear.elements.material3 + +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.wear.compose.material3.Button +import androidx.wear.compose.material3.ButtonColors +import androidx.wear.compose.material3.ButtonDefaults +import androidx.wear.compose.material3.LocalTextConfiguration +import androidx.wear.compose.material3.Text +import com.android.permissioncontroller.permission.ui.wear.elements.Chip +import com.android.permissioncontroller.permission.ui.wear.theme.WearPermissionMaterialUIVersion + +/** + * This component is wrapper on material Button component + * 1. It takes icon, primary, secondary label resources and construct them applying permission app + * defaults + */ +@Composable +fun WearPermissionButton( + label: String, + modifier: Modifier = Modifier, + materialUIVersion: WearPermissionMaterialUIVersion = + WearPermissionMaterialUIVersion.MATERIAL2_5, + iconBuilder: WearPermissionIconBuilder? = null, + labelMaxLines: Int? = null, + secondaryLabel: String? = null, + secondaryLabelMaxLines: Int? = null, + onClick: () -> Unit, + enabled: Boolean = true, + style: WearPermissionButtonStyle = WearPermissionButtonStyle.Secondary, +) { + if (materialUIVersion == WearPermissionMaterialUIVersion.MATERIAL2_5) { + Chip( + label = label, + labelMaxLines = labelMaxLines, + onClick = onClick, + modifier = modifier, + secondaryLabel = secondaryLabel, + secondaryLabelMaxLines = secondaryLabelMaxLines, + icon = { iconBuilder?.build() }, + largeIcon = false, + colors = style.material2ChipColors(), + enabled = enabled, + ) + } else { + WearPermissionButtonInternal( + iconBuilder = iconBuilder, + label = label, + labelMaxLines = labelMaxLines, + secondaryLabel = secondaryLabel, + secondaryLabelMaxLines = secondaryLabelMaxLines, + onClick = onClick, + modifier = modifier, + enabled = enabled, + colors = style.material3ButtonColors(), + ) + } +} + +@Composable +private fun WearPermissionButtonInternal( + label: String, + modifier: Modifier = Modifier, + iconBuilder: WearPermissionIconBuilder? = null, + labelMaxLines: Int? = null, + secondaryLabel: String? = null, + secondaryLabelMaxLines: Int? = null, + onClick: () -> Unit, + enabled: Boolean = true, + colors: ButtonColors = ButtonDefaults.filledTonalButtonColors(), +) { + val iconParam: (@Composable BoxScope.() -> Unit)? = iconBuilder?.let { { it.build() } } + + val labelParam: (@Composable RowScope.() -> Unit) = { + Text( + text = label, + modifier = Modifier.fillMaxWidth(), + maxLines = labelMaxLines ?: LocalTextConfiguration.current.maxLines, + ) + } + + val secondaryLabelParam: (@Composable RowScope.() -> Unit)? = + secondaryLabel?.let { + { + Text( + text = secondaryLabel, + modifier = Modifier.fillMaxWidth(), + maxLines = secondaryLabelMaxLines ?: LocalTextConfiguration.current.maxLines, + ) + } + } + + Button( + icon = iconParam, + label = labelParam, + secondaryLabel = secondaryLabelParam, + enabled = enabled, + onClick = onClick, + modifier = modifier.fillMaxWidth(), + colors = colors, + ) +} diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/material3/WearPermissionButtonStyle.kt b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/material3/WearPermissionButtonStyle.kt new file mode 100644 index 000000000..5a91ae46c --- /dev/null +++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/material3/WearPermissionButtonStyle.kt @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2024 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.android.permissioncontroller.permission.ui.wear.elements.material3 + +import androidx.compose.runtime.Composable +import androidx.wear.compose.material.ChipColors +import androidx.wear.compose.material.ChipDefaults +import androidx.wear.compose.material3.ButtonColors +import androidx.wear.compose.material3.ButtonDefaults +import com.android.permissioncontroller.permission.ui.wear.elements.chipDisabledColors +import com.android.permissioncontroller.permission.ui.wear.elements.material3.WearPermissionButtonStyle.DisabledLike +import com.android.permissioncontroller.permission.ui.wear.elements.material3.WearPermissionButtonStyle.Primary +import com.android.permissioncontroller.permission.ui.wear.elements.material3.WearPermissionButtonStyle.Secondary +import com.android.permissioncontroller.permission.ui.wear.elements.material3.WearPermissionButtonStyle.Transparent + +/** + * This component is wrapper on material control colors, It applies the right colors based material + * ui version. + */ +enum class WearPermissionButtonStyle { + Primary, + Secondary, + Transparent, + DisabledLike, +} + +@Composable +internal fun WearPermissionButtonStyle.material2ChipColors(): ChipColors { + return when (this) { + Primary -> ChipDefaults.primaryChipColors() + Secondary -> ChipDefaults.secondaryChipColors() + Transparent -> ChipDefaults.childChipColors() + DisabledLike -> chipDisabledColors() + } +} + +@Composable +internal fun WearPermissionButtonStyle.material3ButtonColors(): ButtonColors { + return when (this) { + Primary -> ButtonDefaults.buttonColors() + Secondary -> ButtonDefaults.filledTonalButtonColors() + Transparent -> ButtonDefaults.childButtonColors() + DisabledLike -> ButtonDefaults.disabledLikeColors() + } +} + +@Composable +private fun ButtonDefaults.disabledLikeColors() = + filledTonalButtonColors().run { + ButtonColors( + containerPainter = disabledContainerPainter, + contentColor = disabledContentColor, + secondaryContentColor = disabledSecondaryContentColor, + iconColor = disabledIconColor, + disabledContainerPainter = disabledContainerPainter, + disabledContentColor = disabledContentColor, + disabledSecondaryContentColor = disabledSecondaryContentColor, + disabledIconColor = disabledIconColor, + ) + } diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/material3/WearPermissionIconBuilder.kt b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/material3/WearPermissionIconBuilder.kt new file mode 100644 index 000000000..65a85db7e --- /dev/null +++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/material3/WearPermissionIconBuilder.kt @@ -0,0 +1,101 @@ +/* + * Copyright 2024 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 + * + * https://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.android.permissioncontroller.permission.ui.wear.elements.material3 + +import android.graphics.drawable.Drawable +import androidx.compose.foundation.layout.size +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.painterResource +import androidx.wear.compose.material3.ButtonDefaults +import androidx.wear.compose.material3.Icon +import com.android.permissioncontroller.permission.ui.wear.elements.rememberDrawablePainter + +/** + * This class simplifies the construction of icons with various attributes like resource type, + * content description, modifier, and tint. It supports different icon resource types, including: + * - ImageVector + * - Resource ID (Int) + * - Drawable + * - ImageBitmap + * + * Usage: + * ``` + * val icon = WearPermissionIconBuilder.builder(IconResourceId) + * .contentDescription("Location Permission") + * .modifier(Modifier.size(24.dp)) + * .tint(Color.Red) + * .build() + * ``` + * + * Note: This builder uses a private constructor and is initialized through the `builder()` + * companion object method. + */ +class WearPermissionIconBuilder private constructor() { + var iconResource: Any? = null + private set + + var contentDescription: String? = null + private set + + var modifier: Modifier = Modifier.size(ButtonDefaults.IconSize) + private set + + var tint: Color = Color.Unspecified + private set + + fun contentDescription(description: String?): WearPermissionIconBuilder { + contentDescription = description + return this + } + + fun modifier(modifier: Modifier): WearPermissionIconBuilder { + this.modifier then modifier + return this + } + + fun tint(tint: Color): WearPermissionIconBuilder { + this.tint = tint + return this + } + + @Composable + fun build() { + when (iconResource) { + is ImageVector -> Icon(iconResource as ImageVector, contentDescription, modifier, tint) + is Int -> + Icon(painterResource(id = iconResource as Int), contentDescription, modifier, tint) + + is Drawable -> + Icon( + rememberDrawablePainter(iconResource as Drawable), + contentDescription, + modifier, + tint, + ) + + is ImageBitmap -> Icon(iconResource as ImageBitmap, contentDescription, modifier, tint) + else -> throw IllegalArgumentException("Type not supported.") + } + } + + companion object { + fun builder(icon: Any) = WearPermissionIconBuilder().apply { iconResource = icon } + } +} diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/material3/WearPermissionScaffold.kt b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/material3/WearPermissionScaffold.kt new file mode 100644 index 000000000..bd7636273 --- /dev/null +++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/material3/WearPermissionScaffold.kt @@ -0,0 +1,298 @@ +/* + * Copyright 2024 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 + * + * https://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.android.permissioncontroller.permission.ui.wear.elements.material3 + +import android.graphics.drawable.Drawable +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.requiredHeightIn +import androidx.compose.foundation.layout.size +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.wear.compose.foundation.ScrollInfoProvider +import androidx.wear.compose.foundation.lazy.ScalingLazyListScope +import androidx.wear.compose.material3.AppScaffold +import androidx.wear.compose.material3.CircularProgressIndicator +import androidx.wear.compose.material3.ListHeader +import androidx.wear.compose.material3.MaterialTheme +import androidx.wear.compose.material3.ScreenScaffold +import androidx.wear.compose.material3.ScrollIndicator +import androidx.wear.compose.material3.Text +import androidx.wear.compose.material3.TimeText +import com.android.permissioncontroller.permission.ui.wear.elements.AnnotatedText +import com.android.permissioncontroller.permission.ui.wear.elements.Wear2Scaffold +import com.android.permissioncontroller.permission.ui.wear.elements.layout.ScalingLazyColumn +import com.android.permissioncontroller.permission.ui.wear.elements.layout.ScalingLazyColumnState +import com.android.permissioncontroller.permission.ui.wear.elements.layout.rememberResponsiveColumnState +import com.android.permissioncontroller.permission.ui.wear.elements.rememberDrawablePainter +import com.android.permissioncontroller.permission.ui.wear.theme.WearPermissionMaterialUIVersion +import com.android.permissioncontroller.permission.ui.wear.theme.WearPermissionMaterialUIVersion.MATERIAL2_5 +import com.android.permissioncontroller.permission.ui.wear.theme.WearPermissionTheme + +/** + * This component is wrapper on material scaffold component. It helps with time text, scroll + * indicator and standard list elements like title, icon and subtitle. + */ +@Composable +internal fun WearPermissionScaffold( + materialUIVersion: WearPermissionMaterialUIVersion = MATERIAL2_5, + showTimeText: Boolean, + title: String?, + subtitle: CharSequence?, + image: Any?, + isLoading: Boolean, + content: ScalingLazyListScope.() -> Unit, + titleTestTag: String? = null, + subtitleTestTag: String? = null, +) { + + if (materialUIVersion == MATERIAL2_5) { + Wear2Scaffold( + showTimeText, + title, + subtitle, + image, + isLoading, + content, + titleTestTag, + subtitleTestTag, + ) + } else { + WearPermissionScaffoldInternal( + showTimeText, + title, + subtitle, + image, + isLoading, + content, + titleTestTag, + subtitleTestTag, + ) + } +} + +@Composable +private fun WearPermissionScaffoldInternal( + showTimeText: Boolean, + title: String?, + subtitle: CharSequence?, + image: Any?, + isLoading: Boolean, + content: ScalingLazyListScope.() -> Unit, + titleTestTag: String? = null, + subtitleTestTag: String? = null, +) { + val screenWidth = LocalConfiguration.current.screenWidthDp + val screenHeight = LocalConfiguration.current.screenHeightDp + val paddingDefaults = + WearPermissionScaffoldPaddingDefaults( + screenWidth = screenWidth, + screenHeight = screenHeight, + titleNeedsLargePadding = subtitle == null, + ) + val columnState = + rememberResponsiveColumnState(contentPadding = { paddingDefaults.scrollContentPadding }) + WearPermissionTheme(version = WearPermissionMaterialUIVersion.MATERIAL3) { + AppScaffold(timeText = wearPermissionTimeText(showTimeText && !isLoading)) { + ScreenScaffold( + scrollInfoProvider = ScrollInfoProvider(columnState.state), + scrollIndicator = wearPermissionScrollIndicator(!isLoading, columnState), + ) { + Box(modifier = Modifier.fillMaxSize()) { + if (isLoading) { + CircularProgressIndicator(modifier = Modifier.align(Alignment.Center)) + } else { + ScrollingView( + columnState = columnState, + icon = painterFromImage(image), + title = title, + titleTestTag = titleTestTag, + titlePaddingValues = paddingDefaults.titlePaddingValues, + subtitle = subtitle, + subtitleTestTag = subtitleTestTag, + subTitlePaddingValues = paddingDefaults.subTitlePaddingValues, + content = content, + ) + } + } + } + } + } +} + +private class WearPermissionScaffoldPaddingDefaults( + screenWidth: Int, + screenHeight: Int, + titleNeedsLargePadding: Boolean, +) { + private val firstSpacerItemHeight = 0.dp + private val scrollContentHorizontalPadding = (screenWidth * 0.052).dp + private val titleHorizontalPadding = (screenWidth * 0.0884).dp + private val subtitleHorizontalPadding = (screenWidth * 0.0416).dp + private val scrollContentTopPadding = (screenHeight * 0.1456).dp - firstSpacerItemHeight + private val scrollContentBottomPadding = (screenHeight * 0.3636).dp + private val defaultItemPadding = 4.dp + private val largeItemPadding = 8.dp + val titlePaddingValues = + PaddingValues( + start = titleHorizontalPadding, + top = defaultItemPadding, + bottom = if (titleNeedsLargePadding) largeItemPadding else defaultItemPadding, + end = titleHorizontalPadding, + ) + val subTitlePaddingValues = + PaddingValues( + start = subtitleHorizontalPadding, + top = defaultItemPadding, + bottom = largeItemPadding, + end = subtitleHorizontalPadding, + ) + val scrollContentPadding = + PaddingValues( + start = scrollContentHorizontalPadding, + end = scrollContentHorizontalPadding, + top = scrollContentTopPadding, + bottom = scrollContentBottomPadding, + ) +} + +@Composable +private fun BoxScope.ScrollingView( + columnState: ScalingLazyColumnState, + icon: Painter?, + title: String?, + titleTestTag: String?, + subtitle: CharSequence?, + subtitleTestTag: String?, + titlePaddingValues: PaddingValues, + subTitlePaddingValues: PaddingValues, + content: ScalingLazyListScope.() -> Unit, +) { + ScalingLazyColumn(columnState = columnState) { + iconItem(icon, Modifier.size(24.dp)) + titleItem(text = title, testTag = titleTestTag, contentPaddingValues = titlePaddingValues) + subtitleItem( + text = subtitle, + testTag = subtitleTestTag, + modifier = Modifier.align(Alignment.Center).padding(subTitlePaddingValues), + ) + content() + } +} + +private fun wearPermissionTimeText(showTime: Boolean): @Composable () -> Unit { + return if (showTime) { + { TimeText { time() } } + } else { + {} + } +} + +private fun wearPermissionScrollIndicator( + showIndicator: Boolean, + columnState: ScalingLazyColumnState, +): @Composable (BoxScope.() -> Unit)? { + return if (showIndicator) { + { + ScrollIndicator( + modifier = Modifier.align(Alignment.CenterEnd), + state = columnState.state, + ) + } + } else { + null + } +} + +@Composable +private fun painterFromImage(image: Any?): Painter? { + return when (image) { + is Int -> painterResource(id = image) + is Drawable -> rememberDrawablePainter(image) + else -> null + } +} + +private fun Modifier.optionalTestTag(tag: String?): Modifier { + if (tag == null) { + return this + } + return this then testTag(tag) +} + +private fun ScalingLazyListScope.iconItem(painter: Painter?, modifier: Modifier = Modifier) = + painter?.let { + item { + Image( + painter = it, + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = modifier, + ) + } + } + +private fun ScalingLazyListScope.titleItem( + text: String?, + testTag: String?, + contentPaddingValues: PaddingValues, + modifier: Modifier = Modifier, +) = + text?.let { + item { + ListHeader( + modifier = modifier.requiredHeightIn(1.dp), // We do not want default min height + contentPadding = contentPaddingValues, + ) { + Text( + text = it, + textAlign = TextAlign.Center, + modifier = Modifier.optionalTestTag(testTag), + ) + } + } + } + +private fun ScalingLazyListScope.subtitleItem( + text: CharSequence?, + testTag: String?, + modifier: Modifier = Modifier, +) = + text?.let { + item { + AnnotatedText( + text = it, + style = + MaterialTheme.typography.bodyMedium.copy( + color = MaterialTheme.colorScheme.onSurfaceVariant + ), + modifier = modifier.optionalTestTag(testTag), + shouldCapitalize = true, + ) + } + } diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/material3/WearPermissionToggleControl.kt b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/material3/WearPermissionToggleControl.kt new file mode 100644 index 000000000..4a139f91f --- /dev/null +++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/material3/WearPermissionToggleControl.kt @@ -0,0 +1,165 @@ +/* + * Copyright 2024 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 + * + * https://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.android.permissioncontroller.permission.ui.wear.elements.material3 + +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.wear.compose.material3.CheckboxButton +import androidx.wear.compose.material3.LocalTextConfiguration +import androidx.wear.compose.material3.RadioButton +import androidx.wear.compose.material3.SwitchButton +import androidx.wear.compose.material3.Text +import com.android.permissioncontroller.permission.ui.wear.elements.ToggleChip +import com.android.permissioncontroller.permission.ui.wear.elements.ToggleChipToggleControl +import com.android.permissioncontroller.permission.ui.wear.elements.toggleControlSemantics +import com.android.permissioncontroller.permission.ui.wear.theme.WearPermissionMaterialUIVersion + +/** + * The custom component is a wrapper on different material3 toggle controls. + * 1. It provides an unified interface for RadioButton,CheckButton and SwitchButton. + * 2. It takes icon, primary, secondary label resources and construct them applying permission app + * defaults + * 3. Applies custom semantics for based on the toggle control type + */ +@Composable +fun WearPermissionToggleControl( + toggleControl: ToggleChipToggleControl, + label: String, + checked: Boolean, + onCheckedChanged: (Boolean) -> Unit, + modifier: Modifier = Modifier, + labelMaxLines: Int? = null, + materialUIVersion: WearPermissionMaterialUIVersion = + WearPermissionMaterialUIVersion.MATERIAL2_5, + iconBuilder: WearPermissionIconBuilder? = null, + secondaryLabel: String? = null, + secondaryLabelMaxLines: Int? = null, + enabled: Boolean = true, + style: WearPermissionToggleControlStyle = WearPermissionToggleControlStyle.Default, +) { + if (materialUIVersion == WearPermissionMaterialUIVersion.MATERIAL2_5) { + ToggleChip( + toggleControl = toggleControl, + label = label, + labelMaxLine = labelMaxLines, + checked = checked, + onCheckedChanged = onCheckedChanged, + modifier = modifier, + icon = iconBuilder?.iconResource, + secondaryLabel = secondaryLabel, + secondaryLabelMaxLine = secondaryLabelMaxLines, + enabled = enabled, + colors = style.material2ToggleControlColors(), + ) + } else { + WearPermissionToggleControlInternal( + label = label, + toggleControl = toggleControl, + checked = checked, + onCheckedChanged = onCheckedChanged, + modifier = modifier, + iconBuilder = iconBuilder, + labelMaxLines = labelMaxLines, + secondaryLabel = secondaryLabel, + secondaryLabelMaxLines = secondaryLabelMaxLines, + enabled = enabled, + style = style, + ) + } +} + +@Composable +private fun WearPermissionToggleControlInternal( + label: String, + toggleControl: ToggleChipToggleControl, + checked: Boolean, + onCheckedChanged: (Boolean) -> Unit, + modifier: Modifier = Modifier, + iconBuilder: WearPermissionIconBuilder? = null, + labelMaxLines: Int? = null, + secondaryLabel: String? = null, + secondaryLabelMaxLines: Int? = null, + enabled: Boolean = true, + style: WearPermissionToggleControlStyle = WearPermissionToggleControlStyle.Default, +) { + val labelParam: (@Composable RowScope.() -> Unit) = { + Text( + text = label, + modifier = Modifier.fillMaxWidth(), + maxLines = labelMaxLines ?: LocalTextConfiguration.current.maxLines, + ) + } + + val secondaryLabelParam: (@Composable RowScope.() -> Unit)? = + secondaryLabel?.let { + { + Text( + text = it, + modifier = Modifier.fillMaxWidth(), + maxLines = secondaryLabelMaxLines ?: LocalTextConfiguration.current.maxLines, + ) + } + } + + val iconParam: (@Composable BoxScope.() -> Unit)? = iconBuilder?.let { { it.build() } } + + val updatedModifier = + modifier + .fillMaxWidth() + // .heightIn(min = 58.dp) // TODO(b/370783358): This should be a overlaid value + .toggleControlSemantics(toggleControl, checked) + + when (toggleControl) { + ToggleChipToggleControl.Radio -> + RadioButton( + selected = checked, + onSelect = { onCheckedChanged(true) }, + modifier = updatedModifier, + enabled = enabled, + icon = iconParam, + secondaryLabel = secondaryLabelParam, + label = labelParam, + colors = style.radioButtonColorScheme(), + ) + + ToggleChipToggleControl.Checkbox -> + CheckboxButton( + checked = checked, + onCheckedChange = onCheckedChanged, + modifier = updatedModifier, + enabled = enabled, + icon = iconParam, + secondaryLabel = secondaryLabelParam, + label = labelParam, + colors = style.checkboxColorScheme(), + ) + + ToggleChipToggleControl.Switch -> + SwitchButton( + checked = checked, + onCheckedChange = onCheckedChanged, + modifier = updatedModifier, + enabled = enabled, + icon = iconParam, + secondaryLabel = secondaryLabelParam, + label = labelParam, + colors = style.switchButtonColorScheme(), + ) + } +} diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/material3/WearPermissionToggleControlStyle.kt b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/material3/WearPermissionToggleControlStyle.kt new file mode 100644 index 000000000..b5746f019 --- /dev/null +++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/material3/WearPermissionToggleControlStyle.kt @@ -0,0 +1,158 @@ +/* + * Copyright 2024 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 + * + * https://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.android.permissioncontroller.permission.ui.wear.elements.material3 + +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color +import androidx.wear.compose.material.ToggleChipColors +import androidx.wear.compose.material.ToggleChipDefaults.toggleChipColors +import androidx.wear.compose.material3.CheckboxButtonColors +import androidx.wear.compose.material3.CheckboxButtonDefaults.checkboxButtonColors +import androidx.wear.compose.material3.RadioButtonColors +import androidx.wear.compose.material3.RadioButtonDefaults.radioButtonColors +import androidx.wear.compose.material3.SwitchButtonColors +import androidx.wear.compose.material3.SwitchButtonDefaults.switchButtonColors +import com.android.permissioncontroller.permission.ui.wear.elements.toggleChipBackgroundColors +import com.android.permissioncontroller.permission.ui.wear.elements.toggleChipDisabledColors + +/** + * Defines toggle control styles, It helps in setting the right colors scheme to a toggle control. + */ +enum class WearPermissionToggleControlStyle { + Default, + Transparent, + DisabledLike, +} + +@Composable +internal fun WearPermissionToggleControlStyle.radioButtonColorScheme(): RadioButtonColors { + return when (this) { + WearPermissionToggleControlStyle.Default -> radioButtonColors() + WearPermissionToggleControlStyle.Transparent -> radioButtonTransparentColors() + WearPermissionToggleControlStyle.DisabledLike -> radioButtonDisabledLikeColors() + } +} + +@Composable +internal fun WearPermissionToggleControlStyle.checkboxColorScheme(): CheckboxButtonColors { + return when (this) { + WearPermissionToggleControlStyle.Default -> checkboxButtonColors() + WearPermissionToggleControlStyle.Transparent -> checkButtonTransparentColors() + WearPermissionToggleControlStyle.DisabledLike -> checkboxDisabledLikeColors() + } +} + +@Composable +internal fun WearPermissionToggleControlStyle.switchButtonColorScheme(): SwitchButtonColors { + return when (this) { + WearPermissionToggleControlStyle.Default -> switchButtonColors() + WearPermissionToggleControlStyle.Transparent -> switchButtonTransparentColors() + WearPermissionToggleControlStyle.DisabledLike -> switchButtonDisabledLikeColors() + } +} + +@Composable +internal fun WearPermissionToggleControlStyle.material2ToggleControlColors(): ToggleChipColors { + return when (this) { + WearPermissionToggleControlStyle.Default -> toggleChipColors() + WearPermissionToggleControlStyle.Transparent -> toggleChipBackgroundColors() + WearPermissionToggleControlStyle.DisabledLike -> toggleChipDisabledColors() + } +} + +@Composable +private fun checkButtonTransparentColors() = + checkboxButtonColors( + checkedContainerColor = Color.Transparent, + uncheckedContainerColor = Color.Transparent, + disabledCheckedContainerColor = Color.Transparent, + disabledUncheckedContainerColor = Color.Transparent, + ) + +@Composable +private fun radioButtonTransparentColors() = + radioButtonColors( + selectedContainerColor = Color.Transparent, + unselectedContainerColor = Color.Transparent, + disabledSelectedContainerColor = Color.Transparent, + disabledUnselectedContainerColor = Color.Transparent, + ) + +@Composable +private fun switchButtonTransparentColors() = + switchButtonColors( + checkedContainerColor = Color.Transparent, + uncheckedContainerColor = Color.Transparent, + disabledCheckedContainerColor = Color.Transparent, + disabledUncheckedContainerColor = Color.Transparent, + ) + +@Composable +private fun checkboxDisabledLikeColors(): CheckboxButtonColors { + val defaultColors = checkboxButtonColors() + return checkboxButtonColors( + checkedContainerColor = defaultColors.disabledCheckedContainerColor, + checkedContentColor = defaultColors.disabledCheckedContentColor, + checkedSecondaryContentColor = defaultColors.disabledCheckedSecondaryContentColor, + checkedIconColor = defaultColors.disabledCheckedIconColor, + checkedBoxColor = defaultColors.disabledCheckedBoxColor, + checkedCheckmarkColor = defaultColors.disabledCheckedCheckmarkColor, + uncheckedContainerColor = defaultColors.disabledUncheckedContainerColor, + uncheckedContentColor = defaultColors.disabledUncheckedContentColor, + uncheckedSecondaryContentColor = defaultColors.disabledUncheckedSecondaryContentColor, + uncheckedIconColor = defaultColors.disabledUncheckedIconColor, + uncheckedBoxColor = defaultColors.disabledUncheckedBoxColor, + ) +} + +@Composable +private fun radioButtonDisabledLikeColors(): RadioButtonColors { + val defaultColors = radioButtonColors() + return radioButtonColors( + selectedContainerColor = defaultColors.disabledSelectedContainerColor, + selectedContentColor = defaultColors.disabledSelectedContentColor, + selectedSecondaryContentColor = defaultColors.disabledSelectedSecondaryContentColor, + selectedIconColor = defaultColors.disabledSelectedIconColor, + selectedControlColor = defaultColors.disabledSelectedControlColor, + unselectedContentColor = defaultColors.disabledUnselectedContentColor, + unselectedContainerColor = defaultColors.disabledUnselectedContainerColor, + unselectedSecondaryContentColor = defaultColors.disabledUnselectedSecondaryContentColor, + unselectedIconColor = defaultColors.disabledUnselectedIconColor, + unselectedControlColor = defaultColors.disabledUnselectedControlColor, + ) +} + +@Composable +private fun switchButtonDisabledLikeColors(): SwitchButtonColors { + val defaultColors = switchButtonColors() + return switchButtonColors( + checkedContainerColor = defaultColors.disabledCheckedContainerColor, + checkedContentColor = defaultColors.disabledCheckedContentColor, + checkedSecondaryContentColor = defaultColors.disabledCheckedSecondaryContentColor, + checkedIconColor = defaultColors.disabledCheckedIconColor, + checkedThumbColor = defaultColors.disabledCheckedThumbColor, + checkedThumbIconColor = defaultColors.disabledCheckedThumbIconColor, + checkedTrackColor = defaultColors.disabledCheckedTrackColor, + checkedTrackBorderColor = defaultColors.disabledCheckedTrackBorderColor, + uncheckedContainerColor = defaultColors.disabledUncheckedContainerColor, + uncheckedContentColor = defaultColors.disabledUncheckedContentColor, + uncheckedSecondaryContentColor = defaultColors.disabledUncheckedSecondaryContentColor, + uncheckedIconColor = defaultColors.disabledUncheckedIconColor, + uncheckedThumbColor = defaultColors.disabledUncheckedThumbColor, + uncheckedTrackColor = defaultColors.checkedTrackColor.run { copy(alpha = alpha * 0.12f) }, + uncheckedTrackBorderColor = defaultColors.disabledUncheckedTrackBorderColor, + ) +} diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/theme/WearPermissionTheme.kt b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/theme/WearPermissionTheme.kt index 86a05673a..cfaaa0df9 100644 --- a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/theme/WearPermissionTheme.kt +++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/theme/WearPermissionTheme.kt @@ -32,11 +32,11 @@ import androidx.wear.compose.material.Typography import androidx.wear.compose.material3.MaterialTheme as Material3Theme import com.android.permission.flags.Flags import com.android.permissioncontroller.R -import com.android.permissioncontroller.permission.ui.wear.theme.WearPermissionThemeVersion.LEGACY -import com.android.permissioncontroller.permission.ui.wear.theme.WearPermissionThemeVersion.MATERIAL3 +import com.android.permissioncontroller.permission.ui.wear.theme.WearPermissionMaterialUIVersion.MATERIAL2_5 +import com.android.permissioncontroller.permission.ui.wear.theme.WearPermissionMaterialUIVersion.MATERIAL3 -enum class WearPermissionThemeVersion { - LEGACY, +enum class WearPermissionMaterialUIVersion { + MATERIAL2_5, MATERIAL3, } @@ -47,13 +47,13 @@ enum class WearPermissionThemeVersion { */ @Composable fun WearPermissionTheme( - version: WearPermissionThemeVersion = LEGACY, + version: WearPermissionMaterialUIVersion = MATERIAL2_5, content: @Composable () -> Unit, ) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.VANILLA_ICE_CREAM) { WearPermissionLegacyTheme(content) } else { - val material3OverlayResourcesEnabled = Flags.wearComposeMaterial3() + val useBridgedTheme = Flags.wearComposeMaterial3() if (version == MATERIAL3) { val material3Theme = WearOverlayableMaterial3Theme(LocalContext.current) Material3Theme( @@ -62,7 +62,7 @@ fun WearPermissionTheme( shapes = material3Theme.shapes, content = content, ) - } else if (version == LEGACY && material3OverlayResourcesEnabled) { + } else if (version == MATERIAL2_5 && useBridgedTheme) { val material3Theme = WearOverlayableMaterial3Theme(LocalContext.current) val bridgedLegacyTheme = WearMaterialBridgedLegacyTheme.createFrom(material3Theme) MaterialTheme( |