summaryrefslogtreecommitdiff
path: root/PermissionController/src
diff options
context:
space:
mode:
author Xin Li <delphij@google.com> 2025-03-08 16:10:00 -0800
committer Xin Li <delphij@google.com> 2025-03-08 16:10:00 -0800
commit2f62693aee03f7c1c6c7c6518e96f33062ba9583 (patch)
tree8b926ecdad1b8692df679665f5d239252b6dbd77 /PermissionController/src
parent40f9ebe87a821bdafdeabf9964f54309259483a4 (diff)
parent31dcc618ad79bff7c733867614496c7de7059168 (diff)
Merge 25Q1 (ab/BP1A.250305.020) to AOSP main
Bug: 385190204 Merged-In: I47038580b42eebc4d420ba35d2bb6bedc6d660e3 Change-Id: Ic922b8088ee6990eaa48309755befafe4ceab036
Diffstat (limited to 'PermissionController/src')
-rw-r--r--PermissionController/src/com/android/permissioncontroller/ecm/EnhancedConfirmationDialogActivity.kt59
-rw-r--r--PermissionController/src/com/android/permissioncontroller/hibernation/HibernationPolicy.kt31
-rw-r--r--PermissionController/src/com/android/permissioncontroller/hibernation/TEST_MAPPING10
-rw-r--r--PermissionController/src/com/android/permissioncontroller/permission/compat/AppOpsManagerCompat.java49
-rw-r--r--PermissionController/src/com/android/permissioncontroller/permission/compat/AppPermissionFragmentCompat.java8
-rw-r--r--PermissionController/src/com/android/permissioncontroller/permission/data/AppOpLiveData.kt7
-rw-r--r--PermissionController/src/com/android/permissioncontroller/permission/data/AppPermGroupUiInfoLiveData.kt4
-rw-r--r--PermissionController/src/com/android/permissioncontroller/permission/data/FullStoragePermissionAppsLiveData.kt21
-rw-r--r--PermissionController/src/com/android/permissioncontroller/permission/domain/usecase/v31/GetPermissionGroupUsageDetailsUseCase.kt91
-rw-r--r--PermissionController/src/com/android/permissioncontroller/permission/service/BackupHelper.java26
-rw-r--r--PermissionController/src/com/android/permissioncontroller/permission/service/RuntimePermissionsUpgradeController.kt15
-rw-r--r--PermissionController/src/com/android/permissioncontroller/permission/ui/GrantPermissionsActivity.java36
-rw-r--r--PermissionController/src/com/android/permissioncontroller/permission/ui/auto/dashboard/AutoPermissionHistoryPreference.kt16
-rw-r--r--PermissionController/src/com/android/permissioncontroller/permission/ui/auto/dashboard/AutoPermissionUsageDetailsFragment.kt127
-rw-r--r--PermissionController/src/com/android/permissioncontroller/permission/ui/auto/dashboard/AutoPermissionUsageFragment.kt12
-rw-r--r--PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/AppPermissionWrapperFragment.java2
-rw-r--r--PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/ManageCustomPermissionsFragment.java14
-rw-r--r--PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/ManageStandardPermissionsFragment.java35
-rw-r--r--PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/PermissionFooterPreference.kt9
-rw-r--r--PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/PermissionPreference.kt19
-rw-r--r--PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/PermissionPreferenceCategory.kt19
-rw-r--r--PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/v35/SectionPreferenceGroupAdapter.kt6
-rw-r--r--PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/v36/AppPermissionFooterLinkPreference.kt64
-rw-r--r--PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/v36/AppPermissionFragment.java13
-rw-r--r--PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/v36/PermissionSelectorWithWidgetPreference.kt (renamed from PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/PermissionSelectorWithWidgetPreference.kt)16
-rw-r--r--PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/v36/PermissionTwoTargetPreference.kt (renamed from PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/PermissionTwoTargetPreference.kt)10
-rw-r--r--PermissionController/src/com/android/permissioncontroller/permission/ui/legacy/PermissionUsageDetailsViewModelLegacy.kt596
-rw-r--r--PermissionController/src/com/android/permissioncontroller/permission/ui/model/GrantPermissionsViewModel.kt25
-rw-r--r--PermissionController/src/com/android/permissioncontroller/permission/ui/model/ManageCustomPermissionsViewModel.kt53
-rw-r--r--PermissionController/src/com/android/permissioncontroller/permission/ui/model/ManageStandardPermissionsViewModel.kt61
-rw-r--r--PermissionController/src/com/android/permissioncontroller/permission/ui/wear/LocationProviderDialogScreen.kt10
-rw-r--r--PermissionController/src/com/android/permissioncontroller/permission/ui/wear/WearAppPermissionGroupsHelper.kt5
-rw-r--r--PermissionController/src/com/android/permissioncontroller/permission/ui/wear/WearGrantPermissionsScreen.kt36
-rw-r--r--PermissionController/src/com/android/permissioncontroller/permission/ui/wear/WearManageStandardPermissionScreen.kt9
-rw-r--r--PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/AnnotatedText.kt67
-rw-r--r--PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/Button.kt133
-rw-r--r--PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/ScrollableScreen.kt37
-rw-r--r--PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/ToggleChip.kt43
-rw-r--r--PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/ToggleChipToggleControl.kt39
-rw-r--r--PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/material3/WearPermissionButton.kt143
-rw-r--r--PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/material3/WearPermissionButtonStyle.kt75
-rw-r--r--PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/material3/WearPermissionIconBuilder.kt101
-rw-r--r--PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/material3/WearPermissionListFooter.kt51
-rw-r--r--PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/material3/WearPermissionScaffold.kt298
-rw-r--r--PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/material3/WearPermissionToggleControl.kt165
-rw-r--r--PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/material3/WearPermissionToggleControlStyle.kt158
-rw-r--r--PermissionController/src/com/android/permissioncontroller/permission/ui/wear/theme/ResourceHelper.kt62
-rw-r--r--PermissionController/src/com/android/permissioncontroller/permission/ui/wear/theme/WearComposeMaterial3ColorScheme.kt209
-rw-r--r--PermissionController/src/com/android/permissioncontroller/permission/ui/wear/theme/WearComposeMaterial3Shapes.kt66
-rw-r--r--PermissionController/src/com/android/permissioncontroller/permission/ui/wear/theme/WearComposeMaterial3TypeScaleTokens.kt109
-rw-r--r--PermissionController/src/com/android/permissioncontroller/permission/ui/wear/theme/WearComposeMaterial3Typography.kt240
-rw-r--r--PermissionController/src/com/android/permissioncontroller/permission/ui/wear/theme/WearComposeMaterial3VariableFontTokens.kt186
-rw-r--r--PermissionController/src/com/android/permissioncontroller/permission/ui/wear/theme/WearMaterialBridgedLegacyTheme.kt82
-rw-r--r--PermissionController/src/com/android/permissioncontroller/permission/ui/wear/theme/WearOverlayableMaterial3Theme.kt41
-rw-r--r--PermissionController/src/com/android/permissioncontroller/permission/ui/wear/theme/WearPermissionTheme.kt79
-rw-r--r--PermissionController/src/com/android/permissioncontroller/permission/utils/PermissionMapping.kt1
-rw-r--r--PermissionController/src/com/android/permissioncontroller/permission/utils/Utils.java33
-rw-r--r--PermissionController/src/com/android/permissioncontroller/role/Role.md7
-rw-r--r--PermissionController/src/com/android/permissioncontroller/role/ui/DefaultAppListChildFragment.java18
-rw-r--r--PermissionController/src/com/android/permissioncontroller/role/ui/wear/WearRequestRoleScreen.kt90
-rw-r--r--PermissionController/src/com/android/permissioncontroller/safetycenter/ui/ClickableDisabledSwitchPreference.java4
-rw-r--r--PermissionController/src/com/android/permissioncontroller/safetycenter/ui/IssueCardPreference.java32
-rw-r--r--PermissionController/src/com/android/permissioncontroller/safetycenter/ui/MoreIssuesCardPreference.kt14
-rw-r--r--PermissionController/src/com/android/permissioncontroller/safetycenter/ui/SafetyBrandChipPreference.kt5
-rw-r--r--PermissionController/src/com/android/permissioncontroller/safetycenter/ui/SafetyCenterActivity.java29
-rw-r--r--PermissionController/src/com/android/permissioncontroller/safetycenter/ui/SafetyCenterDashboardFragment.java7
-rw-r--r--PermissionController/src/com/android/permissioncontroller/safetycenter/ui/SafetyCenterFragment.kt20
-rw-r--r--PermissionController/src/com/android/permissioncontroller/safetycenter/ui/SafetyCenterQsActivity.java8
-rw-r--r--PermissionController/src/com/android/permissioncontroller/safetycenter/ui/SafetyCenterSubpageFragment.kt50
-rw-r--r--PermissionController/src/com/android/permissioncontroller/safetycenter/ui/SafetyIllustrationPreference.kt3
-rw-r--r--PermissionController/src/com/android/permissioncontroller/safetycenter/ui/SafetyStatusPreference.java7
-rw-r--r--PermissionController/src/com/android/permissioncontroller/safetycenter/ui/SpacerPreference.kt3
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)
}