diff options
Diffstat (limited to 'PermissionController/src')
72 files changed, 3067 insertions, 1162 deletions
diff --git a/PermissionController/src/com/android/permissioncontroller/ecm/EnhancedConfirmationDialogActivity.kt b/PermissionController/src/com/android/permissioncontroller/ecm/EnhancedConfirmationDialogActivity.kt index bc6f774ad..e6cf094e3 100644 --- a/PermissionController/src/com/android/permissioncontroller/ecm/EnhancedConfirmationDialogActivity.kt +++ b/PermissionController/src/com/android/permissioncontroller/ecm/EnhancedConfirmationDialogActivity.kt @@ -18,6 +18,7 @@ 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 @@ -65,6 +66,7 @@ class EnhancedConfirmationDialogActivity : FragmentActivity() { finish() return } + if (savedInstanceState != null) { wasClearRestrictionAllowed = savedInstanceState.getBoolean(KEY_WAS_CLEAR_RESTRICTION_ALLOWED) @@ -79,11 +81,19 @@ class EnhancedConfirmationDialogActivity : FragmentActivity() { require(uid != Process.INVALID_UID) { "EXTRA_UID cannot be null or invalid" } require(!packageName.isNullOrEmpty()) { "EXTRA_PACKAGE_NAME cannot be null or empty" } require(!settingIdentifier.isNullOrEmpty()) { "EXTRA_SUBJECT cannot be null or empty" } - wasClearRestrictionAllowed = setClearRestrictionAllowed(packageName, UserHandle.getUserHandleForUid(uid)) val setting = Setting.fromIdentifier(this, settingIdentifier, isEcmInApp) + if ( + SettingType.fromIdentifier(this, settingIdentifier, isEcmInApp) == + SettingType.BLOCKED_DUE_TO_PHONE_STATE && + !Flags.unknownCallPackageInstallBlockingEnabled() + ) { + finish() + return + } + if (DeviceUtils.isWear(this)) { WearEnhancedConfirmationDialogFragment.newInstance(setting.title, setting.message) .show(supportFragmentManager, WearEnhancedConfirmationDialogFragment.TAG) @@ -116,7 +126,7 @@ class EnhancedConfirmationDialogActivity : FragmentActivity() { fun fromIdentifier( context: Context, settingIdentifier: String, - isEcmInApp: Boolean + isEcmInApp: Boolean, ): Setting { val settingType = SettingType.fromIdentifier(context, settingIdentifier, isEcmInApp) val label = @@ -124,7 +134,7 @@ class EnhancedConfirmationDialogActivity : FragmentActivity() { SettingType.PLATFORM_PERMISSION -> KotlinUtils.getPermGroupLabel( context, - PermissionMapping.getGroupOfPlatformPermission(settingIdentifier)!! + PermissionMapping.getGroupOfPlatformPermission(settingIdentifier)!!, ) SettingType.PLATFORM_PERMISSION_GROUP -> KotlinUtils.getPermGroupLabel(context, settingIdentifier) @@ -132,15 +142,22 @@ class EnhancedConfirmationDialogActivity : FragmentActivity() { context.getString( Roles.get(context)[settingIdentifier]!!.shortLabelResource ) + SettingType.BLOCKED_DUE_TO_PHONE_STATE, SettingType.OTHER -> null } - val url = - context.getString(R.string.help_url_action_disabled_by_restricted_settings) - return Setting( - title = settingType.titleRes?.let { context.getString(it, label) }, + var title: String? + var message: CharSequence? + if (settingType == SettingType.BLOCKED_DUE_TO_PHONE_STATE) { + title = settingType.titleRes?.let { context.getString(it) } + message = settingType.messageRes?.let { context.getString(it) } + } else { + val url = + context.getString(R.string.help_url_action_disabled_by_restricted_settings) + title = (settingType.titleRes?.let { context.getString(it, label) }) message = settingType.messageRes?.let { Html.fromHtml(context.getString(it, url), 0) } - ) + } + return Setting(title, message) } } } @@ -148,29 +165,35 @@ class EnhancedConfirmationDialogActivity : FragmentActivity() { private enum class SettingType(val titleRes: Int?, val messageRes: Int?) { PLATFORM_PERMISSION( R.string.enhanced_confirmation_dialog_title_permission, - R.string.enhanced_confirmation_dialog_desc_permission + R.string.enhanced_confirmation_dialog_desc_permission, ), PLATFORM_PERMISSION_GROUP( R.string.enhanced_confirmation_dialog_title_permission, - R.string.enhanced_confirmation_dialog_desc_permission + R.string.enhanced_confirmation_dialog_desc_permission, ), ROLE( R.string.enhanced_confirmation_dialog_title_role, - R.string.enhanced_confirmation_dialog_desc_role + R.string.enhanced_confirmation_dialog_desc_role, ), OTHER( R.string.enhanced_confirmation_dialog_title_settings_default, - R.string.enhanced_confirmation_dialog_desc_settings_default + R.string.enhanced_confirmation_dialog_desc_settings_default, + ), + BLOCKED_DUE_TO_PHONE_STATE( + R.string.enhanced_confirmation_phone_state_dialog_title, + R.string.enhanced_confirmation_phone_state_dialog_desc, ); companion object { fun fromIdentifier( context: Context, settingIdentifier: String, - isEcmInApp: Boolean + isEcmInApp: Boolean, ): SettingType { - if (!isEcmInApp) return SettingType.OTHER return when { + settingIdentifier == AppOpsManager.OPSTR_REQUEST_INSTALL_PACKAGES -> + BLOCKED_DUE_TO_PHONE_STATE + !isEcmInApp -> OTHER PermissionMapping.isRuntimePlatformPermission(settingIdentifier) && PermissionMapping.getGroupOfPlatformPermission(settingIdentifier) != null -> PLATFORM_PERMISSION @@ -178,7 +201,7 @@ class EnhancedConfirmationDialogActivity : FragmentActivity() { PLATFORM_PERMISSION_GROUP settingIdentifier.startsWith("android.app.role.") && Roles.get(context).containsKey(settingIdentifier) -> ROLE - else -> SettingType.OTHER + else -> OTHER } } } @@ -188,7 +211,7 @@ class EnhancedConfirmationDialogActivity : FragmentActivity() { this.dialogResult = dialogResult setResult( RESULT_OK, - Intent().apply { putExtra(Intent.EXTRA_RETURN_RESULT, dialogResult.statsLogValue) } + Intent().apply { putExtra(Intent.EXTRA_RETURN_RESULT, dialogResult.statsLogValue) }, ) finish() } @@ -200,7 +223,7 @@ class EnhancedConfirmationDialogActivity : FragmentActivity() { uid = intent.getIntExtra(Intent.EXTRA_UID, Process.INVALID_UID), settingIdentifier = intent.getStringExtra(Intent.EXTRA_SUBJECT)!!, firstShowForApp = !wasClearRestrictionAllowed, - dialogResult = dialogResult + dialogResult = dialogResult, ) } } @@ -249,7 +272,7 @@ class EnhancedConfirmationDialogActivity : FragmentActivity() { private fun createDialogView( context: Context, title: String?, - message: CharSequence? + message: CharSequence?, ): View = LayoutInflater.from(context) .inflate(R.layout.enhanced_confirmation_dialog, null) diff --git a/PermissionController/src/com/android/permissioncontroller/hibernation/HibernationPolicy.kt b/PermissionController/src/com/android/permissioncontroller/hibernation/HibernationPolicy.kt index d23225ed3..8b11036e8 100644 --- a/PermissionController/src/com/android/permissioncontroller/hibernation/HibernationPolicy.kt +++ b/PermissionController/src/com/android/permissioncontroller/hibernation/HibernationPolicy.kt @@ -73,6 +73,7 @@ import android.service.dreams.DreamService import android.service.notification.NotificationListenerService import android.service.voice.VoiceInteractionService import android.service.wallpaper.WallpaperService +import android.telecom.TelecomManager import android.telephony.TelephonyManager.CARRIER_PRIVILEGE_STATUS_HAS_ACCESS import android.telephony.TelephonyManager.CARRIER_PRIVILEGE_STATUS_NO_ACCESS import android.util.Log @@ -658,6 +659,36 @@ suspend fun isPackageHibernationExemptBySystem( return true } + // Note that it's fine to check permissions instead of app ops as all these permissions were + // introduced before auto-revoke / hibernation in R. + val hasCallRelatedPermissions = + context.checkPermission(Manifest.permission.MANAGE_OWN_CALLS, -1 /* pid */, pkg.uid) == + PERMISSION_GRANTED + && context.checkPermission(Manifest.permission.RECORD_AUDIO, -1 /* pid */, pkg.uid) == + PERMISSION_GRANTED + && context.checkPermission(Manifest.permission.WRITE_CALL_LOG, -1 /* pid */, pkg.uid) == + PERMISSION_GRANTED + if (hasCallRelatedPermissions) { + val phoneAccounts = context.getSystemService(TelecomManager::class.java)!! + .selfManagedPhoneAccounts + var hasRegisteredPhoneAccount = false + for (phoneAccount in phoneAccounts) { + if (pkg.packageName == phoneAccount.componentName.packageName) { + hasRegisteredPhoneAccount = true + break + } + } + if (hasRegisteredPhoneAccount) { + if (DEBUG_HIBERNATION_POLICY) { + DumpableLog.i( + LOG_TAG, + "Exempted ${pkg.packageName} - caller app" + ) + } + return true + } + } + if (SdkLevel.isAtLeastS()) { val hasInstallOrUpdatePermissions = context.checkPermission(Manifest.permission.INSTALL_PACKAGES, -1 /* pid */, pkg.uid) == diff --git a/PermissionController/src/com/android/permissioncontroller/hibernation/TEST_MAPPING b/PermissionController/src/com/android/permissioncontroller/hibernation/TEST_MAPPING index 69a8f74be..038b2f992 100644 --- a/PermissionController/src/com/android/permissioncontroller/hibernation/TEST_MAPPING +++ b/PermissionController/src/com/android/permissioncontroller/hibernation/TEST_MAPPING @@ -11,5 +11,15 @@ } ] } + ], + "postsubmit": [ + { + "name": "PermissionControllerMockingTests", + "options": [ + { + "include-filter": "com.android.permissioncontroller.tests.mocking.hibernation" + } + ] + } ] } diff --git a/PermissionController/src/com/android/permissioncontroller/permission/compat/AppOpsManagerCompat.java b/PermissionController/src/com/android/permissioncontroller/permission/compat/AppOpsManagerCompat.java new file mode 100644 index 000000000..05d69e998 --- /dev/null +++ b/PermissionController/src/com/android/permissioncontroller/permission/compat/AppOpsManagerCompat.java @@ -0,0 +1,49 @@ +/* + * 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.compat; + +import android.annotation.SuppressLint; +import android.app.AppOpsManager; +import android.permission.flags.Flags; + +import androidx.annotation.NonNull; + +/** + * Helper AppOpsManager compat class + */ +public class AppOpsManagerCompat { + + private AppOpsManagerCompat() {} + + /** + * For platform version <= V, call the deprecated unsafeCheckOpRawNoThrow. For newer platforms, + * call the new API checkOpRawNoThrow. + * + * @return the raw mode of the op + */ + // TODO: b/379749734 + @SuppressWarnings("deprecation") + @SuppressLint("NewApi") + public static int checkOpRawNoThrow(@NonNull AppOpsManager appOpsManager, @NonNull String op, + int uid, @NonNull String packageName) { + if (Flags.checkOpOverloadApiEnabled()) { + return appOpsManager.checkOpRawNoThrow(op, uid, packageName, null); + } else { + return appOpsManager.unsafeCheckOpRawNoThrow(op, uid, packageName); + } + } +} diff --git a/PermissionController/src/com/android/permissioncontroller/permission/compat/AppPermissionFragmentCompat.java b/PermissionController/src/com/android/permissioncontroller/permission/compat/AppPermissionFragmentCompat.java index 188e3a9d0..de7404ead 100644 --- a/PermissionController/src/com/android/permissioncontroller/permission/compat/AppPermissionFragmentCompat.java +++ b/PermissionController/src/com/android/permissioncontroller/permission/compat/AppPermissionFragmentCompat.java @@ -19,6 +19,7 @@ package com.android.permissioncontroller.permission.compat; import static com.android.permissioncontroller.Constants.EXTRA_SESSION_ID; import static com.android.permissioncontroller.permission.ui.ManagePermissionsActivity.EXTRA_CALLER_NAME; +import android.content.Context; import android.content.Intent; import android.os.Bundle; import android.os.UserHandle; @@ -29,6 +30,7 @@ import androidx.preference.PreferenceFragmentCompat; import com.android.modules.utils.build.SdkLevel; import com.android.permission.flags.Flags; +import com.android.permissioncontroller.R; import com.android.permissioncontroller.permission.ui.handheld.max35.LegacyAppPermissionFragment; import com.android.permissioncontroller.permission.ui.handheld.v36.AppPermissionFragment; @@ -41,8 +43,10 @@ public class AppPermissionFragmentCompat { * Create an instance of this fragment */ @NonNull - public static PreferenceFragmentCompat createFragment() { - if (SdkLevel.isAtLeastV() && Flags.appPermissionFragmentUsesPreferences()) { + public static PreferenceFragmentCompat createFragment(@NonNull Context context) { + if (SdkLevel.isAtLeastV() && (Flags.appPermissionFragmentUsesPreferences() + || context.getResources().getBoolean( + R.bool.config_usePreferenceForAppPermissionSettings))) { return new AppPermissionFragment(); } else { return new LegacyAppPermissionFragment(); diff --git a/PermissionController/src/com/android/permissioncontroller/permission/data/AppOpLiveData.kt b/PermissionController/src/com/android/permissioncontroller/permission/data/AppOpLiveData.kt index 1e44f16bd..3202f5b69 100644 --- a/PermissionController/src/com/android/permissioncontroller/permission/data/AppOpLiveData.kt +++ b/PermissionController/src/com/android/permissioncontroller/permission/data/AppOpLiveData.kt @@ -19,6 +19,7 @@ package com.android.permissioncontroller.permission.data import android.app.AppOpsManager import android.app.Application import com.android.permissioncontroller.PermissionControllerApplication +import com.android.permissioncontroller.permission.compat.AppOpsManagerCompat /** * A LiveData which represents the appop state @@ -36,13 +37,13 @@ private constructor( private val app: Application, private val packageName: String, private val op: String, - private val uid: Int + private val uid: Int, ) : SmartUpdateMediatorLiveData<Int>() { private val appOpsManager = app.getSystemService(AppOpsManager::class.java)!! override fun onUpdate() { - value = appOpsManager.unsafeCheckOpNoThrow(op, uid, packageName) + value = AppOpsManagerCompat.checkOpRawNoThrow(appOpsManager, op, uid, packageName) } override fun onActive() { @@ -62,7 +63,7 @@ private constructor( PermissionControllerApplication.get(), key.first, key.second, - key.third + key.third, ) } } diff --git a/PermissionController/src/com/android/permissioncontroller/permission/data/AppPermGroupUiInfoLiveData.kt b/PermissionController/src/com/android/permissioncontroller/permission/data/AppPermGroupUiInfoLiveData.kt index b17098a13..394cb3eb7 100644 --- a/PermissionController/src/com/android/permissioncontroller/permission/data/AppPermGroupUiInfoLiveData.kt +++ b/PermissionController/src/com/android/permissioncontroller/permission/data/AppPermGroupUiInfoLiveData.kt @@ -233,8 +233,8 @@ private constructor( * user */ private fun isUserSet(permissionState: Map<String, PermState>): Boolean { - val flagMask = - PackageManager.FLAG_PERMISSION_USER_SET or PackageManager.FLAG_PERMISSION_USER_FIXED + val flagMask = PackageManager.FLAG_PERMISSION_USER_SET or + PackageManager.FLAG_PERMISSION_USER_FIXED or PackageManager.FLAG_PERMISSION_ONE_TIME return permissionState.any { (it.value.permFlags and flagMask) != 0 } } diff --git a/PermissionController/src/com/android/permissioncontroller/permission/data/FullStoragePermissionAppsLiveData.kt b/PermissionController/src/com/android/permissioncontroller/permission/data/FullStoragePermissionAppsLiveData.kt index 4a2d3b68a..2b6d9728e 100644 --- a/PermissionController/src/com/android/permissioncontroller/permission/data/FullStoragePermissionAppsLiveData.kt +++ b/PermissionController/src/com/android/permissioncontroller/permission/data/FullStoragePermissionAppsLiveData.kt @@ -28,6 +28,7 @@ import android.app.Application import android.os.Build import android.os.UserHandle import com.android.permissioncontroller.PermissionControllerApplication +import com.android.permissioncontroller.permission.compat.AppOpsManagerCompat import com.android.permissioncontroller.permission.model.livedatatypes.LightPackageInfo import kotlinx.coroutines.Job @@ -46,7 +47,7 @@ object FullStoragePermissionAppsLiveData : val packageName: String, val user: UserHandle, val isLegacy: Boolean, - val isGranted: Boolean + val isGranted: Boolean, ) init { @@ -88,7 +89,7 @@ object FullStoragePermissionAppsLiveData : fun getFullStorageStateForPackage( appOpsManager: AppOpsManager, packageInfo: LightPackageInfo, - userHandle: UserHandle? = null + userHandle: UserHandle? = null, ): FullStoragePackageState? { val sdk = packageInfo.targetSdkVersion val user = userHandle ?: UserHandle.getUserHandleForUid(packageInfo.uid) @@ -97,29 +98,31 @@ object FullStoragePermissionAppsLiveData : packageInfo.packageName, user, isLegacy = true, - isGranted = true + isGranted = true, ) } else if ( sdk <= Build.VERSION_CODES.Q && - appOpsManager.unsafeCheckOpNoThrow( + AppOpsManagerCompat.checkOpRawNoThrow( + appOpsManager, OPSTR_LEGACY_STORAGE, packageInfo.uid, - packageInfo.packageName + packageInfo.packageName, ) == MODE_ALLOWED ) { return FullStoragePackageState( packageInfo.packageName, user, isLegacy = true, - isGranted = true + isGranted = true, ) } if (MANAGE_EXTERNAL_STORAGE in packageInfo.requestedPermissions) { val mode = - appOpsManager.unsafeCheckOpNoThrow( + AppOpsManagerCompat.checkOpRawNoThrow( + appOpsManager, OPSTR_MANAGE_EXTERNAL_STORAGE, packageInfo.uid, - packageInfo.packageName + packageInfo.packageName, ) val granted = mode == MODE_ALLOWED || @@ -130,7 +133,7 @@ object FullStoragePermissionAppsLiveData : packageInfo.packageName, user, isLegacy = false, - isGranted = granted + isGranted = granted, ) } return null diff --git a/PermissionController/src/com/android/permissioncontroller/permission/domain/usecase/v31/GetPermissionGroupUsageDetailsUseCase.kt b/PermissionController/src/com/android/permissioncontroller/permission/domain/usecase/v31/GetPermissionGroupUsageDetailsUseCase.kt index 16eaaf6f2..5ba649fd3 100644 --- a/PermissionController/src/com/android/permissioncontroller/permission/domain/usecase/v31/GetPermissionGroupUsageDetailsUseCase.kt +++ b/PermissionController/src/com/android/permissioncontroller/permission/domain/usecase/v31/GetPermissionGroupUsageDetailsUseCase.kt @@ -21,6 +21,7 @@ import android.app.AppOpsManager import android.os.UserHandle import android.permission.flags.Flags import com.android.modules.utils.build.SdkLevel +import com.android.permissioncontroller.PermissionControllerApplication import com.android.permissioncontroller.appops.data.model.v31.DiscretePackageOpsModel import com.android.permissioncontroller.appops.data.model.v31.DiscretePackageOpsModel.DiscreteOpModel import com.android.permissioncontroller.appops.data.repository.v31.AppOpRepository @@ -29,6 +30,7 @@ import com.android.permissioncontroller.permission.domain.model.v31.PermissionTi import com.android.permissioncontroller.permission.domain.model.v31.PermissionTimelineUsageModelWrapper import com.android.permissioncontroller.permission.ui.model.v31.PermissionUsageDetailsViewModel.Companion.CLUSTER_SPACING_MINUTES import com.android.permissioncontroller.permission.ui.model.v31.PermissionUsageDetailsViewModel.Companion.ONE_MINUTE_MS +import com.android.permissioncontroller.permission.utils.LocationUtils import com.android.permissioncontroller.permission.utils.PermissionMapping import com.android.permissioncontroller.pm.data.repository.v31.PackageRepository import com.android.permissioncontroller.role.data.repository.v31.RoleRepository @@ -48,6 +50,9 @@ class GetPermissionGroupUsageDetailsUseCase( private val appOpRepository: AppOpRepository, private val roleRepository: RoleRepository, private val userRepository: UserRepository, + // Allow tests to inject as on T- READ_DEVICE_CONFIG permission check is enforced. + private val attributionLabelFix: Boolean = + com.android.permission.flags.Flags.permissionTimelineAttributionLabelFix(), ) { operator fun invoke(coroutineScope: CoroutineScope): Flow<PermissionTimelineUsageModelWrapper> { val opNames = requireNotNull(permissionGroupToOpNames[permissionGroup]) @@ -72,7 +77,7 @@ class GetPermissionGroupUsageDetailsUseCase( permissionGroup, packageOps.userId, permissionRepository, - packageRepository + packageRepository, ) packageOps } @@ -86,43 +91,61 @@ class GetPermissionGroupUsageDetailsUseCase( } } + // show attribution on T+ for location provider only.. + private fun shouldShowAttributionLabel(packageName: String): Boolean { + return if (attributionLabelFix) { + SdkLevel.isAtLeastT() && + LocationUtils.isLocationProvider(PermissionControllerApplication.get(), packageName) + } else true + } + /** Group app op accesses by attribution label if it is available and user visible. */ private suspend fun List<DiscretePackageOpsModel>.groupByAttributionLabelIfNeeded() = map { packageOps -> - val attributionInfo = - packageRepository.getPackageAttributionInfo( - packageOps.packageName, - UserHandle.of(packageOps.userId) - ) - if (attributionInfo != null) { - if (attributionInfo.areUserVisible && attributionInfo.tagResourceMap != null) { - val attributionLabelOpsMap: Map<String?, List<DiscreteOpModel>> = - packageOps.appOpAccesses - .map { appOpEntry -> - val resourceId = - attributionInfo.tagResourceMap[appOpEntry.attributionTag] - val label = attributionInfo.resourceLabelMap?.get(resourceId) - label to appOpEntry - } - .groupBy { labelAppOpEntryPair -> labelAppOpEntryPair.first } - .mapValues { (_, values) -> - values.map { labelAppOpEntryPair -> labelAppOpEntryPair.second } - } + if (!shouldShowAttributionLabel(packageOps.packageName)) { + listOf(packageOps) + } else { + val attributionInfo = + packageRepository.getPackageAttributionInfo( + packageOps.packageName, + UserHandle.of(packageOps.userId), + ) + if (attributionInfo != null) { + if ( + attributionInfo.areUserVisible && attributionInfo.tagResourceMap != null + ) { + val attributionLabelOpsMap: Map<String?, List<DiscreteOpModel>> = + packageOps.appOpAccesses + .map { appOpEntry -> + val resourceId = + attributionInfo.tagResourceMap[ + appOpEntry.attributionTag] + val label = + attributionInfo.resourceLabelMap?.get(resourceId) + label to appOpEntry + } + .groupBy { labelAppOpEntryPair -> labelAppOpEntryPair.first } + .mapValues { (_, values) -> + values.map { labelAppOpEntryPair -> + labelAppOpEntryPair.second + } + } - attributionLabelOpsMap.map { labelAppOpsEntry -> - DiscretePackageOpsModel( - packageOps.packageName, - packageOps.userId, - appOpAccesses = labelAppOpsEntry.value, - attributionLabel = labelAppOpsEntry.key, - isUserSensitive = packageOps.isUserSensitive, - ) + attributionLabelOpsMap.map { labelAppOpsEntry -> + DiscretePackageOpsModel( + packageOps.packageName, + packageOps.userId, + appOpAccesses = labelAppOpsEntry.value, + attributionLabel = labelAppOpsEntry.key, + isUserSensitive = packageOps.isUserSensitive, + ) + } + } else { + listOf(packageOps) } } else { listOf(packageOps) } - } else { - listOf(packageOps) } } .flatten() @@ -147,7 +170,7 @@ class GetPermissionGroupUsageDetailsUseCase( packageOps.userId, currentCluster.toMutableList(), packageOps.attributionLabel, - packageOps.isUserSensitive + packageOps.isUserSensitive, ) ) currentCluster.clear() @@ -164,7 +187,7 @@ class GetPermissionGroupUsageDetailsUseCase( packageOps.userId, currentCluster.toMutableList(), packageOps.attributionLabel, - packageOps.isUserSensitive + packageOps.isUserSensitive, ) ) } @@ -220,7 +243,7 @@ class GetPermissionGroupUsageDetailsUseCase( private fun canAccessBeAddedToCluster( currentAccess: DiscreteOpModel, - clusteredAccesses: List<DiscreteOpModel> + clusteredAccesses: List<DiscreteOpModel>, ): Boolean { val clusterOp = clusteredAccesses.last().opName if ( @@ -282,7 +305,7 @@ class GetPermissionGroupUsageDetailsUseCase( listOf( Manifest.permission_group.CAMERA, Manifest.permission_group.LOCATION, - Manifest.permission_group.MICROPHONE + Manifest.permission_group.MICROPHONE, ) permissionGroups.forEach { permissionGroup -> val opNames = diff --git a/PermissionController/src/com/android/permissioncontroller/permission/service/BackupHelper.java b/PermissionController/src/com/android/permissioncontroller/permission/service/BackupHelper.java index 24aab174c..2fa809c6d 100644 --- a/PermissionController/src/com/android/permissioncontroller/permission/service/BackupHelper.java +++ b/PermissionController/src/com/android/permissioncontroller/permission/service/BackupHelper.java @@ -96,6 +96,7 @@ public class BackupHelper { private static final String ATTR_USER_SET = "set"; private static final String ATTR_USER_FIXED = "fixed"; private static final String ATTR_WAS_REVIEWED = "was-reviewed"; + private static final String ATTR_ONE_TIME = "one-time"; /** Flags of permissions to <u>not</u> back up */ private static final int SYSTEM_RUNTIME_GRANT_MASK = FLAG_PERMISSION_POLICY_FIXED @@ -452,19 +453,21 @@ public class BackupHelper { private final boolean mIsUserSet; private final boolean mIsUserFixed; private final boolean mWasReviewed; + private final boolean mIsOneTime; // Not persisted, used during parsing so explicitly defined state takes precedence private final boolean mIsAddedFromSplit; private BackupPermissionState(@NonNull String permissionName, boolean isGranted, boolean isUserSet, boolean isUserFixed, boolean wasReviewed, - boolean isAddedFromSplit) { + boolean isOneTime, boolean isAddedFromSplit) { mPermissionName = permissionName; mIsGranted = isGranted; mIsUserSet = isUserSet; mIsUserFixed = isUserFixed; mWasReviewed = wasReviewed; mIsAddedFromSplit = isAddedFromSplit; + mIsOneTime = isOneTime; } /** @@ -512,6 +515,7 @@ public class BackupHelper { "true".equals(parser.getAttributeValue(null, ATTR_USER_SET)), "true".equals(parser.getAttributeValue(null, ATTR_USER_FIXED)), "true".equals(parser.getAttributeValue(null, ATTR_WAS_REVIEWED)), + "true".equals(parser.getAttributeValue(null, ATTR_ONE_TIME)), /* isAddedFromSplit */ i > 0)); } @@ -519,7 +523,8 @@ public class BackupHelper { } /** - * Is the permission granted, also considering the app-op. + * Is the permission granted, also considering the app-op. Don't consider one time grant + * as a permission grant for backup/restore. * * <p>This does not consider the review-required state of the permission. * @@ -528,7 +533,8 @@ public class BackupHelper { * @return {@code true} iff the permission and app-op is granted */ private static boolean isPermGrantedIncludingAppOp(@NonNull Permission perm) { - return perm.isGranted() && (!perm.affectsAppOp() || perm.isAppOpAllowed()); + return perm.isGranted() && (!perm.affectsAppOp() || perm.isAppOpAllowed()) + && !perm.isOneTime(); } /** @@ -549,7 +555,7 @@ public class BackupHelper { return null; } - if (!perm.isUserSet() && perm.isGrantedByDefault()) { + if (!perm.isUserSet() && !perm.isOneTime() && perm.isGrantedByDefault()) { return null; } @@ -564,10 +570,10 @@ public class BackupHelper { } if (isNotInDefaultGrantState || perm.isUserSet() || perm.isUserFixed() - || permissionWasReviewed) { + || perm.isOneTime() || permissionWasReviewed) { return new BackupPermissionState(perm.getName(), isPermGrantedIncludingAppOp(perm), perm.isUserSet(), perm.isUserFixed(), permissionWasReviewed, - /* isAddedFromSplit */ false); + perm.isOneTime(), /* isAddedFromSplit */ false); } else { return null; } @@ -628,6 +634,10 @@ public class BackupHelper { serializer.attribute(null, ATTR_WAS_REVIEWED, "true"); } + if (mIsOneTime) { + serializer.attribute(null, ATTR_ONE_TIME, "true"); + } + serializer.endTag(null, TAG_PERMISSION); } @@ -671,6 +681,10 @@ public class BackupHelper { perm.setUserSet(mIsUserSet); } + + if (!perm.isOneTime()) { + perm.setOneTime(mIsOneTime); + } } } diff --git a/PermissionController/src/com/android/permissioncontroller/permission/service/RuntimePermissionsUpgradeController.kt b/PermissionController/src/com/android/permissioncontroller/permission/service/RuntimePermissionsUpgradeController.kt index 2734116dd..85145f346 100644 --- a/PermissionController/src/com/android/permissioncontroller/permission/service/RuntimePermissionsUpgradeController.kt +++ b/PermissionController/src/com/android/permissioncontroller/permission/service/RuntimePermissionsUpgradeController.kt @@ -566,10 +566,11 @@ object RuntimePermissionsUpgradeController { appPermGroup.permissions[permission.ACCESS_MEDIA_LOCATION] ?: continue if ( + !perm.isGranted && !perm.isUserSet && - !perm.isSystemFixed && - !perm.isPolicyFixed && - !perm.isGranted + !perm.isOneTime && + !perm.isSystemFixed && + !perm.isPolicyFixed ) { grants.add( Grant(false, appPermGroup, listOf(permission.ACCESS_MEDIA_LOCATION)) @@ -610,20 +611,21 @@ object RuntimePermissionsUpgradeController { // Upon upgrading to platform 33, for all targetSdk>=33 apps, do the following: // If STORAGE is granted, and the user has not set READ_MEDIA_AURAL or // READ_MEDIA_VISUAL, grant READ_MEDIA_AURAL and READ_MEDIA_VISUAL - val storageAppPermGroups = + val grantedStorageAppPermGroups = storageAndMediaAppPermGroups.filter { it.packageInfo.targetSdkVersion >= Build.VERSION_CODES.TIRAMISU && it.permGroupInfo.name == permission_group.STORAGE && it.isGranted && it.isUserSet } - for (storageAppPermGroup in storageAppPermGroups) { + for (storageAppPermGroup in grantedStorageAppPermGroups) { val pkgName = storageAppPermGroup.packageInfo.packageName val auralAppPermGroup = storageAndMediaAppPermGroups.firstOrNull { it.packageInfo.packageName == pkgName && it.permGroupInfo.name == permission_group.READ_MEDIA_AURAL && !it.isUserSet && + !it.isOneTime && !it.isUserFixed } val visualAppPermGroup = @@ -632,7 +634,8 @@ object RuntimePermissionsUpgradeController { it.permGroupInfo.name == permission_group.READ_MEDIA_VISUAL && !it.permissions .filter { it.key != permission.ACCESS_MEDIA_LOCATION } - .any { it.value.isUserSet || it.value.isUserFixed } + .any { it.value.isUserSet || it.value.isOneTime || + it.value.isUserFixed } } if (auralAppPermGroup != null) { diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/GrantPermissionsActivity.java b/PermissionController/src/com/android/permissioncontroller/permission/ui/GrantPermissionsActivity.java index a4f629d80..c1479caf2 100644 --- a/PermissionController/src/com/android/permissioncontroller/permission/ui/GrantPermissionsActivity.java +++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/GrantPermissionsActivity.java @@ -46,7 +46,6 @@ import android.app.KeyguardManager; import android.app.ecm.EnhancedConfirmationManager; import android.content.Context; import android.content.Intent; -import android.content.pm.PackageInfo; import android.content.pm.PackageItemInfo; import android.content.pm.PackageManager; import android.content.res.Resources; @@ -195,7 +194,7 @@ public class GrantPermissionsActivity extends SettingsActivity /** A list of permissions requested on an app's behalf by the system. Usually Implicitly * requested, although this isn't necessarily always the case. */ - private List<String> mSystemRequestedPermissions = new ArrayList<>(); + private final List<String> mSystemRequestedPermissions = new ArrayList<>(); /** A copy of the list of permissions originally requested in the intent to this activity */ private String[] mOriginalRequestedPermissions = new String[0]; @@ -209,7 +208,7 @@ public class GrantPermissionsActivity extends SettingsActivity * A list of other GrantPermissionActivities for the same package which passed their list of * permissions to this one. They need to be informed when this activity finishes. */ - private List<GrantPermissionsActivity> mFollowerActivities = new ArrayList<>(); + private final List<GrantPermissionsActivity> mFollowerActivities = new ArrayList<>(); /** Whether this activity has asked another GrantPermissionsActivity to show on its behalf */ private boolean mDelegated; @@ -235,7 +234,7 @@ public class GrantPermissionsActivity extends SettingsActivity private PackageManager mPackageManager; - private ActivityResultLauncher<Intent> mShowWarningDialog = + private final ActivityResultLauncher<Intent> mShowWarningDialog = registerForActivityResult( new ActivityResultContracts.StartActivityForResult(), result -> { @@ -284,7 +283,7 @@ public class GrantPermissionsActivity extends SettingsActivity return; } try { - PackageInfo packageInfo = mPackageManager.getPackageInfo(mTargetPackage, 0); + mPackageManager.getPackageInfo(mTargetPackage, 0); } catch (PackageManager.NameNotFoundException e) { Log.e(LOG_TAG, "Unable to get package info for the calling package.", e); finishAfterTransition(); @@ -314,20 +313,23 @@ public class GrantPermissionsActivity extends SettingsActivity .getPackageManager(); } - // When the dialog is streamed to a remote device, verify requested permissions are all - // device aware and target device is the same as the remote device. Otherwise show a - // warning dialog. + // When the permission grant dialog is streamed to a virtual device, and when requested + // permissions include both device-aware permissions and non-device aware permissions, + // device-aware permissions will use virtual device id and non-device aware permissions + // will use default device id for granting. If flag is not enabled, we would show a + // warning dialog for this use case. if (getDeviceId() != ContextCompat.DEVICE_ID_DEFAULT) { boolean showWarningDialog = mTargetDeviceId != getDeviceId(); for (String permission : mRequestedPermissions) { - if (!MultiDeviceUtils.isPermissionDeviceAware( - getApplicationContext(), mTargetDeviceId, permission)) { + if (!MultiDeviceUtils.isPermissionDeviceAware(getApplicationContext(), + mTargetDeviceId, permission)) { showWarningDialog = true; + break; } } - if (showWarningDialog) { + if (showWarningDialog && !Flags.allowHostPermissionDialogsOnVirtualDevices()) { mShowWarningDialog.launch( new Intent(this, PermissionDialogStreamingBlockedActivity.class)); return; @@ -1115,9 +1117,17 @@ public class GrantPermissionsActivity extends SettingsActivity if ((mDelegated || (mViewModel != null && mViewModel.shouldReturnPermissionState())) && mTargetPackage != null) { + PackageManager defaultDevicePackageManager = SdkLevel.isAtLeastV() + && mTargetDeviceId != ContextCompat.DEVICE_ID_DEFAULT + ? createDeviceContext(ContextCompat.DEVICE_ID_DEFAULT).getPackageManager() + : mPackageManager; + PackageManager targetDevicePackageManager = mPackageManager; for (int i = 0; i < resultPermissions.length; i++) { - grantResults[i] = - mPackageManager.checkPermission(resultPermissions[i], mTargetPackage); + String permission = resultPermissions[i]; + PackageManager pm = MultiDeviceUtils.isPermissionDeviceAware( + getApplicationContext(), mTargetDeviceId, permission) + ? targetDevicePackageManager : defaultDevicePackageManager; + grantResults[i] = pm.checkPermission(resultPermissions[i], mTargetPackage); } } else { grantResults = new int[0]; diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/auto/dashboard/AutoPermissionHistoryPreference.kt b/PermissionController/src/com/android/permissioncontroller/permission/ui/auto/dashboard/AutoPermissionHistoryPreference.kt index 2d14260a2..36597a3a3 100644 --- a/PermissionController/src/com/android/permissioncontroller/permission/ui/auto/dashboard/AutoPermissionHistoryPreference.kt +++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/auto/dashboard/AutoPermissionHistoryPreference.kt @@ -23,30 +23,30 @@ import androidx.annotation.RequiresApi import androidx.preference.Preference.OnPreferenceClickListener import com.android.car.ui.preference.CarUiPreference import com.android.permissioncontroller.R -import com.android.permissioncontroller.permission.ui.legacy.PermissionUsageDetailsViewModelLegacy import com.android.permissioncontroller.permission.ui.model.v31.PermissionUsageDetailsViewModel +import com.android.permissioncontroller.permission.ui.model.v31.PermissionUsageDetailsViewModel.AppPermissionAccessUiInfo /** Preference that displays a permission usage for an app. */ @RequiresApi(Build.VERSION_CODES.S) class AutoPermissionHistoryPreference( context: Context, - historyPreferenceData: PermissionUsageDetailsViewModelLegacy.HistoryPreferenceData + historyPreferenceData: AppPermissionAccessUiInfo, ) : CarUiPreference(context) { init { - title = historyPreferenceData.preferenceTitle + title = historyPreferenceData.packageLabel summary = if (historyPreferenceData.summaryText != null) { context.getString( R.string.auto_permission_usage_timeline_summary, DateFormat.getTimeFormat(context).format(historyPreferenceData.accessEndTime), - historyPreferenceData.summaryText + historyPreferenceData.summaryText, ) } else { DateFormat.getTimeFormat(context).format(historyPreferenceData.accessEndTime) } - if (historyPreferenceData.appIcon != null) { - icon = historyPreferenceData.appIcon + if (historyPreferenceData.badgedPackageIcon != null) { + icon = historyPreferenceData.badgedPackageIcon } onPreferenceClickListener = OnPreferenceClickListener { @@ -56,12 +56,12 @@ class AutoPermissionHistoryPreference( PermissionUsageDetailsViewModel.createHistoryPreferenceClickIntent( context = context, userHandle = historyPreferenceData.userHandle, - packageName = historyPreferenceData.pkgName, + packageName = historyPreferenceData.packageName, permissionGroup = historyPreferenceData.permissionGroup, accessEndTime = historyPreferenceData.accessEndTime, accessStartTime = historyPreferenceData.accessStartTime, showingAttribution = historyPreferenceData.showingAttribution, - attributionTags = historyPreferenceData.attributionTags.toSet() + attributionTags = historyPreferenceData.attributionTags.toSet(), ) ) true diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/auto/dashboard/AutoPermissionUsageDetailsFragment.kt b/PermissionController/src/com/android/permissioncontroller/permission/ui/auto/dashboard/AutoPermissionUsageDetailsFragment.kt index 481543eb6..8edd39913 100644 --- a/PermissionController/src/com/android/permissioncontroller/permission/ui/auto/dashboard/AutoPermissionUsageDetailsFragment.kt +++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/auto/dashboard/AutoPermissionUsageDetailsFragment.kt @@ -13,11 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -@file:Suppress("DEPRECATION") - package com.android.permissioncontroller.permission.ui.auto.dashboard -import android.app.role.RoleManager import android.content.Intent import android.os.Build import android.os.Bundle @@ -36,16 +33,12 @@ import com.android.permissioncontroller.PermissionControllerStatsLog.PERMISSION_ import com.android.permissioncontroller.PermissionControllerStatsLog.PERMISSION_USAGE_FRAGMENT_INTERACTION__ACTION__SHOW_SYSTEM_CLICKED import com.android.permissioncontroller.R import com.android.permissioncontroller.auto.AutoSettingsFrameFragment -import com.android.permissioncontroller.permission.model.legacy.PermissionApps.AppDataLoader -import com.android.permissioncontroller.permission.model.v31.AppPermissionUsage -import com.android.permissioncontroller.permission.model.v31.PermissionUsages -import com.android.permissioncontroller.permission.model.v31.PermissionUsages.PermissionsUsagesChangeCallback import com.android.permissioncontroller.permission.ui.ManagePermissionsActivity import com.android.permissioncontroller.permission.ui.auto.AutoDividerPreference -import com.android.permissioncontroller.permission.ui.legacy.PermissionUsageDetailsViewModelFactoryLegacy -import com.android.permissioncontroller.permission.ui.legacy.PermissionUsageDetailsViewModelLegacy +import com.android.permissioncontroller.permission.ui.model.v31.BasePermissionUsageDetailsViewModel +import com.android.permissioncontroller.permission.ui.model.v31.PermissionUsageDetailsViewModel +import com.android.permissioncontroller.permission.ui.model.v31.PermissionUsageDetailsViewModel.AppPermissionAccessUiInfo import com.android.permissioncontroller.permission.utils.KotlinUtils.getPermGroupLabel -import com.android.permissioncontroller.permission.utils.Utils import java.time.Clock import java.time.Instant import java.time.ZoneId @@ -54,9 +47,7 @@ import java.time.temporal.ChronoUnit import java.util.concurrent.atomic.AtomicReference @RequiresApi(Build.VERSION_CODES.S) -class AutoPermissionUsageDetailsFragment : - AutoSettingsFrameFragment(), PermissionsUsagesChangeCallback { - +class AutoPermissionUsageDetailsFragment : AutoSettingsFrameFragment() { companion object { private const val LOG_TAG = "AutoPermissionUsageDetailsFragment" private const val KEY_SESSION_ID = "_session_id" @@ -70,14 +61,11 @@ class AutoPermissionUsageDetailsFragment : .truncatedTo(ChronoUnit.DAYS) .toEpochSecond() * 1000L - // Only show the last 24 hours on Auto right now - private const val SHOW_7_DAYS = false - /** Creates a new instance of [AutoPermissionUsageDetailsFragment]. */ fun newInstance( groupName: String?, showSystem: Boolean, - sessionId: Long + sessionId: Long, ): AutoPermissionUsageDetailsFragment { return AutoPermissionUsageDetailsFragment().apply { arguments = @@ -92,14 +80,10 @@ class AutoPermissionUsageDetailsFragment : private val SESSION_ID_KEY = (AutoPermissionUsageFragment::class.java.name + KEY_SESSION_ID) - private lateinit var permissionUsages: PermissionUsages - private lateinit var usageViewModel: PermissionUsageDetailsViewModelLegacy + private lateinit var usageViewModel: BasePermissionUsageDetailsViewModel private lateinit var filterGroup: String - private lateinit var roleManager: RoleManager - private var appPermissionUsages: List<AppPermissionUsage> = listOf() private var showSystem = false - private var finishedInitialLoad = false private var hasSystemApps = false /** Unique Id of a request */ @@ -116,7 +100,7 @@ class AutoPermissionUsageDetailsFragment : !requireArguments().containsKey(Intent.EXTRA_PERMISSION_GROUP_NAME) or (requireArguments().getString(Intent.EXTRA_PERMISSION_GROUP_NAME) == null) ) { - DumpableLog.e(LOG_TAG, "Missing argument ${Intent.EXTRA_USER}") + DumpableLog.e(LOG_TAG, "Missing argument ${Intent.EXTRA_PERMISSION_GROUP_NAME}") activity?.finish() return } @@ -130,28 +114,21 @@ class AutoPermissionUsageDetailsFragment : headerLabel = resources.getString( R.string.permission_group_usage_title, - getPermGroupLabel(requireContext(), filterGroup) + getPermGroupLabel(requireContext(), filterGroup), ) - - val context = preferenceManager.getContext() - permissionUsages = PermissionUsages(context) - roleManager = Utils.getSystemServiceSafe(context, RoleManager::class.java) - val usageViewModelFactory = - PermissionUsageDetailsViewModelFactoryLegacy( + val factory = + PermissionUsageDetailsViewModel.PermissionUsageDetailsViewModelFactory( PermissionControllerApplication.get(), - roleManager, + this, filterGroup, - sessionId ) usageViewModel = - ViewModelProvider(this, usageViewModelFactory)[ - PermissionUsageDetailsViewModelLegacy::class.java] - - reloadData() + ViewModelProvider(this, factory)[BasePermissionUsageDetailsViewModel::class.java] + usageViewModel.getPermissionUsagesDetailsInfoUiLiveData().observe(this, this::updateUI) } override fun onCreatePreferences(bundlle: Bundle?, s: String?) { - preferenceScreen = preferenceManager.createPreferenceScreen(context!!) + preferenceScreen = preferenceManager.createPreferenceScreen(requireContext()) } private fun setupHeaderPreferences() { @@ -161,38 +138,16 @@ class AutoPermissionUsageDetailsFragment : preferenceScreen.addPreference(AutoDividerPreference(context)) } - /** Reloads the data to show. */ - private fun reloadData() { - usageViewModel.loadPermissionUsages( - requireActivity().getLoaderManager(), - permissionUsages, - this, - FILTER_24_HOURS - ) - if (finishedInitialLoad) { - setLoading(true) - } - } - - override fun onPermissionUsagesChanged() { - if (permissionUsages.usages.isEmpty()) { - return - } - appPermissionUsages = ArrayList(permissionUsages.usages) - updateUI() - } - private fun updateSystemToggle() { if (!showSystem) { PermissionControllerStatsLog.write( PERMISSION_USAGE_FRAGMENT_INTERACTION, sessionId, - PERMISSION_USAGE_FRAGMENT_INTERACTION__ACTION__SHOW_SYSTEM_CLICKED + PERMISSION_USAGE_FRAGMENT_INTERACTION__ACTION__SHOW_SYSTEM_CLICKED, ) } showSystem = !showSystem updateAction() - updateUI() } private fun updateAction() { @@ -206,47 +161,36 @@ class AutoPermissionUsageDetailsFragment : } else { getString(R.string.menu_show_system) } - setAction(label) { updateSystemToggle() } + setAction(label) { + usageViewModel.updateShowSystemAppsToggle(!showSystem) + updateSystemToggle() + } } - private fun updateUI() { - if (appPermissionUsages.isEmpty()) { + private fun updateUI(uiInfo: PermissionUsageDetailsViewModel.PermissionUsageDetailsUiState) { + if ( + activity == null || + uiInfo is PermissionUsageDetailsViewModel.PermissionUsageDetailsUiState.Loading + ) { return } preferenceScreen.removeAll() setupHeaderPreferences() - - val uiData = - usageViewModel.buildPermissionUsageDetailsUiData( - appPermissionUsages, - showSystem, - SHOW_7_DAYS - ) - - if (hasSystemApps != uiData.shouldDisplayShowSystemToggle) { - hasSystemApps = uiData.shouldDisplayShowSystemToggle + val uiData = uiInfo as PermissionUsageDetailsViewModel.PermissionUsageDetailsUiState.Success + if (hasSystemApps != uiData.containsSystemAppUsage) { + hasSystemApps = uiData.containsSystemAppUsage updateAction() } - val category = AtomicReference(PreferenceCategory(requireContext())) preferenceScreen.addPreference(category.get()) - AppDataLoader(context) { - renderHistoryPreferences( - uiData.getHistoryPreferenceDataList(), - category, - preferenceScreen - ) + renderHistoryPreferences(uiData.appPermissionAccessUiInfoList, category, preferenceScreen) - setLoading(false) - finishedInitialLoad = true - permissionUsages.stopLoader(requireActivity().getLoaderManager()) - } - .execute(*uiData.permissionApps.toTypedArray()) + setLoading(false) } fun createPermissionHistoryPreference( - historyPreferenceData: PermissionUsageDetailsViewModelLegacy.HistoryPreferenceData + historyPreferenceData: AppPermissionAccessUiInfo ): Preference { return AutoPermissionHistoryPreference(requireContext(), historyPreferenceData) } @@ -257,7 +201,7 @@ class AutoPermissionUsageDetailsFragment : summary = getString( R.string.permission_group_usage_subtitle_24h, - getPermGroupLabel(requireContext(), filterGroup) + getPermGroupLabel(requireContext(), filterGroup), ) isSelectable = false } @@ -271,7 +215,7 @@ class AutoPermissionUsageDetailsFragment : summary = getString( R.string.manage_permission_summary, - getPermGroupLabel(requireContext(), filterGroup) + getPermGroupLabel(requireContext(), filterGroup), ) onPreferenceClickListener = Preference.OnPreferenceClickListener { @@ -287,9 +231,8 @@ class AutoPermissionUsageDetailsFragment : } /** Render the provided [historyPreferenceDataList] into the [preferenceScreen] UI. */ - fun renderHistoryPreferences( - historyPreferenceDataList: - List<PermissionUsageDetailsViewModelLegacy.HistoryPreferenceData>, + private fun renderHistoryPreferences( + historyPreferenceDataList: List<AppPermissionAccessUiInfo>, category: AtomicReference<PreferenceCategory>, preferenceScreen: PreferenceScreen, ) { @@ -299,7 +242,7 @@ class AutoPermissionUsageDetailsFragment : val currentDateMs = ZonedDateTime.ofInstant( Instant.ofEpochMilli(usageTimestamp), - Clock.system(ZoneId.systemDefault()).zone + Clock.system(ZoneId.systemDefault()).zone, ) .truncatedTo(ChronoUnit.DAYS) .toEpochSecond() * 1000L diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/auto/dashboard/AutoPermissionUsageFragment.kt b/PermissionController/src/com/android/permissioncontroller/permission/ui/auto/dashboard/AutoPermissionUsageFragment.kt index f2e453447..f52eaadcd 100644 --- a/PermissionController/src/com/android/permissioncontroller/permission/ui/auto/dashboard/AutoPermissionUsageFragment.kt +++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/auto/dashboard/AutoPermissionUsageFragment.kt @@ -46,7 +46,7 @@ class AutoPermissionUsageFragment : AutoSettingsFrameFragment() { Manifest.permission_group.CAMERA, 1, Manifest.permission_group.MICROPHONE, - 2 + 2, ) private const val DEFAULT_ORDER: Int = 3 } @@ -54,7 +54,6 @@ class AutoPermissionUsageFragment : AutoSettingsFrameFragment() { private val SESSION_ID_KEY = (AutoPermissionUsageFragment::class.java.name + KEY_SESSION_ID) private var showSystem = false - private var finishedInitialLoad = false private var hasSystemApps = false /** Unique Id of a request */ @@ -89,7 +88,7 @@ class AutoPermissionUsageFragment : AutoSettingsFrameFragment() { PermissionControllerStatsLog.write( PERMISSION_USAGE_FRAGMENT_INTERACTION, sessionId, - PERMISSION_USAGE_FRAGMENT_INTERACTION__ACTION__SHOW_SYSTEM_CLICKED + PERMISSION_USAGE_FRAGMENT_INTERACTION__ACTION__SHOW_SYSTEM_CLICKED, ) } showSystem = !showSystem @@ -133,13 +132,13 @@ class AutoPermissionUsageFragment : AutoSettingsFrameFragment() { Comparator.comparing { permissionGroupWithUsageCount: Map.Entry<String, Int> -> PERMISSION_GROUP_ORDER.getOrDefault( permissionGroupWithUsageCount.key, - DEFAULT_ORDER + DEFAULT_ORDER, ) } .thenComparing { permissionGroupWithUsageCount: Map.Entry<String, Int> -> mViewModel.getPermissionGroupLabel( requireContext(), - permissionGroupWithUsageCount.key + permissionGroupWithUsageCount.key, ) } ) @@ -153,11 +152,10 @@ class AutoPermissionUsageFragment : AutoSettingsFrameFragment() { permissionGroupWithUsageCountsEntries[i].value, showSystem, sessionId, - false + false, ) getPreferenceScreen().addPreference(permissionUsagePreference) } - finishedInitialLoad = true setLoading(false) } } diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/AppPermissionWrapperFragment.java b/PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/AppPermissionWrapperFragment.java index 8650d99fc..080c7cfdc 100644 --- a/PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/AppPermissionWrapperFragment.java +++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/AppPermissionWrapperFragment.java @@ -29,7 +29,7 @@ public class AppPermissionWrapperFragment extends PermissionsCollapsingToolbarBa @NonNull @Override public PreferenceFragmentCompat createPreferenceFragment() { - return AppPermissionFragmentCompat.createFragment(); + return AppPermissionFragmentCompat.createFragment(getContext()); } @Override diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/ManageCustomPermissionsFragment.java b/PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/ManageCustomPermissionsFragment.java index 35236b8de..dd460aa2f 100644 --- a/PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/ManageCustomPermissionsFragment.java +++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/ManageCustomPermissionsFragment.java @@ -24,6 +24,8 @@ import android.view.MenuItem; import androidx.lifecycle.ViewModelProvider; +import com.android.permission.flags.Flags; +import com.android.permissioncontroller.permission.data.PermGroupsPackagesUiInfoLiveData; import com.android.permissioncontroller.permission.ui.model.ManageCustomPermissionsViewModel; import com.android.permissioncontroller.permission.ui.model.ManageCustomPermissionsViewModelFactory; @@ -48,6 +50,14 @@ public class ManageCustomPermissionsFragment extends ManagePermissionsFragment { return arguments; } + private PermGroupsPackagesUiInfoLiveData getPermGroupsLiveData() { + if (Flags.declutteredPermissionManagerEnabled()) { + return mViewModel.getAdditionaPermGroupsUiInfo(); + } else { + return mViewModel.getUiDataLiveData(); + } + } + @Override public void onCreate(Bundle icicle) { super.onCreate(icicle); @@ -56,9 +66,9 @@ public class ManageCustomPermissionsFragment extends ManagePermissionsFragment { new ManageCustomPermissionsViewModelFactory(getActivity().getApplication()); mViewModel = new ViewModelProvider(this, factory) .get(ManageCustomPermissionsViewModel.class); - mPermissionGroups = mViewModel.getUiDataLiveData().getValue(); + mPermissionGroups = getPermGroupsLiveData().getValue(); - mViewModel.getUiDataLiveData().observe(this, permissionGroups -> { + getPermGroupsLiveData().observe(this, permissionGroups -> { if (permissionGroups == null) { mPermissionGroups = new HashMap<>(); } else { diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/ManageStandardPermissionsFragment.java b/PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/ManageStandardPermissionsFragment.java index bf99b7134..51c0906a2 100644 --- a/PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/ManageStandardPermissionsFragment.java +++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/ManageStandardPermissionsFragment.java @@ -31,7 +31,9 @@ import androidx.preference.Preference; import androidx.preference.PreferenceScreen; import com.android.modules.utils.build.SdkLevel; +import com.android.permission.flags.Flags; import com.android.permissioncontroller.R; +import com.android.permissioncontroller.permission.data.PermGroupsPackagesUiInfoLiveData; import com.android.permissioncontroller.permission.ui.UnusedAppsFragment; import com.android.permissioncontroller.permission.ui.model.ManageStandardPermissionsViewModel; import com.android.permissioncontroller.permission.utils.StringUtils; @@ -58,6 +60,14 @@ public final class ManageStandardPermissionsFragment extends ManagePermissionsFr return arguments; } + private PermGroupsPackagesUiInfoLiveData getPermGroupsLiveData() { + if (Flags.declutteredPermissionManagerEnabled()) { + return mViewModel.getUsedStandardPermGroupsUiInfo(); + } else { + return mViewModel.getUiDataLiveData(); + } + } + @Override public void onCreate(Bundle icicle) { super.onCreate(icicle); @@ -65,12 +75,12 @@ public final class ManageStandardPermissionsFragment extends ManagePermissionsFr final Application application = getActivity().getApplication(); mViewModel = new ViewModelProvider(this, AndroidViewModelFactory.getInstance(application)) .get(ManageStandardPermissionsViewModel.class); - mPermissionGroups = mViewModel.getUiDataLiveData().getValue(); + mPermissionGroups = getPermGroupsLiveData().getValue(); - mViewModel.getUiDataLiveData().observe(this, permissionGroups -> { + getPermGroupsLiveData().observe(this, permissionGroups -> { // Once we have loaded data for the first time, further loads should be staggered, // for performance reasons. - mViewModel.getUiDataLiveData().setLoadStaggered(true); + getPermGroupsLiveData().setLoadStaggered(true); if (permissionGroups != null) { mPermissionGroups = permissionGroups; updatePermissionsUi(); @@ -80,13 +90,18 @@ public final class ManageStandardPermissionsFragment extends ManagePermissionsFr } // If we've loaded all LiveDatas, no need to prioritize loading any particular one - if (!mViewModel.getUiDataLiveData().isStale()) { - mViewModel.getUiDataLiveData().setFirstLoadGroup(null); + if (!getPermGroupsLiveData().isStale()) { + getPermGroupsLiveData().setFirstLoadGroup(null); } }); mViewModel.getNumCustomPermGroups().observe(this, permNames -> updatePermissionsUi()); mViewModel.getNumAutoRevoked().observe(this, show -> updatePermissionsUi()); + if (Flags.declutteredPermissionManagerEnabled()) { + mViewModel.getNumUnusedStandardPermGroups().observe( + this, show -> updatePermissionsUi() + ); + } } @Override @@ -118,6 +133,14 @@ public final class ManageStandardPermissionsFragment extends ManagePermissionsFr if (mViewModel.getNumCustomPermGroups().getValue() != null) { numExtraPermissions = mViewModel.getNumCustomPermGroups().getValue(); } + if (Flags.declutteredPermissionManagerEnabled()) { + if (mViewModel.getNumUnusedStandardPermGroups().getValue() != null) { + // When decluttered permission manager is enabled, unused + // permission groups will also be displayed in the additional + // permissions screen. + numExtraPermissions += mViewModel.getNumUnusedStandardPermGroups().getValue(); + } + } Preference additionalPermissionsPreference = screen.findPreference(EXTRA_PREFS_KEY); if (numExtraPermissions == 0) { @@ -198,7 +221,7 @@ public final class ManageStandardPermissionsFragment extends ManagePermissionsFr public void showPermissionApps(String permissionGroupName) { // If we return to this page within a reasonable time, prioritize loading data from the // permission group whose page we are going to, as that is group most likely to have changed - mViewModel.getUiDataLiveData().setFirstLoadGroup(permissionGroupName); + getPermGroupsLiveData().setFirstLoadGroup(permissionGroupName); mViewModel.showPermissionApps(this, PermissionAppsFragment.createArgs( permissionGroupName, getArguments().getLong(EXTRA_SESSION_ID))); } diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/PermissionFooterPreference.kt b/PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/PermissionFooterPreference.kt index 1cd4ed23a..e7749d827 100644 --- a/PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/PermissionFooterPreference.kt +++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/PermissionFooterPreference.kt @@ -17,16 +17,21 @@ package com.android.permissioncontroller.permission.ui.handheld import android.content.Context +import android.util.AttributeSet import android.view.View import com.android.modules.utils.build.SdkLevel import com.android.permissioncontroller.R import com.android.settingslib.widget.FooterPreference -class PermissionFooterPreference(c: Context) : FooterPreference(c) { +class PermissionFooterPreference : FooterPreference { + constructor(context: Context) : super(context) + + constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) + init { if (SdkLevel.isAtLeastV()) { layoutResource = R.layout.permission_footer_preference - if (c.resources.getBoolean(R.bool.config_permissionFooterPreferenceIconVisible)) { + if (context.resources.getBoolean(R.bool.config_permissionFooterPreferenceIconVisible)) { setIconVisibility(View.VISIBLE) } else { setIconVisibility(View.GONE) diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/PermissionPreference.kt b/PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/PermissionPreference.kt index 5e30183ec..010ca28a7 100644 --- a/PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/PermissionPreference.kt +++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/PermissionPreference.kt @@ -18,15 +18,30 @@ package com.android.permissioncontroller.permission.ui.handheld import android.content.Context import android.util.AttributeSet +import androidx.annotation.AttrRes +import androidx.annotation.StyleRes import androidx.preference.Preference import com.android.modules.utils.build.SdkLevel import com.android.permissioncontroller.DeviceUtils import com.android.permissioncontroller.R open class PermissionPreference : Preference { - constructor(c: Context) : super(c) + constructor(context: Context) : super(context) - constructor(c: Context, a: AttributeSet) : super(c, a) + constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) + + constructor( + context: Context, + attrs: AttributeSet?, + @AttrRes defStyleAttr: Int, + ) : super(context, attrs, defStyleAttr) + + constructor( + context: Context, + attrs: AttributeSet?, + @AttrRes defStyleAttr: Int, + @StyleRes defStyleRes: Int, + ) : super(context, attrs, defStyleAttr, defStyleRes) init { if (SdkLevel.isAtLeastV() && DeviceUtils.isHandheld(context)) { diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/PermissionPreferenceCategory.kt b/PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/PermissionPreferenceCategory.kt index ef1752530..ef95c6c5c 100644 --- a/PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/PermissionPreferenceCategory.kt +++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/PermissionPreferenceCategory.kt @@ -18,15 +18,30 @@ package com.android.permissioncontroller.permission.ui.handheld import android.content.Context import android.util.AttributeSet +import androidx.annotation.AttrRes +import androidx.annotation.StyleRes import androidx.preference.PreferenceCategory import com.android.modules.utils.build.SdkLevel import com.android.permissioncontroller.DeviceUtils import com.android.permissioncontroller.R open class PermissionPreferenceCategory : PreferenceCategory { - constructor(c: Context) : super(c) + constructor(context: Context) : super(context) - constructor(c: Context, a: AttributeSet) : super(c, a) + constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) + + constructor( + context: Context, + attrs: AttributeSet?, + @AttrRes defStyleAttr: Int, + ) : super(context, attrs, defStyleAttr) + + constructor( + context: Context, + attrs: AttributeSet?, + @AttrRes defStyleAttr: Int, + @StyleRes defStyleRes: Int, + ) : super(context, attrs, defStyleAttr, defStyleRes) init { if (SdkLevel.isAtLeastV() && DeviceUtils.isHandheld(context)) { diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/v35/SectionPreferenceGroupAdapter.kt b/PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/v35/SectionPreferenceGroupAdapter.kt index 72e066777..e5dce40b0 100644 --- a/PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/v35/SectionPreferenceGroupAdapter.kt +++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/v35/SectionPreferenceGroupAdapter.kt @@ -25,6 +25,7 @@ import androidx.preference.PreferenceGroup import androidx.preference.PreferenceGroupAdapter import androidx.preference.PreferenceViewHolder import com.android.permissioncontroller.R +import com.android.permissioncontroller.permission.ui.handheld.v36.AppPermissionFooterLinkPreference import com.android.settingslib.widget.FooterPreference /** @@ -106,7 +107,10 @@ class SectionPreferenceGroupAdapter(preferenceGroup: PreferenceGroup) : } private val Preference.isSectionDivider: Boolean - get() = this is PreferenceCategory || this is FooterPreference + get() = + this is PreferenceCategory || + this is FooterPreference || + this is AppPermissionFooterLinkPreference override fun onBindViewHolder(holder: PreferenceViewHolder, position: Int) { super.onBindViewHolder(holder, position) diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/v36/AppPermissionFooterLinkPreference.kt b/PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/v36/AppPermissionFooterLinkPreference.kt new file mode 100644 index 000000000..01554880a --- /dev/null +++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/v36/AppPermissionFooterLinkPreference.kt @@ -0,0 +1,64 @@ +/* + * 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.handheld.v36 + +import android.content.Context +import android.os.Build +import android.util.AttributeSet +import android.widget.TextView +import androidx.annotation.AttrRes +import androidx.annotation.RequiresApi +import androidx.annotation.StyleRes +import androidx.preference.PreferenceViewHolder +import com.android.permissioncontroller.R +import com.android.permissioncontroller.permission.ui.handheld.PermissionPreference + +@RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM) +class AppPermissionFooterLinkPreference : PermissionPreference { + constructor(context: Context) : super(context) + + constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) + + constructor( + context: Context, + attrs: AttributeSet?, + @AttrRes defStyleAttr: Int, + ) : super(context, attrs, defStyleAttr) + + constructor( + context: Context, + attrs: AttributeSet?, + @AttrRes defStyleAttr: Int, + @StyleRes defStyleRes: Int, + ) : super(context, attrs, defStyleAttr, defStyleRes) + + init { + layoutResource = R.layout.app_permission_footer_link_preference + } + + override fun onBindViewHolder(holder: PreferenceViewHolder) { + super.onBindViewHolder(holder) + if ( + context.resources.getBoolean( + R.bool.config_appPermissionFooterLinkPreferenceSummaryUnderlined + ) + ) { + val summary = holder.findViewById(android.R.id.summary) as TextView + summary.paint.isUnderlineText = true + } + } +} diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/v36/AppPermissionFragment.java b/PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/v36/AppPermissionFragment.java index 481dc0dac..4fde26c9d 100644 --- a/PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/v36/AppPermissionFragment.java +++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/v36/AppPermissionFragment.java @@ -79,11 +79,10 @@ import com.android.permissioncontroller.permission.ui.GrantPermissionsViewHandle import com.android.permissioncontroller.permission.ui.handheld.AllAppPermissionsFragment; import com.android.permissioncontroller.permission.ui.handheld.AppPermissionGroupsFragment; import com.android.permissioncontroller.permission.ui.handheld.PermissionAppsFragment; +import com.android.permissioncontroller.permission.ui.handheld.PermissionFooterPreference; import com.android.permissioncontroller.permission.ui.handheld.PermissionPreference; import com.android.permissioncontroller.permission.ui.handheld.PermissionPreferenceCategory; -import com.android.permissioncontroller.permission.ui.handheld.PermissionSelectorWithWidgetPreference; import com.android.permissioncontroller.permission.ui.handheld.PermissionSwitchPreference; -import com.android.permissioncontroller.permission.ui.handheld.PermissionTwoTargetPreference; import com.android.permissioncontroller.permission.ui.handheld.SettingsWithLargeHeader; import com.android.permissioncontroller.permission.ui.model.AppPermissionViewModel; import com.android.permissioncontroller.permission.ui.model.AppPermissionViewModel.ButtonState; @@ -133,10 +132,10 @@ public class AppPermissionFragment extends SettingsWithLargeHeader private @NonNull SelectorWithWidgetPreference mDenyForegroundButton; private @NonNull PermissionSwitchPreference mLocationAccuracySwitch; private @NonNull PermissionTwoTargetPreference mDetails; - private @NonNull PermissionPreference mFooterLink1; - private @NonNull PermissionPreference mFooterLink2; - private @NonNull PermissionPreference mFooterStorageSpecialAppAccess; - private @NonNull PermissionPreference mAdditionalInfo; + private @NonNull AppPermissionFooterLinkPreference mFooterLink1; + private @NonNull AppPermissionFooterLinkPreference mFooterLink2; + private @NonNull PermissionFooterPreference mFooterStorageSpecialAppAccess; + private @NonNull PermissionFooterPreference mAdditionalInfo; private @NonNull String mPackageName; private @NonNull String mPermGroupName; @@ -268,7 +267,7 @@ public class AppPermissionFragment extends SettingsWithLargeHeader if (exemptedPackages.contains(mPackageName)) { int additional_info_label = Utils.isStatusBarIndicatorPermission(mPermGroupName) ? R.string.exempt_mic_camera_info_label : R.string.exempt_info_label; - mAdditionalInfo.setSummary(context.getString(additional_info_label, mPackageLabel)); + mAdditionalInfo.setTitle(context.getString(additional_info_label, mPackageLabel)); mAdditionalInfo.setVisible(true); } else { mAdditionalInfo.setVisible(false); diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/PermissionSelectorWithWidgetPreference.kt b/PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/v36/PermissionSelectorWithWidgetPreference.kt index 9d095c7dc..1574eaba3 100644 --- a/PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/PermissionSelectorWithWidgetPreference.kt +++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/v36/PermissionSelectorWithWidgetPreference.kt @@ -14,14 +14,16 @@ * limitations under the License. */ -package com.android.permissioncontroller.permission.ui.handheld +package com.android.permissioncontroller.permission.ui.handheld.v36 import android.content.Context +import android.os.Build import android.util.AttributeSet import android.widget.ImageView import androidx.annotation.AttrRes import androidx.annotation.DrawableRes import androidx.annotation.IdRes +import androidx.annotation.RequiresApi import androidx.preference.PreferenceViewHolder import com.android.permissioncontroller.R import com.android.permissioncontroller.permission.utils.ResourceUtils @@ -34,6 +36,7 @@ import com.android.settingslib.widget.SelectorWithWidgetPreference * - Propagates the supplied `app:checkboxId` id to the checkbox (or radio button, on the left) * - Allows defining a "disabled click listener" handler that handles clicks when disabled */ +@RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM) class PermissionSelectorWithWidgetPreference : SelectorWithWidgetPreference { constructor(context: Context) : super(context) { init(context, null) @@ -46,7 +49,7 @@ class PermissionSelectorWithWidgetPreference : SelectorWithWidgetPreference { constructor( context: Context, attrs: AttributeSet?, - @AttrRes defStyleAttr: Int + @AttrRes defStyleAttr: Int, ) : super(context, attrs, defStyleAttr) { init(context, attrs) } @@ -56,6 +59,8 @@ class PermissionSelectorWithWidgetPreference : SelectorWithWidgetPreference { } private fun init(context: Context, attrs: AttributeSet?) { + layoutResource = R.layout.permission_preference_selector_with_widget + widgetLayoutResource = R.layout.permission_preference_widget_radiobutton extraWidgetIconRes = ResourceUtils.getResourceIdByAttr(context, attrs, R.attr.extraWidgetIcon) extraWidgetIdRes = ResourceUtils.getResourceIdByAttr(context, attrs, R.attr.extraWidgetId) @@ -69,9 +74,10 @@ class PermissionSelectorWithWidgetPreference : SelectorWithWidgetPreference { override fun onBindViewHolder(holder: PreferenceViewHolder) { super.onBindViewHolder(holder) - val extraWidget = holder.findViewById( - com.android.settingslib.widget.preference.selector.R.id.selector_extra_widget - ) as? ImageView + val extraWidget = + holder.findViewById( + com.android.settingslib.widget.preference.selector.R.id.selector_extra_widget + ) as? ImageView val checkbox = holder.findViewById(android.R.id.checkbox) if (extraWidgetIconRes != 0) { extraWidget?.setImageResource(extraWidgetIconRes) diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/PermissionTwoTargetPreference.kt b/PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/v36/PermissionTwoTargetPreference.kt index 13c9ee7c4..cf6585f4d 100644 --- a/PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/PermissionTwoTargetPreference.kt +++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/v36/PermissionTwoTargetPreference.kt @@ -14,13 +14,15 @@ * limitations under the License. */ -package com.android.permissioncontroller.permission.ui.handheld +package com.android.permissioncontroller.permission.ui.handheld.v36 import android.content.Context +import android.os.Build import android.util.AttributeSet import android.widget.ImageView import androidx.annotation.AttrRes import androidx.annotation.DrawableRes +import androidx.annotation.RequiresApi import androidx.annotation.StyleRes import androidx.preference.PreferenceViewHolder import com.android.permissioncontroller.R @@ -32,6 +34,7 @@ import com.android.settingslib.widget.TwoTargetPreference * - Propagates the supplied `app:extraWidgetIcon` drawable to the second target * - Allows defining a click listener on the second target (the icon on the right) */ +@RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM) class PermissionTwoTargetPreference : TwoTargetPreference { constructor(context: Context) : super(context) { init(context, null) @@ -44,7 +47,7 @@ class PermissionTwoTargetPreference : TwoTargetPreference { constructor( context: Context, attrs: AttributeSet?, - @AttrRes defStyleAttr: Int + @AttrRes defStyleAttr: Int, ) : super(context, attrs, defStyleAttr) { init(context, attrs) } @@ -53,12 +56,13 @@ class PermissionTwoTargetPreference : TwoTargetPreference { context: Context, attrs: AttributeSet?, @AttrRes defStyleAttr: Int, - @StyleRes defStyleRes: Int + @StyleRes defStyleRes: Int, ) : super(context, attrs, defStyleAttr, defStyleRes) { init(context, attrs) } private fun init(context: Context, attrs: AttributeSet?) { + layoutResource = R.layout.permission_preference_two_target extraWidgetIconRes = ResourceUtils.getResourceIdByAttr(context, attrs, R.attr.extraWidgetIcon) } diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/legacy/PermissionUsageDetailsViewModelLegacy.kt b/PermissionController/src/com/android/permissioncontroller/permission/ui/legacy/PermissionUsageDetailsViewModelLegacy.kt deleted file mode 100644 index 1369bfdaa..000000000 --- a/PermissionController/src/com/android/permissioncontroller/permission/ui/legacy/PermissionUsageDetailsViewModelLegacy.kt +++ /dev/null @@ -1,596 +0,0 @@ -/* - * Copyright (C) 2022 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. - */ -@file:Suppress("DEPRECATION") - -package com.android.permissioncontroller.permission.ui.legacy - -import android.Manifest -import android.app.AppOpsManager -import android.app.Application -import android.app.LoaderManager -import android.app.role.RoleManager -import android.content.Context -import android.content.pm.ApplicationInfo -import android.content.res.Resources -import android.graphics.drawable.Drawable -import android.os.Build -import android.os.UserHandle -import androidx.annotation.RequiresApi -import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider -import com.android.permissioncontroller.PermissionControllerApplication -import com.android.permissioncontroller.R -import com.android.permissioncontroller.permission.model.AppPermissionGroup -import com.android.permissioncontroller.permission.model.legacy.PermissionApps.PermissionApp -import com.android.permissioncontroller.permission.model.v31.AppPermissionUsage -import com.android.permissioncontroller.permission.model.v31.AppPermissionUsage.TimelineUsage -import com.android.permissioncontroller.permission.model.v31.PermissionUsages -import com.android.permissioncontroller.permission.ui.handheld.v31.getDurationUsedStr -import com.android.permissioncontroller.permission.ui.handheld.v31.shouldShowSubattributionInPermissionsDashboard -import com.android.permissioncontroller.permission.utils.KotlinUtils.getPackageLabel -import com.android.permissioncontroller.permission.utils.PermissionMapping -import com.android.permissioncontroller.permission.utils.StringUtils -import com.android.permissioncontroller.permission.utils.Utils -import com.android.permissioncontroller.permission.utils.v31.SubattributionUtils -import java.time.Instant -import java.util.concurrent.TimeUnit -import java.util.concurrent.TimeUnit.DAYS -import kotlin.math.max - -/** View model for the permission details fragment. */ -@RequiresApi(Build.VERSION_CODES.S) -class PermissionUsageDetailsViewModelLegacy( - val application: Application, - val roleManager: RoleManager, - private val permissionGroup: String, - val sessionId: Long -) : ViewModel() { - - companion object { - private const val ONE_HOUR_MS = 3_600_000 - private const val ONE_MINUTE_MS = 60_000 - private const val CLUSTER_SPACING_MINUTES: Long = 1L - private val TIME_7_DAYS_DURATION: Long = DAYS.toMillis(7) - private val TIME_24_HOURS_DURATION: Long = DAYS.toMillis(1) - } - - private val mTimeFilterItemMs = mutableListOf<TimeFilterItemMs>() - - init { - initializeTimeFilterItems(application) - } - - /** Loads permission usages using [PermissionUsages]. Response is returned to the [callback]. */ - fun loadPermissionUsages( - loaderManager: LoaderManager, - permissionUsages: PermissionUsages, - callback: PermissionUsages.PermissionsUsagesChangeCallback, - filterTimesIndex: Int - ) { - val timeFilterItemMs: TimeFilterItemMs = mTimeFilterItemMs[filterTimesIndex] - val filterTimeBeginMillis = max(System.currentTimeMillis() - timeFilterItemMs.timeMs, 0) - permissionUsages.load( - /* filterPackageName= */ null, - /* filterPermissionGroups= */ null, - filterTimeBeginMillis, - Long.MAX_VALUE, - PermissionUsages.USAGE_FLAG_LAST or PermissionUsages.USAGE_FLAG_HISTORICAL, - loaderManager, - /* getUiInfo= */ false, - /* getNonPlatformPermissions= */ false, - /* callback= */ callback, - /* sync= */ false - ) - } - - /** - * Create a [PermissionUsageDetailsUiData] based on the provided data. - * - * @param appPermissionUsages data about app permission usages - * @param showSystem whether system apps should be shown - * @param show7Days whether the last 7 days of history should be shown - */ - fun buildPermissionUsageDetailsUiData( - appPermissionUsages: List<AppPermissionUsage>, - showSystem: Boolean, - show7Days: Boolean - ): PermissionUsageDetailsUiData { - val showPermissionUsagesDuration = - if (show7Days) { - TIME_7_DAYS_DURATION - } else { - TIME_24_HOURS_DURATION - } - val startTime = - (System.currentTimeMillis() - showPermissionUsagesDuration).coerceAtLeast( - Instant.EPOCH.toEpochMilli() - ) - val appPermissionTimelineUsages: List<AppPermissionTimelineUsage> = - extractAppPermissionTimelineUsagesForGroup(appPermissionUsages, permissionGroup) - val shouldDisplayShowSystemToggle = - shouldDisplayShowSystemToggle(appPermissionTimelineUsages) - val permissionApps: List<PermissionApp> = - getPermissionAppsWithRecentDiscreteUsage( - appPermissionTimelineUsages, - showSystem, - startTime - ) - val appPermissionUsageEntries = - buildDiscreteAccessClusterData(appPermissionTimelineUsages, showSystem, startTime) - - return PermissionUsageDetailsUiData( - permissionApps, - shouldDisplayShowSystemToggle, - appPermissionUsageEntries - ) - } - - private fun getHistoryPreferenceData( - discreteAccessClusterData: DiscreteAccessClusterData, - ): HistoryPreferenceData { - val context = application - val accessTimeList = - discreteAccessClusterData.discreteAccessDataList.map { p -> p.accessTimeMs } - val durationSummaryLabel = - getDurationSummary(discreteAccessClusterData, accessTimeList, context) - val proxyLabel = getProxyPackageLabel(discreteAccessClusterData) - val subattributionLabel = getSubattributionLabel(discreteAccessClusterData) - val showingSubattribution = subattributionLabel != null && subattributionLabel.isNotEmpty() - val summary = - buildUsageSummary(durationSummaryLabel, proxyLabel, subattributionLabel, context) - - return HistoryPreferenceData( - UserHandle.getUserHandleForUid( - discreteAccessClusterData.appPermissionTimelineUsage.permissionApp.uid - ), - discreteAccessClusterData.appPermissionTimelineUsage.permissionApp.packageName, - discreteAccessClusterData.appPermissionTimelineUsage.permissionApp.icon, - discreteAccessClusterData.appPermissionTimelineUsage.permissionApp.label, - permissionGroup, - discreteAccessClusterData.discreteAccessDataList.last().accessTimeMs, - discreteAccessClusterData.discreteAccessDataList.first().accessTimeMs, - summary, - showingSubattribution, - discreteAccessClusterData.appPermissionTimelineUsage.attributionTags, - sessionId - ) - } - - /** - * Returns whether the provided [AppPermissionUsage] instances contains the provided platform - * permission group. - */ - fun containsPlatformAppPermissionGroup( - appPermissionUsages: List<AppPermissionUsage>, - groupName: String, - ) = appPermissionUsages.extractAllPlatformAppPermissionGroups().any { it.name == groupName } - - /** Extracts a list of [AppPermissionTimelineUsage] for a particular permission group. */ - private fun extractAppPermissionTimelineUsagesForGroup( - appPermissionUsages: List<AppPermissionUsage>, - group: String - ): List<AppPermissionTimelineUsage> { - val exemptedPackages = Utils.getExemptedPackages(roleManager) - return appPermissionUsages - .filter { !exemptedPackages.contains(it.packageName) } - .map { appPermissionUsage -> - getAppPermissionTimelineUsages( - appPermissionUsage.app, - appPermissionUsage.groupUsages.firstOrNull { it.group.name == group } - ) - } - .flatten() - } - - /** Returns whether the show/hide system toggle should be displayed in the UI. */ - private fun shouldDisplayShowSystemToggle( - appPermissionTimelineUsages: List<AppPermissionTimelineUsage>, - ): Boolean = - appPermissionTimelineUsages - .map { it.timelineUsage } - .filter { it.hasDiscreteData() } - .any { it.group.isSystem() } - - /** - * Returns a list of [PermissionApp] instances which had recent discrete permission usage - * (recent here refers to usages occurring after the provided start time). - */ - private fun getPermissionAppsWithRecentDiscreteUsage( - appPermissionTimelineUsageList: List<AppPermissionTimelineUsage>, - showSystem: Boolean, - startTime: Long, - ): List<PermissionApp> = - appPermissionTimelineUsageList - .filter { it.timelineUsage.hasDiscreteData() } - .filter { showSystem || !it.timelineUsage.group.isSystem() } - .filter { it.timelineUsage.allDiscreteAccessTime.any { it.first >= startTime } } - .map { it.permissionApp } - - /** - * Builds a list of [DiscreteAccessClusterData] from the provided list of - * [AppPermissionTimelineUsage]. - */ - private fun buildDiscreteAccessClusterData( - appPermissionTimelineUsageList: List<AppPermissionTimelineUsage>, - showSystem: Boolean, - startTime: Long, - ): List<DiscreteAccessClusterData> = - appPermissionTimelineUsageList - .map { appPermissionTimelineUsages -> - val accessDataList = - extractRecentDiscreteAccessData( - appPermissionTimelineUsages.timelineUsage, - showSystem, - startTime - ) - - if (accessDataList.size <= 1) { - return@map accessDataList.map { - DiscreteAccessClusterData(appPermissionTimelineUsages, listOf(it)) - } - } - - clusterDiscreteAccessData(appPermissionTimelineUsages, accessDataList) - } - .flatten() - .sortedWith( - compareBy( - { -it.discreteAccessDataList.first().accessTimeMs }, - { it.appPermissionTimelineUsage.permissionApp.label } - ) - ) - .toList() - - /** - * Clusters a list of [DiscreteAccessData] into a list of [DiscreteAccessClusterData] instances. - * - * [DiscreteAccessData] which have accesses sufficiently close together in time will be places - * in the same cluster. - */ - private fun clusterDiscreteAccessData( - appPermissionTimelineUsage: AppPermissionTimelineUsage, - discreteAccessDataList: List<DiscreteAccessData> - ): List<DiscreteAccessClusterData> { - val clusterDataList = mutableListOf<DiscreteAccessClusterData>() - val currentDiscreteAccessDataList: MutableList<DiscreteAccessData> = mutableListOf() - for (discreteAccessData in discreteAccessDataList) { - if (currentDiscreteAccessDataList.isEmpty()) { - currentDiscreteAccessDataList.add(discreteAccessData) - } else if ( - !canAccessBeAddedToCluster(discreteAccessData, currentDiscreteAccessDataList) - ) { - clusterDataList.add( - DiscreteAccessClusterData( - appPermissionTimelineUsage, - currentDiscreteAccessDataList.toMutableList() - ) - ) - currentDiscreteAccessDataList.clear() - currentDiscreteAccessDataList.add(discreteAccessData) - } else { - currentDiscreteAccessDataList.add(discreteAccessData) - } - } - if (currentDiscreteAccessDataList.isNotEmpty()) { - clusterDataList.add( - DiscreteAccessClusterData(appPermissionTimelineUsage, currentDiscreteAccessDataList) - ) - } - return clusterDataList - } - - /** - * Extract recent [DiscreteAccessData] from a list of [TimelineUsage] instances, and return them - * ordered descending by access time (recent here refers to accesses occurring after the - * provided start time). - */ - private fun extractRecentDiscreteAccessData( - timelineUsages: TimelineUsage, - showSystem: Boolean, - startTime: Long - ): List<DiscreteAccessData> { - return if ( - timelineUsages.hasDiscreteData() && (showSystem || !timelineUsages.group.isSystem()) - ) { - getRecentDiscreteAccessData(timelineUsages, startTime) - .sortedWith(compareBy { -it.accessTimeMs }) - .toList() - } else { - listOf() - } - } - - /** - * Extract recent [DiscreteAccessData] from a [TimelineUsage]. (recent here refers to accesses - * occurring after the provided start time). - */ - private fun getRecentDiscreteAccessData( - timelineUsage: TimelineUsage, - startTime: Long - ): List<DiscreteAccessData> { - return timelineUsage.allDiscreteAccessTime - .filter { it.first >= startTime } - .map { - DiscreteAccessData( - it.first, - it.second, - it.third, - ) - } - } - - /** - * Returns whether the provided [DiscreteAccessData] occurred close enough to those in the - * clustered list that it can be added to the cluster - */ - private fun canAccessBeAddedToCluster( - accessData: DiscreteAccessData, - clusteredAccessDataList: List<DiscreteAccessData> - ): Boolean = - accessData.accessTimeMs / ONE_HOUR_MS == - clusteredAccessDataList.first().accessTimeMs / ONE_HOUR_MS && - clusteredAccessDataList.last().accessTimeMs / ONE_MINUTE_MS - - accessData.accessTimeMs / ONE_MINUTE_MS > CLUSTER_SPACING_MINUTES - - /** - * Returns whether the provided [AppPermissionGroup] is considered a system group. - * - * For the purpose of Permissions Hub UI, non user-sensitive [AppPermissionGroup]s are - * considered "system" and should be hidden from the main page unless requested by the user - * through the "show/hide system" toggle. - */ - private fun AppPermissionGroup.isSystem() = !Utils.isGroupOrBgGroupUserSensitive(this) - - /** Returns whether app subattribution should be shown. */ - private fun shouldShowSubattributionForApp(appInfo: ApplicationInfo): Boolean { - return shouldShowSubattributionInPermissionsDashboard() && - SubattributionUtils.isSubattributionSupported(application, appInfo) - } - - /** Returns a summary of the duration the permission was accessed for. */ - private fun getDurationSummary( - usage: DiscreteAccessClusterData, - accessTimeList: List<Long>, - context: Context - ): String? { - if (accessTimeList.isEmpty()) { - return null - } - - var durationMs: Long - - // Since Location accesses are atomic, we manually calculate the access duration - // by comparing the first and last access within the cluster. - if (permissionGroup == Manifest.permission_group.LOCATION) { - durationMs = accessTimeList[0] - accessTimeList[accessTimeList.size - 1] - } else { - durationMs = - usage.discreteAccessDataList.map { it.accessDurationMs }.filter { it > 0 }.sum() - } - // Only show the duration summary if it is at least (CLUSTER_SPACING_MINUTES + 1) minutes. - // Displaying a time that is shorter than the cluster granularity - // (CLUSTER_SPACING_MINUTES) will not convey useful information. - if (durationMs >= TimeUnit.MINUTES.toMillis(CLUSTER_SPACING_MINUTES + 1)) { - return getDurationUsedStr(context, durationMs) - } - - return null - } - - /** Returns the proxied package label if the permission access was proxied. */ - private fun getProxyPackageLabel(usage: DiscreteAccessClusterData): String? = - usage.discreteAccessDataList - .firstOrNull { it.proxy?.packageName != null } - ?.let { - getPackageLabel( - PermissionControllerApplication.get(), - it.proxy!!.packageName!!, - UserHandle.getUserHandleForUid(it.proxy.uid) - ) - } - - /** Returns the attribution label for the permission access, if any. */ - private fun getSubattributionLabel(usage: DiscreteAccessClusterData): String? = - if (usage.appPermissionTimelineUsage.label == Resources.ID_NULL) null - else - usage.appPermissionTimelineUsage.permissionApp.attributionLabels?.let { - it[usage.appPermissionTimelineUsage.label] - } - - /** Builds a summary of the permission access. */ - private fun buildUsageSummary( - subattributionLabel: String?, - proxyPackageLabel: String?, - durationSummary: String?, - context: Context - ): String? { - val subTextStrings: MutableList<String?> = mutableListOf() - - subattributionLabel?.let { subTextStrings.add(subattributionLabel) } - proxyPackageLabel?.let { subTextStrings.add(it) } - durationSummary?.let { subTextStrings.add(it) } - return when (subTextStrings.size) { - 3 -> - context.getString( - R.string.history_preference_subtext_3, - subTextStrings[0], - subTextStrings[1], - subTextStrings[2] - ) - 2 -> - context.getString( - R.string.history_preference_subtext_2, - subTextStrings[0], - subTextStrings[1] - ) - 1 -> subTextStrings[0] - else -> null - } - } - - /** - * Builds a list of [AppPermissionTimelineUsage] from the provided - * [AppPermissionUsage.GroupUsage]. - */ - private fun getAppPermissionTimelineUsages( - app: PermissionApp, - groupUsage: AppPermissionUsage.GroupUsage? - ): List<AppPermissionTimelineUsage> { - if (groupUsage == null) { - return listOf() - } - - if (shouldShowSubattributionForApp(app.appInfo)) { - return groupUsage.attributionLabelledGroupUsages.map { - AppPermissionTimelineUsage(permissionGroup, app, it, it.label) - } - } - - return listOf( - AppPermissionTimelineUsage(permissionGroup, app, groupUsage, Resources.ID_NULL) - ) - } - - /** Extracts to a set all the permission groups declared by the platform. */ - private fun List<AppPermissionUsage>.extractAllPlatformAppPermissionGroups(): - Set<AppPermissionGroup> = - this.flatMap { it.groupUsages } - .map { it.group } - .filter { PermissionMapping.isPlatformPermissionGroup(it.name) } - .toSet() - - /** Initialize all relevant [TimeFilterItemMs] values. */ - private fun initializeTimeFilterItems(context: Context) { - mTimeFilterItemMs.add( - TimeFilterItemMs(Long.MAX_VALUE, context.getString(R.string.permission_usage_any_time)) - ) - mTimeFilterItemMs.add( - TimeFilterItemMs( - DAYS.toMillis(7), - StringUtils.getIcuPluralsString(context, R.string.permission_usage_last_n_days, 7) - ) - ) - mTimeFilterItemMs.add( - TimeFilterItemMs( - DAYS.toMillis(1), - StringUtils.getIcuPluralsString(context, R.string.permission_usage_last_n_days, 1) - ) - ) - - // TODO: theianchen add code for filtering by time here. - } - - /** Data used to create a preference for an app's permission usage. */ - data class HistoryPreferenceData( - val userHandle: UserHandle, - val pkgName: String, - val appIcon: Drawable?, - val preferenceTitle: String, - val permissionGroup: String, - val accessStartTime: Long, - val accessEndTime: Long, - val summaryText: CharSequence?, - val showingAttribution: Boolean, - val attributionTags: ArrayList<String>, - val sessionId: Long - ) - - /** - * A class representing a given time, e.g., "in the last hour". - * - * @param timeMs the time represented by this object in milliseconds. - * @param label the label to describe the timeframe - */ - data class TimeFilterItemMs(val timeMs: Long, val label: String) - - /** - * Class containing all the information needed by the permission usage details fragments to - * render UI. - */ - inner class PermissionUsageDetailsUiData( - /** List of [PermissionApp] instances */ - // Note that these are used only to cache app data for the permission usage details - // fragment, and have no bearing on the UI on the main permission usage page. - val permissionApps: List<PermissionApp>, - /** Whether to show the "show/hide system" toggle. */ - val shouldDisplayShowSystemToggle: Boolean, - /** [DiscreteAccessClusterData] instances ordered for display in UI */ - private val discreteAccessClusterDataList: List<DiscreteAccessClusterData>, - ) { - // Note that the HistoryPreferenceData are not initialized within the - // PermissionUsageDetailsUiData instance as the need to be constructed only after the - // calling fragment loads the necessary PermissionApp instances. We will attempt to remove - // this dependency in b/240978905. - /** Builds a list of [HistoryPreferenceData] to be displayed in the UI. */ - fun getHistoryPreferenceDataList(): List<HistoryPreferenceData> { - return discreteAccessClusterDataList.map { - this@PermissionUsageDetailsViewModelLegacy.getHistoryPreferenceData(it) - } - } - } - - /** - * Data class representing a cluster of accesses, to be represented as a single entry in the UI. - */ - data class DiscreteAccessClusterData( - val appPermissionTimelineUsage: AppPermissionTimelineUsage, - val discreteAccessDataList: List<DiscreteAccessData> - ) - - /** Data class representing a discrete permission access. */ - data class DiscreteAccessData( - val accessTimeMs: Long, - val accessDurationMs: Long, - val proxy: AppOpsManager.OpEventProxyInfo? - ) - - /** Data class representing an app's permissions usages for a particular permission group. */ - data class AppPermissionTimelineUsage( - /** Permission group whose usage is being tracked. */ - val permissionGroup: String, - // we need a PermissionApp because the loader takes the PermissionApp - // object and loads the icon and label information asynchronously - /** App whose permissions are being tracked. */ - val permissionApp: PermissionApp, - /** Timeline usage for the given app and permission. */ - val timelineUsage: TimelineUsage, - val label: Int - ) { - val attributionTags: java.util.ArrayList<String> - get() = ArrayList(timelineUsage.attributionTags) - } -} - -/** Factory for an [PermissionUsageDetailsViewModelLegacy] */ -@RequiresApi(Build.VERSION_CODES.S) -class PermissionUsageDetailsViewModelFactoryLegacy( - private val application: Application, - private val roleManager: RoleManager, - private val filterGroup: String, - private val sessionId: Long -) : ViewModelProvider.Factory { - - override fun <T : ViewModel> create(modelClass: Class<T>): T { - @Suppress("UNCHECKED_CAST") - return PermissionUsageDetailsViewModelLegacy( - application, - roleManager, - filterGroup, - sessionId - ) - as T - } -} diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/model/GrantPermissionsViewModel.kt b/PermissionController/src/com/android/permissioncontroller/permission/ui/model/GrantPermissionsViewModel.kt index 0a01929e6..1e5b96c2e 100644 --- a/PermissionController/src/com/android/permissioncontroller/permission/ui/model/GrantPermissionsViewModel.kt +++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/model/GrantPermissionsViewModel.kt @@ -29,6 +29,7 @@ import android.annotation.SuppressLint import android.app.Activity import android.app.Application import android.app.admin.DevicePolicyManager +import android.content.Context import android.content.Intent import android.content.pm.PackageManager import android.content.pm.PackageManager.FLAG_PERMISSION_POLICY_FIXED @@ -41,6 +42,8 @@ import android.os.Build import android.os.Bundle import android.os.Process import android.permission.PermissionManager +import android.permission.flags.Flags +import android.util.ArrayMap import android.util.Log import androidx.core.util.Consumer import androidx.lifecycle.ViewModel @@ -116,6 +119,7 @@ import com.android.permissioncontroller.permission.utils.SafetyNetLogger import com.android.permissioncontroller.permission.utils.Utils import com.android.permissioncontroller.permission.utils.v31.AdminRestrictedPermissionsUtils import com.android.permissioncontroller.permission.utils.v34.SafetyLabelUtils +import com.android.permissioncontroller.permission.utils.v35.MultiDeviceUtils.isPermissionDeviceAware /** * ViewModel for the GrantPermissionsActivity. Tracks all permission groups that are affected by the @@ -153,6 +157,21 @@ class GrantPermissionsViewModel( } else { null } + private val permissionGroupToDeviceIdMap: Map<String, Int> = + if (SdkLevel.isAtLeastV() && Flags.allowHostPermissionDialogsOnVirtualDevices()) { + requestedPermissions + .filter({ PermissionMapping.getGroupOfPlatformPermission(it) != null }) + .associateBy({ PermissionMapping.getGroupOfPlatformPermission(it)!! }, { + if (isPermissionDeviceAware( + app.applicationContext, + deviceId, + it + ) + ) deviceId else Context.DEVICE_ID_DEFAULT + }) + } else { + ArrayMap() + } private val dpm = app.getSystemService(DevicePolicyManager::class.java)!! private val permissionPolicy = dpm.getPermissionPolicy(null) private val groupStates = mutableMapOf<String, GroupState>() @@ -314,7 +333,8 @@ class GrantPermissionsViewModel( } val getLiveDataFun = { groupName: String -> - LightAppPermGroupLiveData[packageName, groupName, user, deviceId] + LightAppPermGroupLiveData[packageName, groupName, user, + permissionGroupToDeviceIdMap.get(groupName) ?: deviceId] } setSourcesToDifference(requestedGroups.keys, appPermGroupLiveDatas, getLiveDataFun) } @@ -398,7 +418,8 @@ class GrantPermissionsViewModel( safetyLabel, groupState.group.permGroupName ), - deviceId + permissionGroupToDeviceIdMap.get(groupState.group.permGroupName) + ?: deviceId ) ) } diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/model/ManageCustomPermissionsViewModel.kt b/PermissionController/src/com/android/permissioncontroller/permission/ui/model/ManageCustomPermissionsViewModel.kt index bd80a88cd..429799157 100644 --- a/PermissionController/src/com/android/permissioncontroller/permission/ui/model/ManageCustomPermissionsViewModel.kt +++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/model/ManageCustomPermissionsViewModel.kt @@ -16,17 +16,23 @@ package com.android.permissioncontroller.permission.ui.model +import android.Manifest import android.app.Application +import android.content.Intent +import android.health.connect.HealthPermissions.HEALTH_PERMISSION_GROUP import android.os.Bundle import androidx.fragment.app.Fragment import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.navigation.fragment.findNavController +import com.android.permission.flags.Flags import com.android.permissioncontroller.R import com.android.permissioncontroller.permission.data.PermGroupsPackagesLiveData import com.android.permissioncontroller.permission.data.PermGroupsPackagesUiInfoLiveData import com.android.permissioncontroller.permission.data.SmartUpdateMediatorLiveData +import com.android.permissioncontroller.permission.utils.Utils import com.android.permissioncontroller.permission.utils.navigateSafe /** @@ -38,6 +44,12 @@ import com.android.permissioncontroller.permission.utils.navigateSafe class ManageCustomPermissionsViewModel(private val app: Application) : AndroidViewModel(app) { val uiDataLiveData = PermGroupsPackagesUiInfoLiveData(app, UsedCustomPermGroupNamesLiveData()) + val additionaPermGroupsUiInfo = + PermGroupsPackagesUiInfoLiveData( + app, + if (Flags.declutteredPermissionManagerEnabled()) AdditionalPermGroupNamesLiveData(app) + else MutableLiveData<List<String>>(), + ) /** * Navigate to a Permission Apps fragment @@ -46,6 +58,15 @@ class ManageCustomPermissionsViewModel(private val app: Application) : AndroidVi * @param args The args to pass to the new fragment */ fun showPermissionApps(fragment: Fragment, args: Bundle) { + val groupName = args.getString(Intent.EXTRA_PERMISSION_GROUP_NAME) + if (groupName == Manifest.permission_group.NOTIFICATIONS) { + Utils.navigateToNotificationSettings(fragment.context!!) + return + } + if (Utils.isHealthPermissionUiEnabled() && groupName == HEALTH_PERMISSION_GROUP) { + Utils.navigateToHealthConnectSettings(fragment.context!!) + return + } fragment.findNavController().navigateSafe(R.id.manage_to_perm_apps, args) } } @@ -58,7 +79,8 @@ class ManageCustomPermissionsViewModel(private val app: Application) : AndroidVi class ManageCustomPermissionsViewModelFactory(private val app: Application) : ViewModelProvider.Factory { override fun <T : ViewModel> create(modelClass: Class<T>): T { - @Suppress("UNCHECKED_CAST") return ManageCustomPermissionsViewModel(app) as T + @Suppress("UNCHECKED_CAST") + return ManageCustomPermissionsViewModel(app) as T } } @@ -77,3 +99,32 @@ class UsedCustomPermGroupNamesLiveData : SmartUpdateMediatorLiveData<List<String /* No op override */ } } + +/** + * A LiveData that is the union of LiveData UsedCustomPermGroupNamesLiveData and + * UnusedStandardPermGroupNamesLiveData. + * + * @param app The current application of the fragment + */ +class AdditionalPermGroupNamesLiveData(private val app: Application) : + SmartUpdateMediatorLiveData<List<String>>() { + + val usedCustomGroupNames = UsedCustomPermGroupNamesLiveData() + val unusedStandardGroupNames = UnusedStandardPermGroupNamesLiveData(app) + + init { + addSource(usedCustomGroupNames) { update() } + addSource(unusedStandardGroupNames) { update() } + } + + private fun combineGroupNames( + groupNames1: List<String>?, + groupNames2: List<String>?, + ): List<String> { + return (groupNames1 ?: emptyList()) + (groupNames2 ?: emptyList()) + } + + override fun onUpdate() { + value = combineGroupNames(usedCustomGroupNames.value, unusedStandardGroupNames.value) + } +} diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/model/ManageStandardPermissionsViewModel.kt b/PermissionController/src/com/android/permissioncontroller/permission/ui/model/ManageStandardPermissionsViewModel.kt index aeab0aa89..6dabe8ab7 100644 --- a/PermissionController/src/com/android/permissioncontroller/permission/ui/model/ManageStandardPermissionsViewModel.kt +++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/model/ManageStandardPermissionsViewModel.kt @@ -23,8 +23,11 @@ import android.health.connect.HealthPermissions.HEALTH_PERMISSION_GROUP import android.os.Bundle import androidx.fragment.app.Fragment import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.MediatorLiveData +import androidx.lifecycle.MutableLiveData import androidx.lifecycle.map import androidx.navigation.fragment.findNavController +import com.android.permission.flags.Flags import com.android.permissioncontroller.R import com.android.permissioncontroller.permission.data.PermGroupsPackagesLiveData import com.android.permissioncontroller.permission.data.PermGroupsPackagesUiInfoLiveData @@ -45,7 +48,21 @@ import com.android.permissioncontroller.permission.utils.navigateSafe class ManageStandardPermissionsViewModel(private val app: Application) : AndroidViewModel(app) { val uiDataLiveData = PermGroupsPackagesUiInfoLiveData(app, StandardPermGroupNamesLiveData) + val usedStandardPermGroupsUiInfo = + PermGroupsPackagesUiInfoLiveData( + app, + if (Flags.declutteredPermissionManagerEnabled()) UsedStandardPermGroupNamesLiveData(app) + else MutableLiveData<List<String>>(), + ) val numCustomPermGroups = NumCustomPermGroupsWithPackagesLiveData() + val numUnusedStandardPermGroups = + MediatorLiveData<Int>().apply { + if (Flags.declutteredPermissionManagerEnabled()) { + addSource(UnusedStandardPermGroupNamesLiveData(app)) { groupNames -> + value = groupNames.size + } + } + } val numAutoRevoked = unusedAutoRevokePackagesLiveData.map { it?.size ?: 0 } /** @@ -98,3 +115,47 @@ class NumCustomPermGroupsWithPackagesLiveData() : SmartUpdateMediatorLiveData<In value = customPermGroupPackages.value?.size ?: 0 } } + +/** + * A LiveData that tracks the names of the platform-defined permission groups, such that at least + * one of the permissions in the group has been requested at runtime by at least one non-system + * application. + * + * @param app The current application of the fragment + */ +class UsedStandardPermGroupNamesLiveData(private val app: Application) : + SmartUpdateMediatorLiveData<List<String>>() { + init { + addSource(PermGroupsPackagesUiInfoLiveData(app, StandardPermGroupNamesLiveData)) { + permGroups -> + if (permGroups.values.any { it != null }) { + value = + permGroups.filterValues { it != null && it.nonSystemTotal > 0 }.keys.toList() + } + } + } + + override fun onUpdate() { + /* No op override */ + } +} + +/** + * A LiveData that tracks the names of the platform-defined permission groups, such that none of the + * the permissions in the group has been requested at runtime by any non-system application. + * + * @param app The current application of the fragment + */ +class UnusedStandardPermGroupNamesLiveData(private val app: Application) : + SmartUpdateMediatorLiveData<List<String>>() { + init { + addSource(PermGroupsPackagesUiInfoLiveData(app, StandardPermGroupNamesLiveData)) { + permGroups -> + value = permGroups.filterValues { it != null && it.nonSystemTotal == 0 }.keys.toList() + } + } + + override fun onUpdate() { + /* No op override */ + } +} 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/WearAppPermissionGroupsHelper.kt b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/WearAppPermissionGroupsHelper.kt index 078eefe3b..2933d6fda 100644 --- a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/WearAppPermissionGroupsHelper.kt +++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/WearAppPermissionGroupsHelper.kt @@ -19,6 +19,7 @@ package com.android.permissioncontroller.permission.ui.wear import android.content.Context import android.content.pm.PackageManager import android.content.pm.PermissionInfo +import android.health.connect.HealthPermissions.HEALTH_PERMISSION_GROUP import android.os.Build import android.os.UserHandle import android.util.ArraySet @@ -319,6 +320,10 @@ class WearAppPermissionGroupsHelper( ) { // Redirect to location controller extra package settings. LocationUtils.startLocationControllerExtraPackageSettings(context, user) + } else if (permGroupName.equals(HEALTH_PERMISSION_GROUP) + && android.permission.flags.Flags.replaceBodySensorPermissionEnabled()) { + // Redirect to Health&Fitness UI + Utils.navigateToAppHealthConnectSettings(fragment.requireContext(), packageName, user) } else { val args = WearAppPermissionFragment.createArgs( 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..1498b91b6 100644 --- a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/WearGrantPermissionsScreen.kt +++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/WearGrantPermissionsScreen.kt @@ -37,17 +37,20 @@ 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.ResourceHelper +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,15 @@ 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 + } ScrollableScreen( + materialUIVersion = materialUIVersion, showTimeText = false, image = icon.value, title = groupMessage.value, @@ -69,13 +79,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 +98,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 +120,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/WearManageStandardPermissionScreen.kt b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/WearManageStandardPermissionScreen.kt index bd1946759..9aacd65d3 100644 --- a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/WearManageStandardPermissionScreen.kt +++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/WearManageStandardPermissionScreen.kt @@ -17,6 +17,7 @@ package com.android.permissioncontroller.permission.ui.wear import android.graphics.drawable.Drawable +import android.permission.flags.Flags import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState @@ -77,7 +78,13 @@ internal fun getPermGroupChipParams( } return permissionGroups // Removing Health Connect from the list of permissions to fix b/331260850 - .filterNot { Utils.isHealthPermissionGroup(it.key) } + .let { + if (Flags.replaceBodySensorPermissionEnabled()) { + it + } else { + it.filterNot { Utils.isHealthPermissionGroup(it.key) } + } + } .mapNotNull { val uiInfo = it.value ?: return@mapNotNull null PermGroupChipParam( 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 bcdf3b661..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 @@ -32,47 +32,70 @@ import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.style.TextDecoration import androidx.wear.compose.material.MaterialTheme +import com.android.permissioncontroller.permission.ui.wear.WearUtils.capitalize const val CLICKABLE_SPAN_TAG = "CLICKABLE_SPAN_TAG" @Composable -fun AnnotatedText(text: CharSequence, style: TextStyle, modifier: Modifier = Modifier) { +fun AnnotatedText( + text: CharSequence, + style: TextStyle, + modifier: Modifier = Modifier, + shouldCapitalize: Boolean, +) { val onClickCallbacks = mutableMapOf<String, (View) -> Unit>() val context = LocalContext.current val listener = LinkInteractionListener { if (it is LinkAnnotation.Clickable) { - onClickCallbacks.get(it.tag)?.invoke(View(context)) + onClickCallbacks[it.tag]?.invoke(View(context)) } } val annotatedString = - spannableStringToAnnotatedString(text, onClickCallbacks, listener = listener) + spannableStringToAnnotatedString( + text, + shouldCapitalize, + onClickCallbacks, + listener = listener, + ) BasicText(text = annotatedString, style = style, modifier = modifier) } @Composable private fun spannableStringToAnnotatedString( text: CharSequence, + shouldCapitalize: Boolean, onClickCallbacks: MutableMap<String, (View) -> Unit>, spanColor: Color = MaterialTheme.colors.primary, - listener: LinkInteractionListener -) = - if (text is Spanned) { - buildAnnotatedString { - append((text.toString())) - for (span in text.getSpans(0, text.length, Any::class.java)) { - val start = text.getSpanStart(span) - val end = text.getSpanEnd(span) - when (span) { - is ClickableSpan -> - addClickableSpan(span, spanColor, start, end, onClickCallbacks, listener) - else -> addStyle(SpanStyle(), start, end) + listener: LinkInteractionListener, +): AnnotatedString { + val finalString = if (shouldCapitalize) text.toString().capitalize() else text.toString() + val annotatedString = + if (text is Spanned) { + buildAnnotatedString { + append(finalString) + for (span in text.getSpans(0, text.length, Any::class.java)) { + val start = text.getSpanStart(span) + val end = text.getSpanEnd(span) + when (span) { + is ClickableSpan -> + addClickableSpan( + span, + spanColor, + start, + end, + onClickCallbacks, + listener, + ) + else -> addStyle(SpanStyle(), start, end) + } } } + } else { + AnnotatedString(finalString) } - } else { - AnnotatedString(text.toString()) - } + return annotatedString +} private fun AnnotatedString.Builder.addClickableSpan( span: ClickableSpan, @@ -80,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 53013def7..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,6 +288,7 @@ internal fun Scaffold( color = MaterialTheme.colors.onSurfaceVariant ), modifier = modifier, + 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..1d660ca35 --- /dev/null +++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/material3/WearPermissionButton.kt @@ -0,0 +1,143 @@ +/* + * 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.PaddingValues +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.requiredSizeIn +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.Hyphens +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +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.LocalTextStyle +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 +internal fun WearPermissionButtonInternal( + modifier: Modifier = Modifier, + label: String? = null, + iconBuilder: WearPermissionIconBuilder? = null, + labelMaxLines: Int? = null, + secondaryLabel: String? = null, + secondaryLabelMaxLines: Int? = null, + onClick: () -> Unit, + enabled: Boolean = true, + colors: ButtonColors = ButtonDefaults.filledTonalButtonColors(), + contentPadding: PaddingValues = ButtonDefaults.ContentPadding, + requiresMinimumHeight: Boolean = true, +) { + val minHeight: Dp = + if (requiresMinimumHeight) { + 0.dp + } else { + 1.dp + } + val iconParam: (@Composable BoxScope.() -> Unit)? = iconBuilder?.let { { it.build() } } + val labelParam: (@Composable RowScope.() -> Unit)? = + label?.let { + { + Text( + text = label, + modifier = Modifier.fillMaxWidth(), + maxLines = labelMaxLines ?: LocalTextConfiguration.current.maxLines, + style = + LocalTextStyle.current.copy( + fontWeight = FontWeight.W600, + hyphens = Hyphens.Auto, + ), + ) + } + } + + 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.requiredSizeIn(minHeight = minHeight).fillMaxWidth(), + contentPadding = contentPadding, + 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..504c69bb0 --- /dev/null +++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/material3/WearPermissionButtonStyle.kt @@ -0,0 +1,75 @@ +/* + * 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.chipDefaultColors +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 -> chipDefaultColors() + 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..b7521d073 --- /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.Icon +import androidx.wear.compose.material3.IconButtonDefaults +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(IconButtonDefaults.LargeIconSize) + 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/WearPermissionListFooter.kt b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/material3/WearPermissionListFooter.kt new file mode 100644 index 000000000..10125c873 --- /dev/null +++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/material3/WearPermissionListFooter.kt @@ -0,0 +1,51 @@ +/* + * 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.foundation.layout.PaddingValues +import androidx.compose.runtime.Composable +import androidx.compose.ui.unit.dp +import androidx.wear.compose.material3.ButtonDefaults +import com.android.permissioncontroller.permission.ui.wear.elements.ListFooter +import com.android.permissioncontroller.permission.ui.wear.theme.WearPermissionMaterialUIVersion + +/** This component is creates a transparent styled button to use as a list footer. */ +@Composable +fun WearPermissionListFooter( + materialUIVersion: WearPermissionMaterialUIVersion, + label: String, + iconBuilder: WearPermissionIconBuilder? = null, + onClick: (() -> Unit) = {}, +) { + if (materialUIVersion == WearPermissionMaterialUIVersion.MATERIAL2_5) { + ListFooter( + description = label, + iconRes = iconBuilder?.let { it.iconResource as Int }, + onClick = onClick, + ) + } else { + WearPermissionButtonInternal( + iconBuilder = iconBuilder, + secondaryLabel = label, + secondaryLabelMaxLines = Int.MAX_VALUE, + onClick = onClick, + contentPadding = PaddingValues(0.dp), + colors = ButtonDefaults.childButtonColors(), + requiresMinimumHeight = false, + ) + } +} 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/ResourceHelper.kt b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/theme/ResourceHelper.kt new file mode 100644 index 000000000..c7ed0958c --- /dev/null +++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/theme/ResourceHelper.kt @@ -0,0 +1,62 @@ +/* + * 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.content.Context +import android.os.SystemProperties +import androidx.annotation.ColorRes +import androidx.annotation.DimenRes +import androidx.annotation.DoNotInline +import androidx.annotation.StringRes +import androidx.compose.ui.graphics.Color + +internal object ResourceHelper { + + private const val MATERIAL3_ENABLED_SYSPROP = "persist.cw_build.bluechip.enabled" + + val material3Enabled: Boolean + get() { + return SystemProperties.getBoolean(MATERIAL3_ENABLED_SYSPROP, false) + } + + @DoNotInline + fun getColor(context: Context, @ColorRes id: Int): Color? { + return try { + val colorInt = context.resources.getColor(id, context.theme) + Color(colorInt) + } catch (e: Exception) { + null + } + } + + @DoNotInline + fun getString(context: Context, @StringRes id: Int): String? { + return try { + context.resources.getString(id) + } catch (e: Exception) { + null + } + } + + @DoNotInline + fun getDimen(context: Context, @DimenRes id: Int): Float? { + return try { + context.resources.getDimension(id) / context.resources.displayMetrics.density + } catch (e: Exception) { + null + } + } +} 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 new file mode 100644 index 000000000..7ac6c8114 --- /dev/null +++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/theme/WearComposeMaterial3ColorScheme.kt @@ -0,0 +1,209 @@ +/* + * 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.content.Context +import android.os.Build +import androidx.annotation.ColorRes +import androidx.annotation.RequiresApi +import androidx.compose.ui.graphics.Color +import androidx.wear.compose.material3.ColorScheme + +/** + * Creates a dynamic color maps that can be overlaid. In wear we only support dark theme for the + * time being. If the device supports dynamic color generation these resources are updated with the + * generated colors + */ +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(36) + fun dynamicColorScheme(context: Context): ColorScheme { + val defaultColorScheme = ColorScheme() + return ColorScheme( + primary = + defaultColorScheme.primary.updatedColor( + context, + android.R.color.system_primary_fixed, + ), + primaryDim = + defaultColorScheme.primaryDim.updatedColor( + context, + android.R.color.system_primary_fixed_dim, + ), + primaryContainer = + defaultColorScheme.primaryContainer.updatedColor( + context, + android.R.color.system_primary_container_dark, + ), + onPrimary = + defaultColorScheme.onPrimary.updatedColor( + context, + android.R.color.system_on_primary_fixed, + ), + onPrimaryContainer = + defaultColorScheme.onPrimaryContainer.updatedColor( + context, + android.R.color.system_on_primary_container_dark, + ), + secondary = + defaultColorScheme.secondary.updatedColor( + context, + android.R.color.system_secondary_fixed, + ), + secondaryDim = + defaultColorScheme.secondaryDim.updatedColor( + context, + android.R.color.system_secondary_fixed_dim, + ), + secondaryContainer = + defaultColorScheme.secondaryContainer.updatedColor( + context, + android.R.color.system_secondary_container_dark, + ), + onSecondary = + defaultColorScheme.onSecondary.updatedColor( + context, + android.R.color.system_on_secondary_fixed, + ), + onSecondaryContainer = + defaultColorScheme.onSecondaryContainer.updatedColor( + context, + android.R.color.system_on_secondary_container_dark, + ), + tertiary = + defaultColorScheme.tertiary.updatedColor( + context, + android.R.color.system_tertiary_fixed, + ), + tertiaryDim = + defaultColorScheme.tertiaryDim.updatedColor( + context, + android.R.color.system_tertiary_fixed_dim, + ), + tertiaryContainer = + defaultColorScheme.tertiaryContainer.updatedColor( + context, + android.R.color.system_tertiary_container_dark, + ), + onTertiary = + defaultColorScheme.onTertiary.updatedColor( + context, + android.R.color.system_on_tertiary_fixed, + ), + onTertiaryContainer = + defaultColorScheme.onTertiaryContainer.updatedColor( + context, + android.R.color.system_on_tertiary_container_dark, + ), + surfaceContainerLow = + defaultColorScheme.surfaceContainerLow.updatedColor( + context, + android.R.color.system_surface_container_low_dark, + ), + surfaceContainer = + defaultColorScheme.surfaceContainer.updatedColor( + context, + android.R.color.system_surface_container_dark, + ), + surfaceContainerHigh = + defaultColorScheme.surfaceContainerHigh.updatedColor( + context, + android.R.color.system_surface_container_high_dark, + ), + onSurface = + defaultColorScheme.onSurface.updatedColor( + context, + android.R.color.system_on_surface_dark, + ), + onSurfaceVariant = + defaultColorScheme.onSurfaceVariant.updatedColor( + context, + android.R.color.system_on_surface_variant_dark, + ), + outline = + defaultColorScheme.outline.updatedColor( + context, + android.R.color.system_outline_dark, + ), + outlineVariant = + defaultColorScheme.outlineVariant.updatedColor( + context, + android.R.color.system_outline_variant_dark, + ), + background = + defaultColorScheme.background.updatedColor( + context, + android.R.color.system_background_dark, + ), + onBackground = + defaultColorScheme.onBackground.updatedColor( + context, + android.R.color.system_on_background_dark, + ), + error = + defaultColorScheme.error.updatedColor(context, android.R.color.system_error_dark), + onError = + defaultColorScheme.onError.updatedColor( + context, + android.R.color.system_on_error_dark, + ), + errorContainer = + defaultColorScheme.errorContainer.updatedColor( + context, + android.R.color.system_error_container_dark, + ), + onErrorContainer = + defaultColorScheme.onErrorContainer.updatedColor( + context, + android.R.color.system_on_error_container_dark, + ), + ) + } +} diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/theme/WearComposeMaterial3Shapes.kt b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/theme/WearComposeMaterial3Shapes.kt new file mode 100644 index 000000000..f81022842 --- /dev/null +++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/theme/WearComposeMaterial3Shapes.kt @@ -0,0 +1,66 @@ +/* + * 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.content.Context +import androidx.annotation.DimenRes +import androidx.compose.foundation.shape.CornerBasedShape +import androidx.compose.foundation.shape.CornerSize +import androidx.compose.ui.unit.dp +import androidx.wear.compose.material3.Shapes +import com.android.permissioncontroller.R + +// TODO(b/324928718): Use system defined symbols. +internal object WearComposeMaterial3Shapes { + private fun CornerBasedShape.updatedShape( + context: Context, + @DimenRes cornerSizeRes: Int, + ): CornerBasedShape { + val size = ResourceHelper.getDimen(context, cornerSizeRes)?.dp ?: return this + return copy(CornerSize(size)) + } + + fun dynamicShapes(context: Context): Shapes { + val defaultShapes = Shapes() + return Shapes( + extraLarge = + defaultShapes.extraLarge.updatedShape( + context, + R.dimen.wear_compose_material3_shape_corner_extra_large_size, + ), + large = + defaultShapes.large.updatedShape( + context, + R.dimen.wear_compose_material3_shape_corner_large_size, + ), + medium = + defaultShapes.medium.updatedShape( + context, + R.dimen.wear_compose_material3_shape_corner_medium_size, + ), + small = + defaultShapes.small.updatedShape( + context, + R.dimen.wear_compose_material3_shape_corner_small_size, + ), + extraSmall = + defaultShapes.extraSmall.updatedShape( + context, + R.dimen.wear_compose_material3_shape_corner_extra_small_size, + ), + ) + } +} diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/theme/WearComposeMaterial3TypeScaleTokens.kt b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/theme/WearComposeMaterial3TypeScaleTokens.kt new file mode 100644 index 000000000..a4ec9ee1d --- /dev/null +++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/theme/WearComposeMaterial3TypeScaleTokens.kt @@ -0,0 +1,109 @@ +/* + * 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 + +/* + * These values are retrieved from https://carbon.googleplex.com/wear-m3/pages and + * modified by UX. + * These values are internal to material3 library. We copied them to support flex font families + * Do not edit directly. Copy paste from compose material library. + */ + +internal object WearComposeMaterial3TypeScaleTokens { + val ArcLargeRoundness = 100.0f + val ArcLargeWeight = 600.0f + val ArcLargeWidth = 100.0f + + val ArcMediumRoundness = 100.0f + val ArcMediumWeight = 600.0f + val ArcMediumWidth = 90.0f + + val ArcSmallRoundness = 100.0f + val ArcSmallWeight = 550.0f + val ArcSmallWidth = 90.0f + + val BodyExtraSmallRoundness = 100.0f + val BodyExtraSmallWeight = 500.0f + val BodyExtraSmallWidth = 84.0f + + val BodyLargeRoundness = 100.0f + val BodyLargeWeight = 450.0f + val BodyLargeWidth = 90.0f + + val BodyMediumRoundness = 100.0f + val BodyMediumWeight = 450.0f + val BodyMediumWidth = 90.0f + + val BodySmallRoundness = 100.0f + val BodySmallWeight = 500.0f + val BodySmallWidth = 86.0f + + val DisplayLargeRoundness = 100.0f + val DisplayLargeWeight = 450.0f + val DisplayLargeWidth = 100.0f + + val DisplayMediumRoundness = 100.0f + val DisplayMediumWeight = 500.0f + val DisplayMediumWidth = 100.0f + + val DisplaySmallRoundness = 100.0f + val DisplaySmallWeight = 500.0f + val DisplaySmallWidth = 100.0f + + val LabelLargeRoundness = 100.0f + val LabelLargeWeight = 500.0f + val LabelLargeWidth = 100.0f + + val LabelMediumRoundness = 100.0f + val LabelMediumWeight = 500.0f + val LabelMediumWidth = 90.0f + + val LabelSmallRoundness = 100.0f + val LabelSmallWeight = 500.0f + val LabelSmallWidth = 84.0f + + val NumeralExtraLargeRoundness = 100.0f + val NumeralExtraLargeWeight = 550.0f + val NumeralExtraLargeWidth = 100.0f + + val NumeralExtraSmallRoundness = 100.0f + val NumeralExtraSmallWeight = 550.0f + val NumeralExtraSmallWidth = 100.0f + + val NumeralLargeRoundness = 100.0f + val NumeralLargeWeight = 600.0f + val NumeralLargeWidth = 100.0f + + val NumeralMediumRoundness = 100.0f + val NumeralMediumWidth = 100.0f + val NumeralMediumWeight = 600.0f + + val NumeralSmallRoundness = 100.0f + val NumeralSmallWeight = 600.0f + val NumeralSmallWidth = 100.0f + + val TitleLargeRoundness = 100.0f + val TitleLargeWeight = 500.0f + val TitleLargeWidth = 100.0f + + val TitleMediumRoundness = 100.0f + val TitleMediumWeight = 550.0f + val TitleMediumWidth = 100.0f + + val TitleSmallRoundness = 100.0f + val TitleSmallWeight = 550.0f + val TitleSmallWidth = 100.0f +} diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/theme/WearComposeMaterial3Typography.kt b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/theme/WearComposeMaterial3Typography.kt new file mode 100644 index 000000000..ceae526a7 --- /dev/null +++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/theme/WearComposeMaterial3Typography.kt @@ -0,0 +1,240 @@ +/* + * 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.content.Context +import androidx.annotation.DimenRes +import androidx.annotation.StringRes +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.DeviceFontFamilyName +import androidx.compose.ui.text.font.Font +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontVariation +import androidx.compose.ui.unit.sp +import androidx.wear.compose.material3.Typography +import com.android.permissioncontroller.R + +internal object WearComposeMaterial3Typography { + + private const val DEVICE_DEFAULT_FLEX_FONT_TYPE = "font-family-flex-device-default" + + fun fontFamily( + context: Context, + @StringRes id: Int, + variationSettings: FontVariation.Settings? = null, + ): FontFamily { + val typefaceName = ResourceHelper.getString(context, id) ?: DEVICE_DEFAULT_FLEX_FONT_TYPE + + val font = + if (variationSettings != null) { + Font( + familyName = DeviceFontFamilyName(typefaceName), + variationSettings = variationSettings, + ) + } else { + Font(familyName = DeviceFontFamilyName(typefaceName)) + } + return FontFamily(font) + } + + private fun TextStyle.updatedTextStyle( + context: Context, + @StringRes fontRes: Int, + variationSettings: FontVariation.Settings? = null, + @DimenRes fontSizeRes: Int, + ): TextStyle { + + val fontFamily = + fontFamily(context = context, id = fontRes, variationSettings = variationSettings) + val fontSize = ResourceHelper.getDimen(context = context, id = fontSizeRes)?.sp ?: fontSize + + return copy(fontFamily = fontFamily, fontSize = fontSize) + } + + fun dynamicTypography(context: Context): Typography { + val defaultTypography = Typography() + return Typography( + arcLarge = + defaultTypography.arcLarge.updatedTextStyle( + context = context, + fontRes = R.string.wear_compose_material3_arc_large_font_family, + fontSizeRes = R.dimen.wear_compose_material3_arc_large_font_size, + variationSettings = + WearComposeMaterial3VariableFontTokens.ArcLargeVariationSettings, + ), + arcMedium = + defaultTypography.arcMedium.updatedTextStyle( + context = context, + fontRes = R.string.wear_compose_material3_arc_medium_font_family, + fontSizeRes = R.dimen.wear_compose_material3_arc_medium_font_size, + variationSettings = + WearComposeMaterial3VariableFontTokens.ArcMediumVariationSettings, + ), + arcSmall = + defaultTypography.arcSmall.updatedTextStyle( + context = context, + fontRes = R.string.wear_compose_material3_arc_small_font_family, + fontSizeRes = R.dimen.wear_compose_material3_arc_small_font_size, + variationSettings = + WearComposeMaterial3VariableFontTokens.ArcSmallVariationSettings, + ), + bodyLarge = + defaultTypography.bodyLarge.updatedTextStyle( + context = context, + fontRes = R.string.wear_compose_material3_body_large_font_family, + fontSizeRes = R.dimen.wear_compose_material3_body_large_font_size, + variationSettings = + WearComposeMaterial3VariableFontTokens.BodyLargeVariationSettings, + ), + bodyMedium = + defaultTypography.bodyMedium.updatedTextStyle( + context = context, + fontRes = R.string.wear_compose_material3_body_medium_font_family, + fontSizeRes = R.dimen.wear_compose_material3_body_medium_font_size, + variationSettings = + WearComposeMaterial3VariableFontTokens.BodyMediumVariationSettings, + ), + bodySmall = + defaultTypography.bodySmall.updatedTextStyle( + context = context, + fontRes = R.string.wear_compose_material3_body_small_font_family, + fontSizeRes = R.dimen.wear_compose_material3_body_small_font_size, + variationSettings = + WearComposeMaterial3VariableFontTokens.BodySmallVariationSettings, + ), + bodyExtraSmall = + defaultTypography.bodyExtraSmall.updatedTextStyle( + context = context, + fontRes = R.string.wear_compose_material3_body_extra_small_font_family, + fontSizeRes = R.dimen.wear_compose_material3_body_extra_small_font_size, + variationSettings = + WearComposeMaterial3VariableFontTokens.BodyExtraSmallVariationSettings, + ), + displayLarge = + defaultTypography.displayLarge.updatedTextStyle( + context = context, + fontRes = R.string.wear_compose_material3_display_large_font_family, + fontSizeRes = R.dimen.wear_compose_material3_display_large_font_size, + variationSettings = + WearComposeMaterial3VariableFontTokens.DisplayLargeVariationSettings, + ), + displayMedium = + defaultTypography.displayMedium.updatedTextStyle( + context = context, + fontRes = R.string.wear_compose_material3_display_medium_font_family, + fontSizeRes = R.dimen.wear_compose_material3_display_medium_font_size, + variationSettings = + WearComposeMaterial3VariableFontTokens.DisplayMediumVariationSettings, + ), + displaySmall = + defaultTypography.displaySmall.updatedTextStyle( + context = context, + fontRes = R.string.wear_compose_material3_display_small_font_family, + fontSizeRes = R.dimen.wear_compose_material3_display_small_font_size, + variationSettings = + WearComposeMaterial3VariableFontTokens.DisplaySmallVariationSettings, + ), + labelLarge = + defaultTypography.labelLarge.updatedTextStyle( + context = context, + fontRes = R.string.wear_compose_material3_label_large_font_family, + fontSizeRes = R.dimen.wear_compose_material3_label_large_font_size, + variationSettings = + WearComposeMaterial3VariableFontTokens.LabelLargeVariationSettings, + ), + labelMedium = + defaultTypography.labelMedium.updatedTextStyle( + context = context, + fontRes = R.string.wear_compose_material3_label_medium_font_family, + fontSizeRes = R.dimen.wear_compose_material3_label_medium_font_size, + variationSettings = + WearComposeMaterial3VariableFontTokens.LabelMediumVariationSettings, + ), + labelSmall = + defaultTypography.labelSmall.updatedTextStyle( + context = context, + fontRes = R.string.wear_compose_material3_label_small_font_family, + fontSizeRes = R.dimen.wear_compose_material3_label_small_font_size, + variationSettings = + WearComposeMaterial3VariableFontTokens.LabelSmallVariationSettings, + ), + numeralExtraLarge = + defaultTypography.numeralExtraLarge.updatedTextStyle( + context = context, + fontRes = R.string.wear_compose_material3_numeral_extra_large_font_family, + fontSizeRes = R.dimen.wear_compose_material3_numeral_extra_large_font_size, + variationSettings = + WearComposeMaterial3VariableFontTokens.NumeralExtraLargeVariationSettings, + ), + numeralLarge = + defaultTypography.numeralLarge.updatedTextStyle( + context = context, + fontRes = R.string.wear_compose_material3_numeral_large_font_family, + fontSizeRes = R.dimen.wear_compose_material3_numeral_large_font_size, + variationSettings = + WearComposeMaterial3VariableFontTokens.NumeralLargeVariationSettings, + ), + numeralMedium = + defaultTypography.numeralMedium.updatedTextStyle( + context = context, + fontRes = R.string.wear_compose_material3_numeral_medium_font_family, + fontSizeRes = R.dimen.wear_compose_material3_numeral_medium_font_size, + variationSettings = + WearComposeMaterial3VariableFontTokens.NumeralMediumVariationSettings, + ), + numeralSmall = + defaultTypography.numeralSmall.updatedTextStyle( + context = context, + fontRes = R.string.wear_compose_material3_numeral_small_font_family, + fontSizeRes = R.dimen.wear_compose_material3_numeral_small_font_size, + variationSettings = + WearComposeMaterial3VariableFontTokens.NumeralSmallVariationSettings, + ), + numeralExtraSmall = + defaultTypography.numeralExtraSmall.updatedTextStyle( + context = context, + fontRes = R.string.wear_compose_material3_numeral_extra_small_font_family, + fontSizeRes = R.dimen.wear_compose_material3_numeral_extra_small_font_size, + variationSettings = + WearComposeMaterial3VariableFontTokens.NumeralExtraSmallVariationSettings, + ), + titleLarge = + defaultTypography.titleLarge.updatedTextStyle( + context = context, + fontRes = R.string.wear_compose_material3_title_large_font_family, + fontSizeRes = R.dimen.wear_compose_material3_title_large_font_size, + variationSettings = + WearComposeMaterial3VariableFontTokens.TitleLargeVariationSettings, + ), + titleMedium = + defaultTypography.titleMedium.updatedTextStyle( + context = context, + fontRes = R.string.wear_compose_material3_title_medium_font_family, + fontSizeRes = R.dimen.wear_compose_material3_title_medium_font_size, + variationSettings = + WearComposeMaterial3VariableFontTokens.TitleMediumVariationSettings, + ), + titleSmall = + defaultTypography.titleSmall.updatedTextStyle( + context = context, + fontRes = R.string.wear_compose_material3_title_small_font_family, + fontSizeRes = R.dimen.wear_compose_material3_title_small_font_size, + variationSettings = + WearComposeMaterial3VariableFontTokens.TitleSmallVariationSettings, + ), + ) + } +} diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/theme/WearComposeMaterial3VariableFontTokens.kt b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/theme/WearComposeMaterial3VariableFontTokens.kt new file mode 100644 index 000000000..1b42a3b05 --- /dev/null +++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/theme/WearComposeMaterial3VariableFontTokens.kt @@ -0,0 +1,186 @@ +/* + * 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 androidx.compose.ui.text.font.FontVariation + +internal object WearComposeMaterial3VariableFontTokens { + val ArcLargeVariationSettings = + FontVariation.Settings( + FontVariation.Setting("ROND", WearComposeMaterial3TypeScaleTokens.ArcLargeRoundness), + FontVariation.Setting("wdth", WearComposeMaterial3TypeScaleTokens.ArcLargeWidth), + FontVariation.Setting("wght", WearComposeMaterial3TypeScaleTokens.ArcLargeWeight), + ) + val ArcMediumVariationSettings = + FontVariation.Settings( + FontVariation.Setting("ROND", WearComposeMaterial3TypeScaleTokens.ArcMediumRoundness), + FontVariation.Setting("wdth", WearComposeMaterial3TypeScaleTokens.ArcMediumWidth), + FontVariation.Setting("wght", WearComposeMaterial3TypeScaleTokens.ArcMediumWeight), + ) + val ArcSmallVariationSettings = + FontVariation.Settings( + FontVariation.Setting("ROND", WearComposeMaterial3TypeScaleTokens.ArcSmallRoundness), + FontVariation.Setting("wdth", WearComposeMaterial3TypeScaleTokens.ArcSmallWidth), + FontVariation.Setting("wght", WearComposeMaterial3TypeScaleTokens.ArcSmallWeight), + ) + val BodyExtraSmallVariationSettings = + FontVariation.Settings( + FontVariation.Setting( + "ROND", + WearComposeMaterial3TypeScaleTokens.BodyExtraSmallRoundness, + ), + FontVariation.Setting("wdth", WearComposeMaterial3TypeScaleTokens.BodyExtraSmallWidth), + FontVariation.Setting("wght", WearComposeMaterial3TypeScaleTokens.BodyExtraSmallWeight), + ) + val BodyLargeVariationSettings = + FontVariation.Settings( + FontVariation.Setting("ROND", WearComposeMaterial3TypeScaleTokens.BodyLargeRoundness), + FontVariation.Setting("wdth", WearComposeMaterial3TypeScaleTokens.BodyLargeWidth), + FontVariation.Setting("wght", WearComposeMaterial3TypeScaleTokens.BodyLargeWeight), + ) + val BodyMediumVariationSettings = + FontVariation.Settings( + FontVariation.Setting("ROND", WearComposeMaterial3TypeScaleTokens.BodyMediumRoundness), + FontVariation.Setting("wdth", WearComposeMaterial3TypeScaleTokens.BodyMediumWidth), + FontVariation.Setting("wght", WearComposeMaterial3TypeScaleTokens.BodyMediumWeight), + ) + val BodySmallVariationSettings = + FontVariation.Settings( + FontVariation.Setting("ROND", WearComposeMaterial3TypeScaleTokens.BodySmallRoundness), + FontVariation.Setting("wdth", WearComposeMaterial3TypeScaleTokens.BodySmallWidth), + FontVariation.Setting("wght", WearComposeMaterial3TypeScaleTokens.BodySmallWeight), + ) + val DisplayLargeVariationSettings = + FontVariation.Settings( + FontVariation.Setting( + "ROND", + WearComposeMaterial3TypeScaleTokens.DisplayLargeRoundness, + ), + FontVariation.Setting("wdth", WearComposeMaterial3TypeScaleTokens.DisplayLargeWidth), + FontVariation.Setting("wght", WearComposeMaterial3TypeScaleTokens.DisplayLargeWeight), + ) + val DisplayMediumVariationSettings = + FontVariation.Settings( + FontVariation.Setting( + "ROND", + WearComposeMaterial3TypeScaleTokens.DisplayMediumRoundness, + ), + FontVariation.Setting("wdth", WearComposeMaterial3TypeScaleTokens.DisplayMediumWidth), + FontVariation.Setting("wght", WearComposeMaterial3TypeScaleTokens.DisplayMediumWeight), + ) + val DisplaySmallVariationSettings = + FontVariation.Settings( + FontVariation.Setting( + "ROND", + WearComposeMaterial3TypeScaleTokens.DisplaySmallRoundness, + ), + FontVariation.Setting("wdth", WearComposeMaterial3TypeScaleTokens.DisplaySmallWidth), + FontVariation.Setting("wght", WearComposeMaterial3TypeScaleTokens.DisplaySmallWeight), + ) + val LabelLargeVariationSettings = + FontVariation.Settings( + FontVariation.Setting("ROND", WearComposeMaterial3TypeScaleTokens.LabelLargeRoundness), + FontVariation.Setting("wdth", WearComposeMaterial3TypeScaleTokens.LabelLargeWidth), + FontVariation.Setting("wght", WearComposeMaterial3TypeScaleTokens.LabelLargeWeight), + ) + val LabelMediumVariationSettings = + FontVariation.Settings( + FontVariation.Setting("ROND", WearComposeMaterial3TypeScaleTokens.LabelMediumRoundness), + FontVariation.Setting("wdth", WearComposeMaterial3TypeScaleTokens.LabelMediumWidth), + FontVariation.Setting("wght", WearComposeMaterial3TypeScaleTokens.LabelMediumWeight), + ) + val LabelSmallVariationSettings = + FontVariation.Settings( + FontVariation.Setting("ROND", WearComposeMaterial3TypeScaleTokens.LabelSmallRoundness), + FontVariation.Setting("wdth", WearComposeMaterial3TypeScaleTokens.LabelSmallWidth), + FontVariation.Setting("wght", WearComposeMaterial3TypeScaleTokens.LabelSmallWeight), + ) + val NumeralExtraLargeVariationSettings = + FontVariation.Settings( + FontVariation.Setting( + "ROND", + WearComposeMaterial3TypeScaleTokens.NumeralExtraLargeRoundness, + ), + FontVariation.Setting( + "wdth", + WearComposeMaterial3TypeScaleTokens.NumeralExtraLargeWidth, + ), + FontVariation.Setting( + "wght", + WearComposeMaterial3TypeScaleTokens.NumeralExtraLargeWeight, + ), + ) + val NumeralExtraSmallVariationSettings = + FontVariation.Settings( + FontVariation.Setting( + "ROND", + WearComposeMaterial3TypeScaleTokens.NumeralExtraSmallRoundness, + ), + FontVariation.Setting( + "wdth", + WearComposeMaterial3TypeScaleTokens.NumeralExtraSmallWidth, + ), + FontVariation.Setting( + "wght", + WearComposeMaterial3TypeScaleTokens.NumeralExtraSmallWeight, + ), + ) + val NumeralLargeVariationSettings = + FontVariation.Settings( + FontVariation.Setting( + "ROND", + WearComposeMaterial3TypeScaleTokens.NumeralLargeRoundness, + ), + FontVariation.Setting("wdth", WearComposeMaterial3TypeScaleTokens.NumeralLargeWidth), + FontVariation.Setting("wght", WearComposeMaterial3TypeScaleTokens.NumeralLargeWeight), + ) + val NumeralMediumVariationSettings = + FontVariation.Settings( + FontVariation.Setting( + "ROND", + WearComposeMaterial3TypeScaleTokens.NumeralMediumRoundness, + ), + FontVariation.Setting("wdth", WearComposeMaterial3TypeScaleTokens.NumeralMediumWidth), + FontVariation.Setting("wght", WearComposeMaterial3TypeScaleTokens.NumeralMediumWeight), + ) + val NumeralSmallVariationSettings = + FontVariation.Settings( + FontVariation.Setting( + "ROND", + WearComposeMaterial3TypeScaleTokens.NumeralSmallRoundness, + ), + FontVariation.Setting("wdth", WearComposeMaterial3TypeScaleTokens.NumeralSmallWidth), + FontVariation.Setting("wght", WearComposeMaterial3TypeScaleTokens.NumeralSmallWeight), + ) + val TitleLargeVariationSettings = + FontVariation.Settings( + FontVariation.Setting("ROND", WearComposeMaterial3TypeScaleTokens.TitleLargeRoundness), + FontVariation.Setting("wdth", WearComposeMaterial3TypeScaleTokens.TitleLargeWidth), + FontVariation.Setting("wght", WearComposeMaterial3TypeScaleTokens.TitleLargeWeight), + ) + val TitleMediumVariationSettings = + FontVariation.Settings( + FontVariation.Setting("ROND", WearComposeMaterial3TypeScaleTokens.TitleMediumRoundness), + FontVariation.Setting("wdth", WearComposeMaterial3TypeScaleTokens.TitleMediumWidth), + FontVariation.Setting("wght", WearComposeMaterial3TypeScaleTokens.TitleMediumWeight), + ) + val TitleSmallVariationSettings = + FontVariation.Settings( + FontVariation.Setting("ROND", WearComposeMaterial3TypeScaleTokens.TitleSmallRoundness), + FontVariation.Setting("wdth", WearComposeMaterial3TypeScaleTokens.TitleSmallWidth), + FontVariation.Setting("wght", WearComposeMaterial3TypeScaleTokens.TitleSmallWeight), + ) +} 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 new file mode 100644 index 000000000..8aeb5f74d --- /dev/null +++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/theme/WearOverlayableMaterial3Theme.kt @@ -0,0 +1,41 @@ +/* + * 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.content.Context +import android.os.Build +import androidx.annotation.RequiresApi + +/** + * 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 933cf19f9..8823bee07 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 @@ -1,3 +1,18 @@ +/* + * 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.content.Context @@ -14,11 +29,71 @@ 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.permissioncontroller.R +import com.android.permissioncontroller.permission.ui.wear.theme.WearPermissionMaterialUIVersion.MATERIAL2_5 +import com.android.permissioncontroller.permission.ui.wear.theme.WearPermissionMaterialUIVersion.MATERIAL3 + +/** This enum is used to specify the material version used for a specific screen */ +enum class WearPermissionMaterialUIVersion { + MATERIAL2_5, + MATERIAL3, +} + +/** + * Supports both Material 3 and Material 2_5 theme. default version for permission theme will be 2_5 + * until we migrate enough screens to 3. 2_5 version will use material 3 overlay resources if we + * enable material3 for even one screen (Permission screens will be migrated in phases). + */ +@Composable +fun WearPermissionTheme( + version: WearPermissionMaterialUIVersion = MATERIAL2_5, + content: @Composable () -> Unit, +) { + 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 -/** The Material 3 Theme Wrapper for Supporting RRO. */ + // Material3 UI controls are still being used in the screen that the theme is applied + if (version == MATERIAL3) { + val material3Theme = WearOverlayableMaterial3Theme(LocalContext.current) + Material3Theme( + colorScheme = material3Theme.colorScheme, + typography = material3Theme.typography, + shapes = material3Theme.shapes, + content = content, + ) + } + // Material2_5 UI controls are still being used in the screen that the theme is applied, + // 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) { + val material3Theme = WearOverlayableMaterial3Theme(LocalContext.current) + val bridgedLegacyTheme = WearMaterialBridgedLegacyTheme.createFrom(material3Theme) + MaterialTheme( + colors = bridgedLegacyTheme.colors, + typography = bridgedLegacyTheme.typography, + shapes = bridgedLegacyTheme.shapes, + content = content, + ) + } + // We are not ready for material3 yet in any screens. + 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) { diff --git a/PermissionController/src/com/android/permissioncontroller/permission/utils/PermissionMapping.kt b/PermissionController/src/com/android/permissioncontroller/permission/utils/PermissionMapping.kt index 0d1d960ab..a3446f802 100644 --- a/PermissionController/src/com/android/permissioncontroller/permission/utils/PermissionMapping.kt +++ b/PermissionController/src/com/android/permissioncontroller/permission/utils/PermissionMapping.kt @@ -139,7 +139,6 @@ object PermissionMapping { PLATFORM_PERMISSIONS[Manifest.permission.NEARBY_WIFI_DEVICES] = Manifest.permission_group.NEARBY_DEVICES } - // Ranging permission will be supported from Android B+, update this when isAtLeastB() // is available. if (SdkLevel.isAtLeastV() && Flags.rangingPermissionEnabled()) { diff --git a/PermissionController/src/com/android/permissioncontroller/permission/utils/Utils.java b/PermissionController/src/com/android/permissioncontroller/permission/utils/Utils.java index e5de63f32..3d3b47272 100644 --- a/PermissionController/src/com/android/permissioncontroller/permission/utils/Utils.java +++ b/PermissionController/src/com/android/permissioncontroller/permission/utils/Utils.java @@ -126,6 +126,7 @@ import java.util.List; import java.util.Locale; import java.util.Random; import java.util.Set; +import java.util.function.Supplier; public final class Utils { @@ -1566,18 +1567,40 @@ public final class Utils { public static String getEnterpriseString(@NonNull Context context, @NonNull String updatableStringId, int defaultStringId, @NonNull Object... formatArgs) { return SdkLevel.isAtLeastT() - ? getUpdatableEnterpriseString( - context, updatableStringId, defaultStringId, formatArgs) + ? getUpdatableEnterpriseString(context, updatableStringId, + () -> context.getString(defaultStringId, formatArgs), formatArgs) : context.getString(defaultStringId, formatArgs); } + /** + * Selects the appropriate enterprise string for the provided resource ID and a fallback string + */ + @NonNull + public static String getEnterpriseString(@NonNull Context context, + @NonNull String updatableStringId, @NonNull String defaultString) { + return SdkLevel.isAtLeastT() + ? getUpdatableEnterpriseString(context, updatableStringId, () -> defaultString) + : defaultString; + } + @RequiresApi(Build.VERSION_CODES.TIRAMISU) @NonNull private static String getUpdatableEnterpriseString(@NonNull Context context, - @NonNull String updatableStringId, int defaultStringId, @NonNull Object... formatArgs) { + @NonNull String updatableStringId, @NonNull Supplier<String> defaultStringLoader, + @NonNull Object... formatArgs) { DevicePolicyManager dpm = getSystemServiceSafe(context, DevicePolicyManager.class); - return dpm.getResources().getString(updatableStringId, () -> context.getString( - defaultStringId, formatArgs), formatArgs); + return dpm.getResources().getString(updatableStringId, defaultStringLoader, formatArgs); + } + + /** + * Returns the profile label from the {@link UserManager} for the provided profile + */ + @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM) + @NonNull + public static String getProfileLabel(@NonNull UserHandle profile, @NonNull Context context) { + Context profileContext = context.createContextAsUser(profile, 0); + UserManager profileUserManager = profileContext.getSystemService(UserManager.class); + return profileUserManager.getProfileLabel(); } /** diff --git a/PermissionController/src/com/android/permissioncontroller/role/Role.md b/PermissionController/src/com/android/permissioncontroller/role/Role.md index d4a514784..255214495 100644 --- a/PermissionController/src/com/android/permissioncontroller/role/Role.md +++ b/PermissionController/src/com/android/permissioncontroller/role/Role.md @@ -62,6 +62,13 @@ is optional and defaults to `false`. the Java method on the `Flags` class which will be invoked via reflection. Note that any new aconfig library dependency will need corresponding jarjar rules for PermissionController and the system service JAR. +- `ignoreDisabledSystemPackageWhenGranting`: Whether the role should ignore the requested +permissions of the disabled system package (if any) when granting permissions. If `false`, the +permission will need to be requested by the disabled system package as well, if there is one. This +attribute is optional and defaults to the opposite of `systemOnly` on Android S+, or `true` below +Android S. **Note:** Extra care should be taken when adding a runtime permission to a role with +this attribute explicitly set to `true`, because that may allow apps to update and silently obtain +a new runtime permission. - `label`: The string resource for the label of the role, e.g. `@string/role_sms_label`, which says "Default SMS app". For default apps, this string will appear in the default app detail page as the title. This attribute is required if the role is `visible`. diff --git a/PermissionController/src/com/android/permissioncontroller/role/ui/DefaultAppListChildFragment.java b/PermissionController/src/com/android/permissioncontroller/role/ui/DefaultAppListChildFragment.java index 0b96eb8ba..48472bc5e 100644 --- a/PermissionController/src/com/android/permissioncontroller/role/ui/DefaultAppListChildFragment.java +++ b/PermissionController/src/com/android/permissioncontroller/role/ui/DefaultAppListChildFragment.java @@ -22,6 +22,7 @@ import android.content.Intent; import android.content.pm.ApplicationInfo; import android.os.Bundle; import android.os.UserHandle; +import android.permission.flags.Flags; import android.provider.Settings; import android.util.ArrayMap; @@ -36,6 +37,7 @@ import androidx.preference.PreferenceGroup; import androidx.preference.PreferenceManager; import androidx.preference.PreferenceScreen; +import com.android.modules.utils.build.SdkLevel; import com.android.permissioncontroller.R; import com.android.permissioncontroller.permission.utils.Utils; import com.android.permissioncontroller.role.utils.PackageUtils; @@ -145,15 +147,25 @@ public class DefaultAppListChildFragment<PF extends PreferenceFragmentCompat addMoreDefaultAppsPreference(preferenceScreen, oldPreferences, context); addManageDomainUrlsPreference(preferenceScreen, oldPreferences, context); if (hasWorkProfile && !workRoleItems.isEmpty()) { + String defaultWorkTitle; + if (SdkLevel.isAtLeastV() && Flags.useProfileLabelsForDefaultAppSectionTitles()) { + defaultWorkTitle = Utils.getProfileLabel(mViewModel.getWorkProfile(), context); + } else { + defaultWorkTitle = context.getString(R.string.default_apps_for_work); + } String workTitle = Utils.getEnterpriseString(context, - DefaultAppSettings.WORK_PROFILE_DEFAULT_APPS_TITLE, - R.string.default_apps_for_work); + DefaultAppSettings.WORK_PROFILE_DEFAULT_APPS_TITLE, defaultWorkTitle); addPreferenceCategory(oldWorkPreferenceCategory, PREFERENCE_KEY_WORK_CATEGORY, workTitle, preferenceScreen, workRoleItems, oldWorkPreferences, this, mViewModel.getWorkProfile(), context); } if (hasPrivateProfile && !privateRoleItems.isEmpty()) { - String privateTitle = context.getString(R.string.default_apps_for_private_profile); + String privateTitle; + if (SdkLevel.isAtLeastV() && Flags.useProfileLabelsForDefaultAppSectionTitles()) { + privateTitle = Utils.getProfileLabel(mViewModel.getPrivateProfile(), context); + } else { + privateTitle = context.getString(R.string.default_apps_for_private_profile); + } addPreferenceCategory(oldPrivatePreferenceCategory, PREFERENCE_KEY_PRIVATE_CATEGORY, privateTitle, preferenceScreen, privateRoleItems, oldPrivatePreferences, this, mViewModel.getPrivateProfile(), context); 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 13a9cb6d6..aa9b31e0d 100644 --- a/PermissionController/src/com/android/permissioncontroller/role/ui/wear/WearRequestRoleScreen.kt +++ b/PermissionController/src/com/android/permissioncontroller/role/ui/wear/WearRequestRoleScreen.kt @@ -30,22 +30,26 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import androidx.wear.compose.material.ChipDefaults -import androidx.wear.compose.material.MaterialTheme import com.android.permissioncontroller.R -import com.android.permissioncontroller.permission.ui.wear.elements.Chip -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.toggleChipBackgroundColors +import com.android.permissioncontroller.permission.ui.wear.elements.material3.WearPermissionButton +import com.android.permissioncontroller.permission.ui.wear.elements.material3.WearPermissionButtonStyle +import com.android.permissioncontroller.permission.ui.wear.elements.material3.WearPermissionIconBuilder +import com.android.permissioncontroller.permission.ui.wear.elements.material3.WearPermissionListFooter +import com.android.permissioncontroller.permission.ui.wear.elements.material3.WearPermissionToggleControl +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.ui.ManageRoleHolderStateLiveData @Composable fun WearRequestRoleScreen( helper: WearRequestRoleHelper, onSetAsDefault: (Boolean, String?) -> Unit, - onCanceled: () -> Unit + onCanceled: () -> Unit, ) { val roleLiveData = helper.viewModel.roleLiveData.observeAsState(emptyList()) val manageRoleHolderState = @@ -74,8 +78,14 @@ fun WearRequestRoleScreen( helper.initializeSelectedPackageName() } } - + val materialUIVersion = + if (ResourceHelper.material3Enabled) { + MATERIAL3 + } else { + MATERIAL2_5 + } WearRequestRoleContent( + materialUIVersion, isLoading, helper, roleLiveData.value, @@ -85,7 +95,7 @@ fun WearRequestRoleScreen( onCheckedChanged, onDontAskAgainCheckedChanged, onSetAsDefault, - onCanceled + onCanceled, ) if (isLoading && roleLiveData.value.isNotEmpty()) { @@ -95,6 +105,7 @@ fun WearRequestRoleScreen( @Composable internal fun WearRequestRoleContent( + materialUIVersion: WearPermissionMaterialUIVersion, isLoading: Boolean, helper: WearRequestRoleHelper, qualifyingApplications: List<Pair<ApplicationInfo, Boolean>>, @@ -104,56 +115,74 @@ internal fun WearRequestRoleContent( onCheckedChanged: (Boolean, String?, Boolean) -> Unit, onDontAskAgainCheckedChanged: (Boolean) -> Unit, onSetAsDefault: (Boolean, String?) -> Unit, - onCanceled: () -> Unit + onCanceled: () -> Unit, ) { ScrollableScreen( + materialUIVersion = materialUIVersion, image = helper.getIcon(), title = helper.getTitle(), showTimeText = false, - isLoading = isLoading + isLoading = isLoading, ) { - helper.getNonePreference(qualifyingApplications, selectedPackageName)?.let { + helper.getNonePreference(qualifyingApplications, selectedPackageName)?.let { pref -> item { - ToggleChip( - label = it.label, - icon = it.icon, - enabled = enabled && it.enabled, - checked = it.checked, + WearPermissionToggleControl( + materialUIVersion = materialUIVersion, + label = pref.label, + iconBuilder = pref.icon?.let { WearPermissionIconBuilder.builder(it) }, + enabled = enabled && pref.enabled, + checked = pref.checked, onCheckedChanged = { checked -> - run { onCheckedChanged(checked, it.packageName, it.isHolder) } + onCheckedChanged(checked, pref.packageName, pref.isHolder) }, toggleControl = ToggleChipToggleControl.Radio, - labelMaxLine = Integer.MAX_VALUE + labelMaxLines = Integer.MAX_VALUE, ) } - it.subTitle?.let { subTitle -> item { ListFooter(description = subTitle) } } + pref.subTitle?.let { subTitle -> + item { + WearPermissionListFooter( + materialUIVersion = materialUIVersion, + label = subTitle, + ) + } + } } for (pref in helper.getPreferences(qualifyingApplications, selectedPackageName)) { item { - ToggleChip( + WearPermissionToggleControl( + materialUIVersion = materialUIVersion, label = pref.label, - icon = pref.icon, + iconBuilder = pref.icon?.let { WearPermissionIconBuilder.builder(it) }, enabled = enabled && pref.enabled, checked = pref.checked, onCheckedChanged = { checked -> - run { onCheckedChanged(checked, pref.packageName, pref.isHolder) } + onCheckedChanged(checked, pref.packageName, pref.isHolder) }, toggleControl = ToggleChipToggleControl.Radio, ) } - pref.subTitle?.let { subTitle -> item { ListFooter(description = subTitle) } } + pref.subTitle?.let { subTitle -> + item { + WearPermissionListFooter( + materialUIVersion = materialUIVersion, + label = subTitle, + ) + } + } } if (helper.showDontAskButton()) { item { - ToggleChip( + WearPermissionToggleControl( + materialUIVersion = materialUIVersion, checked = dontAskAgain, enabled = enabled, onCheckedChanged = { checked -> run { onDontAskAgainCheckedChanged(checked) } }, label = stringResource(R.string.request_role_dont_ask_again), toggleControl = ToggleChipToggleControl.Checkbox, - colors = toggleChipBackgroundColors(), + style = WearPermissionToggleControlStyle.Transparent, modifier = Modifier.testTag("com.android.permissioncontroller:id/dont_ask_again"), ) @@ -163,17 +192,18 @@ internal fun WearRequestRoleContent( item { Spacer(modifier = Modifier.height(14.dp)) } item { - Chip( + WearPermissionButton( + materialUIVersion = materialUIVersion, label = stringResource(R.string.request_role_set_as_default), - textColor = MaterialTheme.colors.background, - colors = ChipDefaults.primaryChipColors(), + style = WearPermissionButtonStyle.Primary, enabled = helper.shouldSetAsDefaultEnabled(enabled), onClick = { onSetAsDefault(dontAskAgain, selectedPackageName) }, modifier = Modifier.testTag("android:id/button1"), ) } item { - Chip( + WearPermissionButton( + materialUIVersion = materialUIVersion, label = stringResource(R.string.cancel), enabled = enabled, onClick = { onCanceled() }, diff --git a/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/ClickableDisabledSwitchPreference.java b/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/ClickableDisabledSwitchPreference.java index 3ab4faa66..54cb86ff3 100644 --- a/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/ClickableDisabledSwitchPreference.java +++ b/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/ClickableDisabledSwitchPreference.java @@ -26,7 +26,7 @@ import android.view.ViewGroup; import androidx.annotation.RequiresApi; import androidx.fragment.app.Fragment; import androidx.preference.PreferenceViewHolder; -import androidx.preference.SwitchPreference; +import androidx.preference.SwitchPreferenceCompat; import com.android.permissioncontroller.R; import com.android.permissioncontroller.safetycenter.ui.model.PrivacyControlsViewModel; @@ -38,7 +38,7 @@ import com.android.permissioncontroller.safetycenter.ui.model.PrivacyControlsVie * method will not register any changes while it appears disabled. */ @RequiresApi(TIRAMISU) -public class ClickableDisabledSwitchPreference extends SwitchPreference { +public class ClickableDisabledSwitchPreference extends SwitchPreferenceCompat { private boolean mAppearDisabled; diff --git a/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/IssueCardPreference.java b/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/IssueCardPreference.java index 7622270b9..88759797e 100644 --- a/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/IssueCardPreference.java +++ b/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/IssueCardPreference.java @@ -52,6 +52,7 @@ import androidx.preference.PreferenceViewHolder; import com.android.modules.utils.build.SdkLevel; import com.android.permissioncontroller.R; import com.android.permissioncontroller.safetycenter.ui.model.SafetyCenterViewModel; +import com.android.settingslib.widget.GroupSectionDividerMixin; import com.google.android.material.button.MaterialButton; import com.google.android.material.shape.AbsoluteCornerSize; @@ -62,7 +63,8 @@ import java.util.Objects; /** A preference that displays a card representing a {@link SafetyCenterIssue}. */ @RequiresApi(TIRAMISU) -public class IssueCardPreference extends Preference implements ComparablePreference { +public class IssueCardPreference extends Preference + implements ComparablePreference, GroupSectionDividerMixin { public static final String TAG = IssueCardPreference.class.getSimpleName(); @@ -101,12 +103,13 @@ public class IssueCardPreference extends Preference implements ComparablePrefere public void onBindViewHolder(PreferenceViewHolder holder) { super.onBindViewHolder(holder); - holder.itemView.setBackgroundResource(mPositionInCardList.getBackgroundDrawableResId()); + View issueCardView = holder.itemView.requireViewById(R.id.issue_card); + issueCardView.setBackgroundResource(mPositionInCardList.getBackgroundDrawableResId()); int topMargin = getTopMargin(mPositionInCardList, getContext()); - MarginLayoutParams layoutParams = (MarginLayoutParams) holder.itemView.getLayoutParams(); + MarginLayoutParams layoutParams = (MarginLayoutParams) issueCardView.getLayoutParams(); if (layoutParams.topMargin != topMargin) { layoutParams.topMargin = topMargin; - holder.itemView.setLayoutParams(layoutParams); + issueCardView.setLayoutParams(layoutParams); } // Set default group visibility in case view is being reused @@ -202,19 +205,20 @@ public class IssueCardPreference extends Preference implements ComparablePrefere private void configureSafetyProtectionView(PreferenceViewHolder holder) { View safetyProtectionSectionView = holder.findViewById(R.id.issue_card_protected_by_android); + View issueCard = holder.findViewById(R.id.issue_card); if (safetyProtectionSectionView.getVisibility() == View.GONE) { - holder.itemView.setPaddingRelative( - holder.itemView.getPaddingStart(), - holder.itemView.getPaddingTop(), - holder.itemView.getPaddingEnd(), + issueCard.setPaddingRelative( + issueCard.getPaddingStart(), + issueCard.getPaddingTop(), + issueCard.getPaddingEnd(), /* bottom= */ getContext() .getResources() .getDimensionPixelSize(R.dimen.sc_card_margin_bottom)); } else { - holder.itemView.setPaddingRelative( - holder.itemView.getPaddingStart(), - holder.itemView.getPaddingTop(), - holder.itemView.getPaddingEnd(), + issueCard.setPaddingRelative( + issueCard.getPaddingStart(), + issueCard.getPaddingTop(), + issueCard.getPaddingEnd(), /* bottom= */ 0); } } @@ -427,9 +431,7 @@ public class IssueCardPreference extends Preference implements ComparablePrefere TypedValue buttonThemeValue = new TypedValue(); mContext.getTheme() .resolveAttribute( - R.attr.scActionButtonTheme, - buttonThemeValue, - /* resolveRefs= */ false); + R.attr.scActionButtonTheme, buttonThemeValue, /* resolveRefs= */ false); mContextThemeWrapper = new ContextThemeWrapper(context, buttonThemeValue.data); } diff --git a/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/MoreIssuesCardPreference.kt b/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/MoreIssuesCardPreference.kt index a63e19984..5c86b4515 100644 --- a/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/MoreIssuesCardPreference.kt +++ b/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/MoreIssuesCardPreference.kt @@ -24,6 +24,7 @@ import androidx.preference.Preference import androidx.preference.PreferenceViewHolder import com.android.permissioncontroller.R import com.android.permissioncontroller.safetycenter.ui.view.MoreIssuesHeaderView +import com.android.settingslib.widget.GroupSectionDividerMixin /** A preference that displays a card linking to a list of more {@link SafetyCenterIssue}. */ @RequiresApi(Build.VERSION_CODES.TIRAMISU) @@ -34,8 +35,8 @@ internal class MoreIssuesCardPreference( private var newMoreIssuesCardData: MoreIssuesCardData, private val dismissedOnly: Boolean, val isStaticHeader: Boolean, - private val onClickListener: () -> Unit -) : Preference(context), ComparablePreference { + private val onClickListener: () -> Unit, +) : Preference(context), ComparablePreference, GroupSectionDividerMixin { init { layoutResource = R.layout.preference_more_issues_card @@ -44,11 +45,12 @@ internal class MoreIssuesCardPreference( override fun onBindViewHolder(holder: PreferenceViewHolder) { super.onBindViewHolder(holder) - val issueHeaderView = holder.itemView as MoreIssuesHeaderView + val issueHeaderView = + holder.itemView.requireViewById<MoreIssuesHeaderView>(R.id.more_issues_card) if (isStaticHeader) { issueHeaderView.showStaticHeader( context.getString(R.string.safety_center_dismissed_issues_card_title), - newMoreIssuesCardData.severityLevel + newMoreIssuesCardData.severityLevel, ) } else { issueHeaderView.showExpandableHeader( @@ -62,7 +64,7 @@ internal class MoreIssuesCardPreference( } ), overrideChevronIconResId, - onClickListener + onClickListener, ) } } @@ -94,5 +96,5 @@ internal class MoreIssuesCardPreference( internal data class MoreIssuesCardData( val severityLevel: Int, val hiddenIssueCount: Int, - val isExpanded: Boolean + val isExpanded: Boolean, ) diff --git a/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/SafetyBrandChipPreference.kt b/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/SafetyBrandChipPreference.kt index 57e4175ca..c5287af53 100644 --- a/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/SafetyBrandChipPreference.kt +++ b/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/SafetyBrandChipPreference.kt @@ -28,11 +28,12 @@ import androidx.preference.PreferenceViewHolder import com.android.permissioncontroller.Constants.EXTRA_SESSION_ID import com.android.permissioncontroller.R import com.android.permissioncontroller.safetycenter.SafetyCenterConstants +import com.android.settingslib.widget.GroupSectionDividerMixin /** A preference that displays the Security and Privacy brand name on a Safety Center subpage. */ @RequiresApi(UPSIDE_DOWN_CAKE) internal class SafetyBrandChipPreference(context: Context, attrs: AttributeSet) : - Preference(context, attrs) { + Preference(context, attrs), GroupSectionDividerMixin { init { setLayoutResource(R.layout.preference_brand_chip) @@ -67,7 +68,7 @@ internal class SafetyBrandChipPreference(context: Context, attrs: AttributeSet) fun closeSubpage( fragmentActivity: FragmentActivity, fragmentContext: Context, - sessionId: Long + sessionId: Long, ) { val openedFromHomepage = fragmentActivity diff --git a/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/SafetyCenterActivity.java b/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/SafetyCenterActivity.java index c6f2d146f..04206479f 100644 --- a/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/SafetyCenterActivity.java +++ b/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/SafetyCenterActivity.java @@ -29,6 +29,8 @@ import static com.android.permissioncontroller.safetycenter.SafetyCenterConstant import static com.android.permissioncontroller.safetycenter.SafetyCenterConstants.PRIVATE_PROFILE_SUFFIX; import static com.android.permissioncontroller.safetycenter.SafetyCenterConstants.WORK_PROFILE_SUFFIX; +import static java.util.Objects.requireNonNull; + import android.app.ActionBar; import android.content.Intent; import android.content.res.Configuration; @@ -60,6 +62,7 @@ import com.android.permissioncontroller.permission.utils.Utils; import com.android.permissioncontroller.safetycenter.ui.model.PrivacyControlsViewModel.Pref; import com.android.settingslib.activityembedding.ActivityEmbeddingUtils; import com.android.settingslib.collapsingtoolbar.CollapsingToolbarBaseActivity; +import com.android.settingslib.widget.SettingsThemeHelper; import java.util.List; import java.util.Objects; @@ -78,10 +81,10 @@ public final class SafetyCenterActivity extends CollapsingToolbarBaseActivity { private static final String EXTRA_PREVENT_TRAMPOLINE_TO_SETTINGS = "com.android.permissioncontroller.safetycenter.extra.PREVENT_TRAMPOLINE_TO_SETTINGS"; - private SafetyCenterManager mSafetyCenterManager; + @Nullable private SafetyCenterManager mSafetyCenterManager; @Override - public void onCreate(Bundle savedInstanceState) { + public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); mSafetyCenterManager = getSystemService(SafetyCenterManager.class); @@ -93,18 +96,25 @@ public final class SafetyCenterActivity extends CollapsingToolbarBaseActivity { return; } + if (SettingsThemeHelper.isExpressiveTheme(this)) { + // Setting a theme programmatically causes standard preferences to display weirdly. + // See b/377519324. + setTheme(R.style.Theme_SafetyCenterExpressive); + } + Fragment frag; + Intent intent = getIntent(); final boolean maybeOpenSubpage = SafetyCenterUiFlags.getShowSubpages() - && getIntent().getAction().equals(ACTION_SAFETY_CENTER); - if (maybeOpenSubpage && getIntent().hasExtra(EXTRA_SAFETY_SOURCES_GROUP_ID)) { - String groupId = getIntent().getStringExtra(EXTRA_SAFETY_SOURCES_GROUP_ID); + && Objects.equals(intent.getAction(), ACTION_SAFETY_CENTER); + if (maybeOpenSubpage && intent.hasExtra(EXTRA_SAFETY_SOURCES_GROUP_ID)) { + String groupId = intent.getStringExtra(EXTRA_SAFETY_SOURCES_GROUP_ID); frag = openRelevantSubpage(groupId); - } else if (maybeOpenSubpage && getIntent().hasExtra(EXTRA_SETTINGS_FRAGMENT_ARGS_KEY)) { - String preferenceKey = getIntent().getStringExtra(EXTRA_SETTINGS_FRAGMENT_ARGS_KEY); + } else if (maybeOpenSubpage && intent.hasExtra(EXTRA_SETTINGS_FRAGMENT_ARGS_KEY)) { + String preferenceKey = intent.getStringExtra(EXTRA_SETTINGS_FRAGMENT_ARGS_KEY); String groupId = getParentGroupId(preferenceKey); frag = openRelevantSubpage(groupId); - } else if (getIntent().getAction().equals(PRIVACY_CONTROLS_ACTION)) { + } else if (Objects.equals(intent.getAction(), PRIVACY_CONTROLS_ACTION)) { setTitle(R.string.privacy_controls_title); frag = PrivacyControlsFragment.newInstance(); } else { @@ -306,7 +316,8 @@ public final class SafetyCenterActivity extends CollapsingToolbarBaseActivity { return PRIVACY_SOURCES_GROUP_ID; } - SafetyCenterConfig safetyCenterConfig = mSafetyCenterManager.getSafetyCenterConfig(); + SafetyCenterConfig safetyCenterConfig = + requireNonNull(mSafetyCenterManager).getSafetyCenterConfig(); String[] splitKey; if (preferenceKey.endsWith(PERSONAL_PROFILE_SUFFIX)) { splitKey = preferenceKey.split("_" + PERSONAL_PROFILE_SUFFIX); diff --git a/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/SafetyCenterDashboardFragment.java b/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/SafetyCenterDashboardFragment.java index efbd57080..ed6bc382c 100644 --- a/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/SafetyCenterDashboardFragment.java +++ b/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/SafetyCenterDashboardFragment.java @@ -56,6 +56,7 @@ import com.android.permissioncontroller.safetycenter.ui.model.SafetyCenterUiData import com.android.permissioncontroller.safetycenter.ui.model.StatusUiData; import com.android.safetycenter.internaldata.SafetyCenterBundles; import com.android.safetycenter.resources.SafetyCenterResourcesApk; +import com.android.settingslib.widget.SettingsThemeHelper; import kotlin.Unit; @@ -121,12 +122,16 @@ public final class SafetyCenterDashboardFragment extends SafetyCenterFragment { mEntriesGroup = getPreferenceScreen().findPreference(ENTRIES_GROUP_KEY); mStaticEntriesGroup = getPreferenceScreen().findPreference(STATIC_ENTRIES_GROUP_KEY); + Preference spacerPreference = getPreferenceScreen().findPreference(SPACER_KEY); + if (SettingsThemeHelper.isExpressiveTheme(requireContext())) { + getPreferenceScreen().removePreference(spacerPreference); + } + if (mIsQuickSettingsFragment) { getPreferenceScreen().removePreference(mEntriesGroup); mEntriesGroup = null; getPreferenceScreen().removePreference(mStaticEntriesGroup); mStaticEntriesGroup = null; - Preference spacerPreference = getPreferenceScreen().findPreference(SPACER_KEY); getPreferenceScreen().removePreference(spacerPreference); } getSafetyCenterViewModel().getStatusUiLiveData().observe(this, this::updateStatus); diff --git a/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/SafetyCenterFragment.kt b/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/SafetyCenterFragment.kt index 9feecf5d4..04503de5e 100644 --- a/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/SafetyCenterFragment.kt +++ b/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/SafetyCenterFragment.kt @@ -22,7 +22,6 @@ import android.safetycenter.SafetyCenterErrorDetails import android.widget.Toast import androidx.annotation.RequiresApi import androidx.lifecycle.ViewModelProvider -import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceScreen import androidx.recyclerview.widget.RecyclerView import com.android.permissioncontroller.Constants.EXTRA_SESSION_ID @@ -33,10 +32,12 @@ import com.android.permissioncontroller.safetycenter.ui.model.LiveSafetyCenterVi import com.android.permissioncontroller.safetycenter.ui.model.SafetyCenterUiData import com.android.permissioncontroller.safetycenter.ui.model.SafetyCenterViewModel import com.android.safetycenter.resources.SafetyCenterResourcesApk +import com.android.settingslib.widget.SettingsBasePreferenceFragment +import com.android.settingslib.widget.SettingsThemeHelper /** A base fragment that represents a page in Safety Center. */ @RequiresApi(TIRAMISU) -abstract class SafetyCenterFragment : PreferenceFragmentCompat() { +abstract class SafetyCenterFragment : SettingsBasePreferenceFragment() { lateinit var safetyCenterViewModel: SafetyCenterViewModel lateinit var sameTaskSourceIds: List<String> @@ -51,12 +52,17 @@ abstract class SafetyCenterFragment : PreferenceFragmentCompat() { override fun onCreateAdapter( preferenceScreen: PreferenceScreen - ): RecyclerView.Adapter<RecyclerView.ViewHolder> { + ): RecyclerView.Adapter<out RecyclerView.ViewHolder> { /* The scroll-to-result functionality for settings search is currently implemented only for * subpages i.e. non expand-and-collapse type entries. Hence, we check that the flag is * enabled before using an adapter that does the highlighting and scrolling. */ - val adapter: RecyclerView.Adapter<RecyclerView.ViewHolder> = - if (SafetyCenterUiFlags.getShowSubpages()) { + val adapter: RecyclerView.Adapter<out RecyclerView.ViewHolder> = + if ( + SafetyCenterUiFlags.getShowSubpages() && + !SettingsThemeHelper.isExpressiveTheme(requireContext()) + ) { + // TODO: b/378433878 - Create highlight adapter for settings expressive theme, which + // has a different base class. highlightManager.createAdapter(preferenceScreen) } else { super.onCreateAdapter(preferenceScreen) @@ -80,7 +86,7 @@ abstract class SafetyCenterFragment : PreferenceFragmentCompat() { safetyCenterViewModel = ViewModelProvider( requireActivity(), - LiveSafetyCenterViewModelFactory(requireActivity().getApplication()) + LiveSafetyCenterViewModelFactory(requireActivity().getApplication()), ) .get(SafetyCenterViewModel::class.java) safetyCenterViewModel.safetyCenterUiLiveData.observe(this) { uiData: SafetyCenterUiData? -> @@ -177,7 +183,7 @@ abstract class SafetyCenterFragment : PreferenceFragmentCompat() { safetyCenterViewModel.interactionLogger.recordForIssue( Action.SAFETY_CENTER_VIEWED, maybeIssue, - isDismissed = false + isDismissed = false, ) } } diff --git a/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/SafetyCenterQsActivity.java b/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/SafetyCenterQsActivity.java index 2ad282449..d9f45cc08 100644 --- a/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/SafetyCenterQsActivity.java +++ b/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/SafetyCenterQsActivity.java @@ -24,7 +24,9 @@ import android.permission.PermissionManager; import androidx.fragment.app.FragmentActivity; import com.android.modules.utils.build.SdkLevel; +import com.android.permissioncontroller.R; import com.android.permissioncontroller.permission.utils.Utils; +import com.android.settingslib.widget.SettingsThemeHelper; /** Activity for the Safety Center Quick Settings Activity */ public class SafetyCenterQsActivity extends FragmentActivity { @@ -39,6 +41,12 @@ public class SafetyCenterQsActivity extends FragmentActivity { return; } + if (SettingsThemeHelper.isExpressiveTheme(this)) { + // Safe to set expressive theme here since QS doesn't display vanilla preferences. + // See b/377519324. + setTheme(R.style.Theme_SafetyCenterQsExpressive); + } + configureFragment(); } diff --git a/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/SafetyCenterSubpageFragment.kt b/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/SafetyCenterSubpageFragment.kt index fdade2189..2e5ba49ae 100644 --- a/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/SafetyCenterSubpageFragment.kt +++ b/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/SafetyCenterSubpageFragment.kt @@ -28,6 +28,7 @@ import com.android.permissioncontroller.safetycenter.ui.SafetyBrandChipPreferenc import com.android.permissioncontroller.safetycenter.ui.model.SafetyCenterUiData import com.android.safetycenter.resources.SafetyCenterResourcesApk import com.android.settingslib.widget.FooterPreference +import com.android.settingslib.widget.SettingsThemeHelper /** A fragment that represents a generic subpage in Safety Center. */ @RequiresApi(UPSIDE_DOWN_CAKE) @@ -45,15 +46,16 @@ class SafetyCenterSubpageFragment : SafetyCenterFragment() { setPreferencesFromResource(R.xml.safety_center_subpage, rootKey) sourceGroupId = requireArguments().getString(SOURCE_GROUP_ID_KEY)!! - subpageBrandChip = getPreferenceScreen().findPreference(BRAND_CHIP_KEY)!! - subpageIllustration = getPreferenceScreen().findPreference(ILLUSTRATION_KEY)!! - subpageIssueGroup = getPreferenceScreen().findPreference(ISSUE_GROUP_KEY)!! - subpageEntryGroup = getPreferenceScreen().findPreference(ENTRY_GROUP_KEY)!! - subpageFooter = getPreferenceScreen().findPreference(FOOTER_KEY)!! + subpageBrandChip = preferenceScreen.findPreference(BRAND_CHIP_KEY)!! + subpageIllustration = preferenceScreen.findPreference(ILLUSTRATION_KEY)!! + subpageIssueGroup = preferenceScreen.findPreference(ISSUE_GROUP_KEY)!! + subpageEntryGroup = preferenceScreen.findPreference(ENTRY_GROUP_KEY)!! + subpageFooter = preferenceScreen.findPreference(FOOTER_KEY)!! subpageBrandChip.setupListener(requireActivity(), safetyCenterSessionId) setupIllustration() setupFooter() + maybeRemoveSpacer() prerenderCurrentSafetyCenterData() } @@ -80,7 +82,7 @@ class SafetyCenterSubpageFragment : SafetyCenterFragment() { return } - requireActivity().setTitle(entryGroup.title) + requireActivity().title = entryGroup.title updateSafetyCenterIssues(uiData) updateSafetyCenterEntries(entryGroup) } @@ -91,7 +93,7 @@ class SafetyCenterSubpageFragment : SafetyCenterFragment() { val drawable = SafetyCenterResourcesApk(context).getDrawableByName(resName, context.theme) if (drawable == null) { Log.w(TAG, "$sourceGroupId doesn't have any matching illustration") - subpageIllustration.setVisible(false) + subpageIllustration.isVisible = false } subpageIllustration.illustrationDrawable = drawable @@ -102,12 +104,19 @@ class SafetyCenterSubpageFragment : SafetyCenterFragment() { val footerText = SafetyCenterResourcesApk(requireContext()).getStringByName(resName) if (footerText.isEmpty()) { Log.w(TAG, "$sourceGroupId doesn't have any matching footer") - subpageFooter.setVisible(false) + subpageFooter.isVisible = false } // footer is ordered last by default // in order to keep a spacer after the footer, footer needs to be the second from last - subpageFooter.setOrder(Int.MAX_VALUE - 2) - subpageFooter.setSummary(footerText) + subpageFooter.order = Int.MAX_VALUE - 2 + subpageFooter.summary = footerText + } + + private fun maybeRemoveSpacer() { + if (SettingsThemeHelper.isExpressiveTheme(requireContext())) { + val spacerPreference = preferenceScreen.findPreference<SpacerPreference>(SPACER_KEY)!! + preferenceScreen.removePreference(spacerPreference) + } } private fun updateSafetyCenterIssues(uiData: SafetyCenterUiData?) { @@ -131,7 +140,7 @@ class SafetyCenterSubpageFragment : SafetyCenterFragment() { subpageIssues, subpageDismissedIssues, uiData.resolvedIssues, - requireActivity().getTaskId() + requireActivity().taskId, ) } @@ -145,23 +154,24 @@ class SafetyCenterSubpageFragment : SafetyCenterFragment() { PendingIntentSender.getTaskIdForEntry( entry.id, sameTaskSourceIds, - requireActivity() + requireActivity(), ), entry, - safetyCenterViewModel + safetyCenterViewModel, ) ) } } companion object { - private val TAG: String = SafetyCenterSubpageFragment::class.java.simpleName - private const val BRAND_CHIP_KEY: String = "subpage_brand_chip" - private const val ILLUSTRATION_KEY: String = "subpage_illustration" - private const val ISSUE_GROUP_KEY: String = "subpage_issue_group" - private const val ENTRY_GROUP_KEY: String = "subpage_entry_group" - private const val FOOTER_KEY: String = "subpage_footer" - private const val SOURCE_GROUP_ID_KEY: String = "source_group_id" + private val TAG = SafetyCenterSubpageFragment::class.java.simpleName + private const val BRAND_CHIP_KEY = "subpage_brand_chip" + private const val ILLUSTRATION_KEY = "subpage_illustration" + private const val ISSUE_GROUP_KEY = "subpage_issue_group" + private const val ENTRY_GROUP_KEY = "subpage_entry_group" + private const val FOOTER_KEY = "subpage_footer" + private const val SPACER_KEY = "subpage_spacer" + private const val SOURCE_GROUP_ID_KEY = "source_group_id" /** Creates an instance of SafetyCenterSubpageFragment with the arguments set */ @JvmStatic diff --git a/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/SafetyIllustrationPreference.kt b/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/SafetyIllustrationPreference.kt index 5acb27131..3976fa7e3 100644 --- a/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/SafetyIllustrationPreference.kt +++ b/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/SafetyIllustrationPreference.kt @@ -25,11 +25,12 @@ import androidx.annotation.RequiresApi import androidx.preference.Preference import androidx.preference.PreferenceViewHolder import com.android.permissioncontroller.R +import com.android.settingslib.widget.GroupSectionDividerMixin /** A preference that displays the illustration on a Safety Center subpage. */ @RequiresApi(UPSIDE_DOWN_CAKE) internal class SafetyIllustrationPreference(context: Context, attrs: AttributeSet) : - Preference(context, attrs) { + Preference(context, attrs), GroupSectionDividerMixin { init { layoutResource = R.layout.preference_illustration diff --git a/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/SafetyStatusPreference.java b/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/SafetyStatusPreference.java index 811841845..abf159955 100644 --- a/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/SafetyStatusPreference.java +++ b/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/SafetyStatusPreference.java @@ -40,6 +40,7 @@ import com.android.permissioncontroller.R; import com.android.permissioncontroller.safetycenter.ui.model.SafetyCenterViewModel; import com.android.permissioncontroller.safetycenter.ui.model.StatusUiData; import com.android.permissioncontroller.safetycenter.ui.view.StatusCardView; +import com.android.settingslib.widget.GroupSectionDividerMixin; import kotlin.Pair; @@ -48,7 +49,8 @@ import java.util.Objects; /** Preference which displays a visual representation of {@link SafetyCenterStatus}. */ @RequiresApi(TIRAMISU) -public class SafetyStatusPreference extends Preference implements ComparablePreference { +public class SafetyStatusPreference extends Preference + implements ComparablePreference, GroupSectionDividerMixin { private static final String TAG = "SafetyStatusPreference"; @@ -82,7 +84,8 @@ public class SafetyStatusPreference extends Preference implements ComparablePref } Context context = getContext(); - StatusCardView statusCardView = (StatusCardView) holder.itemView; + StatusCardView statusCardView = holder.itemView.requireViewById(R.id.status_card); + configureButtons(context, statusCardView); statusCardView .getTitleAndSummaryContainerView() diff --git a/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/SpacerPreference.kt b/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/SpacerPreference.kt index 030b67be9..7f619d1ca 100644 --- a/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/SpacerPreference.kt +++ b/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/SpacerPreference.kt @@ -55,6 +55,7 @@ internal class SpacerPreference(context: Context, attrs: AttributeSet) : } private var maxKnownToolbarHeight = 0 + override fun onBindViewHolder(holder: PreferenceViewHolder) { super.onBindViewHolder(holder) val spacer = holder.itemView @@ -74,7 +75,7 @@ internal class SpacerPreference(context: Context, attrs: AttributeSet) : oldLeft: Int, oldTop: Int, oldRight: Int, - oldBottom: Int + oldBottom: Int, ) { adjustHeight(spacer) } |