diff options
author | 2024-10-16 14:30:29 +0000 | |
---|---|---|
committer | 2024-10-16 14:30:29 +0000 | |
commit | 07cf2c314da63b903ade9c972f90b6b3c625579a (patch) | |
tree | a4d5c53dbfffc1ab1500fc98592c1635a14ab88b | |
parent | a3039bfcaf18fcb5be220da7ef4c957efc42ad85 (diff) | |
parent | cb4e88735bf7b41e6c29ea4ddfc5228542405ed5 (diff) |
Merge changes I143642f6,I42827b5e into main
* changes:
Supporting Material 3 UI in Permission Grant Screen
Material 2.5 theme based on Material3 resources
17 files changed, 1208 insertions, 232 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/WearComposeMaterial3ColorScheme.kt b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/theme/WearComposeMaterial3ColorScheme.kt index 6af9a28ec..7ac6c8114 100644 --- a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/theme/WearComposeMaterial3ColorScheme.kt +++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/theme/WearComposeMaterial3ColorScheme.kt @@ -29,11 +29,41 @@ import androidx.wear.compose.material3.ColorScheme */ internal object WearComposeMaterial3ColorScheme { + @RequiresApi(Build.VERSION_CODES.S) + fun tonalColorScheme(context: Context): ColorScheme { + val tonalPalette = dynamicTonalPalette(context) + return ColorScheme( + background = tonalPalette.neutral0, + onBackground = tonalPalette.neutral100, + onPrimary = tonalPalette.primary10, + onPrimaryContainer = tonalPalette.primary90, + onSecondary = tonalPalette.secondary10, + onSecondaryContainer = tonalPalette.secondary90, + onSurface = tonalPalette.neutral95, + onSurfaceVariant = tonalPalette.neutralVariant80, + onTertiary = tonalPalette.tertiary10, + onTertiaryContainer = tonalPalette.tertiary90, + outline = tonalPalette.neutralVariant60, + outlineVariant = tonalPalette.neutralVariant40, + primary = tonalPalette.primary90, + primaryContainer = tonalPalette.primary30, + primaryDim = tonalPalette.primary80, + secondary = tonalPalette.secondary90, + secondaryContainer = tonalPalette.secondary30, + secondaryDim = tonalPalette.secondary80, + surfaceContainer = tonalPalette.neutral20, + surfaceContainerHigh = tonalPalette.neutral30, + tertiary = tonalPalette.tertiary90, + tertiaryContainer = tonalPalette.tertiary30, + tertiaryDim = tonalPalette.tertiary80, + ) + } + private fun Color.updatedColor(context: Context, @ColorRes colorRes: Int): Color { return ResourceHelper.getColor(context, colorRes) ?: this } - @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) + @RequiresApi(36) fun dynamicColorScheme(context: Context): ColorScheme { val defaultColorScheme = ColorScheme() return ColorScheme( diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/theme/WearMaterialBridgedLegacyTheme.kt b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/theme/WearMaterialBridgedLegacyTheme.kt new file mode 100644 index 000000000..160dc2e93 --- /dev/null +++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/theme/WearMaterialBridgedLegacyTheme.kt @@ -0,0 +1,82 @@ +/* + * 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.theme + +import android.os.Build +import androidx.annotation.RequiresApi +import androidx.compose.ui.unit.sp +import androidx.wear.compose.material.Colors +import androidx.wear.compose.material.Shapes +import androidx.wear.compose.material.Typography + +/** + * This exists to support Permission Controller screens that may still use Material 2.5 components + * to maintain consistency with the settings screens. + * + * However to avoid maintaining two sets of resources for overlays, this class construct 2.5 theme + * from 3.0 + */ +@RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM) +internal class WearMaterialBridgedLegacyTheme +private constructor(newTheme: WearOverlayableMaterial3Theme) { + + val colors = + newTheme.colorScheme.run { + Colors( + background = background, + onBackground = onBackground, + primary = onPrimaryContainer, // primary90 + primaryVariant = primaryDim, // primary80 + onPrimary = onPrimary, // primary10 + secondary = tertiary, // Tertiary90 + secondaryVariant = tertiaryDim, // Tertiary60 - Tertiary80 BestFit. + onSecondary = onTertiary, // Tertiary10 + surface = surfaceContainer, // neutral20 + onSurface = onSurface, // neutral95 + onSurfaceVariant = onSurfaceVariant, // neutralVariant80 + ) + } + + // Based on: + // Material 2: + // wear/compose/compose-material/src/main/java/androidx/wear/compose/material/Typography.kt + // Material 3: + // wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/tokens/TypeScaleTokens.kt + val typography = + newTheme.typography.run { + Typography( + display1 = displayLarge, // 40.sp + display2 = displayMedium.copy(fontSize = 34.sp, lineHeight = 40.sp), + display3 = displayMedium, // 30.sp + title1 = displaySmall, // 24.sp + title2 = titleLarge, // 20.sp + title3 = titleMedium, // 16.sp + body1 = bodyLarge, // 16.sp + body2 = bodyMedium, // 14.sp + caption1 = bodyMedium, // 14.sp + caption2 = bodySmall, // 12.sp + caption3 = bodyExtraSmall, // 10.sp + button = labelMedium, // 15.sp + ) + } + + val shapes = newTheme.shapes.run { Shapes(large = large, medium = medium, small = small) } + + companion object { + fun createFrom(newTheme: WearOverlayableMaterial3Theme) = + WearMaterialBridgedLegacyTheme(newTheme) + } +} diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/theme/WearOverlayableMaterial3Theme.kt b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/theme/WearOverlayableMaterial3Theme.kt index d2b2324ea..8aeb5f74d 100644 --- a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/theme/WearOverlayableMaterial3Theme.kt +++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/theme/WearOverlayableMaterial3Theme.kt @@ -15,23 +15,27 @@ */ package com.android.permissioncontroller.permission.ui.wear.theme +import android.content.Context import android.os.Build -import androidx.compose.runtime.Composable -import androidx.compose.ui.platform.LocalContext -import androidx.wear.compose.material3.MaterialTheme +import androidx.annotation.RequiresApi -/** The Material 3 Theme Wrapper for Supporting RRO. */ -@Composable -fun WearOverlayableMaterial3Theme(content: @Composable () -> Unit) { - val context = LocalContext.current - if (Build.VERSION.SDK_INT >= 36) { - MaterialTheme( - colorScheme = WearComposeMaterial3ColorScheme.dynamicColorScheme(context), - typography = WearComposeMaterial3Typography.dynamicTypography(context), - shapes = WearComposeMaterial3Shapes.dynamicShapes(context), - content = content, - ) - } else { - MaterialTheme(content = content) - } +/** + * Theme wrapper providing Material 3 styling while maintaining compatibility with Runtime Resource + * Overlay (RRO). + * + * Uses the tonal palette from the previous Material Design version until dynamic color tokens are + * available in SDK 36. + */ +@RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM) +internal class WearOverlayableMaterial3Theme(context: Context) { + val colorScheme = + if (Build.VERSION.SDK_INT >= 36) { + WearComposeMaterial3ColorScheme.dynamicColorScheme(context) + } else { + WearComposeMaterial3ColorScheme.tonalColorScheme(context) + } + + val typography = WearComposeMaterial3Typography.dynamicTypography(context) + + val shapes = WearComposeMaterial3Shapes.dynamicShapes(context) } 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 1d6d25ab1..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 @@ -29,11 +29,61 @@ import androidx.compose.ui.text.font.FontFamily import androidx.wear.compose.material.Colors import androidx.wear.compose.material.MaterialTheme 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.WearPermissionMaterialUIVersion.MATERIAL2_5 +import com.android.permissioncontroller.permission.ui.wear.theme.WearPermissionMaterialUIVersion.MATERIAL3 -/** The Material 2.5 Theme Wrapper for Supporting RRO. */ +enum class WearPermissionMaterialUIVersion { + MATERIAL2_5, + MATERIAL3, +} + +/** + * Supports both Material 3 and Material 2 theme. default version for permission theme will be + * LEGACY until we migrate enough screens to 3. LEGACY version will use material 3 overlay resources + * by default. + */ +@Composable +fun WearPermissionTheme( + version: WearPermissionMaterialUIVersion = MATERIAL2_5, + content: @Composable () -> Unit, +) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.VANILLA_ICE_CREAM) { + WearPermissionLegacyTheme(content) + } else { + val useBridgedTheme = Flags.wearComposeMaterial3() + if (version == MATERIAL3) { + val material3Theme = WearOverlayableMaterial3Theme(LocalContext.current) + Material3Theme( + colorScheme = material3Theme.colorScheme, + typography = material3Theme.typography, + shapes = material3Theme.shapes, + content = content, + ) + } else if (version == MATERIAL2_5 && useBridgedTheme) { + val material3Theme = WearOverlayableMaterial3Theme(LocalContext.current) + val bridgedLegacyTheme = WearMaterialBridgedLegacyTheme.createFrom(material3Theme) + MaterialTheme( + colors = bridgedLegacyTheme.colors, + typography = bridgedLegacyTheme.typography, + shapes = bridgedLegacyTheme.shapes, + content = content, + ) + } else { + WearPermissionLegacyTheme(content) + } + } +} + +/** + * The Material 2.5 Theme Wrapper for Supporting RRO with legacy resources. This theme is kept here + * for backward compatibility. When grant screen is updated to material3 will clean up legacy + * resources. + */ @Composable -fun WearPermissionTheme(content: @Composable () -> Unit) { +fun WearPermissionLegacyTheme(content: @Composable () -> Unit) { val context = LocalContext.current val colors = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { |