diff options
18 files changed, 490 insertions, 303 deletions
diff --git a/PermissionController/src/com/android/permissioncontroller/ecm/EnhancedConfirmationDialogActivity.kt b/PermissionController/src/com/android/permissioncontroller/ecm/EnhancedConfirmationDialogActivity.kt index e6cf094e3..e2d46e519 100644 --- a/PermissionController/src/com/android/permissioncontroller/ecm/EnhancedConfirmationDialogActivity.kt +++ b/PermissionController/src/com/android/permissioncontroller/ecm/EnhancedConfirmationDialogActivity.kt @@ -18,7 +18,6 @@ package com.android.permissioncontroller.ecm import android.annotation.SuppressLint import android.app.AlertDialog -import android.app.AppOpsManager import android.app.Dialog import android.app.ecm.EnhancedConfirmationManager import android.content.Context @@ -55,6 +54,8 @@ import com.android.role.controller.model.Roles class EnhancedConfirmationDialogActivity : FragmentActivity() { companion object { private const val KEY_WAS_CLEAR_RESTRICTION_ALLOWED = "KEY_WAS_CLEAR_RESTRICTION_ALLOWED" + private const val REASON_PHONE_STATE = "phone_state" + private const val REASON_APP_OP_RESTRICTED = "app_op_restricted" } private var wasClearRestrictionAllowed: Boolean = false @@ -77,6 +78,7 @@ class EnhancedConfirmationDialogActivity : FragmentActivity() { val packageName = intent.getStringExtra(Intent.EXTRA_PACKAGE_NAME) val settingIdentifier = intent.getStringExtra(Intent.EXTRA_SUBJECT) val isEcmInApp = intent.getBooleanExtra(EXTRA_IS_ECM_IN_APP, false) + val reason = intent.getStringExtra(Intent.EXTRA_REASON) require(uid != Process.INVALID_UID) { "EXTRA_UID cannot be null or invalid" } require(!packageName.isNullOrEmpty()) { "EXTRA_PACKAGE_NAME cannot be null or empty" } @@ -84,9 +86,9 @@ class EnhancedConfirmationDialogActivity : FragmentActivity() { wasClearRestrictionAllowed = setClearRestrictionAllowed(packageName, UserHandle.getUserHandleForUid(uid)) - val setting = Setting.fromIdentifier(this, settingIdentifier, isEcmInApp) + val setting = Setting.fromIdentifier(this, settingIdentifier, isEcmInApp, reason) if ( - SettingType.fromIdentifier(this, settingIdentifier, isEcmInApp) == + SettingType.fromIdentifier(this, settingIdentifier, isEcmInApp, reason) == SettingType.BLOCKED_DUE_TO_PHONE_STATE && !Flags.unknownCallPackageInstallBlockingEnabled() ) { @@ -127,8 +129,10 @@ class EnhancedConfirmationDialogActivity : FragmentActivity() { context: Context, settingIdentifier: String, isEcmInApp: Boolean, + reason: String?, ): Setting { - val settingType = SettingType.fromIdentifier(context, settingIdentifier, isEcmInApp) + val settingType = + SettingType.fromIdentifier(context, settingIdentifier, isEcmInApp, reason) val label = when (settingType) { SettingType.PLATFORM_PERMISSION -> @@ -189,10 +193,10 @@ class EnhancedConfirmationDialogActivity : FragmentActivity() { context: Context, settingIdentifier: String, isEcmInApp: Boolean, + restrictionReason: String?, ): SettingType { return when { - settingIdentifier == AppOpsManager.OPSTR_REQUEST_INSTALL_PACKAGES -> - BLOCKED_DUE_TO_PHONE_STATE + restrictionReason == REASON_PHONE_STATE -> BLOCKED_DUE_TO_PHONE_STATE !isEcmInApp -> OTHER PermissionMapping.isRuntimePlatformPermission(settingIdentifier) && PermissionMapping.getGroupOfPlatformPermission(settingIdentifier) != null -> diff --git a/PermissionController/src/com/android/permissioncontroller/incident/wear/WearConfirmationScreen.kt b/PermissionController/src/com/android/permissioncontroller/incident/wear/WearConfirmationScreen.kt index 8e58d48d9..5f9f58221 100644 --- a/PermissionController/src/com/android/permissioncontroller/incident/wear/WearConfirmationScreen.kt +++ b/PermissionController/src/com/android/permissioncontroller/incident/wear/WearConfirmationScreen.kt @@ -29,14 +29,15 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.ComposeView -import androidx.wear.compose.foundation.lazy.ScalingLazyListState import androidx.wear.compose.material.CircularProgressIndicator -import com.android.permissioncontroller.permission.ui.wear.elements.AlertDialog -import com.android.permissioncontroller.permission.ui.wear.elements.SingleButtonAlertDialog +import com.android.permissioncontroller.permission.ui.wear.elements.DialogButtonContent +import com.android.permissioncontroller.permission.ui.wear.elements.material3.WearPermissionConfirmationDialog +import com.android.permissioncontroller.permission.ui.wear.theme.ResourceHelper import com.android.permissioncontroller.permission.ui.wear.theme.WearPermissionTheme @Composable fun WearConfirmationScreen(viewModel: WearConfirmationActivityViewModel) { + val materialUIVersion = ResourceHelper.materialUIVersionInSettings // Wear screen doesn't show incident/bug report's optional reasons and images. val showDialog = viewModel.showDialogLiveData.observeAsState(false) val showDenyReport = viewModel.showDenyReportLiveData.observeAsState(false) @@ -47,27 +48,25 @@ fun WearConfirmationScreen(viewModel: WearConfirmationActivityViewModel) { if (isLoading) { CircularProgressIndicator(modifier = Modifier.align(Alignment.Center)) } else { - if (showDenyReport.value) { - contentArgs.value?.let { - SingleButtonAlertDialog( - showDialog = showDialog.value, - title = it.title, - message = it.message, - onButtonClick = it.onDenyClick, - scalingLazyListState = ScalingLazyListState(0) + contentArgs.value?.apply { + if (showDenyReport.value) { + WearPermissionConfirmationDialog( + materialUIVersion = materialUIVersion, + show = showDialog.value, + title = title, + message = message, + positiveButtonContent = DialogButtonContent(onClick = onDenyClick), + ) + } else { + WearPermissionConfirmationDialog( + materialUIVersion = materialUIVersion, + show = showDialog.value, + title = title, + message = message, + positiveButtonContent = DialogButtonContent(onClick = onOkClick), + negativeButtonContent = DialogButtonContent(onClick = onCancelClick), ) } - return - } - contentArgs.value?.let { - AlertDialog( - showDialog = showDialog.value, - title = it.title, - message = it.message, - onOKButtonClick = it.onOkClick, - onCancelButtonClick = it.onCancelClick, - scalingLazyListState = ScalingLazyListState(0) - ) } } } diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/WearAppPermissionGroupsScreen.kt b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/WearAppPermissionGroupsScreen.kt index ba37205a6..13fb30d73 100644 --- a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/WearAppPermissionGroupsScreen.kt +++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/WearAppPermissionGroupsScreen.kt @@ -24,17 +24,20 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.res.stringResource -import androidx.wear.compose.foundation.lazy.rememberScalingLazyListState import com.android.permissioncontroller.R -import com.android.permissioncontroller.permission.ui.wear.elements.AlertDialog import com.android.permissioncontroller.permission.ui.wear.elements.Chip +import com.android.permissioncontroller.permission.ui.wear.elements.DialogButtonContent 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.WearPermissionConfirmationDialog import com.android.permissioncontroller.permission.ui.wear.model.RevokeDialogArgs +import com.android.permissioncontroller.permission.ui.wear.theme.ResourceHelper +import com.android.permissioncontroller.permission.ui.wear.theme.WearPermissionMaterialUIVersion @Composable fun WearAppPermissionGroupsScreen(helper: WearAppPermissionGroupsHelper) { + val materialUIVersion = ResourceHelper.materialUIVersionInSettings val packagePermGroups = helper.viewModel.packagePermGroupsLiveData.observeAsState(null) val autoRevoke = helper.viewModel.autoRevokeLiveData.observeAsState(null) val appPermissionUsages = helper.wearViewModel.appPermissionUsages.observeAsState(emptyList()) @@ -50,11 +53,12 @@ fun WearAppPermissionGroupsScreen(helper: WearAppPermissionGroupsHelper) { WearAppPermissionGroupsContent( isLoading, helper.getPermissionGroupChipParams(appPermissionUsages.value), - helper.getAutoRevokeChipParam(autoRevoke.value) + helper.getAutoRevokeChipParam(autoRevoke.value), ) RevokeDialog( + materialUIVersion = materialUIVersion, showDialog = showRevokeDialog.value, - args = helper.revokeDialogViewModel.revokeDialogArgs + args = helper.revokeDialogViewModel.revokeDialogArgs, ) if (showLocationProviderDialog.value) { LocationProviderDialogScreen( @@ -72,7 +76,7 @@ fun WearAppPermissionGroupsScreen(helper: WearAppPermissionGroupsHelper) { internal fun WearAppPermissionGroupsContent( isLoading: Boolean, permissionGroupChipParams: List<PermissionGroupChipParam>, - autoRevokeChipParam: AutoRevokeChipParam? + autoRevokeChipParam: AutoRevokeChipParam?, ) { ScrollableScreen(title = stringResource(R.string.app_permissions), isLoading = isLoading) { if (permissionGroupChipParams.isEmpty()) { @@ -86,7 +90,7 @@ internal fun WearAppPermissionGroupsContent( label = info.label, enabled = info.enabled, toggleControl = ToggleChipToggleControl.Switch, - onCheckedChanged = info.onCheckedChanged + onCheckedChanged = info.onCheckedChanged, ) } else { Chip( @@ -95,7 +99,7 @@ internal fun WearAppPermissionGroupsContent( secondaryLabel = info.summary?.let { info.summary }, secondaryLabelMaxLines = Integer.MAX_VALUE, enabled = info.enabled, - onClick = info.onClick + onClick = info.onClick, ) } } @@ -108,7 +112,7 @@ internal fun WearAppPermissionGroupsContent( label = stringResource(it.labelRes), labelMaxLine = 3, toggleControl = ToggleChipToggleControl.Switch, - onCheckedChanged = it.onCheckedChanged + onCheckedChanged = it.onCheckedChanged, ) } } @@ -118,14 +122,19 @@ internal fun WearAppPermissionGroupsContent( } @Composable -internal fun RevokeDialog(showDialog: Boolean, args: RevokeDialogArgs?) { - args?.let { - AlertDialog( - showDialog = showDialog, - message = stringResource(it.messageId), - onOKButtonClick = it.onOkButtonClick, - onCancelButtonClick = it.onCancelButtonClick, - scalingLazyListState = rememberScalingLazyListState() +internal fun RevokeDialog( + materialUIVersion: WearPermissionMaterialUIVersion, + showDialog: Boolean, + args: RevokeDialogArgs?, +) { + + args?.run { + WearPermissionConfirmationDialog( + materialUIVersion = materialUIVersion, + show = showDialog, + message = stringResource(messageId), + positiveButtonContent = DialogButtonContent(onClick = onOkButtonClick), + negativeButtonContent = DialogButtonContent(onClick = onCancelButtonClick), ) } } diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/WearAppPermissionScreen.kt b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/WearAppPermissionScreen.kt index 202ad49bb..0b96214a2 100644 --- a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/WearAppPermissionScreen.kt +++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/WearAppPermissionScreen.kt @@ -24,21 +24,26 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.res.stringResource -import androidx.wear.compose.foundation.lazy.rememberScalingLazyListState import androidx.wear.compose.material.ToggleChipDefaults import com.android.permissioncontroller.R import com.android.permissioncontroller.permission.ui.model.AppPermissionViewModel import com.android.permissioncontroller.permission.ui.model.AppPermissionViewModel.ButtonState import com.android.permissioncontroller.permission.ui.model.AppPermissionViewModel.ButtonType import com.android.permissioncontroller.permission.ui.v33.AdvancedConfirmDialogArgs -import com.android.permissioncontroller.permission.ui.wear.elements.AlertDialog +import com.android.permissioncontroller.permission.ui.wear.elements.DialogButtonContent import com.android.permissioncontroller.permission.ui.wear.elements.ListFooter 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.WearPermissionConfirmationDialog +import com.android.permissioncontroller.permission.ui.wear.elements.material3.WearPermissionIconBuilder +import com.android.permissioncontroller.permission.ui.wear.elements.material3.defaultAlertConfirmIcon +import com.android.permissioncontroller.permission.ui.wear.elements.material3.defaultAlertDismissIcon import com.android.permissioncontroller.permission.ui.wear.elements.toggleChipDisabledColors import com.android.permissioncontroller.permission.ui.wear.model.AppPermissionConfirmDialogViewModel import com.android.permissioncontroller.permission.ui.wear.model.ConfirmDialogArgs +import com.android.permissioncontroller.permission.ui.wear.theme.ResourceHelper +import com.android.permissioncontroller.permission.ui.wear.theme.WearPermissionMaterialUIVersion import com.android.settingslib.RestrictedLockUtils @Composable @@ -53,8 +58,9 @@ fun WearAppPermissionScreen( onConfirmDialogCancelButtonClick: () -> Unit, onAdvancedConfirmDialogOkButtonClick: (AdvancedConfirmDialogArgs) -> Unit, onAdvancedConfirmDialogCancelButtonClick: () -> Unit, - onDisabledAllowButtonClick: () -> Unit + onDisabledAllowButtonClick: () -> Unit, ) { + val materialUIVersion = ResourceHelper.materialUIVersionInSettings val buttonState = viewModel.buttonStateLiveData.observeAsState(null) val detailResIds = viewModel.detailResIdLiveData.observeAsState(null) val admin = viewModel.showAdminSupportLiveData.observeAsState(null) @@ -73,19 +79,21 @@ fun WearAppPermissionScreen( onLocationSwitchChanged, onGrantedStateChanged, onFooterClicked, - onDisabledAllowButtonClick + onDisabledAllowButtonClick, ) ConfirmDialog( + materialUIVersion = materialUIVersion, showDialog = showConfirmDialog.value, args = confirmDialogViewModel.confirmDialogArgs, onOkButtonClick = onConfirmDialogOkButtonClick, - onCancelButtonClick = onConfirmDialogCancelButtonClick + onCancelButtonClick = onConfirmDialogCancelButtonClick, ) AdvancedConfirmDialog( + materialUIVersion = materialUIVersion, showDialog = showAdvancedConfirmDialog.value, args = confirmDialogViewModel.advancedConfirmDialogArgs, onOkButtonClick = onAdvancedConfirmDialogOkButtonClick, - onCancelButtonClick = onAdvancedConfirmDialogCancelButtonClick + onCancelButtonClick = onAdvancedConfirmDialogCancelButtonClick, ) } if (isLoading && !buttonState.value.isNullOrEmpty()) { @@ -103,7 +111,7 @@ internal fun WearAppPermissionContent( onLocationSwitchChanged: (Boolean) -> Unit, onGrantedStateChanged: (ButtonType, Boolean) -> Unit, onFooterClicked: (RestrictedLockUtils.EnforcedAdmin) -> Unit, - onDisabledAllowButtonClick: () -> Unit + onDisabledAllowButtonClick: () -> Unit, ) { ScrollableScreen(title = title, isLoading = isLoading) { buttonState?.get(ButtonType.LOCATION_ACCURACY)?.let { @@ -115,7 +123,7 @@ internal fun WearAppPermissionContent( label = stringResource(R.string.app_permission_location_accuracy), toggleControl = ToggleChipToggleControl.Switch, onCheckedChanged = onLocationSwitchChanged, - labelMaxLine = Integer.MAX_VALUE + labelMaxLine = Integer.MAX_VALUE, ) } } @@ -141,7 +149,7 @@ internal fun WearAppPermissionContent( onDisabledAllowButtonClick() } }, - labelMaxLine = Integer.MAX_VALUE + labelMaxLine = Integer.MAX_VALUE, ) } } @@ -157,7 +165,7 @@ internal fun WearAppPermissionContent( { onFooterClicked(admin) } } else { null - } + }, ) } } @@ -172,7 +180,7 @@ internal val buttonTypeOrder = ButtonType.ASK_ONCE, ButtonType.ASK, ButtonType.DENY, - ButtonType.DENY_FOREGROUND + ButtonType.DENY_FOREGROUND, ) @Composable @@ -191,45 +199,60 @@ internal fun labelsByButton(buttonType: ButtonType) = @Composable internal fun ConfirmDialog( + materialUIVersion: WearPermissionMaterialUIVersion, showDialog: Boolean, args: ConfirmDialogArgs?, onOkButtonClick: (ConfirmDialogArgs) -> Unit, - onCancelButtonClick: () -> Unit + onCancelButtonClick: () -> Unit, ) { - args?.let { - AlertDialog( - showDialog = showDialog, - message = stringResource(it.messageId), - onOKButtonClick = { onOkButtonClick(it) }, - onCancelButtonClick = onCancelButtonClick, - scalingLazyListState = rememberScalingLazyListState() + args?.run { + WearPermissionConfirmationDialog( + materialUIVersion = materialUIVersion, + show = showDialog, + message = stringResource(messageId), + positiveButtonContent = DialogButtonContent(onClick = { onOkButtonClick(this) }), + negativeButtonContent = DialogButtonContent(onClick = { onCancelButtonClick() }), ) } } @Composable internal fun AdvancedConfirmDialog( + materialUIVersion: WearPermissionMaterialUIVersion, showDialog: Boolean, args: AdvancedConfirmDialogArgs?, onOkButtonClick: (AdvancedConfirmDialogArgs) -> Unit, - onCancelButtonClick: () -> Unit + onCancelButtonClick: () -> Unit, ) { - args?.let { - AlertDialog( - showDialog = showDialog, - title = - if (it.titleId != 0) { - stringResource(it.titleId) - } else { - "" - }, - iconRes = it.iconId, - message = stringResource(it.messageId), - okButtonContentDescription = stringResource(it.positiveButtonTextId), - cancelButtonContentDescription = stringResource(it.negativeButtonTextId), - onOKButtonClick = { onOkButtonClick(it) }, - onCancelButtonClick = onCancelButtonClick, - scalingLazyListState = rememberScalingLazyListState() + args?.run { + val title = + if (titleId != 0) { + stringResource(titleId) + } else { + "" + } + val okButtonIconBuilder = + WearPermissionIconBuilder.defaultAlertConfirmIcon() + .contentDescription(stringResource(positiveButtonTextId)) + val cancelButtonIconBuilder = + WearPermissionIconBuilder.defaultAlertDismissIcon() + .contentDescription(stringResource(negativeButtonTextId)) + WearPermissionConfirmationDialog( + materialUIVersion = materialUIVersion, + show = showDialog, + title = title, + iconRes = WearPermissionIconBuilder.builder(iconId), + message = stringResource(messageId), + positiveButtonContent = + DialogButtonContent( + icon = okButtonIconBuilder, + onClick = { onOkButtonClick(this) }, + ), + negativeButtonContent = + DialogButtonContent( + icon = cancelButtonIconBuilder, + onClick = { onCancelButtonClick() }, + ), ) } } diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/WearEnhancedConfirmationScreen.kt b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/WearEnhancedConfirmationScreen.kt index 1c31ec96f..0f37511de 100644 --- a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/WearEnhancedConfirmationScreen.kt +++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/WearEnhancedConfirmationScreen.kt @@ -33,24 +33,26 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.fragment.app.FragmentActivity import androidx.wear.compose.foundation.SwipeToDismissValue -import androidx.wear.compose.foundation.lazy.ScalingLazyListState import androidx.wear.compose.foundation.rememberSwipeToDismissBoxState import androidx.wear.compose.material.ChipDefaults import androidx.wear.compose.material.CircularProgressIndicator import androidx.wear.compose.material.MaterialTheme import androidx.wear.compose.material.SwipeToDismissBox import com.android.permissioncontroller.R -import com.android.permissioncontroller.permission.ui.wear.elements.AlertDialog import com.android.permissioncontroller.permission.ui.wear.elements.CheckYourPhoneScreen import com.android.permissioncontroller.permission.ui.wear.elements.CheckYourPhoneState import com.android.permissioncontroller.permission.ui.wear.elements.CheckYourPhoneState.InProgress import com.android.permissioncontroller.permission.ui.wear.elements.CheckYourPhoneState.Success import com.android.permissioncontroller.permission.ui.wear.elements.Chip +import com.android.permissioncontroller.permission.ui.wear.elements.DialogButtonContent import com.android.permissioncontroller.permission.ui.wear.elements.ScrollableScreen import com.android.permissioncontroller.permission.ui.wear.elements.dismiss import com.android.permissioncontroller.permission.ui.wear.elements.findActivity +import com.android.permissioncontroller.permission.ui.wear.elements.material3.WearPermissionConfirmationDialog +import com.android.permissioncontroller.permission.ui.wear.elements.material3.WearPermissionIconBuilder import com.android.permissioncontroller.permission.ui.wear.model.WearEnhancedConfirmationViewModel import com.android.permissioncontroller.permission.ui.wear.model.WearEnhancedConfirmationViewModel.ScreenState +import com.android.permissioncontroller.permission.ui.wear.theme.ResourceHelper @Composable fun WearEnhancedConfirmationScreen( @@ -58,6 +60,7 @@ fun WearEnhancedConfirmationScreen( title: String?, message: CharSequence?, ) { + val materialUIVersion = ResourceHelper.materialUIVersionInSettings var dismissed by remember { mutableStateOf(false) } val context = LocalContext.current val ecmScreenState = remember { viewModel.screenState } @@ -97,7 +100,7 @@ fun WearEnhancedConfirmationScreen( onClick = { dismiss(activity) }, modifier = Modifier.fillMaxWidth(), textColor = MaterialTheme.colors.surface, - colors = ChipDefaults.primaryChipColors() + colors = ChipDefaults.primaryChipColors(), ) } item { @@ -107,27 +110,30 @@ fun WearEnhancedConfirmationScreen( modifier = Modifier.fillMaxWidth(), ) } - } + }, ) @Composable fun ShowCheckYourPhoneDialog(state: CheckYourPhoneState) = CheckYourPhoneScreen( title = stringResource(id = R.string.wear_check_your_phone_title), - state = state + state = state, ) @Composable fun ShowRemoteConnectionErrorDialog() = - AlertDialog( + WearPermissionConfirmationDialog( + materialUIVersion = materialUIVersion, + show = true, title = stringResource(R.string.wear_phone_connection_error), message = stringResource(R.string.wear_phone_connection_should_retry), - iconRes = R.drawable.ic_error, - showDialog = true, - okButtonIcon = R.drawable.ic_refresh, - onOKButtonClick = { viewModel.openUriOnPhone(context) }, - onCancelButtonClick = { dismiss(activity) }, - scalingLazyListState = ScalingLazyListState(1) + iconRes = WearPermissionIconBuilder.builder(R.drawable.ic_error), + positiveButtonContent = + DialogButtonContent( + icon = WearPermissionIconBuilder.builder(R.drawable.ic_refresh), + onClick = { viewModel.openUriOnPhone(context) }, + ), + negativeButtonContent = DialogButtonContent(onClick = { dismiss(activity) }), ) SwipeToDismissBox(state = state) { isBackground -> 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 e6c141cc8..4670cfb78 100644 --- a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/WearGrantPermissionsScreen.kt +++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/WearGrantPermissionsScreen.kt @@ -47,7 +47,6 @@ import com.android.permissioncontroller.permission.ui.wear.elements.material3.We 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.ResourceHelper -import com.android.permissioncontroller.permission.ui.wear.theme.WearPermissionMaterialUIVersion.MATERIAL2_5 import com.android.permissioncontroller.permission.ui.wear.theme.WearPermissionMaterialUIVersion.MATERIAL3 import kotlinx.coroutines.delay @@ -63,13 +62,7 @@ fun WearGrantPermissionsScreen( val locationVisibilities = viewModel.locationVisibilitiesLiveData.observeAsState(emptyList()) val preciseLocationChecked = viewModel.preciseLocationCheckedLiveData.observeAsState(false) val buttonVisibilities = viewModel.buttonVisibilitiesLiveData.observeAsState(emptyList()) - val materialUIVersion = - if (ResourceHelper.material3Enabled) { - MATERIAL3 - } else { - MATERIAL2_5 - } - + val materialUIVersion = ResourceHelper.materialUIVersionInApp ScrollableScreen( materialUIVersion = materialUIVersion, showTimeText = false, @@ -129,7 +122,7 @@ fun setContent( onLocationSwitchChanged: (Boolean) -> Unit, ) { composeView.setContent { - if (ResourceHelper.material3Enabled) { + if (ResourceHelper.materialUIVersionInApp == MATERIAL3) { AsDialog(onCancelled) { WearGrantPermissionsScreen(viewModel, onButtonClicked, onLocationSwitchChanged) } diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/AlertDialog.kt b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/AlertDialog.kt index c07d2ba9e..a700c02e2 100644 --- a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/AlertDialog.kt +++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/AlertDialog.kt @@ -17,17 +17,12 @@ package com.android.permissioncontroller.permission.ui.wear.elements import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Check -import androidx.compose.material.icons.filled.Close import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalFocusManager -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.rememberTextMeasurer import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow @@ -35,15 +30,19 @@ import androidx.compose.ui.unit.Constraints import androidx.compose.ui.unit.dp import androidx.wear.compose.foundation.lazy.ScalingLazyListScope import androidx.wear.compose.foundation.lazy.ScalingLazyListState -import androidx.wear.compose.material.Icon import androidx.wear.compose.material.LocalTextStyle import androidx.wear.compose.material.MaterialTheme import androidx.wear.compose.material.Text -import androidx.wear.compose.material.dialog.Alert import androidx.wear.compose.material.dialog.Dialog import com.android.permissioncontroller.permission.ui.wear.elements.layout.ScalingLazyColumnDefaults import com.android.permissioncontroller.permission.ui.wear.elements.layout.ScalingLazyColumnState import com.android.permissioncontroller.permission.ui.wear.elements.layout.rememberColumnState +import com.android.permissioncontroller.permission.ui.wear.elements.material3.WearPermissionIconBuilder + +data class DialogButtonContent( + val icon: WearPermissionIconBuilder? = null, + val onClick: (() -> Unit), +) /** * This component is an alternative to [AlertContent], providing the following: @@ -54,95 +53,44 @@ import com.android.permissioncontroller.permission.ui.wear.elements.layout.remem */ @Composable fun AlertDialog( + title: String? = null, message: String, - iconRes: Int? = null, - okButtonIcon: Any = Icons.Default.Check, - cancelButtonIcon: Any = Icons.Default.Close, - onCancelButtonClick: () -> Unit, - onOKButtonClick: () -> Unit, + positiveButtonContent: DialogButtonContent?, + negativeButtonContent: DialogButtonContent?, showDialog: Boolean, - scalingLazyListState: ScalingLazyListState, modifier: Modifier = Modifier, - title: String? = null, - okButtonContentDescription: String = stringResource(android.R.string.ok), - cancelButtonContentDescription: String = stringResource(android.R.string.cancel) + iconRes: WearPermissionIconBuilder? = null, + scalingLazyListState: ScalingLazyListState, ) { val focusManager = LocalFocusManager.current Dialog( showDialog = showDialog, onDismissRequest = { focusManager.clearFocus() - onCancelButtonClick() + negativeButtonContent?.onClick?.invoke() }, scrollState = scalingLazyListState, - modifier = modifier - ) { - AlertContent( - title = title, - icon = { AlertIcon(iconRes) }, - message = message, - okButtonIcon = okButtonIcon, - cancelButtonIcon = cancelButtonIcon, - onCancel = onCancelButtonClick, - onOk = onOKButtonClick, - okButtonContentDescription = okButtonContentDescription, - cancelButtonContentDescription = cancelButtonContentDescription - ) - } -} - -/** - * This component is an alternative to [Alert], providing the following: - * - a convenient way of passing a title and a message; - * - default one button; - * - wrapped in a [Dialog]; - */ -@Composable -fun SingleButtonAlertDialog( - message: String, - iconRes: Int? = null, - okButtonIcon: Any = Icons.Default.Check, - onButtonClick: () -> Unit, - showDialog: Boolean, - scalingLazyListState: ScalingLazyListState, - modifier: Modifier = Modifier, - title: String? = null, - buttonContentDescription: String = stringResource(android.R.string.ok) -) { - Dialog( - showDialog = showDialog, - onDismissRequest = {}, - scrollState = scalingLazyListState, - modifier = modifier + modifier = modifier, ) { AlertContent( title = title, - icon = { AlertIcon(iconRes) }, + icon = { iconRes?.build() }, message = message, - okButtonIcon = okButtonIcon, - onOk = onButtonClick, - okButtonContentDescription = buttonContentDescription + positiveButtonContent = positiveButtonContent, + negativeButtonContent = negativeButtonContent, ) } } @Composable fun AlertContent( - onCancel: (() -> Unit)? = null, - onOk: (() -> Unit)? = null, icon: @Composable (() -> Unit)? = null, title: String? = null, message: String? = null, - okButtonIcon: Any = Icons.Default.Check, - cancelButtonIcon: Any = Icons.Default.Close, - okButtonContentDescription: String = stringResource(android.R.string.ok), - cancelButtonContentDescription: String = stringResource(android.R.string.cancel), + positiveButtonContent: DialogButtonContent?, + negativeButtonContent: DialogButtonContent?, state: ScalingLazyColumnState = - rememberColumnState( - ScalingLazyColumnDefaults.responsive( - additionalPaddingAtBottom = 0.dp, - ), - ), + rememberColumnState(ScalingLazyColumnDefaults.responsive(additionalPaddingAtBottom = 0.dp)), showPositionIndicator: Boolean = true, content: (ScalingLazyListScope.() -> Unit)? = null, ) { @@ -185,7 +133,7 @@ fun AlertContent( maxWidth = (maxScreenWidthPx * (1f - totalPaddingPercentage * 2f / 100f)) - .toInt(), + .toInt() ), ) .lineCount @@ -200,21 +148,9 @@ fun AlertContent( } }, content = content, - onOk = onOk, - onCancel = onCancel, - okButtonIcon = okButtonIcon, - cancelButtonIcon = cancelButtonIcon, - okButtonContentDescription = okButtonContentDescription, - cancelButtonContentDescription = cancelButtonContentDescription, + positiveButtonContent = positiveButtonContent, + negativeButtonContent = negativeButtonContent, state = state, showPositionIndicator = showPositionIndicator, ) } - -@Composable -private fun AlertIcon(iconRes: Int?) = - if (iconRes != null && iconRes != 0) { - Icon(painter = painterResource(iconRes), contentDescription = null) - } else { - null - } diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/ResponsiveDialog.kt b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/ResponsiveDialog.kt index e1e869f71..361a6c925 100644 --- a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/ResponsiveDialog.kt +++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/ResponsiveDialog.kt @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package com.android.permissioncontroller.permission.ui.wear.elements import androidx.compose.foundation.layout.Arrangement @@ -29,15 +28,11 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Check -import androidx.compose.material.icons.filled.Close import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalConfiguration -import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp @@ -53,9 +48,9 @@ import com.android.permissioncontroller.permission.ui.wear.elements.layout.Scali import com.android.permissioncontroller.permission.ui.wear.elements.layout.ScalingLazyColumnDefaults.responsive import com.android.permissioncontroller.permission.ui.wear.elements.layout.ScalingLazyColumnState import com.android.permissioncontroller.permission.ui.wear.elements.layout.rememberColumnState - -// This file is a copy of ResponsiveDialogContent.kt from Horologist (go/horologist), -// remove it once after wear compose supports large screen dialogs. +import com.android.permissioncontroller.permission.ui.wear.elements.material3.WearPermissionIconBuilder +import com.android.permissioncontroller.permission.ui.wear.elements.material3.defaultAlertConfirmIcon +import com.android.permissioncontroller.permission.ui.wear.elements.material3.defaultAlertDismissIcon @Composable fun ResponsiveDialogContent( @@ -63,18 +58,11 @@ fun ResponsiveDialogContent( icon: @Composable (() -> Unit)? = null, title: @Composable (() -> Unit)? = null, message: @Composable (() -> Unit)? = null, - okButtonIcon: Any = Icons.Default.Check, - cancelButtonIcon: Any = Icons.Default.Close, - onOk: (() -> Unit)? = null, - onCancel: (() -> Unit)? = null, - okButtonContentDescription: String = stringResource(android.R.string.ok), - cancelButtonContentDescription: String = stringResource(android.R.string.cancel), + positiveButtonContent: DialogButtonContent? = null, + negativeButtonContent: DialogButtonContent? = null, state: ScalingLazyColumnState = rememberColumnState( - responsive( - firstItemIsFullWidth = icon == null, - additionalPaddingAtBottom = 0.dp, - ), + responsive(firstItemIsFullWidth = icon == null, additionalPaddingAtBottom = 0.dp) ), showPositionIndicator: Boolean = true, content: (ScalingLazyListScope.() -> Unit)? = null, @@ -89,9 +77,7 @@ fun ResponsiveDialogContent( timeText = {}, ) { // This will be applied only to the content. - CompositionLocalProvider( - LocalTextStyle provides MaterialTheme.typography.body2, - ) { + CompositionLocalProvider(LocalTextStyle provides MaterialTheme.typography.body2) { ScalingLazyColumn(columnState = state) { icon?.let { item { @@ -107,11 +93,11 @@ fun ResponsiveDialogContent( item { CompositionLocalProvider( LocalTextStyle provides - MaterialTheme.typography.title3.copy(fontWeight = FontWeight.W600), + MaterialTheme.typography.title3.copy(fontWeight = FontWeight.W600) ) { Box( Modifier.fillMaxWidth(titleMaxWidthFraction) - .padding(bottom = 8.dp), // 12.dp below icon + .padding(bottom = 8.dp) // 12.dp below icon ) { it() } @@ -123,22 +109,20 @@ fun ResponsiveDialogContent( item { Spacer(Modifier.height(20.dp)) } } message?.let { - item { - Box( - Modifier.fillMaxWidth(messageMaxWidthFraction), - ) { - it() - } - } + item { Box(Modifier.fillMaxWidth(messageMaxWidthFraction)) { it() } } } content?.let { it() } - if (onOk != null || onCancel != null) { + if (positiveButtonContent != null || negativeButtonContent != null) { item { val width = LocalConfiguration.current.screenWidthDp // Single buttons, or buttons on smaller screens are not meant to be // responsive. val buttonWidth = - if (width < 225 || onOk == null || onCancel == null) { + if ( + width < 225 || + positiveButtonContent == null || + negativeButtonContent == null + ) { ButtonDefaults.DefaultButtonSize } else { // 14.56% on top of 5.2% margin on the sides, 12.dp between. @@ -147,25 +131,30 @@ fun ResponsiveDialogContent( Row( Modifier.fillMaxWidth() .padding( - top = if (content != null || message != null) 12.dp else 0.dp, + top = if (content != null || message != null) 12.dp else 0.dp ), horizontalArrangement = spacedBy(12.dp, Alignment.CenterHorizontally), verticalAlignment = Alignment.CenterVertically, ) { - onCancel?.let { + negativeButtonContent?.run { ResponsiveButton( - icon = cancelButtonIcon, - cancelButtonContentDescription, - onClick = it, + this.icon + ?: WearPermissionIconBuilder.defaultAlertDismissIcon() + .tint( + ChipDefaults.secondaryChipColors() + .contentColor(true) + .value + ), + onClick, buttonWidth, ChipDefaults.secondaryChipColors(), ) } - onOk?.let { + positiveButtonContent?.run { ResponsiveButton( - icon = okButtonIcon, - okButtonContentDescription, - onClick = it, + this.icon + ?: WearPermissionIconBuilder.defaultAlertConfirmIcon(), + onClick, buttonWidth, ) } @@ -179,8 +168,7 @@ fun ResponsiveDialogContent( @Composable private fun ResponsiveButton( - icon: Any, - contentDescription: String, + icon: WearPermissionIconBuilder, onClick: () -> Unit, buttonWidth: Dp, colors: ChipColors = ChipDefaults.primaryChipColors(), @@ -188,12 +176,9 @@ private fun ResponsiveButton( androidx.wear.compose.material.Chip( label = { Box(Modifier.fillMaxWidth()) { - Icon( - icon = icon, - contentDescription = contentDescription, - modifier = - Modifier.size(ButtonDefaults.DefaultIconSize).align(Alignment.Center), - ) + icon + .modifier(Modifier.size(ButtonDefaults.DefaultIconSize).align(Alignment.Center)) + .build() } }, contentPadding = PaddingValues(0.dp), @@ -210,19 +195,10 @@ internal const val titleExtraHorizontalPadding = 8.84f // Fraction of the max available width that message should take (after global and message padding) internal val messageMaxWidthFraction = - 1f - - 2f * - calculatePaddingFraction( - messageExtraHorizontalPadding, - ) + 1f - 2f * calculatePaddingFraction(messageExtraHorizontalPadding) // Fraction of the max available width that title should take (after global and message padding) -internal val titleMaxWidthFraction = - 1f - - 2f * - calculatePaddingFraction( - titleExtraHorizontalPadding, - ) +internal val titleMaxWidthFraction = 1f - 2f * calculatePaddingFraction(titleExtraHorizontalPadding) // Calculate total padding given global padding and additional padding required inside that. internal fun calculatePaddingFraction(extraPadding: Float) = diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/material3/WearPermissionConfirmationDialog.kt b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/material3/WearPermissionConfirmationDialog.kt new file mode 100644 index 000000000..4647d6eae --- /dev/null +++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/material3/WearPermissionConfirmationDialog.kt @@ -0,0 +1,159 @@ +/* + * Copyright (C) 2025 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.foundation.layout.RowScope +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.unit.dp +import androidx.wear.compose.foundation.lazy.rememberScalingLazyListState +import androidx.wear.compose.material3.AlertDialog as Material3AlertDialog +import androidx.wear.compose.material3.AlertDialogDefaults +import androidx.wear.compose.material3.Text +import com.android.permissioncontroller.permission.ui.wear.elements.AlertDialog +import com.android.permissioncontroller.permission.ui.wear.elements.DialogButtonContent +import com.android.permissioncontroller.permission.ui.wear.theme.WearPermissionMaterialUIVersion + +@Composable +fun WearPermissionConfirmationDialog( + materialUIVersion: WearPermissionMaterialUIVersion = + WearPermissionMaterialUIVersion.MATERIAL2_5, + show: Boolean, + iconRes: WearPermissionIconBuilder? = null, + title: String? = null, + message: String? = null, + positiveButtonContent: DialogButtonContent? = null, + negativeButtonContent: DialogButtonContent? = null, +) { + + if (materialUIVersion == WearPermissionMaterialUIVersion.MATERIAL3) { + if ( + (positiveButtonContent == null && negativeButtonContent != null) || + (positiveButtonContent != null && negativeButtonContent == null) + ) { + val edgeButtonContent = (positiveButtonContent ?: negativeButtonContent)!! + WearPermissionConfirmationDialogInternal( + show = show, + edgeButtonContent = edgeButtonContent, + iconRes = iconRes, + title = title, + message = message, + ) + } else { + WearPermissionConfirmationDialogInternal( + show = show, + positiveButtonContent = positiveButtonContent, + negativeButtonContent = negativeButtonContent, + iconRes = iconRes, + title = title, + message = message, + ) + } + } else { + AlertDialog( + title = title, + iconRes = iconRes, + message = message ?: "", + positiveButtonContent = positiveButtonContent, + negativeButtonContent = negativeButtonContent, + showDialog = show, + scalingLazyListState = rememberScalingLazyListState(), + ) + } +} + +@Composable +private fun WearPermissionConfirmationDialogInternal( + show: Boolean, + edgeButtonContent: DialogButtonContent, + iconRes: WearPermissionIconBuilder?, + title: String?, + message: String?, +) { + val edgeIcon: @Composable RowScope.() -> Unit = + edgeButtonContent.icon?.let { + { it.modifier(Modifier.size(36.dp).align(Alignment.CenterVertically)).build() } + } ?: AlertDialogDefaults.ConfirmIcon + + Material3AlertDialog( + show = show, + onDismissRequest = edgeButtonContent.onClick, + edgeButton = { + AlertDialogDefaults.EdgeButton(onClick = edgeButtonContent.onClick, content = edgeIcon) + }, + icon = { iconRes?.build() }, + title = title?.let { { Text(text = title) } } ?: {}, + text = message?.let { { Text(text = message) } }, + ) +} + +@Composable +private fun WearPermissionConfirmationDialogInternal( + show: Boolean, + positiveButtonContent: DialogButtonContent?, + negativeButtonContent: DialogButtonContent?, + iconRes: WearPermissionIconBuilder?, + title: String?, + message: String?, +) { + val positiveButton: (@Composable RowScope.() -> Unit)? = + positiveButtonContent?.let { + { + val positiveIcon: @Composable RowScope.() -> Unit = + positiveButtonContent.icon?.let { + { + it.modifier(Modifier.size(36.dp).align(Alignment.CenterVertically)) + .build() + } + } ?: AlertDialogDefaults.ConfirmIcon + + AlertDialogDefaults.ConfirmButton( + onClick = positiveButtonContent.onClick, + content = positiveIcon, + ) + } + } + + val negativeButton: (@Composable RowScope.() -> Unit)? = + negativeButtonContent?.let { + { + val negativeIcon: @Composable RowScope.() -> Unit = + negativeButtonContent.icon?.let { + { + it.modifier(Modifier.size(36.dp).align(Alignment.CenterVertically)) + .build() + } + } ?: AlertDialogDefaults.DismissIcon + + AlertDialogDefaults.DismissButton( + onClick = negativeButtonContent.onClick, + content = negativeIcon, + ) + } + } + + Material3AlertDialog( + show = show, + onDismissRequest = negativeButtonContent?.onClick ?: {}, + confirmButton = positiveButton ?: {}, + dismissButton = negativeButton ?: {}, + icon = { iconRes?.build() }, + title = title?.let { { Text(text = title) } } ?: {}, + text = message?.let { { Text(text = message) } }, + ) +} 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 index b7521d073..52674b50d 100644 --- 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 @@ -17,12 +17,16 @@ package com.android.permissioncontroller.permission.ui.wear.elements.material3 import android.graphics.drawable.Drawable import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Check +import androidx.compose.material.icons.filled.Close 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.compose.ui.res.stringResource import androidx.wear.compose.material3.Icon import androidx.wear.compose.material3.IconButtonDefaults import com.android.permissioncontroller.permission.ui.wear.elements.rememberDrawablePainter @@ -66,7 +70,7 @@ class WearPermissionIconBuilder private constructor() { } fun modifier(modifier: Modifier): WearPermissionIconBuilder { - this.modifier then modifier + this.modifier = modifier then this.modifier return this } @@ -99,3 +103,11 @@ class WearPermissionIconBuilder private constructor() { fun builder(icon: Any) = WearPermissionIconBuilder().apply { iconResource = icon } } } + +@Composable +fun WearPermissionIconBuilder.Companion.defaultAlertConfirmIcon() = + builder(Icons.Default.Check).contentDescription((stringResource(android.R.string.ok))) + +@Composable +fun WearPermissionIconBuilder.Companion.defaultAlertDismissIcon() = + builder(Icons.Default.Close).contentDescription((stringResource(android.R.string.cancel))) diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/theme/ResourceHelper.kt b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/theme/ResourceHelper.kt index c7ed0958c..2a40a625f 100644 --- a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/theme/ResourceHelper.kt +++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/theme/ResourceHelper.kt @@ -27,11 +27,35 @@ internal object ResourceHelper { private const val MATERIAL3_ENABLED_SYSPROP = "persist.cw_build.bluechip.enabled" - val material3Enabled: Boolean + /* This controls in app permission controller experience. */ + private val material3Enabled: Boolean get() { return SystemProperties.getBoolean(MATERIAL3_ENABLED_SYSPROP, false) } + val materialUIVersionInApp: WearPermissionMaterialUIVersion = + if (material3Enabled) { + WearPermissionMaterialUIVersion.MATERIAL3 + } else { + WearPermissionMaterialUIVersion.MATERIAL2_5 + } + + /* + This is to control the permission controller screens in settings. + Currently it is set as false. We will either use the flag or a common property from settings + based on settings implementation when we are ready" */ + private val material3EnabledInSettings: Boolean + get() { + return false + } + + val materialUIVersionInSettings: WearPermissionMaterialUIVersion = + if (material3EnabledInSettings) { + WearPermissionMaterialUIVersion.MATERIAL3 + } else { + WearPermissionMaterialUIVersion.MATERIAL2_5 + } + @DoNotInline fun getColor(context: Context, @ColorRes id: Int): Color? { return try { 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 8823bee07..736d543a3 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 @@ -53,9 +53,6 @@ fun WearPermissionTheme( if (Build.VERSION.SDK_INT < Build.VERSION_CODES.VANILLA_ICE_CREAM) { WearPermissionLegacyTheme(content) } else { - // Whether we are ready to use material3 for any screen. - val useBridgedTheme = ResourceHelper.material3Enabled - // Material3 UI controls are still being used in the screen that the theme is applied if (version == MATERIAL3) { val material3Theme = WearOverlayableMaterial3Theme(LocalContext.current) @@ -70,7 +67,7 @@ fun WearPermissionTheme( // But some in-app screens(like permission grant screen) are migrated to material3. // To avoid having two set of overlay resources, we will use material3 overlay resources to // support material2_5 UI controls as well. - else if (version == MATERIAL2_5 && useBridgedTheme) { + else if (version == MATERIAL2_5 && ResourceHelper.materialUIVersionInApp == MATERIAL3) { val material3Theme = WearOverlayableMaterial3Theme(LocalContext.current) val bridgedLegacyTheme = WearMaterialBridgedLegacyTheme.createFrom(material3Theme) MaterialTheme( diff --git a/PermissionController/src/com/android/permissioncontroller/role/ui/wear/WearDefaultAppScreen.kt b/PermissionController/src/com/android/permissioncontroller/role/ui/wear/WearDefaultAppScreen.kt index 5d4233c6e..5a90380e0 100644 --- a/PermissionController/src/com/android/permissioncontroller/role/ui/wear/WearDefaultAppScreen.kt +++ b/PermissionController/src/com/android/permissioncontroller/role/ui/wear/WearDefaultAppScreen.kt @@ -25,14 +25,16 @@ import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue -import androidx.wear.compose.foundation.lazy.rememberScalingLazyListState import androidx.wear.compose.material.ToggleChipDefaults -import com.android.permissioncontroller.permission.ui.wear.elements.AlertDialog +import com.android.permissioncontroller.permission.ui.wear.elements.DialogButtonContent import com.android.permissioncontroller.permission.ui.wear.elements.ListFooter 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.WearPermissionConfirmationDialog import com.android.permissioncontroller.permission.ui.wear.elements.toggleChipDisabledColors +import com.android.permissioncontroller.permission.ui.wear.theme.ResourceHelper +import com.android.permissioncontroller.permission.ui.wear.theme.WearPermissionMaterialUIVersion import com.android.permissioncontroller.role.ui.wear.model.ConfirmDialogArgs @Composable @@ -41,11 +43,13 @@ fun WearDefaultAppScreen(helper: WearDefaultAppHelper) { val showConfirmDialog = helper.confirmDialogViewModel.showConfirmDialogLiveData.observeAsState(false) var isLoading by remember { mutableStateOf(true) } + val materialUIVersion = ResourceHelper.materialUIVersionInSettings Box { WearDefaultAppContent(isLoading, roleLiveData.value, helper) ConfirmDialog( + materialUIVersion = materialUIVersion, showDialog = showConfirmDialog.value, - args = helper.confirmDialogViewModel.confirmDialogArgs + args = helper.confirmDialogViewModel.confirmDialogArgs, ) } if (isLoading && roleLiveData.value.isNotEmpty()) { @@ -57,7 +61,7 @@ fun WearDefaultAppScreen(helper: WearDefaultAppHelper) { private fun WearDefaultAppContent( isLoading: Boolean, qualifyingApplications: List<Pair<ApplicationInfo, Boolean>>, - helper: WearDefaultAppHelper + helper: WearDefaultAppHelper, ) { ScrollableScreen(title = helper.getTitle(), isLoading = isLoading) { helper.getNonePreference(qualifyingApplications)?.let { @@ -68,7 +72,7 @@ private fun WearDefaultAppContent( checked = it.checked, onCheckedChanged = it.onDefaultCheckChanged, toggleControl = ToggleChipToggleControl.Radio, - labelMaxLine = Integer.MAX_VALUE + labelMaxLine = Integer.MAX_VALUE, ) } } @@ -88,7 +92,7 @@ private fun WearDefaultAppContent( onCheckedChanged = pref.getOnCheckChanged(), toggleControl = ToggleChipToggleControl.Radio, labelMaxLine = Integer.MAX_VALUE, - secondaryLabelMaxLine = Integer.MAX_VALUE + secondaryLabelMaxLine = Integer.MAX_VALUE, ) } } @@ -98,14 +102,18 @@ private fun WearDefaultAppContent( } @Composable -private fun ConfirmDialog(showDialog: Boolean, args: ConfirmDialogArgs?) { - args?.let { - AlertDialog( - showDialog = showDialog, - message = it.message, - onOKButtonClick = it.onOkButtonClick, - onCancelButtonClick = it.onCancelButtonClick, - scalingLazyListState = rememberScalingLazyListState() +private fun ConfirmDialog( + materialUIVersion: WearPermissionMaterialUIVersion, + showDialog: Boolean, + args: ConfirmDialogArgs?, +) { + args?.run { + WearPermissionConfirmationDialog( + materialUIVersion = materialUIVersion, + show = showDialog, + message = message, + positiveButtonContent = DialogButtonContent(onClick = onOkButtonClick), + negativeButtonContent = DialogButtonContent(onClick = onCancelButtonClick), ) } } diff --git a/PermissionController/src/com/android/permissioncontroller/role/ui/wear/WearRequestRoleScreen.kt b/PermissionController/src/com/android/permissioncontroller/role/ui/wear/WearRequestRoleScreen.kt index fcc0d56f9..4a00efa1a 100644 --- a/PermissionController/src/com/android/permissioncontroller/role/ui/wear/WearRequestRoleScreen.kt +++ b/PermissionController/src/com/android/permissioncontroller/role/ui/wear/WearRequestRoleScreen.kt @@ -41,8 +41,6 @@ import com.android.permissioncontroller.permission.ui.wear.elements.material3.We import com.android.permissioncontroller.permission.ui.wear.elements.material3.WearPermissionToggleControlStyle import com.android.permissioncontroller.permission.ui.wear.theme.ResourceHelper 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.WearPermissionMaterialUIVersion.MATERIAL3 import com.android.permissioncontroller.role.UserPackage import com.android.permissioncontroller.role.ui.ManageRoleHolderStateLiveData @@ -80,14 +78,8 @@ fun WearRequestRoleScreen( helper.initializeSelectedPackage() } } - val materialUIVersion = - if (ResourceHelper.material3Enabled) { - MATERIAL3 - } else { - MATERIAL2_5 - } WearRequestRoleContent( - materialUIVersion, + ResourceHelper.materialUIVersionInApp, isLoading, helper, roleLiveData.value, diff --git a/framework-s/java/android/app/ecm/EnhancedConfirmationManager.java b/framework-s/java/android/app/ecm/EnhancedConfirmationManager.java index db05a0af6..290388558 100644 --- a/framework-s/java/android/app/ecm/EnhancedConfirmationManager.java +++ b/framework-s/java/android/app/ecm/EnhancedConfirmationManager.java @@ -33,6 +33,7 @@ import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; import android.os.Build; import android.os.RemoteException; +import android.os.UserHandle; import android.permission.flags.Flags; import android.util.ArraySet; @@ -202,6 +203,19 @@ public final class EnhancedConfirmationManager { public static final String ACTION_SHOW_ECM_RESTRICTED_SETTING_DIALOG = "android.app.ecm.action.SHOW_ECM_RESTRICTED_SETTING_DIALOG"; + /** + * The setting is restricted because of the phone state of the device + * @hide + */ + public static final String REASON_PHONE_STATE = "phone_state"; + + /** + * The setting is restricted because the restricted app op is set for the given package + * @hide + */ + public static final String REASON_APP_OP_RESTRICTED = "app_op_restricted"; + + /** A map of ECM states to their corresponding app op states */ @Retention(java.lang.annotation.RetentionPolicy.SOURCE) @IntDef(prefix = {"ECM_STATE_"}, value = {EcmState.ECM_STATE_NOT_GUARDED, @@ -349,8 +363,16 @@ public final class EnhancedConfirmationManager { @NonNull String settingIdentifier) throws NameNotFoundException { Intent intent = new Intent(ACTION_SHOW_ECM_RESTRICTED_SETTING_DIALOG); intent.putExtra(Intent.EXTRA_PACKAGE_NAME, packageName); - intent.putExtra(Intent.EXTRA_UID, getPackageUid(packageName)); + int uid = getPackageUid(packageName); + intent.putExtra(Intent.EXTRA_UID, uid); intent.putExtra(Intent.EXTRA_SUBJECT, settingIdentifier); + try { + intent.putExtra(Intent.EXTRA_REASON, mService.getRestrictionReason(packageName, + settingIdentifier, UserHandle.getUserHandleForUid(uid).getIdentifier())); + } catch (SecurityException | RemoteException e) { + // The caller of this method does not have permission to read the ECM state, so we + // won't include it in the return + } return intent; } diff --git a/framework-s/java/android/app/ecm/IEnhancedConfirmationManager.aidl b/framework-s/java/android/app/ecm/IEnhancedConfirmationManager.aidl index 5149daa49..79d2322bd 100644 --- a/framework-s/java/android/app/ecm/IEnhancedConfirmationManager.aidl +++ b/framework-s/java/android/app/ecm/IEnhancedConfirmationManager.aidl @@ -25,6 +25,8 @@ interface IEnhancedConfirmationManager { boolean isRestricted(in String packageName, in String settingIdentifier, int userId); + String getRestrictionReason(in String packageName, in String settingIdentifier, int userId); + void clearRestriction(in String packageName, int userId); boolean isClearRestrictionAllowed(in String packageName, int userId); diff --git a/service/java/com/android/ecm/EnhancedConfirmationService.java b/service/java/com/android/ecm/EnhancedConfirmationService.java index 65fde6daf..dde5404a4 100644 --- a/service/java/com/android/ecm/EnhancedConfirmationService.java +++ b/service/java/com/android/ecm/EnhancedConfirmationService.java @@ -16,6 +16,9 @@ package com.android.ecm; +import static android.app.ecm.EnhancedConfirmationManager.REASON_APP_OP_RESTRICTED; +import static android.app.ecm.EnhancedConfirmationManager.REASON_PHONE_STATE; + import android.Manifest; import android.annotation.FlaggedApi; import android.annotation.IntDef; @@ -89,7 +92,7 @@ public class EnhancedConfirmationService extends SystemService { private static final int CALL_TYPE_UNTRUSTED = 0; private static final int CALL_TYPE_TRUSTED = 1; - private static final int CALL_TYPE_EMERGENCY = 2; + private static final int CALL_TYPE_EMERGENCY = 1 << 1; @IntDef(flag = true, value = { CALL_TYPE_UNTRUSTED, CALL_TYPE_TRUSTED, @@ -269,6 +272,8 @@ public class EnhancedConfirmationService extends SystemService { PROTECTED_SETTINGS.add(AppOpsManager.OPSTR_REQUEST_INSTALL_PACKAGES); UNTRUSTED_CALL_RESTRICTED_SETTINGS.add( AppOpsManager.OPSTR_REQUEST_INSTALL_PACKAGES); + UNTRUSTED_CALL_RESTRICTED_SETTINGS.add( + AppOpsManager.OPSTR_BIND_ACCESSIBILITY_SERVICE); } } @@ -287,10 +292,16 @@ public class EnhancedConfirmationService extends SystemService { public boolean isRestricted(@NonNull String packageName, @NonNull String settingIdentifier, @UserIdInt int userId) { + return getRestrictionReason(packageName, settingIdentifier, userId) != null; + } + + public String getRestrictionReason(@NonNull String packageName, + @NonNull String settingIdentifier, + @UserIdInt int userId) { enforcePermissions("isRestricted", userId); if (!UserUtils.isUserExistent(userId, getContext())) { Log.e(LOG_TAG, "user " + userId + " does not exist"); - return false; + return null; } Preconditions.checkStringNotEmpty(packageName, "packageName cannot be null or empty"); @@ -299,12 +310,13 @@ public class EnhancedConfirmationService extends SystemService { try { if (!isSettingEcmProtected(settingIdentifier)) { - return false; + return null; } - if (isSettingProtectedGlobally(settingIdentifier)) { - return true; + String globalProtectionReason = getGlobalProtectionReason(settingIdentifier); + if (globalProtectionReason != null) { + return globalProtectionReason; } - return isPackageEcmGuarded(packageName, userId); + return isPackageEcmGuarded(packageName, userId) ? REASON_APP_OP_RESTRICTED : null; } catch (NameNotFoundException e) { throw new IllegalArgumentException(e); } @@ -513,12 +525,13 @@ public class EnhancedConfirmationService extends SystemService { return false; } - private boolean isSettingProtectedGlobally(@NonNull String settingIdentifier) { - if (UNTRUSTED_CALL_RESTRICTED_SETTINGS.contains(settingIdentifier)) { - return isUntrustedCallOngoing(); + private String getGlobalProtectionReason(@NonNull String settingIdentifier) { + if (UNTRUSTED_CALL_RESTRICTED_SETTINGS.contains(settingIdentifier) + && isUntrustedCallOngoing()) { + return REASON_PHONE_STATE; } - return false; + return null; } @Nullable diff --git a/tests/cts/permissionui/src/android/permissionui/cts/PermissionTapjackingTest.kt b/tests/cts/permissionui/src/android/permissionui/cts/PermissionTapjackingTest.kt index baebfe06f..56072d521 100644 --- a/tests/cts/permissionui/src/android/permissionui/cts/PermissionTapjackingTest.kt +++ b/tests/cts/permissionui/src/android/permissionui/cts/PermissionTapjackingTest.kt @@ -52,14 +52,17 @@ class PermissionTapjackingTest : BaseUsePermissionTest() { requestAppPermissionsForNoResult(ACCESS_FINE_LOCATION) {} val buttonCenter = - waitFindObject(By.text(getPermissionControllerString(ALLOW_FOREGROUND_BUTTON_TEXT)) - .displayId(displayId)) + waitFindObject( + By.text(getPermissionControllerString(ALLOW_FOREGROUND_BUTTON_TEXT)) + .displayId(displayId) + ) .visibleCenter // Wait for overlay to hide the dialog context.sendBroadcast(Intent(ACTION_SHOW_OVERLAY).putExtra(EXTRA_FULL_OVERLAY, true)) waitFindObject( - By.res("android.permissionui.cts.usepermission:id/overlay").displayId(displayId)) + By.res("android.permissionui.cts.usepermission:id/overlay").displayId(displayId) + ) tryClicking(buttonCenter) } @@ -76,18 +79,19 @@ class PermissionTapjackingTest : BaseUsePermissionTest() { assertAppHasPermission(ACCESS_FINE_LOCATION, false) requestAppPermissionsForNoResult(ACCESS_FINE_LOCATION) {} - val foregroundButtonCenter = - waitFindObject(By.text(getPermissionControllerString(ALLOW_FOREGROUND_BUTTON_TEXT)) - .displayId(displayId)) - .visibleCenter val oneTimeButton = - waitFindObjectOrNull(By.text(getPermissionControllerString(ALLOW_ONE_TIME_BUTTON_TEXT)) - .displayId(displayId)) + waitFindObjectOrNull( + By.text(getPermissionControllerString(ALLOW_ONE_TIME_BUTTON_TEXT)) + .displayId(displayId) + ) + // If one-time button is not available, fallback to deny button val overlayButtonBounds = oneTimeButton?.visibleBounds - ?: waitFindObject(By.text(getPermissionControllerString(DENY_BUTTON_TEXT)) - .displayId(displayId)) + ?: waitFindObject( + By.text(getPermissionControllerString(DENY_BUTTON_TEXT)) + .displayId(displayId) + ) .visibleBounds // Wait for overlay to hide the dialog @@ -100,7 +104,15 @@ class PermissionTapjackingTest : BaseUsePermissionTest() { .putExtra(OVERLAY_BOTTOM, overlayButtonBounds.bottom) ) waitFindObject( - By.res("android.permissionui.cts.usepermission:id/overlay").displayId(displayId)) + By.res("android.permissionui.cts.usepermission:id/overlay").displayId(displayId) + ) + + val foregroundButtonCenter = + waitFindObject( + By.text(getPermissionControllerString(ALLOW_FOREGROUND_BUTTON_TEXT)) + .displayId(displayId) + ) + .visibleCenter tryClicking(foregroundButtonCenter) } @@ -119,7 +131,7 @@ class PermissionTapjackingTest : BaseUsePermissionTest() { } assertAppHasPermission(ACCESS_FINE_LOCATION, true) }, - 10000 + 10000, ) } catch (e: RuntimeException) { // expected @@ -140,15 +152,15 @@ class PermissionTapjackingTest : BaseUsePermissionTest() { } assertAppHasPermission(ACCESS_FINE_LOCATION, true) }, - 10000 + 10000, ) } private fun click(buttonCenter: Point) { val downTime = SystemClock.uptimeMillis() - val x= buttonCenter.x.toFloat() + val x = buttonCenter.x.toFloat() val y = buttonCenter.y.toFloat() - var event = MotionEvent.obtain(downTime, downTime, MotionEvent.ACTION_DOWN,x , y, 0) + var event = MotionEvent.obtain(downTime, downTime, MotionEvent.ACTION_DOWN, x, y, 0) event.displayId = displayId uiAutomation.injectInputEvent(event, true) |