summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Android.bp1
-rw-r--r--PermissionController/Android.bp1
-rw-r--r--PermissionController/jarjar-rules.txt4
-rw-r--r--PermissionController/res/layout-v33/preference_issue_card.xml133
-rw-r--r--PermissionController/res/layout-v33/preference_more_issues_card.xml11
-rw-r--r--PermissionController/res/layout-v33/preference_safety_status.xml11
-rw-r--r--PermissionController/res/layout-v35/app_permission_footer_link_preference.xml34
-rw-r--r--PermissionController/res/layout-v35/permission_preference_selector_with_widget.xml69
-rw-r--r--PermissionController/res/layout-v35/permission_preference_two_target.xml60
-rw-r--r--PermissionController/res/layout-v35/permission_preference_widget_radiobutton.xml24
-rw-r--r--PermissionController/res/values-ar/strings.xml2
-rw-r--r--PermissionController/res/values-b+sr+Latn/strings.xml6
-rw-r--r--PermissionController/res/values-bg/strings.xml6
-rw-r--r--PermissionController/res/values-bs/strings.xml2
-rw-r--r--PermissionController/res/values-ca/strings.xml4
-rw-r--r--PermissionController/res/values-cs/strings.xml4
-rw-r--r--PermissionController/res/values-el/strings.xml6
-rw-r--r--PermissionController/res/values-eu/strings.xml8
-rw-r--r--PermissionController/res/values-fa/strings.xml10
-rw-r--r--PermissionController/res/values-fr/strings.xml2
-rw-r--r--PermissionController/res/values-hi/strings.xml4
-rw-r--r--PermissionController/res/values-in/strings.xml2
-rw-r--r--PermissionController/res/values-it/strings.xml4
-rw-r--r--PermissionController/res/values-iw/strings.xml4
-rw-r--r--PermissionController/res/values-ja/strings.xml2
-rw-r--r--PermissionController/res/values-km/strings.xml4
-rw-r--r--PermissionController/res/values-ky/strings.xml2
-rw-r--r--PermissionController/res/values-mr/strings.xml2
-rw-r--r--PermissionController/res/values-my/strings.xml2
-rw-r--r--PermissionController/res/values-ne/strings.xml4
-rw-r--r--PermissionController/res/values-night-v33/themes.xml2
-rw-r--r--PermissionController/res/values-or/strings.xml2
-rw-r--r--PermissionController/res/values-pt-rBR/strings.xml2
-rw-r--r--PermissionController/res/values-pt-rPT/strings.xml2
-rw-r--r--PermissionController/res/values-pt/strings.xml2
-rw-r--r--PermissionController/res/values-ro/strings.xml2
-rw-r--r--PermissionController/res/values-ru/strings.xml2
-rw-r--r--PermissionController/res/values-sr/strings.xml6
-rw-r--r--PermissionController/res/values-sv/strings.xml2
-rw-r--r--PermissionController/res/values-te/strings.xml6
-rw-r--r--PermissionController/res/values-tr/strings.xml4
-rw-r--r--PermissionController/res/values-uk/strings.xml6
-rw-r--r--PermissionController/res/values-v33/attrs.xml5
-rw-r--r--PermissionController/res/values-v33/styles.xml4
-rw-r--r--PermissionController/res/values-v33/themes.xml12
-rw-r--r--PermissionController/res/values-v35/styles.xml216
-rw-r--r--PermissionController/res/values-vi/strings.xml2
-rw-r--r--PermissionController/res/values-watch/donottranslate.xml51
-rw-r--r--PermissionController/res/values-zh-rCN/strings.xml4
-rw-r--r--PermissionController/res/values-zh-rHK/strings.xml4
-rw-r--r--PermissionController/res/values/bools.xml2
-rw-r--r--PermissionController/res/values/overlayable.xml102
-rw-r--r--PermissionController/res/xml-v35/app_permission.xml (renamed from PermissionController/res/xml/app_permission.xml)32
-rw-r--r--PermissionController/res/xml/roles.xml144
-rw-r--r--PermissionController/role-controller/Android.bp5
-rw-r--r--PermissionController/role-controller/java/com/android/role/controller/model/AppOp.java4
-rw-r--r--PermissionController/role-controller/java/com/android/role/controller/model/Permissions.java108
-rw-r--r--PermissionController/role-controller/java/com/android/role/controller/model/Role.java90
-rw-r--r--PermissionController/role-controller/java/com/android/role/controller/model/RoleBehavior.java8
-rw-r--r--PermissionController/role-controller/java/com/android/role/controller/model/RoleParser.java265
-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
-rw-r--r--PermissionController/tests/inprocess/Android.bp3
-rw-r--r--PermissionController/tests/mocking/src/com/android/permissioncontroller/tests/mocking/hibernation/HibernationPolicyTest.kt66
-rw-r--r--PermissionController/tests/mocking/src/com/android/permissioncontroller/tests/mocking/permission/domain/usecase/GetPermissionGroupUsageDetailsUseCaseTest.kt210
-rw-r--r--PermissionController/tests/mocking/src/com/android/permissioncontroller/tests/mocking/permission/ui/model/PermissionUsageDetailsViewModelTest.kt6
-rw-r--r--PermissionController/tests/mocking/src/com/android/permissioncontroller/tests/mocking/role/model/RoleParserTest.kt136
-rw-r--r--PermissionController/tests/permissionui/src/com/android/permissioncontroller/permissionui/ui/HealthConnectAllAppPermissionFragmentTest.kt20
-rw-r--r--PermissionController/tests/permissionui/src/com/android/permissioncontroller/permissionui/ui/HealthConnectAppPermissionFragmentTest.kt10
-rw-r--r--PermissionController/tests/permissionui/src/com/android/permissioncontroller/permissionui/ui/handheld/ManageCustomPermissionsFragmentTest.kt11
-rw-r--r--PermissionController/tests/permissionui/src/com/android/permissioncontroller/permissionui/ui/handheld/ManageStandardPermissionsFragmentTest.kt35
-rw-r--r--SafetyCenter/Resources/res/raw-v36/safety_center_config.xml158
-rw-r--r--SafetyCenter/Resources/res/values-fa-v35/strings.xml2
-rw-r--r--SafetyCenter/Resources/res/values-fa/strings.xml4
-rw-r--r--SafetyCenter/Resources/res/values-hy-v35/strings.xml4
-rw-r--r--SafetyCenter/Resources/res/values-ja/strings.xml4
-rw-r--r--SafetyCenter/Resources/res/values-ka-v35/strings.xml4
-rw-r--r--SafetyCenter/Resources/res/values-kn-v35/strings.xml4
-rw-r--r--SafetyCenter/Resources/res/values-lv-v35/strings.xml4
-rw-r--r--SafetyCenter/Resources/res/values-or-v35/strings.xml2
-rw-r--r--SafetyCenter/Resources/res/values-si-v35/strings.xml4
-rw-r--r--SafetyCenter/Resources/res/values-sv-v35/strings.xml6
-rw-r--r--SafetyCenter/Resources/res/values-tr-v35/strings.xml4
-rw-r--r--SafetyCenter/Resources/res/values-ur-v35/strings.xml4
-rw-r--r--SafetyCenter/Resources/res/values-zh-rTW-v34/strings.xml2
-rw-r--r--SafetyCenter/Resources/res/values-zh-rTW/strings.xml2
-rw-r--r--flags/Android.bp1
-rw-r--r--flags/flags.aconfig47
-rw-r--r--framework-s/api/system-current.txt4
-rw-r--r--framework-s/java/android/app/ecm/EnhancedConfirmationManager.java20
-rw-r--r--framework-s/java/android/app/ecm/IEnhancedConfirmationManager.aidl3
-rw-r--r--framework-s/java/android/app/role/IRoleManager.aidl4
-rw-r--r--framework-s/java/android/app/role/RoleManager.java92
-rw-r--r--framework-s/java/android/permission/internal/compat/UserHandleCompat.java (renamed from service/java/com/android/permission/compat/UserHandleCompat.java)9
-rw-r--r--framework-s/java/android/permission/internal/compat/package-info.java (renamed from service/java/com/android/permission/compat/package-info.java)2
-rw-r--r--service/Android.bp1
-rw-r--r--service/api/system-server-current.txt14
-rw-r--r--service/jarjar-rules.txt4
-rw-r--r--service/java/com/android/ecm/EnhancedConfirmationCallTrackerService.java80
-rw-r--r--service/java/com/android/ecm/EnhancedConfirmationManagerLocal.java56
-rw-r--r--service/java/com/android/ecm/EnhancedConfirmationManagerLocalImpl.java59
-rw-r--r--service/java/com/android/ecm/EnhancedConfirmationService.java136
-rw-r--r--service/java/com/android/permission/util/UserUtils.java113
-rw-r--r--service/java/com/android/role/RoleService.java204
-rw-r--r--service/java/com/android/role/RoleShellCommand.java29
-rw-r--r--service/java/com/android/role/RoleUserState.java73
-rw-r--r--service/java/com/android/role/persistence/RolesPersistenceImpl.java22
-rw-r--r--service/java/com/android/role/persistence/RolesState.java42
-rw-r--r--service/java/com/android/safetycenter/SafetyCenterService.java3
-rw-r--r--service/java/com/android/safetycenter/UserProfileGroup.java64
-rw-r--r--service/lint-baseline.xml11
-rw-r--r--service/proto/role_service.proto3
-rw-r--r--tests/apex/Android.bp2
-rw-r--r--tests/apex/java/com/android/role/persistence/RolesPersistenceTest.kt48
-rw-r--r--tests/cts/permission/src/android/permission/cts/BackgroundPermissionsTest.java35
-rw-r--r--tests/cts/permission/src/android/permission/cts/NoWifiStatePermissionTest.java8
-rw-r--r--tests/cts/permission/src/android/permission/cts/OneTimePermissionTest.java36
-rwxr-xr-xtests/cts/permission/src/android/permission/cts/SplitPermissionsSystemTest.java73
-rw-r--r--tests/cts/permissionmultidevice/src/android/permissionmultidevice/cts/DeviceAwarePermissionGrantTest.kt39
-rw-r--r--tests/cts/permissionmultiuser/Android.bp1
-rw-r--r--tests/cts/permissionmultiuser/AndroidTest.xml4
-rw-r--r--tests/cts/permissionmultiuser/src/android/permissionmultiuser/cts/AppDataSharingUpdatesTest.kt5
-rw-r--r--tests/cts/permissionpolicy/Android.bp1
-rw-r--r--tests/cts/permissionpolicy/res/raw/android_manifest.xml299
-rw-r--r--tests/cts/permissionpolicy/res/raw/automotive_android_manifest.xml12
-rw-r--r--tests/cts/permissionpolicy/src/android/permissionpolicy/cts/RuntimePermissionProperties.kt15
-rw-r--r--tests/cts/permissionui/AndroidManifest.xml18
-rw-r--r--tests/cts/permissionui/src/android/permissionui/cts/CameraMicIndicatorsPermissionTest.kt12
-rw-r--r--tests/cts/permissionui/src/android/permissionui/cts/EnhancedConfirmationInCallTest.kt231
-rw-r--r--tests/cts/permissionui/src/android/permissionui/cts/PermissionSplitTest.kt10
-rw-r--r--tests/cts/permissionui/src/android/permissionui/cts/ReviewAccessibilityServicesTest.kt25
-rw-r--r--tests/cts/permissionui/src/android/permissionui/cts/VoipCallHelper.kt171
-rw-r--r--tests/cts/role/Android.bp3
-rw-r--r--tests/cts/role/src/android/app/role/cts/RoleManagerTest.java53
-rw-r--r--tests/cts/role/src/android/app/role/cts/RoleManagerUtil.kt62
-rw-r--r--tests/cts/role/src/android/app/role/cts/RoleShellCommandTest.kt42
-rw-r--r--tests/cts/rolemultiuser/Android.bp47
-rw-r--r--tests/cts/rolemultiuser/AndroidManifest.xml32
-rw-r--r--tests/cts/rolemultiuser/AndroidTest.xml48
-rw-r--r--tests/cts/rolemultiuser/TEST_MAPPING12
-rw-r--r--tests/cts/rolemultiuser/src/android/app/rolemultiuser/cts/RoleManagerMultiUserTest.kt227
-rw-r--r--tests/functional/safetycenter/multiusers/Android.bp1
-rw-r--r--tests/functional/safetycenter/multiusers/src/android/safetycenter/functional/multiusers/SafetyCenterMultiUsersTest.kt12
-rw-r--r--tests/functional/safetycenter/singleuser/AndroidTest.xml4
-rw-r--r--tests/utils/safetycenter/java/com/android/safetycenter/testing/SafetyCenterFlags.kt3
214 files changed, 7181 insertions, 1905 deletions
diff --git a/Android.bp b/Android.bp
index 9b1857741..f2aab3568 100644
--- a/Android.bp
+++ b/Android.bp
@@ -110,6 +110,7 @@ bootclasspath_fragment {
package_prefixes: [
"android.app.role",
"android.app.ecm",
+ "android.permission.internal",
"android.permission.jarjar",
"android.safetycenter",
"android.safetylabel",
diff --git a/PermissionController/Android.bp b/PermissionController/Android.bp
index 21d6f4774..596b2dbb5 100644
--- a/PermissionController/Android.bp
+++ b/PermissionController/Android.bp
@@ -158,6 +158,7 @@ android_library {
"androidx.compose.runtime_runtime-livedata",
"androidx.compose.ui_ui",
"androidx.wear.compose_compose-material",
+ "androidx.wear.compose_compose-material3",
"android.content.pm.flags-aconfig-java-export",
"android.os.flags-aconfig-java-export",
],
diff --git a/PermissionController/jarjar-rules.txt b/PermissionController/jarjar-rules.txt
index 7bfda1ab1..05fe2a148 100644
--- a/PermissionController/jarjar-rules.txt
+++ b/PermissionController/jarjar-rules.txt
@@ -22,4 +22,8 @@ rule android.os.*FeatureFlags* com.android.permissioncontroller.jarjar.@0
rule android.os.FeatureFlags* com.android.permissioncontroller.jarjar.@0
rule android.os.FeatureFlags com.android.permissioncontroller.jarjar.@0
rule android.os.Flags com.android.permissioncontroller.jarjar.@0
+rule com.android.permission.flags.*FeatureFlags* com.android.permissioncontroller.jarjar.@0
+rule com.android.permission.flags.FeatureFlags* com.android.permissioncontroller.jarjar.@0
+rule com.android.permission.flags.FeatureFlags com.android.permissioncontroller.jarjar.@0
+rule com.android.permission.flags.Flags com.android.permissioncontroller.jarjar.@0
# LINT.ThenChange(PermissionController/role-controller/java/com/android/role/controller/model/RoleParser.java:applyJarjarTransform)
diff --git a/PermissionController/res/layout-v33/preference_issue_card.xml b/PermissionController/res/layout-v33/preference_issue_card.xml
index e6d749142..107c778a1 100644
--- a/PermissionController/res/layout-v33/preference_issue_card.xml
+++ b/PermissionController/res/layout-v33/preference_issue_card.xml
@@ -13,81 +13,84 @@
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
-
-<androidx.constraintlayout.widget.ConstraintLayout
+<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:app="http://schemas.android.com/apk/res-auto"
- android:id="@+id/issue_card"
- android:clickable="false"
- android:screenReaderFocusable="true"
- style="@style/SafetyCenterCard.Issue">
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+ <androidx.constraintlayout.widget.ConstraintLayout
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:id="@+id/issue_card"
+ android:clickable="false"
+ android:screenReaderFocusable="true"
+ style="@style/SafetyCenterCard.Issue">
- <ImageButton
- android:id="@+id/issue_card_dismiss_btn"
- android:src="@drawable/ic_safety_issue_dismiss"
- android:contentDescription="@string/safety_center_issue_card_dismiss_button"
- style="@style/SafetyCenterIssueDismiss" />
+ <ImageButton
+ android:id="@+id/issue_card_dismiss_btn"
+ android:src="@drawable/ic_safety_issue_dismiss"
+ android:contentDescription="@string/safety_center_issue_card_dismiss_button"
+ style="@style/SafetyCenterIssueDismiss" />
- <TextView
- android:id="@+id/issue_card_attribution_title"
- android:text="@string/summary_placeholder"
- android:screenReaderFocusable="false"
- style="@style/SafetyCenterIssueAttributionTitle" />
+ <TextView
+ android:id="@+id/issue_card_attribution_title"
+ android:text="@string/summary_placeholder"
+ android:screenReaderFocusable="false"
+ style="@style/SafetyCenterIssueAttributionTitle" />
- <TextView
- android:id="@+id/issue_card_title"
- android:text="@string/summary_placeholder"
- android:screenReaderFocusable="false"
- style="@style/SafetyCenterIssueTitle" />
+ <TextView
+ android:id="@+id/issue_card_title"
+ android:text="@string/summary_placeholder"
+ android:screenReaderFocusable="false"
+ style="@style/SafetyCenterIssueTitle" />
- <TextView
- android:id="@+id/issue_card_subtitle"
- android:text="@string/summary_placeholder"
- android:screenReaderFocusable="false"
- style="@style/SafetyCenterIssueSubtitle" />
+ <TextView
+ android:id="@+id/issue_card_subtitle"
+ android:text="@string/summary_placeholder"
+ android:screenReaderFocusable="false"
+ style="@style/SafetyCenterIssueSubtitle" />
- <TextView
- android:id="@+id/issue_card_summary"
- android:text="@string/summary_placeholder"
- android:screenReaderFocusable="false"
- style="@style/SafetyCenterIssueSummary" />
+ <TextView
+ android:id="@+id/issue_card_summary"
+ android:text="@string/summary_placeholder"
+ android:screenReaderFocusable="false"
+ style="@style/SafetyCenterIssueSummary" />
- <include
- android:id="@+id/issue_card_action_button_list"
- layout="?attr/scActionButtonListLayout"/>
+ <include
+ android:id="@+id/issue_card_action_button_list"
+ layout="?attr/scActionButtonListLayout"/>
- <com.android.permissioncontroller.permission.ui.v33.widget.SafetyProtectionSectionView
- android:id="@+id/issue_card_protected_by_android"
- android:importantForAccessibility="no"
- style="@style/SafetyCenterIssueSafetyProtectionSection" />
+ <com.android.permissioncontroller.permission.ui.v33.widget.SafetyProtectionSectionView
+ android:id="@+id/issue_card_protected_by_android"
+ android:importantForAccessibility="no"
+ style="@style/SafetyCenterIssueSafetyProtectionSection" />
- <ImageView
- android:id="@+id/resolved_issue_image"
- android:src="@drawable/safety_center_issue_resolved_avd"
- android:importantForAccessibility="no"
- style="@style/SafetyCenterIssueCardResolvedImage" />
+ <ImageView
+ android:id="@+id/resolved_issue_image"
+ android:src="@drawable/safety_center_issue_resolved_avd"
+ android:importantForAccessibility="no"
+ style="@style/SafetyCenterIssueCardResolvedImage" />
- <TextView
- android:id="@+id/resolved_issue_text"
- android:text="@string/safety_center_resolved_issue_fallback"
- style="@style/SafetyCenterIssueCardResolvedTitle" />
+ <TextView
+ android:id="@+id/resolved_issue_text"
+ android:text="@string/safety_center_resolved_issue_fallback"
+ style="@style/SafetyCenterIssueCardResolvedTitle" />
- <!-- This group doesn't contain issue_card_attribution_title, issue_card_dismiss_btn,
- issue_card_subtitle or issue_card_protected_by_android since the version of
- ConstraintLayout we're using doesn't allow us to override the group's visibility on
- individual group members. See b/242705351 for context. -->
- <androidx.constraintlayout.widget.Group
- android:id="@+id/default_issue_content"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:visibility="visible"
- app:constraint_referenced_ids="issue_card_title,issue_card_summary,issue_card_action_button_list" />
+ <!-- This group doesn't contain issue_card_attribution_title, issue_card_dismiss_btn,
+ issue_card_subtitle or issue_card_protected_by_android since the version of
+ ConstraintLayout we're using doesn't allow us to override the group's visibility on
+ individual group members. See b/242705351 for context. -->
+ <androidx.constraintlayout.widget.Group
+ android:id="@+id/default_issue_content"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:visibility="visible"
+ app:constraint_referenced_ids="issue_card_title,issue_card_summary,issue_card_action_button_list" />
- <androidx.constraintlayout.widget.Group
- android:id="@+id/resolved_issue_content"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:visibility="gone"
- app:constraint_referenced_ids="resolved_issue_image,resolved_issue_text" />
+ <androidx.constraintlayout.widget.Group
+ android:id="@+id/resolved_issue_content"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:visibility="gone"
+ app:constraint_referenced_ids="resolved_issue_image,resolved_issue_text" />
-</androidx.constraintlayout.widget.ConstraintLayout>
+ </androidx.constraintlayout.widget.ConstraintLayout>
+</FrameLayout> \ No newline at end of file
diff --git a/PermissionController/res/layout-v33/preference_more_issues_card.xml b/PermissionController/res/layout-v33/preference_more_issues_card.xml
index c93125762..d0ac6b1f5 100644
--- a/PermissionController/res/layout-v33/preference_more_issues_card.xml
+++ b/PermissionController/res/layout-v33/preference_more_issues_card.xml
@@ -13,8 +13,11 @@
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
-
-<com.android.permissioncontroller.safetycenter.ui.view.MoreIssuesHeaderView
+<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
- android:id="@+id/issue_card"
- style="@style/SafetyCenterMoreIssuesCollapsed"/>
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+ <com.android.permissioncontroller.safetycenter.ui.view.MoreIssuesHeaderView
+ android:id="@+id/more_issues_card"
+ style="@style/SafetyCenterMoreIssuesCollapsed"/>
+</FrameLayout> \ No newline at end of file
diff --git a/PermissionController/res/layout-v33/preference_safety_status.xml b/PermissionController/res/layout-v33/preference_safety_status.xml
index 42bf1c22d..17adc035f 100644
--- a/PermissionController/res/layout-v33/preference_safety_status.xml
+++ b/PermissionController/res/layout-v33/preference_safety_status.xml
@@ -14,7 +14,12 @@
~ limitations under the License.
-->
-<com.android.permissioncontroller.safetycenter.ui.view.StatusCardView
+<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
- android:clickable="false"
- style="@style/SafetyCenterCard.Status" /> \ No newline at end of file
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+ <com.android.permissioncontroller.safetycenter.ui.view.StatusCardView
+ android:id="@+id/status_card"
+ android:clickable="false"
+ style="@style/SafetyCenterCard.Status" />
+</FrameLayout> \ No newline at end of file
diff --git a/PermissionController/res/layout-v35/app_permission_footer_link_preference.xml b/PermissionController/res/layout-v35/app_permission_footer_link_preference.xml
new file mode 100644
index 000000000..75381e762
--- /dev/null
+++ b/PermissionController/res/layout-v35/app_permission_footer_link_preference.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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.
+ -->
+
+<!--
+ ~ A copy of PermissionPreference, with custom styles for some elements
+ -->
+<com.android.permissioncontroller.permission.ui.handheld.v35.DrawableStateLinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ style="@style/AppPermissionFooterLinkPreferenceRootLayoutStyle">
+
+ <RelativeLayout
+ style="@style/AppPermissionFooterLinkPreferenceTextLayoutStyle">
+
+ <TextView
+ android:id="@android:id/summary"
+ style="@style/AppPermissionFooterLinkPreferenceSummaryStyle"/>
+
+ </RelativeLayout>
+
+</com.android.permissioncontroller.permission.ui.handheld.v35.DrawableStateLinearLayout>
diff --git a/PermissionController/res/layout-v35/permission_preference_selector_with_widget.xml b/PermissionController/res/layout-v35/permission_preference_selector_with_widget.xml
new file mode 100644
index 000000000..230e51fc3
--- /dev/null
+++ b/PermissionController/res/layout-v35/permission_preference_selector_with_widget.xml
@@ -0,0 +1,69 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ 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.
+ -->
+
+<!-- A direct copy of the following layout, but with overlayable styles:
+ ~ SettingsLib/SelectorWithWidgetPreference/res/layout-v33/preference_selector_with_widget.xml
+ -->
+<com.android.permissioncontroller.permission.ui.handheld.v35.DrawableStateLinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ style="@style/PermissionSelectorWithWidgetPreferenceRootLayoutStyle">
+
+ <LinearLayout
+ android:id="@android:id/widget_frame"
+ style="@style/PermissionSelectorWithWidgetPreferenceWidgetFrameStyle" />
+
+ <LinearLayout
+ android:id="@+id/icon_frame"
+ style="@style/PermissionSelectorWithWidgetPreferenceIconFrameStyle">
+
+ <androidx.preference.internal.PreferenceImageView
+ android:id="@android:id/icon"
+ style="@style/PermissionSelectorWithWidgetPreferenceIconStyle" />
+ </LinearLayout>
+
+ <LinearLayout style="@style/PermissionSelectorWithWidgetPreferenceTextContainerStyle">
+
+ <TextView
+ android:id="@android:id/title"
+ style="@style/PermissionSelectorWithWidgetPreferenceTitleStyle" />
+
+ <LinearLayout
+ android:id="@+id/summary_container"
+ style="@style/PermissionSelectorWithWidgetPreferenceSummaryContainerStyle">
+
+ <TextView
+ android:id="@android:id/summary"
+ style="@style/PermissionSelectorWithWidgetPreferenceSummaryStyle" />
+
+ <TextView
+ android:id="@+id/appendix"
+ style="@style/PermissionSelectorWithWidgetPreferenceAppendixStyle" />
+ </LinearLayout>
+ </LinearLayout>
+
+ <LinearLayout
+ android:id="@+id/selector_extra_widget_container"
+ style="@style/PermissionSelectorWithWidgetPreferenceExtraWidgetContainerStyle">
+
+ <View style="@style/PermissionSelectorWithWidgetPreferenceExtraWidgetDividerStyle" />
+
+ <ImageView
+ android:id="@+id/selector_extra_widget"
+ android:contentDescription="@string/settings_label"
+ style="@style/PermissionSelectorWithWidgetPreferenceExtraWidgetImageStyle" />
+ </LinearLayout>
+</com.android.permissioncontroller.permission.ui.handheld.v35.DrawableStateLinearLayout>
diff --git a/PermissionController/res/layout-v35/permission_preference_two_target.xml b/PermissionController/res/layout-v35/permission_preference_two_target.xml
new file mode 100644
index 000000000..906393b3c
--- /dev/null
+++ b/PermissionController/res/layout-v35/permission_preference_two_target.xml
@@ -0,0 +1,60 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ 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.
+ -->
+
+<!-- A direct copy of the following layout, but with overlayable styles:
+ ~ SettingsLib/TwoTargetPreference/res/layout-v33/preference_two_target.xml
+ -->
+
+<!-- Based off preference_material_settings.xml except that ripple on only on the left side. -->
+<com.android.permissioncontroller.permission.ui.handheld.v35.DrawableStateLinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ style="@style/PermissionTwoTargetPreferenceRootLayoutStyle">
+
+ <LinearLayout
+ android:id="@+id/icon_frame"
+ style="@style/PermissionTwoTargetPreferenceIconFrameStyle">
+
+ <androidx.preference.internal.PreferenceImageView
+ android:id="@android:id/icon"
+ style="@style/PermissionTwoTargetPreferenceIconStyle"/>
+
+ </LinearLayout>
+
+ <RelativeLayout style="@style/PermissionTwoTargetPreferenceTextContainerStyle">
+
+ <TextView
+ android:id="@android:id/title"
+ style="@style/PermissionTwoTargetPreferenceTitleStyle"/>
+
+ <TextView
+ android:id="@android:id/summary"
+ style="@style/PermissionTwoTargetPreferenceSummaryStyle"/>
+
+ </RelativeLayout>
+
+ <LinearLayout
+ android:id="@+id/two_target_divider"
+ style="@style/PermissionTwoTargetPreferenceDividerContainerStyle">
+ <View style="@style/PermissionTwoTargetPreferenceDividerStyle" />
+ </LinearLayout>
+
+ <!-- Preference should place its actual preference widget here. -->
+ <LinearLayout
+ android:id="@android:id/widget_frame"
+ style="@style/PermissionTwoTargetPreferenceWidgetFrameStyle" />
+
+</com.android.permissioncontroller.permission.ui.handheld.v35.DrawableStateLinearLayout>
diff --git a/PermissionController/res/layout-v35/permission_preference_widget_radiobutton.xml b/PermissionController/res/layout-v35/permission_preference_widget_radiobutton.xml
new file mode 100644
index 000000000..10e311110
--- /dev/null
+++ b/PermissionController/res/layout-v35/permission_preference_widget_radiobutton.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ 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.
+ -->
+
+<!-- A direct copy of the following layout, but with overlayable styles:
+ ~ SettingsLib/SelectorWithWidgetPreference/res/layout/preference_widget_radiobutton.xml
+ -->
+<RadioButton
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@android:id/checkbox"
+ style="@style/PermissionSelectorWithWidgetPreferenceWidgetRadioButton" />
diff --git a/PermissionController/res/values-ar/strings.xml b/PermissionController/res/values-ar/strings.xml
index d0bccee97..90fce2d06 100644
--- a/PermissionController/res/values-ar/strings.xml
+++ b/PermissionController/res/values-ar/strings.xml
@@ -580,7 +580,7 @@
<string name="security_settings" msgid="3808106921175271317">"إعدادات الأمان"</string>
<string name="sensor_permissions_qs" msgid="1022267900031317472">"الأذونات"</string>
<string name="safety_privacy_qs_tile_title" msgid="727301867710374052">"الأمان والخصوصية"</string>
- <string name="safety_privacy_qs_tile_subtitle" msgid="3621544532041936749">"فحص الحالة"</string>
+ <string name="safety_privacy_qs_tile_subtitle" msgid="3621544532041936749">"التحقّق من الحالة"</string>
<string name="privacy_controls_qs" msgid="5780144882040591169">"عناصر التحكّم في خصوصيتك"</string>
<string name="security_settings_button_label_qs" msgid="8280343822465962330">"إعدادات إضافية"</string>
<string name="camera_toggle_label_qs" msgid="3880261453066157285">"الوصول إلى الكاميرا"</string>
diff --git a/PermissionController/res/values-b+sr+Latn/strings.xml b/PermissionController/res/values-b+sr+Latn/strings.xml
index 8215737c7..75e5a94d3 100644
--- a/PermissionController/res/values-b+sr+Latn/strings.xml
+++ b/PermissionController/res/values-b+sr+Latn/strings.xml
@@ -369,7 +369,7 @@
<string name="role_dialer_search_keywords" msgid="3324448983559188087">"brojčanik"</string>
<string name="role_sms_label" msgid="8456999857547686640">"Podrazumevana apl. za SMS"</string>
<string name="role_sms_short_label" msgid="4371444488034692243">"Aplikacija za SMS"</string>
- <string name="role_sms_description" msgid="3424020199148153513">"Aplikacije koje vam omogućavaju da koristite broj telefona da biste slali i primali kratke SMS-ove, slike, video snimke i još toga"</string>
+ <string name="role_sms_description" msgid="3424020199148153513">"Aplikacije koje vam omogućavaju da koristite broj telefona da biste slali i primali kratke tekstualne poruke, slike, video snimke i drugo"</string>
<string name="role_sms_request_title" msgid="7953552109601185602">"Želite li da podesite <xliff:g id="APP_NAME">%1$s</xliff:g> kao podrazumevanu aplikacju za SMS?"</string>
<string name="role_sms_request_description" msgid="2691004766132144886">"Ova aplikacija će dobiti pristup kameri, kontaktima, fajlovima i medijima, mikrofonu, telefonu i SMS-ovima"</string>
<string name="role_sms_search_keywords" msgid="8022048144395047352">"SMS, slanje SMS-ova, poruke, slanje poruka"</string>
@@ -471,10 +471,10 @@
<string name="permgrouprequest_device_aware_storage_isolated" msgid="6463062962458809752">"Dozvoljavate da aplikacija &lt;b&gt;<xliff:g id="APP_NAME">%1$s</xliff:g>&lt;/b&gt; pristupa slikama i medijima na: &lt;b&gt;<xliff:g id="DEVICE_NAME">%2$s</xliff:g>&lt;/b&gt;?"</string>
<string name="permgrouprequest_contacts" msgid="8391550064551053695">"Želite da dozvolite da &lt;b&gt;<xliff:g id="APP_NAME">%1$s</xliff:g>&lt;/b&gt; pristupa kontaktima?"</string>
<string name="permgrouprequest_device_aware_contacts" msgid="731025863972535928">"Dozvoljavate da aplikacija &lt;b&gt;<xliff:g id="APP_NAME">%1$s</xliff:g>&lt;/b&gt; pristupa kontaktima na: &lt;b&gt;<xliff:g id="DEVICE_NAME">%2$s</xliff:g>&lt;/b&gt;?"</string>
- <string name="permgrouprequest_location" msgid="6990232580121067883">"Želite da dozvolite da &lt;b&gt;<xliff:g id="APP_NAME">%1$s</xliff:g>&lt;/b&gt; pristupa lokaciji ovog uređaja?"</string>
+ <string name="permgrouprequest_location" msgid="6990232580121067883">"Želite da dozvolite da aplikacija &lt;b&gt;<xliff:g id="APP_NAME">%1$s</xliff:g>&lt;/b&gt; pristupa lokaciji ovog uređaja?"</string>
<string name="permgrouprequest_device_aware_location" msgid="6075412127429878638">"Dozvoljavate da aplikacija &lt;b&gt;<xliff:g id="APP_NAME">%1$s</xliff:g>&lt;/b&gt; pristupa lokaciji uređaja &lt;b&gt;<xliff:g id="DEVICE_NAME">%2$s</xliff:g>&lt;/b&gt;?"</string>
<string name="permgrouprequestdetail_location" msgid="2635935335778429894">"Aplikacija će imati pristup lokaciji samo dok koristite aplikaciju"</string>
- <string name="permgroupbackgroundrequest_location" msgid="1085680897265734809">"Želite da dozvolite da &lt;b&gt;<xliff:g id="APP_NAME">%1$s</xliff:g>&lt;/b&gt; pristupa lokaciji ovog uređaja?"</string>
+ <string name="permgroupbackgroundrequest_location" msgid="1085680897265734809">"Želite da dozvolite da aplikacija &lt;b&gt;<xliff:g id="APP_NAME">%1$s</xliff:g>&lt;/b&gt; pristupa lokaciji ovog uređaja?"</string>
<string name="permgroupbackgroundrequest_device_aware_location" msgid="1264484517831380016">"Dozvoljavate da aplikacija &lt;b&gt;<xliff:g id="APP_NAME">%1$s</xliff:g>&lt;/b&gt; pristupa lokaciji uređaja &lt;b&gt;<xliff:g id="DEVICE_NAME">%2$s</xliff:g>&lt;/b&gt;?"</string>
<string name="permgroupbackgroundrequestdetail_location" msgid="8021219324989662957">"Ova aplikacija možda želi da pristupa lokaciji sve vreme, čak i kada ne koristite aplikaciju. "<annotation id="link">"Dozvolite u podešavanjima."</annotation></string>
<string name="permgroupupgraderequest_location" msgid="8328408946822691636">"Želite li da promenite pristup lokaciji za aplikaciju &lt;b&gt;<xliff:g id="APP_NAME">%1$s</xliff:g>&lt;/b&gt;?"</string>
diff --git a/PermissionController/res/values-bg/strings.xml b/PermissionController/res/values-bg/strings.xml
index b3c16b703..9bd171db2 100644
--- a/PermissionController/res/values-bg/strings.xml
+++ b/PermissionController/res/values-bg/strings.xml
@@ -60,7 +60,7 @@
<string name="grant_dialog_button_allow_all_files" msgid="4955436994954829894">"Разрешаване на управлението на всички файлове"</string>
<string name="grant_dialog_button_allow_media_only" msgid="4832877658422573832">"Разрешаване на достъп до мултимедийните файлове"</string>
<string name="app_permissions_breadcrumb" msgid="5136969550489411650">"Приложения"</string>
- <string name="app_permissions" msgid="3369917736607944781">"Разрешения за приложенията"</string>
+ <string name="app_permissions" msgid="3369917736607944781">"Разрешения за приложението"</string>
<string name="unused_apps" msgid="2058057455175955094">"Неизползвани приложения"</string>
<string name="edit_photos_description" msgid="5540108003480078892">"Редактиране на избраните снимки за това приложение"</string>
<string name="no_unused_apps" msgid="12809387670415295">"Няма неизползвани приложения"</string>
@@ -195,7 +195,7 @@
<string name="app_permission_button_allow_limited_access" msgid="8824410215149764113">"Разрешаване на ограничения достъп"</string>
<string name="precise_image_description" msgid="6349638632303619872">"Точно местоположение"</string>
<string name="approximate_image_description" msgid="938803699637069884">"Приблизително местоположение"</string>
- <string name="app_permission_location_accuracy" msgid="7166912915040018669">"Използване на точното местоположение"</string>
+ <string name="app_permission_location_accuracy" msgid="7166912915040018669">"Използване на точното местоположе­ние"</string>
<string name="app_permission_location_accuracy_subtitle" msgid="2654077606404987210">"Когато точното местоположение е изключено, приложенията могат да осъществяват достъп до приблизителното ви местоположение"</string>
<string name="app_permission_title" msgid="2090897901051370711">"Разрешение за: <xliff:g id="PERM">%1$s</xliff:g>"</string>
<string name="app_permission_header" msgid="2951363137032603806">"Достъп до „<xliff:g id="PERM">%1$s</xliff:g>“ за това приложение"</string>
@@ -471,7 +471,7 @@
<string name="permgrouprequest_device_aware_storage_isolated" msgid="6463062962458809752">"Да се разреши ли на &lt;b&gt;<xliff:g id="APP_NAME">%1$s</xliff:g>&lt;/b&gt; да осъществява достъп до снимките и мултимедията на &lt;b&gt;<xliff:g id="DEVICE_NAME">%2$s</xliff:g>&lt;/b&gt;?"</string>
<string name="permgrouprequest_contacts" msgid="8391550064551053695">"Да се разреши ли на &lt;b&gt;<xliff:g id="APP_NAME">%1$s</xliff:g>&lt;/b&gt; да осъществява достъп до контактите ви?"</string>
<string name="permgrouprequest_device_aware_contacts" msgid="731025863972535928">"Да се разреши ли на &lt;b&gt;<xliff:g id="APP_NAME">%1$s</xliff:g>&lt;/b&gt; да осъществява достъп до контактите ви на &lt;b&gt;<xliff:g id="DEVICE_NAME">%2$s</xliff:g>&lt;/b&gt;?"</string>
- <string name="permgrouprequest_location" msgid="6990232580121067883">"Да се разреши ли на &lt;b&gt;<xliff:g id="APP_NAME">%1$s</xliff:g>&lt;/b&gt; да осъществява достъп до местоположението на това устройство?"</string>
+ <string name="permgrouprequest_location" msgid="6990232580121067883">"Да се разреши ли на &lt;b&gt;<xliff:g id="APP_NAME">%1$s</xliff:g>&lt;/b&gt; да получи достъп до местоположението на това устройство?"</string>
<string name="permgrouprequest_device_aware_location" msgid="6075412127429878638">"Да се разреши ли на &lt;b&gt;<xliff:g id="APP_NAME">%1$s</xliff:g>&lt;/b&gt; да осъществява достъп до местоположението на &lt;b&gt;<xliff:g id="DEVICE_NAME">%2$s</xliff:g>&lt;/b&gt;?"</string>
<string name="permgrouprequestdetail_location" msgid="2635935335778429894">"Само когато използвате приложението, то ще има достъп до местоположението"</string>
<string name="permgroupbackgroundrequest_location" msgid="1085680897265734809">"Да се разреши ли на &lt;b&gt;<xliff:g id="APP_NAME">%1$s</xliff:g>&lt;/b&gt; да осъществява достъп до местоположението на това устройство?"</string>
diff --git a/PermissionController/res/values-bs/strings.xml b/PermissionController/res/values-bs/strings.xml
index 9dbaca940..fe036961d 100644
--- a/PermissionController/res/values-bs/strings.xml
+++ b/PermissionController/res/values-bs/strings.xml
@@ -683,7 +683,7 @@
<string name="enhanced_confirmation_dialog_learn_more" msgid="5226619861379095709">"Saznajte više"</string>
<string name="enhanced_confirmation_dialog_ok" msgid="8560373821598619924">"Uredu"</string>
<string name="permission_grant_dialog_streaming_blocked_title" msgid="8905241017017043649">"Zahtjev za odobrenje je potisnut"</string>
- <string name="permission_grant_dialog_streaming_blocked_description" msgid="838165608934085319">"Aplikacija traži dodatna odobrenja, ali se ona ne mogu dati u sesiji prijenosa. Najprije dajte odobrenje na telefonu."</string>
+ <string name="permission_grant_dialog_streaming_blocked_description" msgid="838165608934085319">"Aplikacija traži dodatna odobrenja, ali se ona ne mogu dati u sesiji prenosa. Najprije dajte odobrenje na telefonu."</string>
<string name="privacy_dashboard_emergency_location_enforced_attribution_label" msgid="5702912511473457693">"Za hitni poziv ili poruku"</string>
<string name="privacy_dashboard_emergency_location_dialog_title" msgid="849723944428031911">"Lokacija je poslana hitnim službama"</string>
<string name="privacy_dashboard_emergency_location_dialog_description" msgid="5815970230573483329">"Ova aplikacija je pristupila lokaciji uređaja tokom poziva ili slanja poruke broju za hitne slučajeve. To se može dogoditi čak i kada aplikacija nema odobrenje za lokaciju ili je lokacija uređaja isključena. "<a href="https://support.google.com/android/answer/9319337">"Saznajte više"</a></string>
diff --git a/PermissionController/res/values-ca/strings.xml b/PermissionController/res/values-ca/strings.xml
index e61f41507..1de8e07e6 100644
--- a/PermissionController/res/values-ca/strings.xml
+++ b/PermissionController/res/values-ca/strings.xml
@@ -457,7 +457,7 @@
<string name="incident_report_dialog_title" msgid="669104389325204095">"Vols compartir les dades de depuració?"</string>
<string name="incident_report_dialog_intro" msgid="5897733669850951832">"El sistema ha detectat un problema."</string>
<string name="incident_report_dialog_text" msgid="5675553296891757523">"<xliff:g id="APP_NAME_0">%1$s</xliff:g> sol·licita penjar un informe d\'errors des d\'aquest dispositiu generat el dia <xliff:g id="DATE">%2$s</xliff:g> (<xliff:g id="TIME">%3$s</xliff:g>). Els informes d\'errors inclouen informació personal sobre el dispositiu o informació registrada per les aplicacions, com ara noms d\'usuaris, dades de la ubicació, identificadors del dispositiu i informació de la xarxa. Comparteix informes d\'errors només amb persones i aplicacions de confiança. Vols permetre que <xliff:g id="APP_NAME_1">%4$s</xliff:g> pengi un informe d\'errors?"</string>
- <string name="incident_report_error_dialog_text" msgid="4189647113387092272">"S\'ha produït un error en processar l\'informe d\'errors de l\'aplicació <xliff:g id="APP_NAME">%1$s</xliff:g> i, per tant, s\'han denegat les dades de depuració detallades. Sentim la interrupció."</string>
+ <string name="incident_report_error_dialog_text" msgid="4189647113387092272">"Hi ha hagut un error en processar l\'informe d\'errors de l\'aplicació <xliff:g id="APP_NAME">%1$s</xliff:g> i, per tant, s\'han denegat les dades de depuració detallades. Sentim la interrupció."</string>
<string name="incident_report_dialog_allow_label" msgid="2970242967721155239">"Permet"</string>
<string name="incident_report_dialog_deny_label" msgid="3535314290677579383">"Denega"</string>
<string name="adjust_user_sensitive_title" msgid="4196724451314280527">"Configuració avançada"</string>
@@ -471,7 +471,7 @@
<string name="permgrouprequest_device_aware_storage_isolated" msgid="6463062962458809752">"Vols permetre que &lt;b&gt;<xliff:g id="APP_NAME">%1$s</xliff:g>&lt;/b&gt; accedeixi a les fotos i al contingut multimèdia del dispositiu &lt;b&gt;<xliff:g id="DEVICE_NAME">%2$s</xliff:g>&lt;/b&gt;?"</string>
<string name="permgrouprequest_contacts" msgid="8391550064551053695">"Vols permetre que &lt;b&gt;<xliff:g id="APP_NAME">%1$s</xliff:g>&lt;/b&gt; accedeixi als contactes?"</string>
<string name="permgrouprequest_device_aware_contacts" msgid="731025863972535928">"Vols permetre que &lt;b&gt;<xliff:g id="APP_NAME">%1$s</xliff:g>&lt;/b&gt; accedeixi als contactes del dispositiu &lt;b&gt;<xliff:g id="DEVICE_NAME">%2$s</xliff:g>&lt;/b&gt;?"</string>
- <string name="permgrouprequest_location" msgid="6990232580121067883">"Vols permetre que la &lt;b&gt;<xliff:g id="APP_NAME">%1$s</xliff:g>&lt;/b&gt; accedeixi a la ubicació del dispositiu?"</string>
+ <string name="permgrouprequest_location" msgid="6990232580121067883">"Vols permetre que &lt;b&gt;<xliff:g id="APP_NAME">%1$s</xliff:g>&lt;/b&gt; accedeixi a la ubicació del dispositiu?"</string>
<string name="permgrouprequest_device_aware_location" msgid="6075412127429878638">"Vols permetre que &lt;b&gt;<xliff:g id="APP_NAME">%1$s</xliff:g>&lt;/b&gt; accedeixi a la ubicació del dispositiu &lt;b&gt;<xliff:g id="DEVICE_NAME">%2$s</xliff:g>&lt;/b&gt;?"</string>
<string name="permgrouprequestdetail_location" msgid="2635935335778429894">"L\'aplicació només tindrà accés a la ubicació quan l\'estiguis utilitzant"</string>
<string name="permgroupbackgroundrequest_location" msgid="1085680897265734809">"Vols permetre que &lt;b&gt;<xliff:g id="APP_NAME">%1$s</xliff:g>&lt;/b&gt; accedeixi a la ubicació del dispositiu?"</string>
diff --git a/PermissionController/res/values-cs/strings.xml b/PermissionController/res/values-cs/strings.xml
index 328647205..48996724a 100644
--- a/PermissionController/res/values-cs/strings.xml
+++ b/PermissionController/res/values-cs/strings.xml
@@ -444,8 +444,8 @@
<string name="default_payment_app_other_nfc_services" msgid="5957633798695758917">"Ostatní služby NFC"</string>
<string name="car_default_app_selected" msgid="5416420830430644174">"Vybráno"</string>
<string name="car_default_app_selected_with_info" msgid="1932204186080593500">"Vybráno – <xliff:g id="ADDITIONAL_INFO">%1$s</xliff:g>"</string>
- <string name="special_app_access_search_keyword" msgid="8032347212290774210">"zvláštní přístup aplikací"</string>
- <string name="special_app_access" msgid="5019319067120213797">"Přístupy pro aplikace"</string>
+ <string name="special_app_access_search_keyword" msgid="8032347212290774210">"speciální přístup aplikací"</string>
+ <string name="special_app_access" msgid="5019319067120213797">"Speciální přístup aplikací"</string>
<string name="no_special_app_access" msgid="6950277571805106247">"Žádný přístup ke spec. aplik."</string>
<string name="special_app_access_no_apps" msgid="4102911722787886970">"Žádné aplikace"</string>
<string name="home_missing_work_profile_support" msgid="1756855847669387977">"Nepodporuje pracovní profil"</string>
diff --git a/PermissionController/res/values-el/strings.xml b/PermissionController/res/values-el/strings.xml
index cc59b6748..e000a2ac2 100644
--- a/PermissionController/res/values-el/strings.xml
+++ b/PermissionController/res/values-el/strings.xml
@@ -381,7 +381,7 @@
<string name="role_emergency_search_keywords" msgid="1920007722599213358">"ice"</string>
<string name="role_home_label" msgid="3871847846649769412">"Προεπ. εφαρμογή αρχικής οθόνης"</string>
<string name="role_home_short_label" msgid="8544733747952272337">"Εφαρμογή αρχικής οθόνης"</string>
- <string name="role_home_description" msgid="7997371519626556675">"Εφαρμογές, που συχνά αποκαλούνται εφαρμογές εκκίνησης, που αντικαθιστούν τις Αρχικές οθόνες στη συσκευή σας Android και σας δίνουν πρόσβαση στα περιεχόμενα και τις λειτουργίες της συσκευή σας"</string>
+ <string name="role_home_description" msgid="7997371519626556675">"Εφαρμογές, που συχνά αποκαλούνται εφαρμογές εκκίνησης, που αντικαθιστούν τις Αρχικές οθόνες στη συσκευή σας Android και σας δίνουν πρόσβαση στα περιεχόμενα και τις λειτουργίες της συσκευής σας"</string>
<string name="role_home_request_title" msgid="738136983453341081">"Ορισμός εφαρμογής <xliff:g id="APP_NAME">%1$s</xliff:g> ως προεπιλεγμένης εφαρμογής αρχικής οθόνης;"</string>
<string name="role_home_request_description" msgid="2658833966716057673">"Δεν απαιτούνται άδειες"</string>
<string name="role_home_search_keywords" msgid="3830755001192666285">"εφαρμογή εκκίνησης"</string>
@@ -471,7 +471,7 @@
<string name="permgrouprequest_device_aware_storage_isolated" msgid="6463062962458809752">"Να επιτρέπεται στην εφαρμογή &lt;b&gt;<xliff:g id="APP_NAME">%1$s</xliff:g>&lt;/b&gt; η πρόσβαση σε φωτογραφίες και μέσα στη συσκευή &lt;b&gt;<xliff:g id="DEVICE_NAME">%2$s</xliff:g>&lt;/b&gt;;"</string>
<string name="permgrouprequest_contacts" msgid="8391550064551053695">"Να επιτρέπεται στο &lt;b&gt;<xliff:g id="APP_NAME">%1$s</xliff:g>&lt;/b&gt; να έχει πρόσβαση στις επαφές σας;"</string>
<string name="permgrouprequest_device_aware_contacts" msgid="731025863972535928">"Να επιτρέπεται στην εφαρμογή &lt;b&gt;<xliff:g id="APP_NAME">%1$s</xliff:g>&lt;/b&gt; η πρόσβαση στις επαφές σας στη συσκευή &lt;b&gt;<xliff:g id="DEVICE_NAME">%2$s</xliff:g>&lt;/b&gt;;"</string>
- <string name="permgrouprequest_location" msgid="6990232580121067883">"Να επιτρέπεται στο &lt;b&gt;<xliff:g id="APP_NAME">%1$s</xliff:g>&lt;/b&gt; η πρόσβαση στην τοποθεσία αυτής της συσκευής;"</string>
+ <string name="permgrouprequest_location" msgid="6990232580121067883">"Να επιτρέπεται στην εφαρμογή &lt;b&gt;<xliff:g id="APP_NAME">%1$s</xliff:g>&lt;/b&gt; η πρόσβαση στην τοποθεσία αυτής της συσκευής;"</string>
<string name="permgrouprequest_device_aware_location" msgid="6075412127429878638">"Να επιτρέπεται στην εφαρμογή &lt;b&gt;<xliff:g id="APP_NAME">%1$s</xliff:g>&lt;/b&gt; η πρόσβαση στην τοποθεσία της συσκευής &lt;b&gt;<xliff:g id="DEVICE_NAME">%2$s</xliff:g>’s&lt;/b&gt;;"</string>
<string name="permgrouprequestdetail_location" msgid="2635935335778429894">"Η εφαρμογή θα έχει πρόσβαση στην τοποθεσία μόνο κατά τη διάρκεια χρήσης της εφαρμογής"</string>
<string name="permgroupbackgroundrequest_location" msgid="1085680897265734809">"Να επιτρέπεται στο &lt;b&gt;<xliff:g id="APP_NAME">%1$s</xliff:g>&lt;/b&gt; να έχει πρόσβαση στην τοποθεσία αυτής της συσκευής;"</string>
@@ -499,7 +499,7 @@
<string name="permgrouprequest_storage_pre_q" msgid="168130651144569428">"Να επιτρέπεται στην εφαρμογή &lt;b&gt;<xliff:g id="APP_NAME">%1$s</xliff:g>&lt;/b&gt; η πρόσβαση σε &lt;b&gt;φωτογραφίες, βίντεο, μουσική, ήχο και άλλα αρχεία&lt;/b&gt; της συσκευής;"</string>
<string name="permgrouprequest_read_media_aural" msgid="2593365397347577812">"Να επιτρέπεται στην εφαρμογή &lt;b&gt;<xliff:g id="APP_NAME">%1$s</xliff:g>&lt;/b&gt; η πρόσβαση στη μουσική και στα αρχεία ήχου αυτής της συσκευής;"</string>
<string name="permgrouprequest_device_aware_read_media_aural" msgid="7927884506238101064">"Να επιτρέπεται στην εφαρμογή &lt;b&gt;<xliff:g id="APP_NAME">%1$s</xliff:g>&lt;/b&gt; η πρόσβαση σε μουσική και σε ήχο στη συσκευή &lt;b&gt;<xliff:g id="DEVICE_NAME">%2$s</xliff:g>&lt;/b&gt;;"</string>
- <string name="permgrouprequest_read_media_visual" msgid="5548780620779729975">"Να επιτρέπεται στην &lt;b&gt;<xliff:g id="APP_NAME">%1$s</xliff:g>&lt;/b&gt; η πρόσβαση στις φωτογραφίες και τα βίντεο αυτής της συσκευής;"</string>
+ <string name="permgrouprequest_read_media_visual" msgid="5548780620779729975">"Να επιτρέπεται στην εφαρμογή &lt;b&gt;<xliff:g id="APP_NAME">%1$s</xliff:g>&lt;/b&gt; η πρόσβαση στις φωτογραφίες και τα βίντεο αυτής της συσκευής;"</string>
<string name="permgrouprequest_device_aware_read_media_visual" msgid="3122576538319059333">"Να επιτρέπεται στην εφαρμογή &lt;b&gt;<xliff:g id="APP_NAME">%1$s</xliff:g>&lt;/b&gt; η πρόσβαση σε φωτογραφίες και σε βίντεο στη συσκευή &lt;b&gt;<xliff:g id="DEVICE_NAME">%2$s</xliff:g>&lt;/b&gt;;"</string>
<string name="permgrouprequest_more_photos" msgid="128933814654231321">"Να επιτρέπεται στην εφαρμογή &lt;b&gt;<xliff:g id="APP_NAME">%1$s</xliff:g>&lt;/b&gt; η πρόσβαση σε περισσότερες φωτογραφίες και βίντεο αυτής της συσκευής;"</string>
<string name="permgrouprequest_device_aware_more_photos" msgid="1703469013613723053">"Να επιτρέπεται στην εφαρμογή &lt;b&gt;<xliff:g id="APP_NAME">%1$s</xliff:g>&lt;/b&gt; η πρόσβαση σε περισσότερες φωτογραφίες και βίντεο στη συσκευή &lt;b&gt;<xliff:g id="DEVICE_NAME">%2$s</xliff:g>&lt;/b&gt;;"</string>
diff --git a/PermissionController/res/values-eu/strings.xml b/PermissionController/res/values-eu/strings.xml
index 8160770ea..139f41668 100644
--- a/PermissionController/res/values-eu/strings.xml
+++ b/PermissionController/res/values-eu/strings.xml
@@ -256,7 +256,7 @@
<string name="allowed_storage_scoped" msgid="5383645873719086975">"Multimedia-fitx. soilik erabiltzeko baimena dutenak"</string>
<string name="allowed_storage_full" msgid="5356699280625693530">"Fitxategi guztiak kudeatzeko baimena dutenak"</string>
<string name="ask_header" msgid="2633816846459944376">"Galdetu beti"</string>
- <string name="denied_header" msgid="903209608358177654">"Baimendu gabekoak"</string>
+ <string name="denied_header" msgid="903209608358177654">"Baimendu gabe"</string>
<string name="permission_group_name_with_device_name" msgid="8798741850536024820">"<xliff:g id="PERM_GROUP_NAME">%1$s</xliff:g> (<xliff:g id="DEVICE_NAME">%2$s</xliff:g>)"</string>
<string name="storage_footer_hyperlink_text" msgid="8873343987957834810">"Ikusi fitxategi guztiak atzi ditzaketen aplikazio gehiago"</string>
<string name="days" msgid="609563020985571393">"{count,plural, =1{1 egun}other{# egun}}"</string>
@@ -355,9 +355,9 @@
<string name="accessibility_service_dialog_bottom_text_multiple" msgid="7009848932395519852">"Aplikazio horiek pantaila, egiten dituzun ekintzak eta idazten dituzun gauzak ikusi ahalko dituzte, bai eta ekintzak gauzatu eta pantaila kontrolatu ere."</string>
<string name="role_assistant_label" msgid="4727586018198208128">"Laguntzaile digitalaren aplikazio lehenetsia"</string>
<string name="role_assistant_short_label" msgid="3369003713187703399">"Laguntzaile digitalaren aplikazioa"</string>
- <string name="role_assistant_description" msgid="6622458130459922952">"Ikusten ari zaren pantailako informazioaren araberako laguntza eskain diezazukete laguntza-aplikazioek. Zenbait aplikaziok abiarazlea eta ahots bidezko zerbitzuak onartzen dituzte laguntza integratua eskaintzeko."</string>
+ <string name="role_assistant_description" msgid="6622458130459922952">"Ikusten ari zaren pantailako informazioaren araberako laguntza eskain diezazukete laguntza-aplikazioek. Zenbait aplikaziok exekutatzeko tresna eta ahots bidezko zerbitzuak onartzen dituzte laguntza integratua eskaintzeko."</string>
<string name="role_browser_label" msgid="2877796144554070207">"Arakatzaile lehenetsia"</string>
- <string name="role_browser_short_label" msgid="6745009127123292296">"Arakatzaile-aplikazioa"</string>
+ <string name="role_browser_short_label" msgid="6745009127123292296">"Arakatzaile-apliU+2060kazioa"</string>
<string name="role_browser_description" msgid="3465253637499842671">"Interneteko sarbidea ematen dizuten eta sakatzen dituzun estekak bistaratzen dituzten aplikazioak"</string>
<string name="role_browser_request_title" msgid="2895200507835937192">"<xliff:g id="APP_NAME">%1$s</xliff:g> ezarri nahi duzu arakatzaile lehenetsi gisa?"</string>
<string name="role_browser_request_description" msgid="5888803407905985941">"Ez du behar baimenik"</string>
@@ -381,7 +381,7 @@
<string name="role_emergency_search_keywords" msgid="1920007722599213358">"ice"</string>
<string name="role_home_label" msgid="3871847846649769412">"Hasierako aplikazio lehenetsia"</string>
<string name="role_home_short_label" msgid="8544733747952272337">"Hasierako aplikazioa"</string>
- <string name="role_home_description" msgid="7997371519626556675">"Android-eko gailuko orri nagusiak ordezten dituzten aplikazioak (\"abiarazle\" ere deitzen zaie). Gailuko eduki eta eginbideetarako sarbidea ematen dute."</string>
+ <string name="role_home_description" msgid="7997371519626556675">"Android-eko gailuko orri nagusiak ordezten dituzten aplikazioak (\"exekutatzeko tresna\" ere deitzen zaie). Gailuko eduki eta eginbideetarako sarbidea ematen dute."</string>
<string name="role_home_request_title" msgid="738136983453341081">"<xliff:g id="APP_NAME">%1$s</xliff:g> ezarri nahi duzu hasierako aplikazio lehenetsi gisa?"</string>
<string name="role_home_request_description" msgid="2658833966716057673">"Ez du behar baimenik"</string>
<string name="role_home_search_keywords" msgid="3830755001192666285">"abiarazlea"</string>
diff --git a/PermissionController/res/values-fa/strings.xml b/PermissionController/res/values-fa/strings.xml
index 4ed186f3e..ae6063dcc 100644
--- a/PermissionController/res/values-fa/strings.xml
+++ b/PermissionController/res/values-fa/strings.xml
@@ -256,7 +256,7 @@
<string name="allowed_storage_scoped" msgid="5383645873719086975">"مجاز فقط برای دسترسی به رسانه‌ها"</string>
<string name="allowed_storage_full" msgid="5356699280625693530">"مجاز برای مدیریت همه فایل‌ها"</string>
<string name="ask_header" msgid="2633816846459944376">"هربار پرسیده شود"</string>
- <string name="denied_header" msgid="903209608358177654">"مجاز نبودن"</string>
+ <string name="denied_header" msgid="903209608358177654">"مجاز نیست"</string>
<string name="permission_group_name_with_device_name" msgid="8798741850536024820">"<xliff:g id="PERM_GROUP_NAME">%1$s</xliff:g> در <xliff:g id="DEVICE_NAME">%2$s</xliff:g>"</string>
<string name="storage_footer_hyperlink_text" msgid="8873343987957834810">"دیدن برنامه‌های دیگری که به همه فایل‌ها دسترسی دارند"</string>
<string name="days" msgid="609563020985571393">"{count,plural, =1{یک روز}one{# روز}other{# روز}}"</string>
@@ -381,7 +381,7 @@
<string name="role_emergency_search_keywords" msgid="1920007722599213358">"ice"</string>
<string name="role_home_label" msgid="3871847846649769412">"برنامه صفحه اصلی پیش‌فرض"</string>
<string name="role_home_short_label" msgid="8544733747952272337">"برنامه صفحه اصلی"</string>
- <string name="role_home_description" msgid="7997371519626556675">"‏برنامه‌های عموماً موسوم به راه‌انداز، که جایگزین صفحه‌های اصلی در دستگاه Android می‌شوند و امکان دسترسی به محتواها و ویژگی‌های دستگاه را می‌دهند"</string>
+ <string name="role_home_description" msgid="7997371519626556675">"‏برنامه‌هایی (موسوم به راه‌انداز) که جایگزین صفحه‌های اصلی در دستگاه Android می‌شوند و امکان دسترسی به محتواها و ویژگی‌های دستگاه را می‌دهند"</string>
<string name="role_home_request_title" msgid="738136983453341081">"<xliff:g id="APP_NAME">%1$s</xliff:g> به‌عنوان برنامه صفحه اصلی پیش‌فرض تنظیم شود؟"</string>
<string name="role_home_request_description" msgid="2658833966716057673">"اجازه لازم نیست"</string>
<string name="role_home_search_keywords" msgid="3830755001192666285">"راه‌انداز"</string>
@@ -471,7 +471,7 @@
<string name="permgrouprequest_device_aware_storage_isolated" msgid="6463062962458809752">"به «<xliff:g id="APP_NAME">%1$s</xliff:g>» اجازه می‌دهید به عکس‌ها و رسانه‌های «<xliff:g id="DEVICE_NAME">%2$s</xliff:g>» دسترسی داشته باشد؟"</string>
<string name="permgrouprequest_contacts" msgid="8391550064551053695">"‏به &lt;b&gt;<xliff:g id="APP_NAME">%1$s</xliff:g>&lt;/b&gt; اجازه دسترسی به مخاطبینتان می‌دهید؟"</string>
<string name="permgrouprequest_device_aware_contacts" msgid="731025863972535928">"به «<xliff:g id="APP_NAME">%1$s</xliff:g>» اجازه می‌دهید به مخاطبین شما در «<xliff:g id="DEVICE_NAME">%2$s</xliff:g>» دسترسی داشته باشد؟"</string>
- <string name="permgrouprequest_location" msgid="6990232580121067883">"‏به &lt;b&gt;<xliff:g id="APP_NAME">%1$s</xliff:g>&lt;/b&gt; اجازه دسترسی به مکان این دستگاه می‌دهید؟"</string>
+ <string name="permgrouprequest_location" msgid="6990232580121067883">"‏به &lt;b&gt;<xliff:g id="APP_NAME">%1$s</xliff:g>&lt;/b&gt; اجازه دسترسی به مکان این دستگاه را می‌دهید؟"</string>
<string name="permgrouprequest_device_aware_location" msgid="6075412127429878638">"به «<xliff:g id="APP_NAME">%1$s</xliff:g>» اجازه می‌دهید به مکان «<xliff:g id="DEVICE_NAME">%2$s</xliff:g>» دسترسی داشته باشد؟"</string>
<string name="permgrouprequestdetail_location" msgid="2635935335778429894">"این برنامه فقط وقتی از آن استفاده می‌کنید، به مکان دسترسی خواهد داشت"</string>
<string name="permgroupbackgroundrequest_location" msgid="1085680897265734809">"‏به &lt;b&gt;<xliff:g id="APP_NAME">%1$s</xliff:g>&lt;/b&gt; اجازه داده شود به مکان این دستگاه دسترسی پیدا کند؟"</string>
@@ -581,7 +581,7 @@
<string name="sensor_permissions_qs" msgid="1022267900031317472">"اجازه‌ها"</string>
<string name="safety_privacy_qs_tile_title" msgid="727301867710374052">"امنیت و حریم خصوصی"</string>
<string name="safety_privacy_qs_tile_subtitle" msgid="3621544532041936749">"بررسی وضعیت"</string>
- <string name="privacy_controls_qs" msgid="5780144882040591169">"تنظیمات حریم خصوصی شما"</string>
+ <string name="privacy_controls_qs" msgid="5780144882040591169">"کنترل‌های حریم خصوصی شما"</string>
<string name="security_settings_button_label_qs" msgid="8280343822465962330">"تنظیمات بیشتر"</string>
<string name="camera_toggle_label_qs" msgid="3880261453066157285">"دسترسی به دوربین"</string>
<string name="microphone_toggle_label_qs" msgid="8132912469813396552">"دسترسی به میکروفون"</string>
@@ -624,7 +624,7 @@
<string name="safety_center_background_location_access_reminder_summary" msgid="7431657777510537658">"این برنامه همیشه می‌تواند به مکانتان دسترسی داشته باشد، حتی وقتی بسته باشد.\n\nبرخی‌از برنامه‌های ایمنی و اضطراری برای اینکه عملکرد موردانتظار را داشته باشند باید به مکان شما در پس‌زمینه دسترسی داشته باشند."</string>
<string name="safety_center_background_location_access_revoked" msgid="6972274943343442213">"دسترسی تغییر کرد"</string>
<string name="safety_center_view_recent_location_access" msgid="3524391299490678243">"دیدن استفاده اخیر از مکان"</string>
- <string name="privacy_controls_title" msgid="7605929972256835199">"تنظیمات حریم خصوصی"</string>
+ <string name="privacy_controls_title" msgid="7605929972256835199">"کنترل‌های حریم خصوصی"</string>
<string name="camera_toggle_title" msgid="1251201397431837666">"دسترسی به دوربین"</string>
<string name="mic_toggle_title" msgid="2649991093496110162">"دسترسی به میکروفون"</string>
<string name="perm_toggle_description" msgid="7801326363741451379">"برای برنامه‌ها و سرویس‌ها"</string>
diff --git a/PermissionController/res/values-fr/strings.xml b/PermissionController/res/values-fr/strings.xml
index 2ea77b4f4..14c29125e 100644
--- a/PermissionController/res/values-fr/strings.xml
+++ b/PermissionController/res/values-fr/strings.xml
@@ -60,7 +60,7 @@
<string name="grant_dialog_button_allow_all_files" msgid="4955436994954829894">"Autoriser la gestion de tous les fichiers"</string>
<string name="grant_dialog_button_allow_media_only" msgid="4832877658422573832">"Autoriser l\'accès aux fichiers multimédias"</string>
<string name="app_permissions_breadcrumb" msgid="5136969550489411650">"Applications"</string>
- <string name="app_permissions" msgid="3369917736607944781">"Autorisations des applications"</string>
+ <string name="app_permissions" msgid="3369917736607944781">"Autorisations de l\'application"</string>
<string name="unused_apps" msgid="2058057455175955094">"Applications inutilisées"</string>
<string name="edit_photos_description" msgid="5540108003480078892">"Modifier les photos sélectionnées pour cette application"</string>
<string name="no_unused_apps" msgid="12809387670415295">"Aucune appli inutilisée"</string>
diff --git a/PermissionController/res/values-hi/strings.xml b/PermissionController/res/values-hi/strings.xml
index c464a3a90..2557708bc 100644
--- a/PermissionController/res/values-hi/strings.xml
+++ b/PermissionController/res/values-hi/strings.xml
@@ -80,7 +80,7 @@
<string name="additional_permissions" msgid="5801285469338873430">"दूसरी अनुमतियां"</string>
<string name="app_permissions_info_button_label" msgid="7633312050729974623">"ऐप्लिकेशन के बारे में जानकारी देखें"</string>
<string name="additional_permissions_more" msgid="5681220714755304407">"{count,plural, =1{# और अनुमति}one{# और अनुमति}other{# और अनुमतियां}}"</string>
- <string name="old_sdk_deny_warning" msgid="2382236998845153919">"यह ऐप्लिकेशन Android के पुराने वर्शन के लिए बनाया गया था. अगर आप अनुमति नहीं देते हैं, तो हो सकता है कि यह ठीक तरह से काम न करे."</string>
+ <string name="old_sdk_deny_warning" msgid="2382236998845153919">"इस ऐप्लिकेशन को Android के पुराने वर्शन के लिए बनाया गया था. अगर अनुमति नहीं दी जाती है, तो हो सकता है कि यह ठीक तरह से काम न करे."</string>
<string name="storage_supergroup_warning_allow" msgid="103093462784523190">"यह ऐप्लिकेशन, Android के पुराने वर्शन के लिए बनाया गया था. यह अनुमति देने पर, ऐप्लिकेशन को आपके डिवाइस में मौजूद सभी फ़ाइलों का ऐक्सेस मिल जाएगा. इसमें फ़ोटो, वीडियो, संगीत, ऑडियो, और दूसरी फ़ाइलें शामिल हैं."</string>
<string name="storage_supergroup_warning_deny" msgid="6420765672683284347">"यह ऐप्लिकेशन, Android के पुराने वर्शन के लिए बनाया गया था. यह अनुमति न देने पर, ऐप्लिकेशन को आपके डिवाइस में मौजूद किसी भी फ़ाइल का ऐक्सेस नहीं मिलेगा. इसमें फ़ोटो, वीडियो, संगीत, ऑडियो, और दूसरी फ़ाइलें शामिल हैं."</string>
<string name="default_permission_description" msgid="4624464917726285203">"ऐसी कार्रवाई करें जिसकी जानकारी नहीं है"</string>
@@ -207,7 +207,7 @@
<string name="auto_revoke_label" msgid="5068393642936571656">"ऐप्लिकेशन का इस्तेमाल न होने पर अनुमतियां हटाएं"</string>
<string name="unused_apps_label" msgid="2595428768404901064">"अनुमतियां हटाएं और जगह खाली करें"</string>
<string name="unused_apps_label_v2" msgid="7058776770056517980">"इस्तेमाल न होने पर ऐप गतिविधि रोकें"</string>
- <string name="unused_apps_label_v3" msgid="693340578642156657">"इस्तेमाल नहीं हुआ ऐप्लिकेशन मैनेज करें"</string>
+ <string name="unused_apps_label_v3" msgid="693340578642156657">"इस्तेमाल नहीं हो रहा ऐप्लिकेशन मैनेज करें"</string>
<string name="unused_apps_summary" msgid="8839466950318403115">"ऐप्लिकेशन की अनुमतियां हटाएं, डिवाइस में कुछ समय के लिए रहने वाली फ़ाइलें मिटाएं, और सूचनाएं रोकें"</string>
<string name="unused_apps_summary_v2" msgid="5011313200815115802">"ऐप्लिकेशन की अनुमतियां हटाएं, डिवाइस में कुछ समय के लिए रहने वाली फ़ाइलें मिटाएं, सूचनाएं रोकें, और ऐप्लिकेशन संग्रहित करें"</string>
<string name="auto_revoke_summary" msgid="5867548789805911683">"अगर इस ऐप्लिकेशन का इस्तेमाल कुछ महीनों तक नहीं किया गया, तो इसे दी गई अनुमतियां हटा दी जाएंगी. ऐसा आपके डेटा को सुरक्षित रखने के लिए किया जाएगा."</string>
diff --git a/PermissionController/res/values-in/strings.xml b/PermissionController/res/values-in/strings.xml
index 4604dbaef..e409aabd9 100644
--- a/PermissionController/res/values-in/strings.xml
+++ b/PermissionController/res/values-in/strings.xml
@@ -80,7 +80,7 @@
<string name="additional_permissions" msgid="5801285469338873430">"Izin tambahan"</string>
<string name="app_permissions_info_button_label" msgid="7633312050729974623">"Buka info aplikasi"</string>
<string name="additional_permissions_more" msgid="5681220714755304407">"{count,plural, =1{# lainnya}other{# lainnya}}"</string>
- <string name="old_sdk_deny_warning" msgid="2382236998845153919">"Aplikasi ini dirancang untuk versi lama Android. Menolak izin dapat menyebabkan aplikasi tidak berfungsi lagi sesuai harapan."</string>
+ <string name="old_sdk_deny_warning" msgid="2382236998845153919">"Aplikasi ini dirancang untuk versi lama Android. Menolak izin dapat membuat aplikasi tidak lagi berfungsi sebagaimana mestinya."</string>
<string name="storage_supergroup_warning_allow" msgid="103093462784523190">"Aplikasi ini dirancang untuk versi lama Android. Jika Anda memberikan izin ini, akses ke semua penyimpanan (termasuk foto, video, musik, audio, dan file lainnya) akan diizinkan."</string>
<string name="storage_supergroup_warning_deny" msgid="6420765672683284347">"Aplikasi ini dirancang untuk versi lama Android. Jika Anda menolak izin ini, akses ke semua penyimpanan (termasuk foto, video, musik, audio, dan file lainnya) akan ditolak."</string>
<string name="default_permission_description" msgid="4624464917726285203">"melakukan tindakan yang tidak dikenal"</string>
diff --git a/PermissionController/res/values-it/strings.xml b/PermissionController/res/values-it/strings.xml
index 857734db1..c855b4ce6 100644
--- a/PermissionController/res/values-it/strings.xml
+++ b/PermissionController/res/values-it/strings.xml
@@ -17,7 +17,7 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="app_name" msgid="6098036489833144040">"App di controllo autorizzazioni"</string>
- <string name="ok" msgid="1936281769725676272">"OK"</string>
+ <string name="ok" msgid="1936281769725676272">"Ok"</string>
<string name="permission_search_keyword" msgid="1214451577494730543">"autorizzazioni"</string>
<string name="cancel" msgid="8943320028373963831">"Annulla"</string>
<string name="back" msgid="6249950659061523680">"Indietro"</string>
@@ -422,7 +422,7 @@
<string name="system_uses_microphone_and_camera" msgid="5124478304275138804">"La videocamera e il microfono sono in uso per un servizio di sistema"</string>
<string name="system_uses_camera" msgid="1911223105234441470">"La videocamera è in uso per un servizio di sistema"</string>
<string name="other_use" msgid="6564855051022776692">"Altro uso:"</string>
- <string name="ongoing_usage_dialog_ok" msgid="103556809118460072">"OK"</string>
+ <string name="ongoing_usage_dialog_ok" msgid="103556809118460072">"Ok"</string>
<string name="ongoing_usage_dialog_title" msgid="683836493556628569">"Uso recente di <xliff:g id="TYPES_LIST">%s</xliff:g>"</string>
<string name="ongoing_usage_dialog_title_mic" msgid="5966714811125593992">"Utilizzo recente di Microfono"</string>
<string name="ongoing_usage_dialog_title_camera" msgid="7819329688650711470">"Utilizzo recente di Fotocamera"</string>
diff --git a/PermissionController/res/values-iw/strings.xml b/PermissionController/res/values-iw/strings.xml
index 90536dcc8..0fc33a62b 100644
--- a/PermissionController/res/values-iw/strings.xml
+++ b/PermissionController/res/values-iw/strings.xml
@@ -346,7 +346,7 @@
<string name="no_apps_allowed" msgid="7718822655254468631">"אין אפליקציות שקיבלו הרשאה"</string>
<string name="no_apps_allowed_full" msgid="8011716991498934104">"לאף אפליקציה אין הרשאה לכל הקבצים"</string>
<string name="no_apps_allowed_scoped" msgid="4908850477787659501">"לאף אפליקציה אין הרשאה למדיה בלבד"</string>
- <string name="no_apps_denied" msgid="7663435886986784743">"אין אפליקציות שלא קיבלו ההרשאה"</string>
+ <string name="no_apps_denied" msgid="7663435886986784743">"אין אפליקציות שלא קיבלו הרשאה"</string>
<string name="car_permission_selected" msgid="180837028920791596">"נבחר"</string>
<string name="settings" msgid="5409109923158713323">"הגדרות"</string>
<string name="accessibility_service_dialog_title_single" msgid="7956432823014102366">"לשירות <xliff:g id="SERVICE_NAME">%s</xliff:g> יש גישה מלאה למכשיר שלך"</string>
@@ -471,7 +471,7 @@
<string name="permgrouprequest_device_aware_storage_isolated" msgid="6463062962458809752">"‏לתת לאפליקציה &lt;b&gt;<xliff:g id="APP_NAME">%1$s</xliff:g>&lt;/b&gt; הרשאת גישה לתמונות ולמדיה במכשיר &lt;b&gt;<xliff:g id="DEVICE_NAME">%2$s</xliff:g>&lt;/b&gt;?"</string>
<string name="permgrouprequest_contacts" msgid="8391550064551053695">"‏לתת לאפליקציה &lt;b&gt;<xliff:g id="APP_NAME">%1$s</xliff:g>&lt;/b&gt; הרשאת גישה לאנשי הקשר שלך?"</string>
<string name="permgrouprequest_device_aware_contacts" msgid="731025863972535928">"‏לתת לאפליקציה &lt;b&gt;<xliff:g id="APP_NAME">%1$s</xliff:g>&lt;/b&gt; הרשאת גישה לאנשי הקשר במכשיר &lt;b&gt;<xliff:g id="DEVICE_NAME">%2$s</xliff:g>&lt;/b&gt;?"</string>
- <string name="permgrouprequest_location" msgid="6990232580121067883">"‏לתת לאפליקציה &lt;b&gt;<xliff:g id="APP_NAME">%1$s</xliff:g>&lt;/b&gt; הרשאת גישה למיקום המכשיר?"</string>
+ <string name="permgrouprequest_location" msgid="6990232580121067883">"‏לתת לאפליקציה \'&lt;b&gt;<xliff:g id="APP_NAME">%1$s</xliff:g>&lt;/b&gt;\' הרשאת גישה למיקום המכשיר?"</string>
<string name="permgrouprequest_device_aware_location" msgid="6075412127429878638">"‏לתת לאפליקציה &lt;b&gt;<xliff:g id="APP_NAME">%1$s</xliff:g>&lt;/b&gt; הרשאת גישה למיקום של &lt;b&gt;<xliff:g id="DEVICE_NAME">%2$s</xliff:g>&lt;/b&gt;?"</string>
<string name="permgrouprequestdetail_location" msgid="2635935335778429894">"לאפליקציה תהיה גישה אל נתוני המיקום רק בזמן השימוש בה"</string>
<string name="permgroupbackgroundrequest_location" msgid="1085680897265734809">"‏לתת לאפליקציה &lt;b&gt;<xliff:g id="APP_NAME">%1$s</xliff:g>&lt;/b&gt; הרשאת גישה למיקום המכשיר?"</string>
diff --git a/PermissionController/res/values-ja/strings.xml b/PermissionController/res/values-ja/strings.xml
index ecd742f55..e7322b07b 100644
--- a/PermissionController/res/values-ja/strings.xml
+++ b/PermissionController/res/values-ja/strings.xml
@@ -74,7 +74,7 @@
<string name="days_ago" msgid="6650359081551335629">"{count,plural, =0{今日}=1{1 日前}other{# 日前}}"</string>
<string name="app_disable_dlg_positive" msgid="7418444149981904940">"アプリを無効にする"</string>
<string name="app_disable_dlg_text" msgid="3126943217146120240">"このアプリを無効にすると、Android などの他のアプリが正しく動作しなくなるおそれがあります。このアプリはデバイスにプリインストールされているため、削除できません。無効にするには、このアプリをオフにし、デバイスにアプリが表示されないようにします。"</string>
- <string name="app_permission_manager" msgid="3903811137630909550">"権限マネージャ"</string>
+ <string name="app_permission_manager" msgid="3903811137630909550">"権限マネージャー"</string>
<string name="never_ask_again" msgid="4728762438198560329">"今後表示しない"</string>
<string name="no_permissions" msgid="3881676756371148563">"権限がありません"</string>
<string name="additional_permissions" msgid="5801285469338873430">"その他の権限"</string>
diff --git a/PermissionController/res/values-km/strings.xml b/PermissionController/res/values-km/strings.xml
index 82f5c1919..60e0590ce 100644
--- a/PermissionController/res/values-km/strings.xml
+++ b/PermissionController/res/values-km/strings.xml
@@ -80,7 +80,7 @@
<string name="additional_permissions" msgid="5801285469338873430">"ការអនុញ្ញាតបន្ថែម"</string>
<string name="app_permissions_info_button_label" msgid="7633312050729974623">"បើក​ព័ត៌មាន​កម្មវិធី"</string>
<string name="additional_permissions_more" msgid="5681220714755304407">"{count,plural, =1{# ទៀត}other{# ទៀត}}"</string>
- <string name="old_sdk_deny_warning" msgid="2382236998845153919">"កម្មវិធីនេះ​ត្រូវបាន​រចនាឡើង​សម្រាប់​កំណែចាស់ជាងនេះ​របស់ Android ។ ការបដិសេធ​ការអនុញ្ញាត​អាចបណ្តាលឱ្យ​វាបំពេញ​មុខងារ​មិនដូចការគ្រោងទុក​តទៅទៀតទេ។"</string>
+ <string name="old_sdk_deny_warning" msgid="2382236998845153919">"កម្មវិធីនេះ​ត្រូវបាន​រចនាឡើង​សម្រាប់​កំណែចាស់ជាងនេះ​របស់ Android ។ ការបដិសេធ​ការអនុញ្ញាត​អាចបណ្តាលឱ្យ​វាលែងអាចបំពេញ​មុខងារ​ដូចការគ្រោងទុក។"</string>
<string name="storage_supergroup_warning_allow" msgid="103093462784523190">"កម្មវិធីនេះ​ត្រូវបាន​រចនាឡើង​សម្រាប់​កំណែចាស់​ជាងនេះ​របស់ Android។ ប្រសិនបើ​អ្នកផ្ដល់ការអនុញ្ញាតនេះ នោះសិទ្ធិចូលប្រើ​ទំហំផ្ទុក​ទាំងអស់ (រូមទាំង​រូបថត វីដេអូ តន្ត្រី សំឡេង និងឯកសារ​ផ្សេងទៀត) នឹងត្រូវបាន​អនុញ្ញាត។"</string>
<string name="storage_supergroup_warning_deny" msgid="6420765672683284347">"កម្មវិធីនេះ​ត្រូវបាន​រចនាឡើង​សម្រាប់​កំណែចាស់​ជាងនេះ​របស់ Android។ ប្រសិនបើអ្នក​បដិសេធ​ការអនុញ្ញាតនេះ នោះសិទ្ធិចូលប្រើ​ទំហំផ្ទុក​ទាំងអស់ (រូមទាំង​រូបថត វីដេអូ តន្ត្រី សំឡេង និងឯកសារ​ផ្សេងទៀត) នឹងត្រូវបាន​បដិសេធ។"</string>
<string name="default_permission_description" msgid="4624464917726285203">"ប្រតិបត្តិការសកម្មភាពមិនស្គាល់"</string>
@@ -381,7 +381,7 @@
<string name="role_emergency_search_keywords" msgid="1920007722599213358">"ក្នុងករណី​មានអាសន្ន"</string>
<string name="role_home_label" msgid="3871847846649769412">"កម្មវិធីទំព័រដើមលំនាំដើម"</string>
<string name="role_home_short_label" msgid="8544733747952272337">"កម្មវិធីទំព័រដើម"</string>
- <string name="role_home_description" msgid="7997371519626556675">"កម្មវិធី​ដែលជាទូទៅហៅថា​កម្មវិធី​ចាប់ផ្ដើម​ ដែលជំនួសអេក្រង់​ដើមនៅលើឧបករណ៍​ Android របស់អ្នក និងផ្ដល់លទ្ធភាពឱ្យអ្នក​ចូលប្រើ​ខ្លឹមសារ និងមុខងារ​របស់ឧបករណ៍​អ្នក"</string>
+ <string name="role_home_description" msgid="7997371519626556675">"កម្មវិធី​ដែលគេច្រើនតែហៅថា​កម្មវិធី​ចាប់ផ្ដើម​ វាជំនួសអេក្រង់​ដើមនៅលើឧបករណ៍​ Android របស់អ្នក និងផ្ដល់លទ្ធភាពឱ្យអ្នក​ចូលប្រើ​ខ្លឹមសារ និងមុខងារ​របស់ឧបករណ៍​អ្នក"</string>
<string name="role_home_request_title" msgid="738136983453341081">"កំណត់ <xliff:g id="APP_NAME">%1$s</xliff:g> ជាកម្មវិធី​ទំព័រដើមលំនាំដើម​របស់អ្នក?"</string>
<string name="role_home_request_description" msgid="2658833966716057673">"មិន​ត្រូវការការអនុញ្ញាត​ទេ"</string>
<string name="role_home_search_keywords" msgid="3830755001192666285">"កម្មវិធី​ចាប់ផ្ដើម"</string>
diff --git a/PermissionController/res/values-ky/strings.xml b/PermissionController/res/values-ky/strings.xml
index ec6bde78b..f72d72a1e 100644
--- a/PermissionController/res/values-ky/strings.xml
+++ b/PermissionController/res/values-ky/strings.xml
@@ -209,7 +209,7 @@
<string name="unused_apps_label_v2" msgid="7058776770056517980">"Колдонулбаган колдонмолордун ишин тындыруу"</string>
<string name="unused_apps_label_v3" msgid="693340578642156657">"Колдонмо колдонулбаса, аны тескеңиз"</string>
<string name="unused_apps_summary" msgid="8839466950318403115">"Уруксаттар өчүрүлүп, убактылуу файлдар тазаланып, билдирмелер келбей калат"</string>
- <string name="unused_apps_summary_v2" msgid="5011313200815115802">"Уруксаттарды алып салып, убактылуу файлдарды жок кылып, билдирмелерди токтотуңуз жана колдонмону архивдеңиз"</string>
+ <string name="unused_apps_summary_v2" msgid="5011313200815115802">"Уруксаттарды алып салып, убактылуу файлдарды жок кылып, билдирмелерди токтотуп, колдонмону архивдейсиз"</string>
<string name="auto_revoke_summary" msgid="5867548789805911683">"Эгер колдонмо бир нече ай пайдаланылбаса, жеке маалыматтарыңызды коргоо үчүн бул колдонмого берилген уруксаттар өчүрүлөт."</string>
<string name="auto_revoke_summary_with_permissions" msgid="389712086597285013">"Эгер колдонмо бир нече ай пайдаланылбаса, жеке дайын-даректериңизди коргоо максатында төмөнкү уруксаттар өчүрүлөт: <xliff:g id="PERMS">%1$s</xliff:g>"</string>
<string name="auto_revoked_apps_page_summary" msgid="6594753657893756536">"Жеке дайын-даректериңизди коргоо максатында, бир нече айдан бери ачылбаган колдонмолордогу уруксаттар өчүрүлдү."</string>
diff --git a/PermissionController/res/values-mr/strings.xml b/PermissionController/res/values-mr/strings.xml
index 72f121d1c..c8c259d93 100644
--- a/PermissionController/res/values-mr/strings.xml
+++ b/PermissionController/res/values-mr/strings.xml
@@ -209,7 +209,7 @@
<string name="unused_apps_label_v2" msgid="7058776770056517980">"न वापरल्यास अ‍ॅप अ‍ॅक्टिव्हिटी थांबवा"</string>
<string name="unused_apps_label_v3" msgid="693340578642156657">"वापरले नसल्यास ॲप व्यवस्थापित करा"</string>
<string name="unused_apps_summary" msgid="8839466950318403115">"परवानग्या काढून टाका, तात्पुरत्या फाइल हटवा आणि सूचना थांबवा"</string>
- <string name="unused_apps_summary_v2" msgid="5011313200815115802">"परवानग्या काढून टाका, तात्पुरत्या फाइल हटवा, सूचना थांबवा आणि ॲप संग्रहित करा"</string>
+ <string name="unused_apps_summary_v2" msgid="5011313200815115802">"परवानग्या काढून टाका, तात्पुरत्या फाइल हटवा, नोटिफिकेशन थांबवा आणि ॲप संग्रहित करा"</string>
<string name="auto_revoke_summary" msgid="5867548789805911683">"तुमच्या डेटाचे संरक्षण करण्यासाठी, अ‍ॅप काही महिन्यांत वापरले गेले नसल्यास, या अ‍ॅपच्या परवानग्या काढल्या जातील."</string>
<string name="auto_revoke_summary_with_permissions" msgid="389712086597285013">"तुमच्या डेटाचे संरक्षण करण्यासाठी, अ‍ॅप काही महिन्यांत वापरले गेले नसल्यास, पुढील परवानग्या काढल्या जातील: <xliff:g id="PERMS">%1$s</xliff:g>"</string>
<string name="auto_revoked_apps_page_summary" msgid="6594753657893756536">"तुमच्या डेटाचे संरक्षण करण्यासाठी, तुम्ही काही महिन्यांत न वापरलेल्या ॲप्समधून परवानग्या काढल्या गेल्या आहेत."</string>
diff --git a/PermissionController/res/values-my/strings.xml b/PermissionController/res/values-my/strings.xml
index b29ee2286..c488a296c 100644
--- a/PermissionController/res/values-my/strings.xml
+++ b/PermissionController/res/values-my/strings.xml
@@ -381,7 +381,7 @@
<string name="role_emergency_search_keywords" msgid="1920007722599213358">"ရေခဲ"</string>
<string name="role_home_label" msgid="3871847846649769412">"မူရင်း ပင်မစာမျက်နှာအက်ပ်"</string>
<string name="role_home_short_label" msgid="8544733747952272337">"ပင်မစာမျက်နှာ အက်ပ်"</string>
- <string name="role_home_description" msgid="7997371519626556675">"သင့် Android စက်ပစ္စည်းပေါ်ရှိ \'ပင်မစာမျက်နှာများ\' ကိုအစားထိုးသော ဖွင့်စနစ်များဟု ခေါ်လေ့ရှိသည့် အက်ပ်များနှင့် သင့်စက်ပစ္စည်း၏ အကြောင်းအရာနှင့် ဝန်ဆောင်မှုများသို့ ဝင်သုံးခွင့်ပေးသော အက်ပ်များ"</string>
+ <string name="role_home_description" msgid="7997371519626556675">"သင့်စက်၏ အကြောင်းအရာနှင့် တူးလ်များကို သုံးခွင့်ပေးပြီး သင့် Android စက်ပစ္စည်းပေါ်ရှိ \'ပင်မစာမျက်နှာများ\' ကိုအစားထိုးသော ဖွင့်စနစ်များဟု ခေါ်လေ့ရှိသည့် အက်ပ်များ"</string>
<string name="role_home_request_title" msgid="738136983453341081">"<xliff:g id="APP_NAME">%1$s</xliff:g> ကို သင့်မူရင်း ပင်မစာမျက်နှာ အက်ပ်အဖြစ် သတ်မှတ်လိုပါသလား။"</string>
<string name="role_home_request_description" msgid="2658833966716057673">"ခွင့်ပြုချက် မလိုပါ"</string>
<string name="role_home_search_keywords" msgid="3830755001192666285">"ဖွင့်တင်ပေးသူ"</string>
diff --git a/PermissionController/res/values-ne/strings.xml b/PermissionController/res/values-ne/strings.xml
index 1a5c620c1..70d474bfc 100644
--- a/PermissionController/res/values-ne/strings.xml
+++ b/PermissionController/res/values-ne/strings.xml
@@ -353,8 +353,8 @@
<string name="accessibility_service_dialog_title_multiple" msgid="5527879210683548175">"पहुँचसम्बन्धी <xliff:g id="NUM_SERVICES">%s</xliff:g> एपहरूले तपाईंको यन्त्रमाथि पूर्ण रूपमा पहुँच राख्न सक्छन्"</string>
<string name="accessibility_service_dialog_bottom_text_single" msgid="1128666197822205958">"<xliff:g id="SERVICE_NAME">%s</xliff:g> ले तपाईंको स्क्रिन, कारबाही र इनपुट हेर्न, कारबाहीहरू गर्न र डिस्प्ले नियन्त्रण गर्न सक्छ।"</string>
<string name="accessibility_service_dialog_bottom_text_multiple" msgid="7009848932395519852">"यी एपहरूले तपाईंको स्क्रिन, कारबाही र इनपुट हेर्न, कारबाहीहरू सम्पादन गर्न र प्रदर्शन नियन्त्रण गर्न सक्छन्।"</string>
- <string name="role_assistant_label" msgid="4727586018198208128">"डिफल्ट डिजिटल सहायक एप"</string>
- <string name="role_assistant_short_label" msgid="3369003713187703399">"डिजिटल सहायक एप"</string>
+ <string name="role_assistant_label" msgid="4727586018198208128">"डिफल्ट डिजिटल एसिस्टेन्ट एप"</string>
+ <string name="role_assistant_short_label" msgid="3369003713187703399">"डिजिटल एसिस्टेन्ट एप"</string>
<string name="role_assistant_description" msgid="6622458130459922952">"सहायक एपहरूले तपाईंले हेर्दै गर्नुभएको स्क्रिनबाट प्राप्त जानकारीमा आधारित भई तपाईंलाई मद्दत गर्न सक्छन्। केही एपहरूले तपाईंलाई एकीकृत सहायता दिन दुवै लन्चर र आवाज संलग्न इनपुट सेवाहरूलाई समर्थन गर्छन्।"</string>
<string name="role_browser_label" msgid="2877796144554070207">"डिफल्ट ब्राउजर एप"</string>
<string name="role_browser_short_label" msgid="6745009127123292296">"ब्राउजर"</string>
diff --git a/PermissionController/res/values-night-v33/themes.xml b/PermissionController/res/values-night-v33/themes.xml
index 9b6f638a6..31940ede5 100644
--- a/PermissionController/res/values-night-v33/themes.xml
+++ b/PermissionController/res/values-night-v33/themes.xml
@@ -52,6 +52,8 @@
@style/SecondarySafetyCenterActionButton.Responsive
</item>
+ <item name="scCardSideMargin">@dimen/sc_spacing_large</item>
+
<item name="textColorScActionButton">@color/sc_primary_action_button_text</item>
<item name="textColorScSecondaryActionButton">?android:attr/textColorPrimary</item>
diff --git a/PermissionController/res/values-or/strings.xml b/PermissionController/res/values-or/strings.xml
index 34a2de9e7..69f4fded7 100644
--- a/PermissionController/res/values-or/strings.xml
+++ b/PermissionController/res/values-or/strings.xml
@@ -60,7 +60,7 @@
<string name="grant_dialog_button_allow_all_files" msgid="4955436994954829894">"ସମସ୍ତ ଫାଇଲର ପରିଚାଳନା ପାଇଁ ଅନୁମତି ଦିଅନ୍ତୁ"</string>
<string name="grant_dialog_button_allow_media_only" msgid="4832877658422573832">"ମିଡିଆ ଫାଇଲଗୁଡ଼ିକୁ ଆକ୍ସେସ୍ ଅନୁମତି ଦିଅନ୍ତୁ"</string>
<string name="app_permissions_breadcrumb" msgid="5136969550489411650">"ଆପ୍ସ"</string>
- <string name="app_permissions" msgid="3369917736607944781">"ଆପ ଅନୁମତିଗୁଡ଼ିକ"</string>
+ <string name="app_permissions" msgid="3369917736607944781">"ଆପ ଅନୁମତି"</string>
<string name="unused_apps" msgid="2058057455175955094">"ଅବ୍ୟବହୃତ ଆପ୍ସ"</string>
<string name="edit_photos_description" msgid="5540108003480078892">"ଏହି ଆପ ପାଇଁ ଚୟନିତ ଫଟୋଗୁଡ଼ିକୁ ଏଡିଟ କରନ୍ତୁ"</string>
<string name="no_unused_apps" msgid="12809387670415295">"କୌଣସି ଅବ୍ୟବହୃତ ଆପ୍ ନାହିଁ"</string>
diff --git a/PermissionController/res/values-pt-rBR/strings.xml b/PermissionController/res/values-pt-rBR/strings.xml
index 330ee5680..fea62d7e7 100644
--- a/PermissionController/res/values-pt-rBR/strings.xml
+++ b/PermissionController/res/values-pt-rBR/strings.xml
@@ -362,7 +362,7 @@
<string name="role_browser_request_title" msgid="2895200507835937192">"Definir <xliff:g id="APP_NAME">%1$s</xliff:g> como navegador padrão?"</string>
<string name="role_browser_request_description" msgid="5888803407905985941">"Nenhuma permissão necessária"</string>
<string name="role_dialer_label" msgid="1100224146343237968">"App de telefone padrão"</string>
- <string name="role_dialer_short_label" msgid="7186888549465352489">"App de Telefone"</string>
+ <string name="role_dialer_short_label" msgid="7186888549465352489">"App de telefone"</string>
<string name="role_dialer_description" msgid="8768708633696539612">"Apps que permitem fazer e receber chamadas no seu dispositivo."</string>
<string name="role_dialer_request_title" msgid="5959618560705912058">"Definir <xliff:g id="APP_NAME">%1$s</xliff:g> como seu app de telefone padrão?"</string>
<string name="role_dialer_request_description" msgid="6288839625724909320">"Este app poderá acessar contatos, câmera, microfone, telefone e SMS"</string>
diff --git a/PermissionController/res/values-pt-rPT/strings.xml b/PermissionController/res/values-pt-rPT/strings.xml
index 590f15b2a..0ad01f7a5 100644
--- a/PermissionController/res/values-pt-rPT/strings.xml
+++ b/PermissionController/res/values-pt-rPT/strings.xml
@@ -210,7 +210,7 @@
<string name="unused_apps_label_v3" msgid="693340578642156657">"Gerir app se não for usada"</string>
<string name="unused_apps_summary" msgid="8839466950318403115">"Remover autorizações, eliminar ficheiros temporários e parar notificações"</string>
<string name="unused_apps_summary_v2" msgid="5011313200815115802">"Remove autorizações, elimina ficheiros temporários, interrompe notificações e arquiva a app"</string>
- <string name="auto_revoke_summary" msgid="5867548789805911683">"Para proteger os seus dados, as autorizações desta app serão removidas se a mesma não for utilizada durante alguns meses."</string>
+ <string name="auto_revoke_summary" msgid="5867548789805911683">"Para proteger os seus dados, as autorizações desta app serão removidas se a mesma não for usada durante alguns meses."</string>
<string name="auto_revoke_summary_with_permissions" msgid="389712086597285013">"Para proteger os seus dados, se a app não for utilizada há alguns meses, serão removidas as seguintes autorizações: <xliff:g id="PERMS">%1$s</xliff:g>"</string>
<string name="auto_revoked_apps_page_summary" msgid="6594753657893756536">"Para proteger os seus dados, foram removidas as autorizações para as apps que não utiliza há alguns meses."</string>
<string name="auto_revoke_open_app_message" msgid="8075556291711205039">"Se pretender permitir novamente as autorizações, abra a app."</string>
diff --git a/PermissionController/res/values-pt/strings.xml b/PermissionController/res/values-pt/strings.xml
index 330ee5680..fea62d7e7 100644
--- a/PermissionController/res/values-pt/strings.xml
+++ b/PermissionController/res/values-pt/strings.xml
@@ -362,7 +362,7 @@
<string name="role_browser_request_title" msgid="2895200507835937192">"Definir <xliff:g id="APP_NAME">%1$s</xliff:g> como navegador padrão?"</string>
<string name="role_browser_request_description" msgid="5888803407905985941">"Nenhuma permissão necessária"</string>
<string name="role_dialer_label" msgid="1100224146343237968">"App de telefone padrão"</string>
- <string name="role_dialer_short_label" msgid="7186888549465352489">"App de Telefone"</string>
+ <string name="role_dialer_short_label" msgid="7186888549465352489">"App de telefone"</string>
<string name="role_dialer_description" msgid="8768708633696539612">"Apps que permitem fazer e receber chamadas no seu dispositivo."</string>
<string name="role_dialer_request_title" msgid="5959618560705912058">"Definir <xliff:g id="APP_NAME">%1$s</xliff:g> como seu app de telefone padrão?"</string>
<string name="role_dialer_request_description" msgid="6288839625724909320">"Este app poderá acessar contatos, câmera, microfone, telefone e SMS"</string>
diff --git a/PermissionController/res/values-ro/strings.xml b/PermissionController/res/values-ro/strings.xml
index 177d5ab17..5f88c1376 100644
--- a/PermissionController/res/values-ro/strings.xml
+++ b/PermissionController/res/values-ro/strings.xml
@@ -514,7 +514,7 @@
<string name="permgroupupgraderequestdetail_microphone" msgid="2870497719571464239">"Aplicația dorește să înregistreze conținut audio permanent, chiar și când nu o folosești. "<annotation id="link">"Acordă această permisiune din setări."</annotation></string>
<string name="permgrouprequest_activityRecognition" msgid="5415121592794230330">"Permiți aplicației &lt;b&gt;<xliff:g id="APP_NAME">%1$s</xliff:g>&lt;/b&gt; să-ți acceseze activitatea fizică?"</string>
<string name="permgrouprequest_device_aware_activityRecognition" msgid="1243869530588745374">"Permiți ca &lt;b&gt;<xliff:g id="APP_NAME">%1$s</xliff:g>&lt;/b&gt; să acceseze activitatea ta fizică de pe &lt;b&gt;<xliff:g id="DEVICE_NAME">%2$s</xliff:g>&lt;/b&gt;?"</string>
- <string name="permgrouprequest_camera" msgid="5123097035410002594">"Permiți ca &lt;b&gt;<xliff:g id="APP_NAME">%1$s</xliff:g>&lt;/b&gt; să fotografieze și să înregistreze video?"</string>
+ <string name="permgrouprequest_camera" msgid="5123097035410002594">"Permiți ca &lt;b&gt;<xliff:g id="APP_NAME">%1$s</xliff:g>&lt;/b&gt; să fotografieze și să înregistreze videoclipuri?"</string>
<string name="permgrouprequest_device_aware_camera" msgid="5340173564041615494">"Permiți ca &lt;b&gt;<xliff:g id="APP_NAME">%1$s</xliff:g>&lt;/b&gt; să facă fotografii și să înregistreze videoclipuri pe &lt;b&gt;<xliff:g id="DEVICE_NAME">%2$s</xliff:g>&lt;/b&gt;?"</string>
<string name="permgrouprequestdetail_camera" msgid="9085323239764667883">"Aplicația va putea să fotografieze și să înregistreze videoclipuri doar când o folosești"</string>
<string name="permgroupbackgroundrequest_camera" msgid="1274286575704213875">"Permiți ca &lt;b&gt;<xliff:g id="APP_NAME">%1$s</xliff:g>&lt;/b&gt; să facă fotografii și să înregistreze videoclipuri?"</string>
diff --git a/PermissionController/res/values-ru/strings.xml b/PermissionController/res/values-ru/strings.xml
index 36b6c8bfe..a96a76df3 100644
--- a/PermissionController/res/values-ru/strings.xml
+++ b/PermissionController/res/values-ru/strings.xml
@@ -580,7 +580,7 @@
<string name="security_settings" msgid="3808106921175271317">"Настройки безопасности"</string>
<string name="sensor_permissions_qs" msgid="1022267900031317472">"Разрешения"</string>
<string name="safety_privacy_qs_tile_title" msgid="727301867710374052">"Защита и кон­фи­ден­циаль­ность"</string>
- <string name="safety_privacy_qs_tile_subtitle" msgid="3621544532041936749">"Проверьте статус."</string>
+ <string name="safety_privacy_qs_tile_subtitle" msgid="3621544532041936749">"Проверьте состояние"</string>
<string name="privacy_controls_qs" msgid="5780144882040591169">"Ваши настройки конфиденциальности"</string>
<string name="security_settings_button_label_qs" msgid="8280343822465962330">"Другие настройки"</string>
<string name="camera_toggle_label_qs" msgid="3880261453066157285">"Доступ к камере"</string>
diff --git a/PermissionController/res/values-sr/strings.xml b/PermissionController/res/values-sr/strings.xml
index 957c026e6..d89ebba68 100644
--- a/PermissionController/res/values-sr/strings.xml
+++ b/PermissionController/res/values-sr/strings.xml
@@ -369,7 +369,7 @@
<string name="role_dialer_search_keywords" msgid="3324448983559188087">"бројчаник"</string>
<string name="role_sms_label" msgid="8456999857547686640">"Подразумевана апл. за SMS"</string>
<string name="role_sms_short_label" msgid="4371444488034692243">"Апликација за SMS"</string>
- <string name="role_sms_description" msgid="3424020199148153513">"Апликације које вам омогућавају да користите број телефона да бисте слали и примали кратке SMS-ове, слике, видео снимке и још тога"</string>
+ <string name="role_sms_description" msgid="3424020199148153513">"Апликације које вам омогућавају да користите број телефона да бисте слали и примали кратке текстуалне поруке, слике, видео снимке и друго"</string>
<string name="role_sms_request_title" msgid="7953552109601185602">"Желите ли да подесите <xliff:g id="APP_NAME">%1$s</xliff:g> као подразумевану апликацју за SMS?"</string>
<string name="role_sms_request_description" msgid="2691004766132144886">"Ова апликација ће добити приступ камери, контактима, фајловима и медијима, микрофону, телефону и SMS-овима"</string>
<string name="role_sms_search_keywords" msgid="8022048144395047352">"SMS, слање SMS-ова, поруке, слање порука"</string>
@@ -471,10 +471,10 @@
<string name="permgrouprequest_device_aware_storage_isolated" msgid="6463062962458809752">"Дозвољавате да апликација &lt;b&gt;<xliff:g id="APP_NAME">%1$s</xliff:g>&lt;/b&gt; приступа сликама и медијима на: &lt;b&gt;<xliff:g id="DEVICE_NAME">%2$s</xliff:g>&lt;/b&gt;?"</string>
<string name="permgrouprequest_contacts" msgid="8391550064551053695">"Желите да дозволите да &lt;b&gt;<xliff:g id="APP_NAME">%1$s</xliff:g>&lt;/b&gt; приступа контактима?"</string>
<string name="permgrouprequest_device_aware_contacts" msgid="731025863972535928">"Дозвољавате да апликација &lt;b&gt;<xliff:g id="APP_NAME">%1$s</xliff:g>&lt;/b&gt; приступа контактима на: &lt;b&gt;<xliff:g id="DEVICE_NAME">%2$s</xliff:g>&lt;/b&gt;?"</string>
- <string name="permgrouprequest_location" msgid="6990232580121067883">"Желите да дозволите да &lt;b&gt;<xliff:g id="APP_NAME">%1$s</xliff:g>&lt;/b&gt; приступа локацији овог уређаја?"</string>
+ <string name="permgrouprequest_location" msgid="6990232580121067883">"Желите да дозволите да апликација &lt;b&gt;<xliff:g id="APP_NAME">%1$s</xliff:g>&lt;/b&gt; приступа локацији овог уређаја?"</string>
<string name="permgrouprequest_device_aware_location" msgid="6075412127429878638">"Дозвољавате да апликација &lt;b&gt;<xliff:g id="APP_NAME">%1$s</xliff:g>&lt;/b&gt; приступа локацији уређаја &lt;b&gt;<xliff:g id="DEVICE_NAME">%2$s</xliff:g>&lt;/b&gt;?"</string>
<string name="permgrouprequestdetail_location" msgid="2635935335778429894">"Апликација ће имати приступ локацији само док користите апликацију"</string>
- <string name="permgroupbackgroundrequest_location" msgid="1085680897265734809">"Желите да дозволите да &lt;b&gt;<xliff:g id="APP_NAME">%1$s</xliff:g>&lt;/b&gt; приступа локацији овог уређаја?"</string>
+ <string name="permgroupbackgroundrequest_location" msgid="1085680897265734809">"Желите да дозволите да апликација &lt;b&gt;<xliff:g id="APP_NAME">%1$s</xliff:g>&lt;/b&gt; приступа локацији овог уређаја?"</string>
<string name="permgroupbackgroundrequest_device_aware_location" msgid="1264484517831380016">"Дозвољавате да апликација &lt;b&gt;<xliff:g id="APP_NAME">%1$s</xliff:g>&lt;/b&gt; приступа локацији уређаја &lt;b&gt;<xliff:g id="DEVICE_NAME">%2$s</xliff:g>&lt;/b&gt;?"</string>
<string name="permgroupbackgroundrequestdetail_location" msgid="8021219324989662957">"Ова апликација можда жели да приступа локацији све време, чак и када не користите апликацију. "<annotation id="link">"Дозволите у подешавањима."</annotation></string>
<string name="permgroupupgraderequest_location" msgid="8328408946822691636">"Желите ли да промените приступ локацији за апликацију &lt;b&gt;<xliff:g id="APP_NAME">%1$s</xliff:g>&lt;/b&gt;?"</string>
diff --git a/PermissionController/res/values-sv/strings.xml b/PermissionController/res/values-sv/strings.xml
index 937a12200..c5d3969de 100644
--- a/PermissionController/res/values-sv/strings.xml
+++ b/PermissionController/res/values-sv/strings.xml
@@ -450,7 +450,7 @@
<string name="special_app_access_no_apps" msgid="4102911722787886970">"Inga appar"</string>
<string name="home_missing_work_profile_support" msgid="1756855847669387977">"Jobbprofiler stöds inte"</string>
<string name="encryption_unaware_confirmation_message" msgid="8274491794636402484">"Obs! Om du startar om mobilen och har ställt in ett skärmlås kan appen inte startas förrän du låser upp mobilen."</string>
- <string name="assistant_confirmation_message" msgid="7476540402884416212">"Assistenten kan läsa information om appar som används i systemet, inklusive information som visas på skärmen eller är åtkomlig i apparna."</string>
+ <string name="assistant_confirmation_message" msgid="7476540402884416212">"Assistent kan läsa information om appar som används i systemet, inklusive information som visas på skärmen eller är åtkomlig i apparna."</string>
<string name="incident_report_channel_name" msgid="3144954065936288440">"Dela felsökningsinformation"</string>
<string name="incident_report_notification_title" msgid="4635984625656519773">"Vill du dela detaljerad felsökningsinformation?"</string>
<string name="incident_report_notification_text" msgid="3376480583513587923">"<xliff:g id="APP_NAME">%1$s</xliff:g> vill ladda upp felsökningsinformation."</string>
diff --git a/PermissionController/res/values-te/strings.xml b/PermissionController/res/values-te/strings.xml
index 13097cdf0..a461319a4 100644
--- a/PermissionController/res/values-te/strings.xml
+++ b/PermissionController/res/values-te/strings.xml
@@ -238,7 +238,7 @@
<string name="permission_description_summary_camera" msgid="108004375101882069">"ఈ అనుమతి ఉన్న యాప్‌లు ఫోటోలు తీయగలవు, వీడియోను రికార్డ్ చేయగలవు"</string>
<string name="permission_description_summary_contacts" msgid="2337798886460408996">"ఈ అనుమతి ఉన్న యాప్‌లు మీ కాంటాక్ట్‌లను యాక్సెస్ చేయగలవు"</string>
<string name="permission_description_summary_location" msgid="2817531799933480694">"ఈ అనుమతి ఉన్న యాప్‌లు ఈ పరికర లొకేషన్‌ను యాక్సెస్ చేయగలవు"</string>
- <string name="permission_description_summary_nearby_devices" msgid="8269183818275073741">"ఈ అనుమతి ఉన్న యాప్‌లు సమీప పరికరాలను గుర్తించవచ్చు, వాటి సంబంధిత స్థానాన్ని తెలుసుకోవచ్చు, అలాగే వాటికి కనెక్ట్ చేయవచ్చు"</string>
+ <string name="permission_description_summary_nearby_devices" msgid="8269183818275073741">"ఈ అనుమతి ఉన్న యాప్‌లు సమీప పరికరాలను గుర్తించవచ్చు, వాటి సంబంధిత పొజిషన్‌ను తెలుసుకోవచ్చు, అలాగే వాటికి కనెక్ట్ చేయవచ్చు"</string>
<string name="permission_description_summary_microphone" msgid="630834800308329907">"ఈ అనుమతి ఉన్న యాప్‌లు ఆడియోను రికార్డ్ చేయగలవు"</string>
<string name="permission_description_summary_phone" msgid="4515277217435233619">"ఈ అనుమతులు ఉన్న యాప్‌లు ఫోన్ కాల్స్‌ చేయగలవు, మేనేజ్ చేయగలవు"</string>
<string name="permission_description_summary_sensors" msgid="1836045815643119949">"ఈ అనుమతి ఉన్న యాప్‌లు మీ ఆరోగ్య స్థితిని తెలియజేసే గణాంకాల సెన్సార్ డేటాను యాక్సెస్ చేయగలవు"</string>
@@ -480,9 +480,9 @@
<string name="permgroupupgraderequest_location" msgid="8328408946822691636">"&lt;b&gt;<xliff:g id="APP_NAME">%1$s</xliff:g>&lt;/b&gt; కోసం లొకేష‌న్‌ యాక్సెస్‌ను మార్చాలా?"</string>
<string name="permgroupupgraderequest_device_aware_location" msgid="1812338666887726191">"&lt;b&gt;<xliff:g id="DEVICE_NAME">%2$s</xliff:g>&lt;/b&gt;‌లో &lt;b&gt;<xliff:g id="APP_NAME">%1$s</xliff:g>&lt;/b&gt;‌కు సంబంధించిన లొకేషన్ యాక్సెస్‌ను మార్చాలా?"</string>
<string name="permgroupupgraderequestdetail_location" msgid="1550899076845189165">"మీరు యాప్ ఉపయోగించనప్పుడు కూడా ఈ యాప్ మీ లొకేష‌న్‌ను ఎప్పటికప్పుడు యాక్సెస్ చేయాల‌ని అనుకుంటోంది."<annotation id="link">"సెట్టింగ్‌లలో అనుమతించండి."</annotation></string>
- <string name="permgrouprequest_nearby_devices" msgid="2272829282660436700">"సమీప పరికరాల సంబంధిత స్థానాన్ని కనుగొనడానికి, కనెక్ట్ చేయడానికి అలాగే నిర్ణయించడానికి \"&lt;b&gt;<xliff:g id="APP_NAME">%1$s</xliff:g>&lt;/b&gt;\"ను అనుమతించాలా?"</string>
+ <string name="permgrouprequest_nearby_devices" msgid="2272829282660436700">"సమీప పరికరాల సంబంధిత పొజిషన్‌ను కనుగొనడానికి, కనెక్ట్ చేయడానికి అలాగే నిర్ణయించడానికి \"&lt;b&gt;<xliff:g id="APP_NAME">%1$s</xliff:g>&lt;/b&gt;\"ను అనుమతించాలా?"</string>
<string name="permgrouprequest_device_aware_nearby_devices" msgid="5293478278408567442">"&lt;b&gt;<xliff:g id="DEVICE_NAME">%2$s</xliff:g>&lt;/b&gt;‌లో సమీప పరికరాలు కనుగొని, కనెక్ట్ అయి, వాటి దూరం అంచనా వేసేలా &lt;b&gt;<xliff:g id="APP_NAME">%1$s</xliff:g>&lt;/b&gt;‌ను అనుమతించాలా?"</string>
- <string name="permgroupupgraderequestdetail_nearby_devices" msgid="6877531270654738614">"సమీప పరికరాల సంబంధిత స్థానాన్ని కనుగొనడానికి, కనెక్ట్ చేయడానికి అలాగే నిర్ణయించడానికి &lt;b&gt;<xliff:g id="APP_NAME">%1$s</xliff:g>&lt;/b&gt;ను అనుమతించాలా? "<annotation id="link">"సెట్టింగ్‌లలో అనుమతించండి."</annotation></string>
+ <string name="permgroupupgraderequestdetail_nearby_devices" msgid="6877531270654738614">"సమీప పరికరాల సంబంధిత పొజిషన్‌ను కనుగొనడానికి, కనెక్ట్ చేయడానికి అలాగే నిర్ణయించడానికి &lt;b&gt;<xliff:g id="APP_NAME">%1$s</xliff:g>&lt;/b&gt;ను అనుమతించాలా? "<annotation id="link">"సెట్టింగ్‌లలో అనుమతించండి."</annotation></string>
<string name="permgrouprequest_fineupgrade" msgid="2334242928821697672">"<xliff:g id="APP_NAME">&lt;b&gt;%1$s&lt;/b&gt;</xliff:g>కు సంబంధించిన లొకేషన్ యాక్సెస్‌ను సుమారు నుండి ఖచ్చితమైనదిగా మార్చాలా?"</string>
<string name="permgrouprequest_device_aware_fineupgrade" msgid="4453775952305587571">"&lt;b&gt;<xliff:g id="DEVICE_NAME">%2$s</xliff:g>&lt;/b&gt;‌లో &lt;b&gt;<xliff:g id="APP_NAME">&lt;b&gt;%1$s&lt;/b&gt;</xliff:g>&lt;/b&gt; లొకేషన్ యాక్సెస్‌ను రమారమి నుండి ఖచ్చితమైన లొకేషన్‌కు మార్చాలా?"</string>
<string name="permgrouprequest_coarselocation" msgid="7244605063736425232">"ఈ పరికరానికి సంబంధించి సుమారుగా ఉన్న లొకేషన్‌ను యాక్సెస్ చేయడానికి &lt;b&gt;<xliff:g id="APP_NAME">%1$s</xliff:g>&lt;/b&gt;ని అనుమతించాలా?"</string>
diff --git a/PermissionController/res/values-tr/strings.xml b/PermissionController/res/values-tr/strings.xml
index 14f6885af..467d98bdc 100644
--- a/PermissionController/res/values-tr/strings.xml
+++ b/PermissionController/res/values-tr/strings.xml
@@ -369,7 +369,7 @@
<string name="role_dialer_search_keywords" msgid="3324448983559188087">"çevirici"</string>
<string name="role_sms_label" msgid="8456999857547686640">"Varsayılan SMS uygulaması"</string>
<string name="role_sms_short_label" msgid="4371444488034692243">"SMS uygulaması"</string>
- <string name="role_sms_description" msgid="3424020199148153513">"Kısa mesajlar, fotoğraflar, videolar ve başka içerikler göndermek ve almak için telefon numaranızı kullanmanıza olanak tanıyan uygulamalar"</string>
+ <string name="role_sms_description" msgid="3424020199148153513">"Kısa mesaj, fotoğraf ve video gibi içerikleri göndermek ve almak için telefon numaranızı kullanabileceğiniz uygulamalar"</string>
<string name="role_sms_request_title" msgid="7953552109601185602">"<xliff:g id="APP_NAME">%1$s</xliff:g> varsayılan SMS uygulamanız olarak ayarlansın mı?"</string>
<string name="role_sms_request_description" msgid="2691004766132144886">"Bu uygulama Kamera, Kişiler, Dosyalar ve medya, Mikrofon, Telefon ve SMS\'inize erişebilecek"</string>
<string name="role_sms_search_keywords" msgid="8022048144395047352">"kısa mesaj, kısa mesaj gönderme, mesajlar, mesajlaşma"</string>
@@ -381,7 +381,7 @@
<string name="role_emergency_search_keywords" msgid="1920007722599213358">"acil durumda"</string>
<string name="role_home_label" msgid="3871847846649769412">"Varsayılan ana ekran uygulaması"</string>
<string name="role_home_short_label" msgid="8544733747952272337">"Ana ekran uygulaması"</string>
- <string name="role_home_description" msgid="7997371519626556675">"Android cihazınızın Ana ekranı yerine geçen ve cihazınızın içeriklerine ve özelliklerine erişmenizi sağlayan, genellikle başlatıcı olarak adlandırılan uygulamalar"</string>
+ <string name="role_home_description" msgid="7997371519626556675">"Android cihazınızın ana ekranı yerine geçen, cihazınızın içeriklerine ve özelliklerine erişmenizi sağlayan, genellikle başlatıcılar olarak adlandırılan uygulamalar"</string>
<string name="role_home_request_title" msgid="738136983453341081">"<xliff:g id="APP_NAME">%1$s</xliff:g>, varsayılan ana ekran uygulamanız olarak ayarlansın mı?"</string>
<string name="role_home_request_description" msgid="2658833966716057673">"Herhangi bir izin gerekli değil"</string>
<string name="role_home_search_keywords" msgid="3830755001192666285">"başlatıcı"</string>
diff --git a/PermissionController/res/values-uk/strings.xml b/PermissionController/res/values-uk/strings.xml
index 7d713e2ff..b05fb81e4 100644
--- a/PermissionController/res/values-uk/strings.xml
+++ b/PermissionController/res/values-uk/strings.xml
@@ -195,7 +195,7 @@
<string name="app_permission_button_allow_limited_access" msgid="8824410215149764113">"Дозволити обмежений доступ"</string>
<string name="precise_image_description" msgid="6349638632303619872">"Точне місцезнаходження"</string>
<string name="approximate_image_description" msgid="938803699637069884">"Приблизне місцезнаходження"</string>
- <string name="app_permission_location_accuracy" msgid="7166912915040018669">"Використовувати точне місцезнаходження"</string>
+ <string name="app_permission_location_accuracy" msgid="7166912915040018669">"Точне місцезнаходження"</string>
<string name="app_permission_location_accuracy_subtitle" msgid="2654077606404987210">"Якщо вимкнено доступ до точного місцезнаходження, додатки можуть отримувати дані про приблизне"</string>
<string name="app_permission_title" msgid="2090897901051370711">"Дозвіл \"<xliff:g id="PERM">%1$s</xliff:g>\""</string>
<string name="app_permission_header" msgid="2951363137032603806">"<xliff:g id="PERM">%1$s</xliff:g>: доступ для цього додатка"</string>
@@ -362,7 +362,7 @@
<string name="role_browser_request_title" msgid="2895200507835937192">"Зробити <xliff:g id="APP_NAME">%1$s</xliff:g> веб-переглядачем за умовчанням?"</string>
<string name="role_browser_request_description" msgid="5888803407905985941">"Дозволи не потрібні"</string>
<string name="role_dialer_label" msgid="1100224146343237968">"Дзвінки за умовчанням"</string>
- <string name="role_dialer_short_label" msgid="7186888549465352489">"Додаток для викликів"</string>
+ <string name="role_dialer_short_label" msgid="7186888549465352489">"Додаток для дзвінків"</string>
<string name="role_dialer_description" msgid="8768708633696539612">"Додатки, у яких можна здійснювати й приймати виклики на пристрої"</string>
<string name="role_dialer_request_title" msgid="5959618560705912058">"Чи має <xliff:g id="APP_NAME">%1$s</xliff:g> використовуватись як додаток для викликів за умовчанням?"</string>
<string name="role_dialer_request_description" msgid="6288839625724909320">"Цьому додатку буде надано дозволи \"Камера\", \"Контакти\", \"Мікрофон\", \"Телефон\" і \"SMS\""</string>
@@ -381,7 +381,7 @@
<string name="role_emergency_search_keywords" msgid="1920007722599213358">"в екстреному випадку"</string>
<string name="role_home_label" msgid="3871847846649769412">"Додаток головного екрана за умовчанням"</string>
<string name="role_home_short_label" msgid="8544733747952272337">"Додаток головного екрана"</string>
- <string name="role_home_description" msgid="7997371519626556675">"Додатки, які заміняють головний екран і забезпечують доступ до вмісту та функцій пристрою Android (так звані панелі запуску)"</string>
+ <string name="role_home_description" msgid="7997371519626556675">"Додатки, які заміняють головний екран і забезпечують доступ до вмісту й функцій пристрою Android (так звані панелі запуску)"</string>
<string name="role_home_request_title" msgid="738136983453341081">"Чи має <xliff:g id="APP_NAME">%1$s</xliff:g> використовуватись як додаток головного екрана за умовчанням?"</string>
<string name="role_home_request_description" msgid="2658833966716057673">"Дозволи не потрібні"</string>
<string name="role_home_search_keywords" msgid="3830755001192666285">"панель запуску"</string>
diff --git a/PermissionController/res/values-v33/attrs.xml b/PermissionController/res/values-v33/attrs.xml
index 4a417076d..b136ab718 100644
--- a/PermissionController/res/values-v33/attrs.xml
+++ b/PermissionController/res/values-v33/attrs.xml
@@ -39,8 +39,9 @@
<attr name="scStatusButtonStyle" format="reference" />
<attr name="scActionButtonListLayout" format="reference" />
<attr name="scActionButtonTheme" format="reference" />
- <attr name="scActionButtonStyle" format="reference"/>
- <attr name="scSecondaryActionButtonStyle" format="reference"/>
+ <attr name="scActionButtonStyle" format="reference" />
+ <attr name="scSecondaryActionButtonStyle" format="reference" />
+ <attr name="scCardSideMargin" format="dimension" />
<attr name="colorScShieldAccent" format="color" />
</resources> \ No newline at end of file
diff --git a/PermissionController/res/values-v33/styles.xml b/PermissionController/res/values-v33/styles.xml
index 94344615b..7e959c2f4 100644
--- a/PermissionController/res/values-v33/styles.xml
+++ b/PermissionController/res/values-v33/styles.xml
@@ -317,8 +317,8 @@
<item name="android:paddingEnd">@dimen/sc_spacing_xxxlarge</item>
<item name="android:paddingTop">@dimen/sc_spacing_xxxlarge</item>
<item name="android:paddingBottom">@dimen/sc_card_margin_bottom</item>
- <item name="android:layout_marginStart">@dimen/sc_spacing_large</item>
- <item name="android:layout_marginEnd">@dimen/sc_spacing_large</item>
+ <item name="android:layout_marginStart">?attr/scCardSideMargin</item>
+ <item name="android:layout_marginEnd">?attr/scCardSideMargin</item>
<item name="android:background">@drawable/safety_center_card_background</item>
</style>
diff --git a/PermissionController/res/values-v33/themes.xml b/PermissionController/res/values-v33/themes.xml
index 82a8ef5b6..a3d1b16c3 100644
--- a/PermissionController/res/values-v33/themes.xml
+++ b/PermissionController/res/values-v33/themes.xml
@@ -57,6 +57,8 @@
@style/SecondarySafetyCenterActionButton.Fixed
</item>
+ <item name="scCardSideMargin">@dimen/sc_spacing_large</item>
+
<item name="textColorScActionButton">@color/sc_primary_action_button_text</item>
<item name="textColorScSecondaryActionButton">?android:attr/textColorPrimary</item>
@@ -69,6 +71,10 @@
<style name="Theme.SafetyCenterQs" parent="Theme.SafetyCenterQsBase" />
+ <style name="Theme.SafetyCenterQsExpressive" parent="Theme.SafetyCenterQs">
+ <item name="scCardSideMargin">0dp</item>
+ </style>
+
<style name="Theme.SafetyCenterBase" parent="Theme.PermissionController.Settings.FilterTouches">
<item name="colorSurface">@color/sc_surface_light</item>
<item name="colorSurfaceVariant">@color/sc_surface_variant_light</item>
@@ -104,6 +110,8 @@
@style/SecondarySafetyCenterActionButton.Responsive
</item>
+ <item name="scCardSideMargin">@dimen/sc_spacing_large</item>
+
<item name="textColorScActionButton">@color/sc_primary_action_button_text</item>
<item name="textColorScSecondaryActionButton">?android:attr/textColorPrimary</item>
@@ -115,4 +123,8 @@
</style>
<style name="Theme.SafetyCenter" parent="Theme.SafetyCenterBase" />
+
+ <style name="Theme.SafetyCenterExpressive" parent="Theme.SafetyCenter">
+ <item name="scCardSideMargin">0dp</item>
+ </style>
</resources> \ No newline at end of file
diff --git a/PermissionController/res/values-v35/styles.xml b/PermissionController/res/values-v35/styles.xml
index 612125fb1..513754bb8 100644
--- a/PermissionController/res/values-v35/styles.xml
+++ b/PermissionController/res/values-v35/styles.xml
@@ -197,5 +197,221 @@
<item name="android:visibility">gone</item>
</style>
+ <style name="PermissionSelectorWithWidgetPreferenceRootLayoutStyle">
+ <item name="android:layout_width">match_parent</item>
+ <item name="android:layout_height">wrap_content</item>
+ <item name="android:background">?android:attr/selectableItemBackground</item>
+ <item name="android:gravity">center_vertical</item>
+ <item name="android:minHeight">?android:attr/listPreferredItemHeightSmall</item>
+ <item name="android:paddingEnd">?android:attr/listPreferredItemPaddingEnd</item>
+ </style>
+
+ <style name="PermissionSelectorWithWidgetPreferenceWidgetFrameStyle">
+ <item name="android:layout_width">wrap_content</item>
+ <item name="android:layout_height">match_parent</item>
+ <item name="android:paddingHorizontal">20dp</item>
+ <item name="android:gravity">center</item>
+ <item name="android:minWidth">56dp</item>
+ <item name="android:orientation">vertical</item>
+ </style>
+
+ <style name="PermissionSelectorWithWidgetPreferenceIconFrameStyle">
+ <item name="android:layout_width">wrap_content</item>
+ <item name="android:layout_height">wrap_content</item>
+ <item name="android:gravity">center_vertical</item>
+ <item name="android:minWidth">32dp</item>
+ <item name="android:orientation">horizontal</item>
+ <item name="android:layout_marginEnd">16dp</item>
+ <item name="android:paddingTop">4dp</item>
+ <item name="android:paddingBottom">4dp</item>
+ </style>
+
+ <style name="PermissionSelectorWithWidgetPreferenceIconStyle">
+ <item name="android:layout_width">wrap_content</item>
+ <item name="android:layout_height">wrap_content</item>
+ <item name="maxWidth">@dimen/secondary_app_icon_size</item>
+ <item name="maxHeight">@dimen/secondary_app_icon_size</item>
+ </style>
+
+ <style name="PermissionSelectorWithWidgetPreferenceTextContainerStyle">
+ <item name="android:layout_width">0dp</item>
+ <item name="android:layout_height">wrap_content</item>
+ <item name="android:layout_weight">1</item>
+ <item name="android:orientation">vertical</item>
+ <item name="android:paddingTop">16dp</item>
+ <item name="android:paddingBottom">16dp</item>
+ <item name="android:paddingEnd">?android:attr/listPreferredItemPaddingEnd</item>
+ </style>
+
+ <style name="PermissionSelectorWithWidgetPreferenceTitleStyle">
+ <item name="android:layout_width">wrap_content</item>
+ <item name="android:layout_height">wrap_content</item>
+ <item name="android:maxLines">2</item>
+ <item name="android:ellipsize">end</item>
+ <item name="android:hyphenationFrequency">normalFast</item>
+ <item name="android:lineBreakWordStyle">phrase</item>
+ <item name="android:textAppearance">?android:attr/textAppearanceListItem</item>
+ </style>
+
+ <style name="PermissionSelectorWithWidgetPreferenceSummaryContainerStyle">
+ <item name="android:layout_width">match_parent</item>
+ <item name="android:layout_height">wrap_content</item>
+ <item name="android:visibility">gone</item>
+ </style>
+
+ <style name="PermissionSelectorWithWidgetPreferenceSummaryStyle">
+ <item name="android:layout_width">0dp</item>
+ <item name="android:layout_height">wrap_content</item>
+ <item name="android:layout_weight">1</item>
+ <item name="android:textAppearance">?android:attr/textAppearanceSmall</item>
+ <item name="android:textAlignment">viewStart</item>
+ <item name="android:hyphenationFrequency">normalFast</item>
+ <item name="android:lineBreakWordStyle">phrase</item>
+ <item name="android:textColor">?android:attr/textColorSecondary</item>
+ </style>
+
+ <style name="PermissionSelectorWithWidgetPreferenceAppendixStyle">
+ <item name="android:layout_width">0dp</item>
+ <item name="android:layout_height">wrap_content</item>
+ <item name="android:layout_weight">1</item>
+ <item name="android:textAppearance">?android:attr/textAppearanceSmall</item>
+ <item name="android:textAlignment">viewEnd</item>
+ <item name="android:textColor">?android:attr/textColorSecondary</item>
+ <item name="android:maxLines">1</item>
+ <item name="android:visibility">gone</item>
+ <item name="android:ellipsize">end</item>
+ </style>
+
+ <style name="PermissionSelectorWithWidgetPreferenceExtraWidgetContainerStyle">
+ <item name="android:layout_width">wrap_content</item>
+ <item name="android:layout_height">match_parent</item>
+ <item name="android:orientation">horizontal</item>
+ <item name="android:gravity">center_vertical</item>
+ </style>
+
+ <style name="PermissionSelectorWithWidgetPreferenceExtraWidgetDividerStyle">
+ <item name="android:layout_width">.75dp</item>
+ <item name="android:layout_height">32dp</item>
+ <item name="android:layout_marginTop">16dp</item>
+ <item name="android:layout_marginBottom">16dp</item>
+ <item name="android:background">?android:attr/dividerVertical</item>
+ </style>
+
+ <style name="PermissionSelectorWithWidgetPreferenceExtraWidgetImageStyle">
+ <item name="android:layout_width">match_parent</item>
+ <item name="android:minWidth">@dimen/two_target_min_width</item>
+ <item name="android:layout_height">fill_parent</item>
+ <item name="android:src">@drawable/ic_settings_accent</item>
+ <item name="android:paddingStart">24dp</item>
+ <item name="android:paddingEnd">24dp</item>
+ <item name="android:layout_gravity">center</item>
+ <item name="android:background">?android:attr/selectableItemBackground</item>
+ </style>
+
+ <style name="PermissionSelectorWithWidgetPreferenceWidgetRadioButton">
+ <item name="android:layout_width">wrap_content</item>
+ <item name="android:layout_height">wrap_content</item>
+ <item name="android:layout_gravity">center</item>
+ <item name="android:background">@null</item>
+ <item name="android:focusable">false</item>
+ <item name="android:clickable">false</item>
+ </style>
+
+ <style name="PermissionTwoTargetPreferenceRootLayoutStyle">
+ <item name="android:layout_width">match_parent</item>
+ <item name="android:layout_height">wrap_content</item>
+ <item name="android:minHeight">?android:attr/listPreferredItemHeightSmall</item>
+ <item name="android:gravity">center_vertical</item>
+ <item name="android:background">?android:attr/selectableItemBackground</item>
+ <item name="android:paddingStart">?android:attr/listPreferredItemPaddingStart</item>
+ <item name="android:paddingEnd">?android:attr/listPreferredItemPaddingEnd</item>
+ <item name="android:clipToPadding">false</item>
+ </style>
+
+ <style name="PermissionTwoTargetPreferenceTextContainerStyle">
+ <item name="android:layout_width">wrap_content</item>
+ <item name="android:layout_height">wrap_content</item>
+ <item name="android:layout_weight">1</item>
+ <item name="android:paddingTop">16dp</item>
+ <item name="android:paddingBottom">16dp</item>
+ </style>
+
+ <style name="PermissionTwoTargetPreferenceTitleStyle">
+ <item name="android:layout_width">wrap_content</item>
+ <item name="android:layout_height">wrap_content</item>
+ <item name="android:maxLines">2</item>
+ <item name="android:hyphenationFrequency">normalFast</item>
+ <item name="android:lineBreakWordStyle">phrase</item>
+ <item name="android:textAppearance">?android:attr/textAppearanceListItem</item>
+ <item name="android:ellipsize">marquee</item>
+ </style>
+
+ <style name="PermissionTwoTargetPreferenceSummaryStyle">
+ <item name="android:layout_width">wrap_content</item>
+ <item name="android:layout_height">wrap_content</item>
+ <item name="android:layout_below">@android:id/title</item>
+ <item name="android:layout_alignStart">@android:id/title</item>
+ <item name="android:textAppearance">?android:attr/textAppearanceListItemSecondary</item>
+ <item name="android:textColor">?android:attr/textColorSecondary</item>
+ <item name="android:hyphenationFrequency">normalFast</item>
+ <item name="android:lineBreakWordStyle">phrase</item>
+ <item name="android:maxLines">10</item>
+ </style>
+
+ <style name="PermissionTwoTargetPreferenceWidgetFrameStyle">
+ <item name="android:layout_width">wrap_content</item>
+ <item name="android:layout_height">match_parent</item>
+ <item name="android:minWidth">@dimen/two_target_min_width</item>
+ <item name="android:gravity">center</item>
+ <item name="android:orientation">vertical</item>
+ </style>
+
+ <style name="PermissionTwoTargetPreferenceIconFrameStyle">
+ <item name="android:layout_width">wrap_content</item>
+ <item name="android:layout_height">wrap_content</item>
+ <item name="android:minWidth">48dp</item>
+ <item name="android:gravity">start|center_vertical</item>
+ <item name="android:orientation">horizontal</item>
+ <item name="android:paddingLeft">0dp</item>
+ <item name="android:paddingStart">0dp</item>
+ <item name="android:paddingRight">8dp</item>
+ <item name="android:paddingEnd">8dp</item>
+ <item name="android:paddingTop">4dp</item>
+ <item name="android:paddingBottom">4dp</item>
+ </style>
+
+ <style name="PermissionTwoTargetPreferenceIconStyle">
+ <item name="android:layout_width">wrap_content</item>
+ <item name="android:layout_height">wrap_content</item>
+ <item name="maxWidth">48dp</item>
+ <item name="maxHeight">48dp</item>
+ </style>
+
+ <style name="PermissionTwoTargetPreferenceDividerContainerStyle">
+ <item name="android:layout_width">wrap_content</item>
+ <item name="android:layout_height">match_parent</item>
+ <item name="android:gravity">start|center_vertical</item>
+ <item name="android:orientation">horizontal</item>
+ <item name="android:paddingStart">?android:attr/listPreferredItemPaddingEnd</item>
+ <item name="android:paddingLeft">?android:attr/listPreferredItemPaddingEnd</item>
+ <item name="android:paddingTop">16dp</item>
+ <item name="android:paddingBottom">16dp</item>
+ </style>
+
+ <style name="PermissionTwoTargetPreferenceDividerStyle">
+ <item name="android:layout_width">1dp</item>
+ <item name="android:layout_height">32dp</item>
+ <item name="android:background">?android:attr/listDivider</item>
+ </style>
+
+ <style name="AppPermissionFooterLinkPreferenceRootLayoutStyle"
+ parent="PermissionPreferenceRootLayoutStyle" />
+
+ <style name="AppPermissionFooterLinkPreferenceTextLayoutStyle"
+ parent="PermissionPreferenceTextRelativeLayoutStyle" />
+
+ <style name="AppPermissionFooterLinkPreferenceSummaryStyle"
+ parent="PermissionPreferenceSummaryTextStyle" />
+
<!-- END PREFERENCE STYLES -->
</resources> \ No newline at end of file
diff --git a/PermissionController/res/values-vi/strings.xml b/PermissionController/res/values-vi/strings.xml
index e4d0e73e7..252b4029a 100644
--- a/PermissionController/res/values-vi/strings.xml
+++ b/PermissionController/res/values-vi/strings.xml
@@ -545,7 +545,7 @@
<string name="not_used_permissions_description" msgid="7595514824169388718">"Quyền mà chỉ có ứng dụng hệ thống sử dụng."</string>
<string name="additional_permissions_label" msgid="7693557637462569046">"Quyền bổ sung"</string>
<string name="additional_permissions_description" msgid="2186611950890732112">"Quyền do ứng dụng xác định."</string>
- <string name="privdash_label_camera" msgid="1426440033626198096">"Máy ảnh"</string>
+ <string name="privdash_label_camera" msgid="1426440033626198096">"Camera"</string>
<string name="privdash_label_microphone" msgid="8415035835803511693">"Micrô"</string>
<string name="privdash_label_location" msgid="6882400763866489291">"Vị trí"</string>
<string name="privdash_label_other" msgid="3710394147423236033">"Khác"</string>
diff --git a/PermissionController/res/values-watch/donottranslate.xml b/PermissionController/res/values-watch/donottranslate.xml
index c3ab3cbb1..43830a93c 100644
--- a/PermissionController/res/values-watch/donottranslate.xml
+++ b/PermissionController/res/values-watch/donottranslate.xml
@@ -28,4 +28,55 @@
<string name="wear_material_compose_caption_1_font_family">font-family-text-medium-device-default</string>
<string name="wear_material_compose_caption_2_font_family">font-family-text-medium-device-default</string>
<string name="wear_material_compose_caption_3_font_family">font-family-text-medium-device-default</string>
+
+ <string name="wear_compose_material3_arc_small_font_family">font-family-flex-device-default</string>
+ <string name="wear_compose_material3_arc_medium_font_family">font-family-flex-device-default</string>
+ <string name="wear_compose_material3_arc_large_font_family">font-family-flex-device-default</string>
+ <string name="wear_compose_material3_body_extra_small_font_family">font-family-flex-device-default</string>
+ <string name="wear_compose_material3_body_small_font_family">font-family-flex-device-default</string>
+ <string name="wear_compose_material3_body_medium_font_family">font-family-flex-device-default</string>
+ <string name="wear_compose_material3_body_large_font_family">font-family-flex-device-default</string>
+ <string name="wear_compose_material3_display_small_font_family">font-family-flex-device-default</string>
+ <string name="wear_compose_material3_display_medium_font_family">font-family-flex-device-default</string>
+ <string name="wear_compose_material3_display_large_font_family">font-family-flex-device-default</string>
+ <string name="wear_compose_material3_label_small_font_family">font-family-flex-device-default</string>
+ <string name="wear_compose_material3_label_medium_font_family">font-family-flex-device-default</string>
+ <string name="wear_compose_material3_label_large_font_family">font-family-flex-device-default</string>
+ <string name="wear_compose_material3_numeral_extra_small_font_family">font-family-flex-device-default</string>
+ <string name="wear_compose_material3_numeral_small_font_family">font-family-flex-device-default</string>
+ <string name="wear_compose_material3_numeral_medium_font_family">font-family-flex-device-default</string>
+ <string name="wear_compose_material3_numeral_large_font_family">font-family-flex-device-default</string>
+ <string name="wear_compose_material3_numeral_extra_large_font_family">font-family-flex-device-default</string>
+ <string name="wear_compose_material3_title_small_font_family">font-family-flex-device-default</string>
+ <string name="wear_compose_material3_title_medium_font_family">font-family-flex-device-default</string>
+ <string name="wear_compose_material3_title_large_font_family">font-family-flex-device-default</string>
+
+ <dimen name="wear_compose_material3_arc_small_font_size">14sp</dimen>
+ <dimen name="wear_compose_material3_arc_medium_font_size">15sp</dimen>
+ <dimen name="wear_compose_material3_arc_large_font_size">20sp</dimen>
+ <dimen name="wear_compose_material3_body_extra_small_font_size">10sp</dimen>
+ <dimen name="wear_compose_material3_body_small_font_size">12sp</dimen>
+ <dimen name="wear_compose_material3_body_medium_font_size">14sp</dimen>
+ <dimen name="wear_compose_material3_body_large_font_size">16sp</dimen>
+ <dimen name="wear_compose_material3_display_small_font_size">24sp</dimen>
+ <dimen name="wear_compose_material3_display_medium_font_size">30sp</dimen>
+ <dimen name="wear_compose_material3_display_large_font_size">40sp</dimen>
+ <dimen name="wear_compose_material3_label_small_font_size">13sp</dimen>
+ <dimen name="wear_compose_material3_label_medium_font_size">15sp</dimen>
+ <dimen name="wear_compose_material3_label_large_font_size">20sp</dimen>
+ <dimen name="wear_compose_material3_numeral_extra_small_font_size">24sp</dimen>
+ <dimen name="wear_compose_material3_numeral_small_font_size">30sp</dimen>
+ <dimen name="wear_compose_material3_numeral_medium_font_size">40sp</dimen>
+ <dimen name="wear_compose_material3_numeral_large_font_size">50sp</dimen>
+ <dimen name="wear_compose_material3_numeral_extra_large_font_size">60sp</dimen>
+ <dimen name="wear_compose_material3_title_small_font_size">14sp</dimen>
+ <dimen name="wear_compose_material3_title_medium_font_size">16sp</dimen>
+ <dimen name="wear_compose_material3_title_large_font_size">20sp</dimen>
+
+ <dimen name="wear_compose_material3_shape_corner_extra_small_size">4dp</dimen>
+ <dimen name="wear_compose_material3_shape_corner_small_size">8dp</dimen>
+ <dimen name="wear_compose_material3_shape_corner_medium_size">18dp</dimen>
+ <dimen name="wear_compose_material3_shape_corner_large_size">26dp</dimen>
+ <dimen name="wear_compose_material3_shape_corner_extra_large_size">36dp</dimen>
+
</resources>
diff --git a/PermissionController/res/values-zh-rCN/strings.xml b/PermissionController/res/values-zh-rCN/strings.xml
index 72ad13f40..ccd1d3183 100644
--- a/PermissionController/res/values-zh-rCN/strings.xml
+++ b/PermissionController/res/values-zh-rCN/strings.xml
@@ -209,8 +209,8 @@
<string name="unused_apps_label_v2" msgid="7058776770056517980">"暂停闲置应用的活动"</string>
<string name="unused_apps_label_v3" msgid="693340578642156657">"管理闲置应用"</string>
<string name="unused_apps_summary" msgid="8839466950318403115">"移除权限、删除临时文件并停止发送通知"</string>
- <string name="unused_apps_summary_v2" msgid="5011313200815115802">"撤消权限、删除临时文件、停止发送通知并归档应用"</string>
- <string name="auto_revoke_summary" msgid="5867548789805911683">"为了保护您的数据,如果您连续几个月未使用此应用,系统会移除它的权限。"</string>
+ <string name="unused_apps_summary_v2" msgid="5011313200815115802">"移除权限、删除临时文件、停止发送通知并归档应用"</string>
+ <string name="auto_revoke_summary" msgid="5867548789805911683">"为了保护您的数据,如果您连续几个月未使用此应用,系统会移除其权限。"</string>
<string name="auto_revoke_summary_with_permissions" msgid="389712086597285013">"为了保护您的数据,如果您连续几个月未使用此应用,系统会移除其以下权限:<xliff:g id="PERMS">%1$s</xliff:g>"</string>
<string name="auto_revoked_apps_page_summary" msgid="6594753657893756536">"为了保护您的数据,对于您连续几个月未使用过的应用,系统已将其权限移除。"</string>
<string name="auto_revoke_open_app_message" msgid="8075556291711205039">"如果您想重新授予权限,请打开应用。"</string>
diff --git a/PermissionController/res/values-zh-rHK/strings.xml b/PermissionController/res/values-zh-rHK/strings.xml
index bfc2db95e..07a401ab3 100644
--- a/PermissionController/res/values-zh-rHK/strings.xml
+++ b/PermissionController/res/values-zh-rHK/strings.xml
@@ -362,14 +362,14 @@
<string name="role_browser_request_title" msgid="2895200507835937192">"要將 <xliff:g id="APP_NAME">%1$s</xliff:g> 設為預設瀏覽器應用程式嗎?"</string>
<string name="role_browser_request_description" msgid="5888803407905985941">"無需任何權限"</string>
<string name="role_dialer_label" msgid="1100224146343237968">"預設電話應用程式"</string>
- <string name="role_dialer_short_label" msgid="7186888549465352489">"手機應用程式"</string>
+ <string name="role_dialer_short_label" msgid="7186888549465352489">"電話應用程式"</string>
<string name="role_dialer_description" msgid="8768708633696539612">"此類應用程式允許你使用自己的裝置撥打和接聽電話"</string>
<string name="role_dialer_request_title" msgid="5959618560705912058">"要將「<xliff:g id="APP_NAME">%1$s</xliff:g>」設為預設手機應用程式嗎?"</string>
<string name="role_dialer_request_description" msgid="6288839625724909320">"此應用程式將可存取你的相機、通訊錄、麥克風、電話及短訊"</string>
<string name="role_dialer_search_keywords" msgid="3324448983559188087">"撥號器"</string>
<string name="role_sms_label" msgid="8456999857547686640">"預設短訊應用程式"</string>
<string name="role_sms_short_label" msgid="4371444488034692243">"短訊應用程式"</string>
- <string name="role_sms_description" msgid="3424020199148153513">"此類應用程式允許你使用自己手機號碼傳送和接收短訊、相片、影片和其他資料"</string>
+ <string name="role_sms_description" msgid="3424020199148153513">"此類應用程式允許你使用自己的手機號碼傳送和接收短訊、相片、影片和其他資料"</string>
<string name="role_sms_request_title" msgid="7953552109601185602">"要將「<xliff:g id="APP_NAME">%1$s</xliff:g>」設為預設短訊應用程式嗎?"</string>
<string name="role_sms_request_description" msgid="2691004766132144886">"此應用程式將可存取你的相機、通訊錄、檔案和媒體、麥克風、電話及短訊"</string>
<string name="role_sms_search_keywords" msgid="8022048144395047352">"短訊, 發短訊, 訊息, 傳送短訊"</string>
diff --git a/PermissionController/res/values/bools.xml b/PermissionController/res/values/bools.xml
index b5f33b081..4483fc48f 100644
--- a/PermissionController/res/values/bools.xml
+++ b/PermissionController/res/values/bools.xml
@@ -20,4 +20,6 @@
<bool name="is_at_least_t">false</bool>
<bool name="is_at_least_u">false</bool>
<bool name="is_at_least_v">false</bool>
+ <bool name="config_usePreferenceForAppPermissionSettings">false</bool>
+ <bool name="config_appPermissionFooterLinkPreferenceSummaryUnderlined">false</bool>
</resources>
diff --git a/PermissionController/res/values/overlayable.xml b/PermissionController/res/values/overlayable.xml
index 9075fa67c..ea7929746 100644
--- a/PermissionController/res/values/overlayable.xml
+++ b/PermissionController/res/values/overlayable.xml
@@ -48,7 +48,37 @@
<item type="style" name="PermissionFooterPreferenceTitleTextStyle" />
<item type="style" name="PermissionFooterPreferenceLearnMoreTextStyle" />
+ <item type="style" name="PermissionSelectorWithWidgetPreferenceRootLayoutStyle" />
+ <item type="style" name="PermissionSelectorWithWidgetPreferenceWidgetFrameStyle" />
+ <item type="style" name="PermissionSelectorWithWidgetPreferenceIconFrameStyle" />
+ <item type="style" name="PermissionSelectorWithWidgetPreferenceIconStyle" />
+ <item type="style" name="PermissionSelectorWithWidgetPreferenceTextContainerStyle" />
+ <item type="style" name="PermissionSelectorWithWidgetPreferenceTitleStyle" />
+ <item type="style" name="PermissionSelectorWithWidgetPreferenceSummaryContainerStyle" />
+ <item type="style" name="PermissionSelectorWithWidgetPreferenceSummaryStyle" />
+ <item type="style" name="PermissionSelectorWithWidgetPreferenceAppendixStyle" />
+ <item type="style" name="PermissionSelectorWithWidgetPreferenceExtraWidgetContainerStyle" />
+ <item type="style" name="PermissionSelectorWithWidgetPreferenceExtraWidgetDividerStyle" />
+ <item type="style" name="PermissionSelectorWithWidgetPreferenceExtraWidgetImageStyle" />
+ <item type="style" name="PermissionSelectorWithWidgetPreferenceWidgetRadioButton" />
+
+ <item type="style" name="PermissionTwoTargetPreferenceRootLayoutStyle" />
+ <item type="style" name="PermissionTwoTargetPreferenceTextContainerStyle" />
+ <item type="style" name="PermissionTwoTargetPreferenceTitleStyle" />
+ <item type="style" name="PermissionTwoTargetPreferenceSummaryStyle" />
+ <item type="style" name="PermissionTwoTargetPreferenceWidgetFrameStyle" />
+ <item type="style" name="PermissionTwoTargetPreferenceIconFrameStyle" />
+ <item type="style" name="PermissionTwoTargetPreferenceIconStyle" />
+ <item type="style" name="PermissionTwoTargetPreferenceDividerContainerStyle" />
+ <item type="style" name="PermissionTwoTargetPreferenceDividerStyle" />
+
+ <item type="style" name="AppPermissionFooterLinkPreferenceRootLayoutStyle" />
+ <item type="style" name="AppPermissionFooterLinkPreferenceTextLayoutStyle" />
+ <item type="style" name="AppPermissionFooterLinkPreferenceSummaryStyle" />
+
+ <item type="bool" name="config_usePreferenceForAppPermissionSettings" />
<item type="bool" name="config_permissionFooterPreferenceIconVisible" />
+ <item type="bool" name="config_appPermissionFooterLinkPreferenceSummaryUnderlined" />
<item type="dimen" name="permission_preference_app_icon_size" />
<item type="dimen" name="permission_preference_permission_group_icon_size" />
@@ -431,7 +461,7 @@
<item type="style" name="AppDataSharingUpdateSettingsIcon" />
<!-- END SAFETY LABELS STYLE -->
- <!--START WEAR SPECIFIC FONT STRINGS -->
+ <!--START WEAR SPECIFIC MATERIAL2 FONT STRINGS -->
<item type="string" name="wear_material_compose_display_1_font_family" />
<item type="string" name="wear_material_compose_display_2_font_family" />
<item type="string" name="wear_material_compose_display_3_font_family" />
@@ -444,7 +474,75 @@
<item type="string" name="wear_material_compose_caption_1_font_family" />
<item type="string" name="wear_material_compose_caption_2_font_family" />
<item type="string" name="wear_material_compose_caption_3_font_family" />
- <!--END WEAR SPECIFIC FONT STRINGS -->
+ <!--END WEAR SPECIFIC MATERIAL2 FONT STRINGS -->
+
+ <!--START WEAR SPECIFIC MATERIAL3 FONT FACE TOKENS-->
+ <item type="string" name="wear_compose_material3_arc_small_font_family" />
+ <item type="string" name="wear_compose_material3_arc_medium_font_family" />
+ <item type="string" name="wear_compose_material3_arc_large_font_family" />
+
+ <item type="string" name="wear_compose_material3_body_extra_small_font_family" />
+ <item type="string" name="wear_compose_material3_body_small_font_family" />
+ <item type="string" name="wear_compose_material3_body_medium_font_family" />
+ <item type="string" name="wear_compose_material3_body_large_font_family" />
+
+ <item type="string" name="wear_compose_material3_display_small_font_family" />
+ <item type="string" name="wear_compose_material3_display_medium_font_family" />
+ <item type="string" name="wear_compose_material3_display_large_font_family" />
+
+ <item type="string" name="wear_compose_material3_label_small_font_family" />
+ <item type="string" name="wear_compose_material3_label_medium_font_family" />
+ <item type="string" name="wear_compose_material3_label_large_font_family" />
+
+ <item type="string" name="wear_compose_material3_numeral_extra_small_font_family" />
+ <item type="string" name="wear_compose_material3_numeral_small_font_family" />
+ <item type="string" name="wear_compose_material3_numeral_medium_font_family" />
+ <item type="string" name="wear_compose_material3_numeral_large_font_family" />
+ <item type="string" name="wear_compose_material3_numeral_extra_large_font_family" />
+
+ <item type="string" name="wear_compose_material3_title_small_font_family" />
+ <item type="string" name="wear_compose_material3_title_medium_font_family" />
+ <item type="string" name="wear_compose_material3_title_large_font_family" />
+ <!--END WEAR SPECIFIC MATERIAL3 FONT FACE TOKENS-->
+
+
+ <!--START WEAR SPECIFIC MATERIAL3 FONT SIZE TOKENS-->
+ <item type="dimen" name="wear_compose_material3_arc_small_font_size" />
+ <item type="dimen" name="wear_compose_material3_arc_medium_font_size" />
+ <item type="dimen" name="wear_compose_material3_arc_large_font_size" />
+
+ <item type="dimen" name="wear_compose_material3_body_extra_small_font_size" />
+ <item type="dimen" name="wear_compose_material3_body_small_font_size" />
+ <item type="dimen" name="wear_compose_material3_body_medium_font_size" />
+ <item type="dimen" name="wear_compose_material3_body_large_font_size" />
+
+ <item type="dimen" name="wear_compose_material3_display_small_font_size" />
+ <item type="dimen" name="wear_compose_material3_display_medium_font_size" />
+ <item type="dimen" name="wear_compose_material3_display_large_font_size" />
+
+ <item type="dimen" name="wear_compose_material3_label_small_font_size" />
+ <item type="dimen" name="wear_compose_material3_label_medium_font_size" />
+ <item type="dimen" name="wear_compose_material3_label_large_font_size" />
+
+ <item type="dimen" name="wear_compose_material3_numeral_extra_small_font_size" />
+ <item type="dimen" name="wear_compose_material3_numeral_small_font_size" />
+ <item type="dimen" name="wear_compose_material3_numeral_medium_font_size" />
+ <item type="dimen" name="wear_compose_material3_numeral_large_font_size" />
+ <item type="dimen" name="wear_compose_material3_numeral_extra_large_font_size" />
+
+ <item type="dimen" name="wear_compose_material3_title_small_font_size" />
+ <item type="dimen" name="wear_compose_material3_title_medium_font_size" />
+ <item type="dimen" name="wear_compose_material3_title_large_font_size" />
+ <!--END WEAR SPECIFIC MATERIAL3 FONT SIZE TOKENS-->
+
+ <!--START WEAR SPECIFIC MATERIAL3 SHAPE TOKENS-->
+ <item type="dimen" name="wear_compose_material3_shape_corner_extra_small_size" />
+ <item type="dimen" name="wear_compose_material3_shape_corner_small_size" />
+ <item type="dimen" name="wear_compose_material3_shape_corner_medium_size" />
+ <item type="dimen" name="wear_compose_material3_shape_corner_large_size" />
+ <item type="dimen" name="wear_compose_material3_shape_corner_extra_large_size" />
+ <!--END WEAR SPECIFIC MATERIAL3 SHAPE TOKENS-->
+
<!-- START ENHANCED CONFIRMATION DIALOG -->
<item type="style" name="Theme.EnhancedConfirmationDialog" />
diff --git a/PermissionController/res/xml/app_permission.xml b/PermissionController/res/xml-v35/app_permission.xml
index 5e6857185..87315815d 100644
--- a/PermissionController/res/xml/app_permission.xml
+++ b/PermissionController/res/xml-v35/app_permission.xml
@@ -19,46 +19,46 @@
android:key="app_permission_button_category"
android:title="@string/app_permission_header">
- <com.android.permissioncontroller.permission.ui.handheld.PermissionSelectorWithWidgetPreference
+ <com.android.permissioncontroller.permission.ui.handheld.v36.PermissionSelectorWithWidgetPreference
android:key="app_permission_allow_radio_button"
android:title="@string/app_permission_button_allow"
app:checkboxId="@+id/allow_radio_button" />
- <com.android.permissioncontroller.permission.ui.handheld.PermissionSelectorWithWidgetPreference
+ <com.android.permissioncontroller.permission.ui.handheld.v36.PermissionSelectorWithWidgetPreference
android:key="app_permission_allow_always_radio_button"
android:title="@string/app_permission_button_allow_always"
app:checkboxId="@+id/allow_always_radio_button"
app:isPreferenceVisible="false" />
- <com.android.permissioncontroller.permission.ui.handheld.PermissionSelectorWithWidgetPreference
+ <com.android.permissioncontroller.permission.ui.handheld.v36.PermissionSelectorWithWidgetPreference
android:key="app_permission_allow_foreground_only_radio_button"
android:title="@string/app_permission_button_allow_foreground"
app:checkboxId="@+id/allow_foreground_only_radio_button" />
- <com.android.permissioncontroller.permission.ui.handheld.PermissionSelectorWithWidgetPreference
+ <com.android.permissioncontroller.permission.ui.handheld.v36.PermissionSelectorWithWidgetPreference
android:key="app_permission_select_photos_radio_button"
android:title="@string/app_permission_button_allow_limited_access"
app:checkboxId="@+id/select_radio_button"
app:extraWidgetIcon="@drawable/ic_edit"
app:extraWidgetId="@+id/edit_selected_button" />
- <com.android.permissioncontroller.permission.ui.handheld.PermissionSelectorWithWidgetPreference
+ <com.android.permissioncontroller.permission.ui.handheld.v36.PermissionSelectorWithWidgetPreference
android:key="app_permission_ask_one_time_radio_button"
android:title="@string/app_permission_button_ask"
app:checkboxId="@+id/ask_one_time_radio_button" />
- <com.android.permissioncontroller.permission.ui.handheld.PermissionSelectorWithWidgetPreference
+ <com.android.permissioncontroller.permission.ui.handheld.v36.PermissionSelectorWithWidgetPreference
android:key="app_permission_ask_radio_button"
android:text="@string/app_permission_button_ask"
android:title="@string/app_permission_button_ask"
app:checkboxId="@+id/ask_radio_button" />
- <com.android.permissioncontroller.permission.ui.handheld.PermissionSelectorWithWidgetPreference
+ <com.android.permissioncontroller.permission.ui.handheld.v36.PermissionSelectorWithWidgetPreference
android:key="app_permission_deny_radio_button"
android:title="@string/app_permission_button_deny"
app:checkboxId="@+id/deny_radio_button" />
- <com.android.permissioncontroller.permission.ui.handheld.PermissionSelectorWithWidgetPreference
+ <com.android.permissioncontroller.permission.ui.handheld.v36.PermissionSelectorWithWidgetPreference
android:key="app_permission_deny_foreground_radio_button"
android:title="@string/app_permission_button_deny"
app:checkboxId="@+id/deny_foreground_radio_button" />
@@ -71,7 +71,7 @@
android:title="@string/app_permission_location_accuracy"
app:isPreferenceVisible="false" />
- <com.android.permissioncontroller.permission.ui.handheld.PermissionTwoTargetPreference
+ <com.android.permissioncontroller.permission.ui.handheld.v36.PermissionTwoTargetPreference
android:key="app_permission_details"
android:selectable="false"
android:summary="@string/permission_summary_enabled_system_fixed"
@@ -79,26 +79,24 @@
app:iconSpaceReserved="true"
app:isPreferenceVisible="false" />
- <com.android.permissioncontroller.permission.ui.handheld.PermissionPreference
+ <com.android.permissioncontroller.permission.ui.handheld.v36.AppPermissionFooterLinkPreference
android:key="app_permission_footer_link_1"
android:summary="@string/app_permission_footer_app_permissions_link" />
- <com.android.permissioncontroller.permission.ui.handheld.PermissionPreference
+ <com.android.permissioncontroller.permission.ui.handheld.v36.AppPermissionFooterLinkPreference
android:key="app_permission_footer_link_2"
android:summary="@string/app_permission_footer_permission_apps_link" />
- <com.android.permissioncontroller.permission.ui.handheld.PermissionPreference
+ <com.android.permissioncontroller.permission.ui.handheld.PermissionFooterPreference
android:key="app_permission_footer_storage_special_app_access"
android:icon="@drawable/ic_info_outline"
- android:selectable="false"
- android:summary="@string/app_permission_footer_special_file_access"
+ android:title="@string/app_permission_footer_special_file_access"
app:isPreferenceVisible="false" />
- <com.android.permissioncontroller.permission.ui.handheld.PermissionPreference
+ <com.android.permissioncontroller.permission.ui.handheld.PermissionFooterPreference
android:key="app_permission_additional_info"
android:icon="@drawable/ic_info_outline"
- android:selectable="false"
- android:summary="@string/exempt_info_label"
+ android:title="@string/exempt_info_label"
app:isPreferenceVisible="false" />
</PreferenceScreen>
diff --git a/PermissionController/res/xml/roles.xml b/PermissionController/res/xml/roles.xml
index 1d01f8208..7b1f84a20 100644
--- a/PermissionController/res/xml/roles.xml
+++ b/PermissionController/res/xml/roles.xml
@@ -107,6 +107,7 @@
defaultHolders="config_defaultAssistant"
description="@string/role_assistant_description"
exclusive="true"
+ exclusivity="user"
fallBackToDefaultHolder="true"
showNone="true"
label="@string/role_assistant_label"
@@ -173,6 +174,7 @@
defaultHolders="config_defaultBrowser"
description="@string/role_browser_description"
exclusive="true"
+ exclusivity="user"
label="@string/role_browser_label"
overrideUserWhenGranting="true"
requestDescription="@string/role_browser_request_description"
@@ -216,6 +218,7 @@
defaultHolders="config_defaultDialer"
description="@string/role_dialer_description"
exclusive="true"
+ exclusivity="user"
fallBackToDefaultHolder="true"
label="@string/role_dialer_label"
overrideUserWhenGranting="true"
@@ -304,6 +307,7 @@
defaultHolders="config_defaultSms"
description="@string/role_sms_description"
exclusive="true"
+ exclusivity="user"
label="@string/role_sms_label"
overrideUserWhenGranting="true"
requestDescription="@string/role_sms_request_description"
@@ -394,6 +398,7 @@
behavior="EmergencyRoleBehavior"
description="@string/role_emergency_description"
exclusive="true"
+ exclusivity="user"
label="@string/role_emergency_label"
overrideUserWhenGranting="true"
requestDescription="@string/role_emergency_request_description"
@@ -426,6 +431,7 @@
behavior="HomeRoleBehavior"
description="@string/role_home_description"
exclusive="true"
+ exclusivity="user"
label="@string/role_home_label"
overrideUserWhenGranting="true"
requestDescription="@string/role_home_request_description"
@@ -472,6 +478,7 @@
defaultHolders="config_defaultCallRedirection"
description="@string/role_call_redirection_description"
exclusive="true"
+ exclusivity="user"
label="@string/role_call_redirection_label"
overrideUserWhenGranting="true"
requestDescription="@string/role_call_redirection_request_description"
@@ -493,6 +500,7 @@
defaultHolders="config_defaultCallScreening"
description="@string/role_call_screening_description"
exclusive="true"
+ exclusivity="user"
label="@string/role_call_screening_label"
overrideUserWhenGranting="true"
requestDescription="@string/role_call_screening_request_description"
@@ -518,6 +526,7 @@
name="android.app.role.SYSTEM_GALLERY"
defaultHolders="config_systemGallery"
exclusive="true"
+ exclusivity="user"
static="true"
systemOnly="true"
visible="false">
@@ -537,6 +546,7 @@
behavior="v31.AutomotiveRoleBehavior"
defaultHolders="config_systemAutomotiveCluster"
exclusive="true"
+ exclusivity="user"
minSdkVersion="31"
static="true"
systemOnly="true"
@@ -554,6 +564,7 @@
behavior="v31.CompanionDeviceWatchRoleBehavior"
description="@string/role_watch_description"
exclusive="false"
+ exclusivity="none"
minSdkVersion="31"
systemOnly="false"
visible="false">
@@ -582,6 +593,7 @@
name="android.app.role.SYSTEM_AUTOMOTIVE_PROJECTION"
defaultHolders="config_systemAutomotiveProjection"
exclusive="true"
+ exclusivity="user"
minSdkVersion="31"
static="true"
systemOnly="true"
@@ -619,13 +631,13 @@
behavior="v31.SystemShellRoleBehavior"
defaultHolders="config_systemShell"
exclusive="true"
+ exclusivity="user"
minSdkVersion="31"
static="true"
systemOnly="true"
visible="false">
<permissions>
<!-- Used for CTS testing -->
- <permission name="android.permission.CREATE_VIRTUAL_DEVICE" minSdkVersion="33" />
<permission name="android.permission.ACCESS_RCS_USER_CAPABILITY_EXCHANGE" />
<permission name="android.permission.ACCESS_AMBIENT_CONTEXT_EVENT" minSdkVersion="33"/>
<permission name="android.permission.PERFORM_IMS_SINGLE_REGISTRATION" />
@@ -642,8 +654,6 @@
minSdkVersion="33" />
<permission name="android.permission.MANAGE_SAFETY_CENTER"
minSdkVersion="33" />
- <permission name="android.permission.ADD_TRUSTED_DISPLAY" minSdkVersion="33" />
- <permission name="android.permission.ADD_ALWAYS_UNLOCKED_DISPLAY" minSdkVersion="33" />
<permission name="android.permission.SUBSCRIBE_TO_KEYGUARD_LOCKED_STATE"
minSdkVersion="33" />
<permission name="android.permission.MANAGE_DEVICE_POLICY_ACCOUNT_MANAGEMENT"
@@ -710,6 +720,10 @@
featureFlag="android.app.appfunctions.flags.Flags.enableAppFunctionManager" />
<permission name="android.permission.EXECUTE_APP_FUNCTIONS_TRUSTED"
featureFlag="android.app.appfunctions.flags.Flags.enableAppFunctionManager" />
+ <permission name="android.permission.COPY_ACCOUNTS"
+ featureFlag="android.permission.flags.Flags.devicePolicyManagementRoleSplitCreateManagedProfileEnabled" />
+ <permission name="android.permission.REMOVE_ACCOUNTS"
+ featureFlag="android.permission.flags.Flags.devicePolicyManagementRoleSplitCreateManagedProfileEnabled" />
</permissions>
</role>
@@ -717,6 +731,7 @@
name="android.app.role.SYSTEM_CONTACTS"
defaultHolders="config_systemContacts"
exclusive="true"
+ exclusivity="user"
minSdkVersion="31"
static="true"
systemOnly="true"
@@ -733,6 +748,7 @@
allowBypassingQualification="true"
defaultHolders="config_systemSpeechRecognizer"
exclusive="true"
+ exclusivity="user"
minSdkVersion="31"
static="true"
systemOnly="true"
@@ -754,6 +770,7 @@
name="android.app.role.SYSTEM_WIFI_COEX_MANAGER"
defaultHolders="config_systemWifiCoexManager"
exclusive="true"
+ exclusivity="user"
minSdkVersion="31"
static="true"
systemOnly="true"
@@ -768,6 +785,7 @@
name="android.app.role.SYSTEM_WELLBEING"
defaultHolders="config_systemWellbeing"
exclusive="true"
+ exclusivity="user"
minSdkVersion="31"
static="true"
systemOnly="true"
@@ -796,6 +814,7 @@
behavior="v31.TelevisionRoleBehavior"
defaultHolders="config_systemTelevisionNotificationHandler"
exclusive="true"
+ exclusivity="user"
minSdkVersion="31"
static="true"
systemOnly="true"
@@ -814,6 +833,7 @@
name="android.app.role.SYSTEM_COMPANION_DEVICE_PROVIDER"
defaultHolders="config_systemCompanionDeviceProvider"
exclusive="true"
+ exclusivity="user"
minSdkVersion="31"
static="true"
systemOnly="true"
@@ -849,6 +869,7 @@
name="android.app.role.SYSTEM_UI_INTELLIGENCE"
defaultHolders="config_systemUiIntelligence"
exclusive="true"
+ exclusivity="user"
minSdkVersion="31"
static="true"
systemOnly="true"
@@ -904,6 +925,7 @@
name="android.app.role.SYSTEM_AMBIENT_AUDIO_INTELLIGENCE"
defaultHolders="config_systemAmbientAudioIntelligence"
exclusive="true"
+ exclusivity="user"
minSdkVersion="31"
static="true"
systemOnly="true"
@@ -950,6 +972,7 @@
name="android.app.role.SYSTEM_AUDIO_INTELLIGENCE"
defaultHolders="config_systemAudioIntelligence"
exclusive="true"
+ exclusivity="user"
minSdkVersion="31"
static="true"
systemOnly="true"
@@ -996,6 +1019,7 @@
name="android.app.role.SYSTEM_NOTIFICATION_INTELLIGENCE"
defaultHolders="config_systemNotificationIntelligence"
exclusive="true"
+ exclusivity="user"
minSdkVersion="31"
static="true"
systemOnly="true"
@@ -1038,6 +1062,7 @@
name="android.app.role.SYSTEM_TEXT_INTELLIGENCE"
defaultHolders="config_systemTextIntelligence"
exclusive="true"
+ exclusivity="user"
minSdkVersion="31"
static="true"
systemOnly="true"
@@ -1078,6 +1103,7 @@
name="android.app.role.SYSTEM_VISUAL_INTELLIGENCE"
defaultHolders="config_systemVisualIntelligence"
exclusive="true"
+ exclusivity="user"
minSdkVersion="31"
static="true"
systemOnly="true"
@@ -1103,6 +1129,7 @@
name="android.app.role.SYSTEM_DOCUMENT_MANAGER"
behavior="v33.DocumentManagerRoleBehavior"
exclusive="true"
+ exclusivity="user"
minSdkVersion="33"
static="true"
systemOnly="true"
@@ -1135,6 +1162,7 @@
allowBypassingQualification="true"
defaultHolders="config_systemActivityRecognizer"
exclusive="false"
+ exclusivity="none"
static="true"
systemOnly="true"
visible="false">
@@ -1154,6 +1182,7 @@
name="android.app.role.SYSTEM_UI"
defaultHolders="config_systemUi"
exclusive="true"
+ exclusivity="user"
minSdkVersion="31"
static="true"
systemOnly="true"
@@ -1180,6 +1209,7 @@
behavior="v31.TelevisionRoleBehavior"
defaultHolders="config_systemTelevisionRemoteService"
exclusive="true"
+ exclusivity="user"
minSdkVersion="31"
static="true"
systemOnly="true"
@@ -1199,6 +1229,7 @@
behavior="v33.CompanionDeviceAppStreamingRoleBehavior"
description="@string/role_app_streaming_description"
exclusive="false"
+ exclusivity="none"
minSdkVersion="33"
systemOnly="true"
visible="false">
@@ -1207,7 +1238,9 @@
<permission-set name="virtual_device" />
<!-- For capturing audio from the app on the device. -->
<permission name="android.permission.RECORD_AUDIO" />
-
+ <permission
+ name="android.permission.ADD_MIRROR_DISPLAY"
+ featureFlag="android.companion.virtualdevice.flags.Flags.enableLimitedVdmRole" />
<!--TODO(b/201605314) For calling Telecom framework API for audio streaming-->
<!--<permission name="android.permission.PROVIDE_CALL_ENDPOINTS" />-->
</permissions>
@@ -1223,6 +1256,7 @@
behavior="v33.CompanionDeviceComputerRoleBehavior"
description="@string/role_companion_device_computer_description"
exclusive="false"
+ exclusivity="none"
minSdkVersion="33"
systemOnly="true"
visible="false">
@@ -1242,6 +1276,7 @@
name="android.app.role.COMPANION_DEVICE_GLASSES"
behavior="v34.CompanionDeviceGlassesRoleBehavior"
exclusive="false"
+ exclusivity="none"
minSdkVersion="34"
systemOnly="false"
visible="false">
@@ -1268,12 +1303,15 @@
name="android.app.role.COMPANION_DEVICE_NEARBY_DEVICE_STREAMING"
allowBypassingQualification="true"
exclusive="false"
+ exclusivity="none"
minSdkVersion="34"
systemOnly="true"
visible="false">
<permissions>
<permission-set name="nearby_devices" />
<permission-set name="virtual_device" />
+ <permission-set name="notifications"
+ featureFlag="android.companion.virtualdevice.flags.Flags.notificationsForDeviceStreaming" />
</permissions>
</role>
@@ -1281,6 +1319,7 @@
name="android.app.role.SYSTEM_SUPERVISION"
defaultHolders="config_systemSupervision"
exclusive="true"
+ exclusivity="user"
minSdkVersion="33"
static="true"
systemOnly="true"
@@ -1291,6 +1330,46 @@
<permission name="android.permission.MANAGE_DEFAULT_APPLICATIONS" minSdkVersion="34"/>
<permission name="android.permission.SUSPEND_APPS"/>
<permission name="android.permission.SYSTEM_APPLICATION_OVERLAY"/>
+ <permission name="android.permission.MANAGE_DEVICE_POLICY_ACCOUNT_MANAGEMENT"
+ featureFlag="android.permission.flags.Flags.supervisionRolePermissionUpdateEnabled"/>
+ <permission name="android.permission.MANAGE_DEVICE_POLICY_APP_RESTRICTIONS"
+ featureFlag="android.permission.flags.Flags.supervisionRolePermissionUpdateEnabled"/>
+ <permission name="android.permission.MANAGE_DEVICE_POLICY_APPS_CONTROL"
+ featureFlag="android.permission.flags.Flags.supervisionRolePermissionUpdateEnabled"/>
+ <permission name="android.permission.MANAGE_DEVICE_POLICY_DEBUGGING_FEATURES"
+ featureFlag="android.permission.flags.Flags.supervisionRolePermissionUpdateEnabled"/>
+ <permission name="android.permission.MANAGE_DEVICE_POLICY_DISPLAY"
+ featureFlag="android.permission.flags.Flags.supervisionRolePermissionUpdateEnabled"/>
+ <permission name="android.permission.MANAGE_DEVICE_POLICY_FACTORY_RESET"
+ featureFlag="android.permission.flags.Flags.supervisionRolePermissionUpdateEnabled"/>
+ <permission name="android.permission.MANAGE_DEVICE_POLICY_FUN"
+ featureFlag="android.permission.flags.Flags.supervisionRolePermissionUpdateEnabled"/>
+ <permission name="android.permission.MANAGE_DEVICE_POLICY_INSTALL_UNKNOWN_SOURCES"
+ featureFlag="android.permission.flags.Flags.supervisionRolePermissionUpdateEnabled"/>
+ <permission name="android.permission.MANAGE_DEVICE_POLICY_KEYGUARD"
+ featureFlag="android.permission.flags.Flags.supervisionRolePermissionUpdateEnabled"/>
+ <permission name="android.permission.MANAGE_DEVICE_POLICY_LOCATION"
+ featureFlag="android.permission.flags.Flags.supervisionRolePermissionUpdateEnabled"/>
+ <permission name="android.permission.MANAGE_DEVICE_POLICY_LOCK"
+ featureFlag="android.permission.flags.Flags.supervisionRolePermissionUpdateEnabled"/>
+ <permission name="android.permission.MANAGE_DEVICE_POLICY_LOCK_CREDENTIALS"
+ featureFlag="android.permission.flags.Flags.supervisionRolePermissionUpdateEnabled"/>
+ <permission name="android.permission.MANAGE_DEVICE_POLICY_LOCK_TASK"
+ featureFlag="android.permission.flags.Flags.supervisionRolePermissionUpdateEnabled"/>
+ <permission name="android.permission.MANAGE_DEVICE_POLICY_MODIFY_USERS"
+ featureFlag="android.permission.flags.Flags.supervisionRolePermissionUpdateEnabled"/>
+ <permission name="android.permission.MANAGE_DEVICE_POLICY_PACKAGE_STATE"
+ featureFlag="android.permission.flags.Flags.supervisionRolePermissionUpdateEnabled"/>
+ <permission name="android.permission.MANAGE_DEVICE_POLICY_RESET_PASSWORD"
+ featureFlag="android.permission.flags.Flags.supervisionRolePermissionUpdateEnabled"/>
+ <permission name="android.permission.MANAGE_DEVICE_POLICY_RUNTIME_PERMISSIONS"
+ featureFlag="android.permission.flags.Flags.supervisionRolePermissionUpdateEnabled"/>
+ <permission name="android.permission.MANAGE_DEVICE_POLICY_SAFE_BOOT"
+ featureFlag="android.permission.flags.Flags.supervisionRolePermissionUpdateEnabled"/>
+ <permission name="android.permission.MANAGE_DEVICE_POLICY_TIME"
+ featureFlag="android.permission.flags.Flags.supervisionRolePermissionUpdateEnabled"/>
+ <permission name="android.permission.MANAGE_PROFILE_AND_DEVICE_OWNERS"
+ featureFlag="android.permission.flags.Flags.supervisionRolePermissionUpdateEnabled"/>
</permissions>
</role>
@@ -1303,6 +1382,7 @@
behavior="v33.DevicePolicyManagementRoleBehavior"
defaultHolders="config_devicePolicyManagement"
exclusive="true"
+ exclusivity="user"
minSdkVersion="33"
static="true"
systemOnly="false"
@@ -1408,6 +1488,10 @@
<permission name="android.permission.MANAGE_DEVICE_POLICY_DISPLAY" minSdkVersion="35" />
<permission name="android.permission.MANAGE_DEVICE_POLICY_LOCALE" minSdkVersion="35" />
<permission name="android.permission.MANAGE_DEVICE_POLICY_SMS" minSdkVersion="35" />
+ <permission name="android.permission.COPY_ACCOUNTS"
+ featureFlag="android.permission.flags.Flags.devicePolicyManagementRoleSplitCreateManagedProfileEnabled" />
+ <permission name="android.permission.REMOVE_ACCOUNTS"
+ featureFlag="android.permission.flags.Flags.devicePolicyManagementRoleSplitCreateManagedProfileEnabled" />
</permissions>
</role>
@@ -1415,6 +1499,7 @@
name="android.app.role.SYSTEM_APP_PROTECTION_SERVICE"
defaultHolders="config_systemAppProtectionService"
exclusive="true"
+ exclusivity="user"
minSdkVersion="33"
static="true"
systemOnly="true"
@@ -1443,6 +1528,7 @@
behavior="v31.AutomotiveRoleBehavior"
defaultHolders="config_systemAutomotiveCalendarSyncManager"
exclusive="true"
+ exclusivity="user"
minSdkVersion="33"
static="true"
systemOnly="true"
@@ -1464,6 +1550,7 @@
defaultHolders="config_defaultAutomotiveNavigation"
description="@string/role_automotive_navigation_description"
exclusive="true"
+ exclusivity="user"
label="@string/role_automotive_navigation_label"
minSdkVersion="33"
overrideUserWhenGranting="true"
@@ -1537,6 +1624,7 @@
name="android.app.role.SYSTEM_SETTINGS_INTELLIGENCE"
defaultHolders="config_systemSettingsIntelligence"
exclusive="true"
+ exclusivity="user"
minSdkVersion="33"
static="true"
systemOnly="true"
@@ -1554,6 +1642,7 @@
name="android.app.role.SYSTEM_BLUETOOTH_STACK"
defaultHolders="config_systemBluetoothStack"
exclusive="true"
+ exclusivity="user"
minSdkVersion="33"
static="true"
systemOnly="true"
@@ -1578,6 +1667,7 @@
<role
name="android.app.role.FINANCED_DEVICE_KIOSK"
exclusive="true"
+ exclusivity="user"
minSdkVersion="34"
visible="false">
<permissions>
@@ -1593,6 +1683,7 @@
name="android.app.role.SYSTEM_FINANCED_DEVICE_CONTROLLER"
defaultHolders="config_systemFinancedDeviceController"
exclusive="true"
+ exclusivity="user"
minSdkVersion="34"
static="true"
systemOnly="true"
@@ -1620,6 +1711,7 @@
behavior="v33.SystemWearHealthServiceRoleBehavior"
defaultHolders="config_systemWearHealthService"
exclusive="true"
+ exclusivity="user"
minSdkVersion="33"
static="true"
systemOnly="true"
@@ -1629,6 +1721,12 @@
<permission-set name="location" />
<permission name="android.permission.ACCESS_BACKGROUND_LOCATION" />
<permission name="android.permission.ACTIVITY_RECOGNITION" />
+ <permission
+ name="android.permission.health.READ_HEART_RATE"
+ featureFlag="android.permission.flags.Flags.replaceBodySensorPermissionEnabled" />
+ <permission
+ name="android.permission.health.READ_HEALTH_DATA_IN_BACKGROUND"
+ featureFlag="android.permission.flags.Flags.replaceBodySensorPermissionEnabled" />
</permissions>
</role>
@@ -1641,6 +1739,7 @@
defaultHolders="config_defaultNotes"
description="@string/role_notes_description"
exclusive="true"
+ exclusivity="user"
label="@string/role_notes_label"
minSdkVersion="34"
overrideUserWhenGranting="true"
@@ -1682,6 +1781,7 @@
allowBypassingQualification="true"
defaultHolders="config_systemCallStreaming"
exclusive="true"
+ exclusivity="user"
minSdkVersion="34"
static="true"
systemOnly="true"
@@ -1704,6 +1804,7 @@
behavior="v35.RetailDemoRoleBehavior"
defaultHolders="config_defaultRetailDemo"
exclusive="true"
+ exclusivity="user"
minSdkVersion="35"
static="true"
visible="false">
@@ -1730,6 +1831,7 @@
defaultHolders="config_defaultWallet"
description="@string/role_wallet_description"
exclusive="true"
+ exclusivity="user"
label="@string/role_wallet_label"
minSdkVersion="35"
overrideUserWhenGranting="true"
@@ -1740,5 +1842,39 @@
shortLabel="@string/role_wallet_short_label"
uiBehavior="v35.WalletRoleUiBehavior"/>
+ <role
+ name="android.app.role.SYSTEM_DEPENDENCY_INSTALLER"
+ allowBypassingQualification="true"
+ defaultHolders="config_systemDependencyInstaller"
+ exclusive="true"
+ exclusivity="user"
+ featureFlag="android.content.pm.Flags.sdkDependencyInstaller"
+ static="true"
+ systemOnly="true"
+ visible="false">
+ <required-components>
+ <service permission="android.permission.BIND_DEPENDENCY_INSTALLER">
+ <intent-filter>
+ <action name="android.content.pm.action.INSTALL_DEPENDENCY" />
+ </intent-filter>
+ </service>
+ </required-components>
+ <permissions>
+ <permission name="android.permission.ACCESS_SHARED_LIBRARIES" />
+ <permission name="android.permission.INSTALL_DEPENDENCY_SHARED_LIBRARIES" />
+ </permissions>
+ </role>
+
+ <!---
+ ~ A role for testing cross-user roles (exclusivity="profileGroup"). This should never be used
+ ~ to gate any actual functionality.
+ -->
+ <role
+ name="android.app.role.RESERVED_FOR_TESTING_PROFILE_GROUP_EXCLUSIVITY"
+ exclusive="true"
+ exclusivity="profileGroup"
+ featureFlag="com.android.permission.flags.Flags.crossUserRoleEnabled"
+ showNone="true"
+ visible="false"/>
</roles>
diff --git a/PermissionController/role-controller/Android.bp b/PermissionController/role-controller/Android.bp
index 166823b08..9a046a397 100644
--- a/PermissionController/role-controller/Android.bp
+++ b/PermissionController/role-controller/Android.bp
@@ -17,6 +17,9 @@ package {
default_applicable_licenses: ["Android-Apache-2.0"],
}
+// Any place that role-controller is added as a dependency must also include
+// "com.android.permission.flags-aconfig-java" or
+// "com.android.permission.flags-aconfig-java-export",
java_library {
name: "role-controller",
srcs: [
@@ -24,11 +27,13 @@ java_library {
],
libs: [
"androidx.annotation_annotation",
+ "com.android.permission.flags-aconfig-java",
],
static_libs: [
"modules-utils-build_system",
"android.app.appfunctions.exported-flags-aconfig-java",
"android.companion.virtualdevice.flags-aconfig-java-export",
+ "android.content.pm.flags-aconfig-java-export",
"android.permission.flags-aconfig-java-export",
"android.os.flags-aconfig-java-export",
],
diff --git a/PermissionController/role-controller/java/com/android/role/controller/model/AppOp.java b/PermissionController/role-controller/java/com/android/role/controller/model/AppOp.java
index 1c71e0c99..56c4944a0 100644
--- a/PermissionController/role-controller/java/com/android/role/controller/model/AppOp.java
+++ b/PermissionController/role-controller/java/com/android/role/controller/model/AppOp.java
@@ -23,6 +23,7 @@ import android.os.UserHandle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
import com.android.modules.utils.build.SdkLevel;
import com.android.role.controller.util.PackageUtils;
@@ -130,7 +131,8 @@ public class AppOp {
return Permissions.setAppOpUidModeAsUser(packageName, mName, defaultMode, user, context);
}
- boolean isAvailableByFeatureFlagAndSdkVersion() {
+ @VisibleForTesting
+ public boolean isAvailableByFeatureFlagAndSdkVersion() {
if (mFeatureFlag != null && !mFeatureFlag.get()) {
return false;
}
diff --git a/PermissionController/role-controller/java/com/android/role/controller/model/Permissions.java b/PermissionController/role-controller/java/com/android/role/controller/model/Permissions.java
index e788fdce1..820ff3d4e 100644
--- a/PermissionController/role-controller/java/com/android/role/controller/model/Permissions.java
+++ b/PermissionController/role-controller/java/com/android/role/controller/model/Permissions.java
@@ -51,11 +51,13 @@ public class Permissions {
private static final boolean DEBUG = false;
+ private static final Object sPermissionInfoLock = new Object();
+ private static final ArrayMap<String, Boolean> sIsRuntimePermission = new ArrayMap<>();
+ private static final ArrayMap<String, Boolean> sIsRestrictedPermission = new ArrayMap<>();
+
+ private static final Object sForegroundBackgroundPermissionMappingsLock = new Object();
private static ArrayMap<String, String> sForegroundToBackgroundPermission;
private static ArrayMap<String, List<String>> sBackgroundToForegroundPermissions;
- private static final Object sForegroundBackgroundPermissionMappingsLock = new Object();
-
- private static final ArrayMap<String, Boolean> sRestrictedPermissions = new ArrayMap<>();
/**
* Filter a list of permissions based on their SDK versions.
@@ -86,8 +88,9 @@ public class Permissions {
*
* @param packageName the package name of the application to be granted permissions to
* @param permissions the list of permissions to be granted
- * @param overrideDisabledSystemPackage whether to ignore the permissions of a disabled system
- * package (if this package is an updated system package)
+ * @param ignoreDisabledSystemPackage whether to ignore the requested permissions of a disabled
+ * system package (if this package is an updated system
+ * package) when granting runtime permissions
* @param overrideUserSetAndFixed whether to override user set and fixed flags on the permission
* @param setGrantedByRole whether the permissions will be granted as granted-by-role
* @param setGrantedByDefault whether the permissions will be granted as granted-by-default
@@ -101,7 +104,7 @@ public class Permissions {
* PackageInfo, java.util.Set, boolean, boolean, int)
*/
public static boolean grantAsUser(@NonNull String packageName,
- @NonNull List<String> permissions, boolean overrideDisabledSystemPackage,
+ @NonNull List<String> permissions, boolean ignoreDisabledSystemPackage,
boolean overrideUserSetAndFixed, boolean setGrantedByRole, boolean setGrantedByDefault,
boolean setSystemFixed, @NonNull UserHandle user, @NonNull Context context) {
if (setGrantedByRole == setGrantedByDefault) {
@@ -145,15 +148,17 @@ public class Permissions {
// choice to grant this app the permissions needed to function. For all other
// apps, (default grants on first boot and user creation) we don't grant default
// permissions if the version on the system image does not declare them.
- if (!overrideDisabledSystemPackage && isUpdatedSystemApp(packageInfo)) {
+ if (!ignoreDisabledSystemPackage && isUpdatedSystemApp(packageInfo)) {
PackageInfo disabledSystemPackageInfo = getFactoryPackageInfoAsUser(packageName, user,
context);
if (disabledSystemPackageInfo != null) {
- if (ArrayUtils.isEmpty(disabledSystemPackageInfo.requestedPermissions)) {
- return false;
+ for (int i = permissionsToGrant.size() - 1; i >= 0; i--) {
+ String permission = permissionsToGrant.valueAt(i);
+ if (isRuntimePermission(permission, context) && !ArrayUtils.contains(
+ disabledSystemPackageInfo.requestedPermissions, permission)) {
+ permissionsToGrant.removeAt(i);
+ }
}
- CollectionUtils.retainAll(permissionsToGrant,
- disabledSystemPackageInfo.requestedPermissions);
if (permissionsToGrant.isEmpty()) {
return false;
}
@@ -258,7 +263,8 @@ public class Permissions {
if (!wasPermissionOrAppOpGranted) {
// If we've granted a permission which wasn't granted, it's no longer user set or fixed.
newMask |= PackageManager.FLAG_PERMISSION_USER_FIXED
- | PackageManager.FLAG_PERMISSION_USER_SET;
+ | PackageManager.FLAG_PERMISSION_USER_SET
+ | PackageManager.FLAG_PERMISSION_ONE_TIME;
}
// If a component gets a permission for being the default handler A and also default handler
// B, we grant the weaker grant form. This only applies to default permission grant.
@@ -629,7 +635,8 @@ public class Permissions {
}
if (!overrideUserSetAndFixed) {
fixedFlags |= PackageManager.FLAG_PERMISSION_USER_FIXED
- | PackageManager.FLAG_PERMISSION_USER_SET;
+ | PackageManager.FLAG_PERMISSION_USER_SET
+ | PackageManager.FLAG_PERMISSION_ONE_TIME;
}
return (flags & fixedFlags) != 0;
}
@@ -701,6 +708,50 @@ public class Permissions {
return true;
}
+ private static boolean isRuntimePermission(@NonNull String permission,
+ @NonNull Context context) {
+ synchronized (sPermissionInfoLock) {
+ Boolean isRuntimePermission = sIsRuntimePermission.get(permission);
+ if (isRuntimePermission != null) {
+ return isRuntimePermission;
+ }
+ fetchPermissionInfoLocked(permission, context);
+ return sIsRuntimePermission.get(permission);
+ }
+ }
+
+ private static boolean isRestrictedPermission(@NonNull String permission,
+ @NonNull Context context) {
+ synchronized (sPermissionInfoLock) {
+ Boolean isRestrictedPermission = sIsRestrictedPermission.get(permission);
+ if (isRestrictedPermission != null) {
+ return isRestrictedPermission;
+ }
+ fetchPermissionInfoLocked(permission, context);
+ return sIsRestrictedPermission.get(permission);
+ }
+ }
+
+ private static void fetchPermissionInfoLocked(@NonNull String permission,
+ @NonNull Context context) {
+ PackageManager packageManager = context.getPackageManager();
+ PermissionInfo permissionInfo = null;
+ try {
+ permissionInfo = packageManager.getPermissionInfo(permission, 0);
+ } catch (PackageManager.NameNotFoundException e) {
+ Log.e(LOG_TAG, "Cannot get PermissionInfo for permission: " + permission);
+ }
+
+ // Don't expect that to be a transient error, so we can still cache the failed information.
+ boolean isRuntimePermission = permissionInfo != null
+ && permissionInfo.getProtection() == PermissionInfo.PROTECTION_DANGEROUS;
+ boolean isRestrictedPermission = permissionInfo != null
+ && (permissionInfo.flags & (PermissionInfo.FLAG_SOFT_RESTRICTED
+ | PermissionInfo.FLAG_HARD_RESTRICTED)) != 0;
+ sIsRuntimePermission.put(permission, isRuntimePermission);
+ sIsRestrictedPermission.put(permission, isRestrictedPermission);
+ }
+
private static boolean isForegroundPermission(@NonNull String permission,
@NonNull Context context) {
ensureForegroundBackgroundPermissionMappings(context);
@@ -731,40 +782,13 @@ public class Permissions {
synchronized (sForegroundBackgroundPermissionMappingsLock) {
if (sForegroundToBackgroundPermission == null
&& sBackgroundToForegroundPermissions == null) {
- createForegroundBackgroundPermissionMappings(context);
+ createForegroundBackgroundPermissionMappingsLocked(context);
}
}
}
- private static boolean isRestrictedPermission(@NonNull String permission,
+ private static void createForegroundBackgroundPermissionMappingsLocked(
@NonNull Context context) {
- synchronized (sRestrictedPermissions) {
- if (sRestrictedPermissions.containsKey(permission)) {
- return sRestrictedPermissions.get(permission);
- }
- }
-
- PackageManager packageManager = context.getPackageManager();
- PermissionInfo permissionInfo = null;
- try {
- permissionInfo = packageManager.getPermissionInfo(permission, 0);
- } catch (PackageManager.NameNotFoundException e) {
- Log.e(LOG_TAG, "Cannot get PermissionInfo for permission: " + permission);
- }
-
- // Don't expect that to be a transient error, so we can still cache the failed information.
- boolean isRestrictedPermission = permissionInfo != null
- && (permissionInfo.flags & (PermissionInfo.FLAG_SOFT_RESTRICTED
- | PermissionInfo.FLAG_HARD_RESTRICTED)) != 0;
-
- synchronized (sRestrictedPermissions) {
- sRestrictedPermissions.put(permission, isRestrictedPermission);
- }
-
- return isRestrictedPermission;
- }
-
- private static void createForegroundBackgroundPermissionMappings(@NonNull Context context) {
List<String> permissions = new ArrayList<>();
sBackgroundToForegroundPermissions = new ArrayMap<>();
diff --git a/PermissionController/role-controller/java/com/android/role/controller/model/Role.java b/PermissionController/role-controller/java/com/android/role/controller/model/Role.java
index 04fd615e1..e3269a146 100644
--- a/PermissionController/role-controller/java/com/android/role/controller/model/Role.java
+++ b/PermissionController/role-controller/java/com/android/role/controller/model/Role.java
@@ -37,10 +37,13 @@ import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.Log;
+import android.util.SparseBooleanArray;
+import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
+import androidx.annotation.VisibleForTesting;
import com.android.modules.utils.build.SdkLevel;
import com.android.role.controller.util.CollectionUtils;
@@ -48,6 +51,8 @@ import com.android.role.controller.util.PackageUtils;
import com.android.role.controller.util.RoleManagerCompat;
import com.android.role.controller.util.UserUtils;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@@ -82,6 +87,35 @@ public class Role {
private static final String CERTIFICATE_SEPARATOR = ":";
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({
+ EXCLUSIVITY_NONE,
+ EXCLUSIVITY_USER,
+ EXCLUSIVITY_PROFILE_GROUP
+ })
+ public @interface Exclusivity {}
+
+ /**
+ * Does not enforce any exclusivity, which means multiple apps may hold this role in a user.
+ */
+ public static final int EXCLUSIVITY_NONE = 0;
+
+ /** Enforces exclusivity within one user. */
+ public static final int EXCLUSIVITY_USER = 1;
+
+ /**
+ * Enforces exclusivity across all users (including profile users) in the same profile group.
+ */
+ public static final int EXCLUSIVITY_PROFILE_GROUP = 2;
+
+ /** Set of valid exclusivity values. */
+ private static final SparseBooleanArray sExclusivityValues = new SparseBooleanArray();
+ static {
+ sExclusivityValues.put(EXCLUSIVITY_NONE, true);
+ sExclusivityValues.put(EXCLUSIVITY_USER, true);
+ sExclusivityValues.put(EXCLUSIVITY_PROFILE_GROUP, true);
+ }
+
/**
* The name of this role. Must be unique.
*/
@@ -109,9 +143,10 @@ public class Role {
private final int mDescriptionResource;
/**
- * Whether this role is exclusive, i.e. allows at most one holder.
+ * The exclusivity of this role, i.e. whether this role allows multiple holders, or allows at
+ * most one holder within a user or a profile group.
*/
- private final boolean mExclusive;
+ private final int mExclusivity;
/**
* Whether this role should fall back to the default holder.
@@ -185,8 +220,8 @@ public class Role {
/**
* Whether the UI for this role will show the "None" item. Only valid if this role is
- * {@link #mExclusive exclusive}, and {@link #getFallbackHolder(Context)} should also return
- * empty to allow actually selecting "None".
+ * {@link #isExclusive()}, and {@link #getFallbackHolder(Context)} should
+ * also return empty to allow actually selecting "None".
*/
private final boolean mShowNone;
@@ -240,14 +275,14 @@ public class Role {
public Role(@NonNull String name, boolean allowBypassingQualification,
@Nullable RoleBehavior behavior, @Nullable String defaultHoldersResourceName,
- @StringRes int descriptionResource, boolean exclusive, boolean fallBackToDefaultHolder,
- @Nullable Supplier<Boolean> featureFlag, @StringRes int labelResource,
- int maxSdkVersion, int minSdkVersion, boolean onlyGrantWhenAdded,
- boolean overrideUserWhenGranting, @StringRes int requestDescriptionResource,
- @StringRes int requestTitleResource, boolean requestable,
- @StringRes int searchKeywordsResource, @StringRes int shortLabelResource,
- boolean showNone, boolean statik, boolean systemOnly, boolean visible,
- @NonNull List<RequiredComponent> requiredComponents,
+ @StringRes int descriptionResource, @Exclusivity int exclusivity,
+ boolean fallBackToDefaultHolder, @Nullable Supplier<Boolean> featureFlag,
+ @StringRes int labelResource, int maxSdkVersion, int minSdkVersion,
+ boolean onlyGrantWhenAdded, boolean overrideUserWhenGranting,
+ @StringRes int requestDescriptionResource, @StringRes int requestTitleResource,
+ boolean requestable, @StringRes int searchKeywordsResource,
+ @StringRes int shortLabelResource, boolean showNone, boolean statik, boolean systemOnly,
+ boolean visible, @NonNull List<RequiredComponent> requiredComponents,
@NonNull List<Permission> permissions, @NonNull List<Permission> appOpPermissions,
@NonNull List<AppOp> appOps, @NonNull List<PreferredActivity> preferredActivities,
@Nullable String uiBehaviorName) {
@@ -256,7 +291,7 @@ public class Role {
mBehavior = behavior;
mDefaultHoldersResourceName = defaultHoldersResourceName;
mDescriptionResource = descriptionResource;
- mExclusive = exclusive;
+ mExclusivity = exclusivity;
mFallBackToDefaultHolder = fallBackToDefaultHolder;
mFeatureFlag = featureFlag;
mLabelResource = labelResource;
@@ -297,7 +332,25 @@ public class Role {
}
public boolean isExclusive() {
- return mExclusive;
+ return getExclusivity() != EXCLUSIVITY_NONE;
+ }
+
+ @Exclusivity
+ public int getExclusivity() {
+ if (com.android.permission.flags.Flags.crossUserRoleEnabled() && mBehavior != null) {
+ Integer exclusivity = mBehavior.getExclusivity();
+ if (exclusivity != null) {
+ if (!sExclusivityValues.get(exclusivity)) {
+ throw new IllegalArgumentException("Invalid exclusivity: " + exclusivity);
+ }
+ if (mShowNone && exclusivity == EXCLUSIVITY_NONE) {
+ throw new IllegalArgumentException(
+ "Role cannot be non-exclusive when showNone is true: " + exclusivity);
+ }
+ return exclusivity;
+ }
+ }
+ return mExclusivity;
}
@Nullable
@@ -413,6 +466,8 @@ public class Role {
if (!isAvailableByFeatureFlagAndSdkVersion()) {
return false;
}
+ // TODO(b/376133070): ensure that cross-user role is only available if also available for
+ // the profile-group's full user
if (mBehavior != null) {
return mBehavior.isAvailableAsUser(this, user, context);
}
@@ -424,7 +479,8 @@ public class Role {
*
* @return whether this role is available based on SDK version
*/
- boolean isAvailableByFeatureFlagAndSdkVersion() {
+ @VisibleForTesting
+ public boolean isAvailableByFeatureFlagAndSdkVersion() {
if (mFeatureFlag != null && !mFeatureFlag.get()) {
return false;
}
@@ -1039,7 +1095,7 @@ public class Role {
*/
@Nullable
public Intent getRestrictionIntentAsUser(@NonNull UserHandle user, @NonNull Context context) {
- if (SdkLevel.isAtLeastU() && mExclusive) {
+ if (SdkLevel.isAtLeastU() && isExclusive()) {
UserManager userManager = context.getSystemService(UserManager.class);
if (userManager.hasUserRestrictionForUser(UserManager.DISALLOW_CONFIG_DEFAULT_APPS,
user)) {
@@ -1102,7 +1158,7 @@ public class Role {
+ ", mBehavior=" + mBehavior
+ ", mDefaultHoldersResourceName=" + mDefaultHoldersResourceName
+ ", mDescriptionResource=" + mDescriptionResource
- + ", mExclusive=" + mExclusive
+ + ", mExclusivity=" + mExclusivity
+ ", mFallBackToDefaultHolder=" + mFallBackToDefaultHolder
+ ", mFeatureFlag=" + mFeatureFlag
+ ", mLabelResource=" + mLabelResource
diff --git a/PermissionController/role-controller/java/com/android/role/controller/model/RoleBehavior.java b/PermissionController/role-controller/java/com/android/role/controller/model/RoleBehavior.java
index 3849a50e3..86ca8e2ce 100644
--- a/PermissionController/role-controller/java/com/android/role/controller/model/RoleBehavior.java
+++ b/PermissionController/role-controller/java/com/android/role/controller/model/RoleBehavior.java
@@ -32,6 +32,14 @@ import java.util.List;
public interface RoleBehavior {
/**
+ * @see Role#getExclusivity()
+ */
+ @Nullable
+ default Integer getExclusivity() {
+ return null;
+ }
+
+ /**
* @see Role#onRoleAddedAsUser(UserHandle, Context)
*/
default void onRoleAddedAsUser(@NonNull Role role, @NonNull UserHandle user,
diff --git a/PermissionController/role-controller/java/com/android/role/controller/model/RoleParser.java b/PermissionController/role-controller/java/com/android/role/controller/model/RoleParser.java
index f1a275daf..061f351de 100644
--- a/PermissionController/role-controller/java/com/android/role/controller/model/RoleParser.java
+++ b/PermissionController/role-controller/java/com/android/role/controller/model/RoleParser.java
@@ -19,12 +19,9 @@ package com.android.role.controller.model;
import android.app.AppOpsManager;
import android.content.Context;
import android.content.Intent;
-import android.content.pm.PackageManager;
-import android.content.pm.PermissionInfo;
import android.content.res.Resources;
import android.content.res.XmlResourceParser;
import android.os.Build;
-import android.os.Process;
import android.permission.flags.Flags;
import android.util.ArrayMap;
import android.util.Log;
@@ -92,6 +89,7 @@ public class RoleParser {
private static final String ATTRIBUTE_DEFAULT_HOLDERS = "defaultHolders";
private static final String ATTRIBUTE_DESCRIPTION = "description";
private static final String ATTRIBUTE_EXCLUSIVE = "exclusive";
+ private static final String ATTRIBUTE_EXCLUSIVITY = "exclusivity";
private static final String ATTRIBUTE_FALL_BACK_TO_DEFAULT_HOLDER = "fallBackToDefaultHolder";
private static final String ATTRIBUTE_FEATURE_FLAG = "featureFlag";
private static final String ATTRIBUTE_LABEL = "label";
@@ -138,6 +136,10 @@ public class RoleParser {
sModeNameToMode.put(MODE_NAME_FOREGROUND, AppOpsManager.MODE_FOREGROUND);
}
+ private static final String EXCLUSIVITY_NONE = "none";
+ private static final String EXCLUSIVITY_USER = "user";
+ private static final String EXCLUSIVITY_PROFILE_GROUP = "profileGroup";
+
private static final Supplier<Boolean> sFeatureFlagFallback = () -> false;
private static final ArrayMap<Class<?>, Class<?>> sPrimitiveToWrapperClass = new ArrayMap<>();
@@ -156,16 +158,16 @@ public class RoleParser {
@NonNull
private final Context mContext;
- private final boolean mValidationEnabled;
+ private final boolean mThrowOnError;
public RoleParser(@NonNull Context context) {
this(context, false);
}
@VisibleForTesting
- public RoleParser(@NonNull Context context, boolean validationEnabled) {
+ public RoleParser(@NonNull Context context, boolean throwOnError) {
mContext = context;
- mValidationEnabled = validationEnabled;
+ mThrowOnError = throwOnError;
}
/**
@@ -175,18 +177,21 @@ public class RoleParser {
*/
@NonNull
public ArrayMap<String, Role> parse() {
+ Pair<ArrayMap<String, PermissionSet>, ArrayMap<String, Role>> xml = parseRolesXml();
+ if (xml == null) {
+ return new ArrayMap<>();
+ }
+ return xml.second;
+ }
+
+ @Nullable
+ @VisibleForTesting
+ public Pair<ArrayMap<String, PermissionSet>, ArrayMap<String, Role>> parseRolesXml() {
try (XmlResourceParser parser = getRolesXml()) {
- Pair<ArrayMap<String, PermissionSet>, ArrayMap<String, Role>> xml = parseXml(parser);
- if (xml == null) {
- return new ArrayMap<>();
- }
- ArrayMap<String, PermissionSet> permissionSets = xml.first;
- ArrayMap<String, Role> roles = xml.second;
- validateResult(permissionSets, roles);
- return roles;
+ return parseXml(parser);
} catch (XmlPullParserException | IOException e) {
throwOrLogMessage("Unable to parse roles.xml", e);
- return new ArrayMap<>();
+ return null;
}
}
@@ -413,13 +418,45 @@ public class RoleParser {
shortLabelResource = 0;
}
- Boolean exclusive = requireAttributeBooleanValue(parser, ATTRIBUTE_EXCLUSIVE, true,
- TAG_ROLE);
- if (exclusive == null) {
- skipCurrentTag(parser);
- return null;
+ int exclusivity;
+ if (com.android.permission.flags.Flags.crossUserRoleEnabled()) {
+ String exclusivityName = requireAttributeValue(parser, ATTRIBUTE_EXCLUSIVITY, TAG_ROLE);
+ if (exclusivityName == null) {
+ skipCurrentTag(parser);
+ return null;
+ }
+ switch (exclusivityName) {
+ case EXCLUSIVITY_NONE:
+ exclusivity = Role.EXCLUSIVITY_NONE;
+ break;
+ case EXCLUSIVITY_USER:
+ exclusivity = Role.EXCLUSIVITY_USER;
+ break;
+ case EXCLUSIVITY_PROFILE_GROUP:
+ // TODO(b/372743073): change to isAtLeastB once available
+ // EXCLUSIVITY_PROFILE behavior only available for B+
+ // fallback to default of EXCLUSIVITY_USER
+ exclusivity = SdkLevel.isAtLeastV()
+ ? Role.EXCLUSIVITY_PROFILE_GROUP
+ : Role.EXCLUSIVITY_USER;
+ break;
+ default:
+ throwOrLogMessage("Invalid value for \"exclusivity\" on <role>: " + name
+ + ", exclusivity: " + exclusivityName);
+ skipCurrentTag(parser);
+ return null;
+ }
+ } else {
+ Boolean exclusive =
+ requireAttributeBooleanValue(parser, ATTRIBUTE_EXCLUSIVE, true, TAG_ROLE);
+ if (exclusive == null) {
+ skipCurrentTag(parser);
+ return null;
+ }
+ exclusivity = exclusive ? Role.EXCLUSIVITY_USER : Role.EXCLUSIVITY_NONE;
}
+
boolean fallBackToDefaultHolder = getAttributeBooleanValue(parser,
ATTRIBUTE_FALL_BACK_TO_DEFAULT_HOLDER, false);
@@ -470,7 +507,7 @@ public class RoleParser {
0);
boolean showNone = getAttributeBooleanValue(parser, ATTRIBUTE_SHOW_NONE, false);
- if (showNone && !exclusive) {
+ if (showNone && exclusivity == Role.EXCLUSIVITY_NONE) {
throwOrLogMessage("showNone=\"true\" is invalid for a non-exclusive role: " + name);
skipCurrentTag(parser);
return null;
@@ -567,12 +604,12 @@ public class RoleParser {
preferredActivities = Collections.emptyList();
}
return new Role(name, allowBypassingQualification, behavior, defaultHoldersResourceName,
- descriptionResource, exclusive, fallBackToDefaultHolder, featureFlag, labelResource,
- maxSdkVersion, minSdkVersion, onlyGrantWhenAdded, overrideUserWhenGranting,
- requestDescriptionResource, requestTitleResource, requestable,
- searchKeywordsResource, shortLabelResource, showNone, statik, systemOnly, visible,
- requiredComponents, permissions, appOpPermissions, appOps, preferredActivities,
- uiBehaviorName);
+ descriptionResource, exclusivity, fallBackToDefaultHolder, featureFlag,
+ labelResource, maxSdkVersion, minSdkVersion, onlyGrantWhenAdded,
+ overrideUserWhenGranting, requestDescriptionResource, requestTitleResource,
+ requestable, searchKeywordsResource, shortLabelResource, showNone, statik,
+ systemOnly, visible, requiredComponents, permissions, appOpPermissions, appOps,
+ preferredActivities, uiBehaviorName);
}
@NonNull
@@ -624,7 +661,7 @@ public class RoleParser {
int queryFlags = getAttributeIntValue(parser, ATTRIBUTE_QUERY_FLAGS, 0);
IntentFilterData intentFilterData = null;
List<RequiredMetaData> metaData = new ArrayList<>();
- List<String> validationMetaDataNames = mValidationEnabled ? new ArrayList<>() : null;
+ List<String> validationMetaDataNames = mThrowOnError ? new ArrayList<>() : null;
int type;
int depth;
@@ -651,7 +688,7 @@ public class RoleParser {
if (metaDataName == null) {
continue;
}
- if (mValidationEnabled) {
+ if (mThrowOnError) {
validateNoDuplicateElement(metaDataName, validationMetaDataNames,
"meta data");
}
@@ -668,7 +705,7 @@ public class RoleParser {
RequiredMetaData requiredMetaData = new RequiredMetaData(metaDataName,
metaDataValue, metaDataProhibited);
metaData.add(requiredMetaData);
- if (mValidationEnabled) {
+ if (mThrowOnError) {
validationMetaDataNames.add(metaDataName);
}
break;
@@ -1204,7 +1241,7 @@ public class RoleParser {
}
private void throwOrLogMessage(String message) {
- if (mValidationEnabled) {
+ if (mThrowOnError) {
throw new IllegalArgumentException(message);
} else {
Log.wtf(LOG_TAG, message);
@@ -1212,7 +1249,7 @@ public class RoleParser {
}
private void throwOrLogMessage(String message, Throwable cause) {
- if (mValidationEnabled) {
+ if (mThrowOnError) {
throw new IllegalArgumentException(message, cause);
} else {
Log.wtf(LOG_TAG, message, cause);
@@ -1222,168 +1259,4 @@ public class RoleParser {
private void throwOrLogForUnknownTag(@NonNull XmlResourceParser parser) {
throwOrLogMessage("Unknown tag: " + parser.getName());
}
-
- /**
- * Validates the permission names with {@code PackageManager} and ensures that all app ops with
- * a permission in {@code AppOpsManager} have declared that permission in its role and ensures
- * that all preferred activities are listed in the required components.
- */
- private void validateResult(@NonNull ArrayMap<String, PermissionSet> permissionSets,
- @NonNull ArrayMap<String, Role> roles) {
- if (!mValidationEnabled) {
- return;
- }
-
- int permissionSetsSize = permissionSets.size();
- for (int permissionSetsIndex = 0; permissionSetsIndex < permissionSetsSize;
- permissionSetsIndex++) {
- PermissionSet permissionSet = permissionSets.valueAt(permissionSetsIndex);
-
- List<Permission> permissions = permissionSet.getPermissions();
- int permissionsSize = permissions.size();
- for (int permissionsIndex = 0; permissionsIndex < permissionsSize; permissionsIndex++) {
- Permission permission = permissions.get(permissionsIndex);
-
- validatePermission(permission);
- }
- }
-
- int rolesSize = roles.size();
- for (int rolesIndex = 0; rolesIndex < rolesSize; rolesIndex++) {
- Role role = roles.valueAt(rolesIndex);
-
- if (!role.isAvailableByFeatureFlagAndSdkVersion()) {
- continue;
- }
-
- List<RequiredComponent> requiredComponents = role.getRequiredComponents();
- int requiredComponentsSize = requiredComponents.size();
- for (int requiredComponentsIndex = 0; requiredComponentsIndex < requiredComponentsSize;
- requiredComponentsIndex++) {
- RequiredComponent requiredComponent = requiredComponents.get(
- requiredComponentsIndex);
-
- String permission = requiredComponent.getPermission();
- if (permission != null) {
- validatePermission(permission);
- }
- }
-
- List<Permission> permissions = role.getPermissions();
- int permissionsSize = permissions.size();
- for (int i = 0; i < permissionsSize; i++) {
- Permission permission = permissions.get(i);
-
- validatePermission(permission);
- }
-
- List<AppOp> appOps = role.getAppOps();
- int appOpsSize = appOps.size();
- for (int i = 0; i < appOpsSize; i++) {
- AppOp appOp = appOps.get(i);
-
- validateAppOp(appOp);
- }
-
- List<Permission> appOpPermissions = role.getAppOpPermissions();
- int appOpPermissionsSize = appOpPermissions.size();
- for (int i = 0; i < appOpPermissionsSize; i++) {
- validateAppOpPermission(appOpPermissions.get(i));
- }
-
- List<PreferredActivity> preferredActivities = role.getPreferredActivities();
- int preferredActivitiesSize = preferredActivities.size();
- for (int preferredActivitiesIndex = 0;
- preferredActivitiesIndex < preferredActivitiesSize;
- preferredActivitiesIndex++) {
- PreferredActivity preferredActivity = preferredActivities.get(
- preferredActivitiesIndex);
-
- if (!role.getRequiredComponents().contains(preferredActivity.getActivity())) {
- throw new IllegalArgumentException("<activity> of <preferred-activity> not"
- + " required in <required-components>, role: " + role.getName()
- + ", preferred activity: " + preferredActivity);
- }
- }
- }
- }
-
- private void validatePermission(@NonNull Permission permission) {
- if (!permission.isAvailableAsUser(Process.myUserHandle(), mContext)) {
- return;
- }
- validatePermission(permission.getName(), true);
- }
-
- private void validatePermission(@NonNull String permission) {
- validatePermission(permission, false);
- }
-
- private void validatePermission(@NonNull String permission, boolean enforceIsRuntimeOrRole) {
- PackageManager packageManager = mContext.getPackageManager();
- boolean isAutomotive = packageManager.hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE);
- // Skip validation for car permissions which may not be available on all build targets.
- if (!isAutomotive && permission.startsWith("android.car")) {
- return;
- }
-
- PermissionInfo permissionInfo;
- try {
- permissionInfo = packageManager.getPermissionInfo(permission, 0);
- } catch (PackageManager.NameNotFoundException e) {
- throw new IllegalArgumentException("Unknown permission: " + permission, e);
- }
-
- if (enforceIsRuntimeOrRole) {
- if (!(permissionInfo.getProtection() == PermissionInfo.PROTECTION_DANGEROUS
- || (permissionInfo.getProtectionFlags() & PermissionInfo.PROTECTION_FLAG_ROLE)
- == PermissionInfo.PROTECTION_FLAG_ROLE)) {
- throw new IllegalArgumentException(
- "Permission is not a runtime or role permission: " + permission);
- }
- }
- }
-
- private void validateAppOpPermission(@NonNull Permission appOpPermission) {
- if (!appOpPermission.isAvailableAsUser(Process.myUserHandle(), mContext)) {
- return;
- }
- validateAppOpPermission(appOpPermission.getName());
- }
-
- private void validateAppOpPermission(@NonNull String appOpPermission) {
- PackageManager packageManager = mContext.getPackageManager();
- PermissionInfo permissionInfo;
- try {
- permissionInfo = packageManager.getPermissionInfo(appOpPermission, 0);
- } catch (PackageManager.NameNotFoundException e) {
- throw new IllegalArgumentException("Unknown app op permission: " + appOpPermission, e);
- }
- if ((permissionInfo.getProtectionFlags() & PermissionInfo.PROTECTION_FLAG_APPOP)
- != PermissionInfo.PROTECTION_FLAG_APPOP) {
- throw new IllegalArgumentException("Permission is not an app op permission: "
- + appOpPermission);
- }
- }
-
- private void validateAppOp(@NonNull AppOp appOp) {
- if (!appOp.isAvailableByFeatureFlagAndSdkVersion()) {
- return;
- }
- // This throws IllegalArgumentException if app op is unknown.
- String permission = AppOpsManager.opToPermission(appOp.getName());
- if (permission != null) {
- PackageManager packageManager = mContext.getPackageManager();
- PermissionInfo permissionInfo;
- try {
- permissionInfo = packageManager.getPermissionInfo(permission, 0);
- } catch (PackageManager.NameNotFoundException e) {
- throw new RuntimeException(e);
- }
- if (permissionInfo.getProtection() == PermissionInfo.PROTECTION_DANGEROUS) {
- throw new IllegalArgumentException("App op has an associated runtime permission: "
- + appOp.getName());
- }
- }
- }
}
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 9f39bd785..36d867b11 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)
}
diff --git a/PermissionController/tests/inprocess/Android.bp b/PermissionController/tests/inprocess/Android.bp
index 60b35e80f..4cd9e0e6f 100644
--- a/PermissionController/tests/inprocess/Android.bp
+++ b/PermissionController/tests/inprocess/Android.bp
@@ -50,6 +50,9 @@ android_test {
"compatibility-device-util-axt",
"kotlin-test",
"permission-test-util-lib",
+ // This may result in two flag libs being included. This should only be used for Flag
+ //string referencing for test annotations.
+ "com.android.permission.flags-aconfig-java-export",
],
data: [
diff --git a/PermissionController/tests/mocking/src/com/android/permissioncontroller/tests/mocking/hibernation/HibernationPolicyTest.kt b/PermissionController/tests/mocking/src/com/android/permissioncontroller/tests/mocking/hibernation/HibernationPolicyTest.kt
index 99aa4baa9..15a37532f 100644
--- a/PermissionController/tests/mocking/src/com/android/permissioncontroller/tests/mocking/hibernation/HibernationPolicyTest.kt
+++ b/PermissionController/tests/mocking/src/com/android/permissioncontroller/tests/mocking/hibernation/HibernationPolicyTest.kt
@@ -16,19 +16,27 @@
package com.android.permissioncontroller.tests.mocking.hibernation
+import android.Manifest
import android.app.job.JobScheduler
+import android.content.ComponentName
import android.content.ContentResolver
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
+import android.content.pm.PackageManager
+import android.content.pm.PackageManager.PERMISSION_GRANTED
import android.database.ContentObserver
import android.net.Uri
+import android.os.Binder
import android.os.Build
import android.os.SystemClock
+import android.os.UserHandle
import android.os.UserManager
import android.preference.PreferenceManager
import android.provider.DeviceConfig
import android.provider.Settings
+import android.telecom.PhoneAccountHandle
+import android.telecom.TelecomManager
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SdkSuppress
@@ -37,14 +45,19 @@ import com.android.permissioncontroller.Constants
import com.android.permissioncontroller.PermissionControllerApplication
import com.android.permissioncontroller.hibernation.HibernationBroadcastReceiver
import com.android.permissioncontroller.hibernation.ONE_DAY_MS
-import com.android.permissioncontroller.hibernation.PREF_KEY_SYSTEM_TIME_SNAPSHOT
import com.android.permissioncontroller.hibernation.PREF_KEY_ELAPSED_REALTIME_SNAPSHOT
import com.android.permissioncontroller.hibernation.PREF_KEY_START_TIME_OF_UNUSED_APP_TRACKING
+import com.android.permissioncontroller.hibernation.PREF_KEY_SYSTEM_TIME_SNAPSHOT
import com.android.permissioncontroller.hibernation.SNAPSHOT_UNINITIALIZED
import com.android.permissioncontroller.hibernation.getStartTimeOfUnusedAppTracking
+import com.android.permissioncontroller.hibernation.isPackageHibernationExemptBySystem
+import com.android.permissioncontroller.permission.model.livedatatypes.LightPackageInfo
+import com.android.permissioncontroller.permission.utils.ContextCompat
import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.runBlocking
import org.junit.After
import org.junit.Before
+import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentCaptor
@@ -71,11 +84,14 @@ class HibernationPolicyTest {
private val application = Mockito.mock(PermissionControllerApplication::class.java)
private const val USER_SETUP_INCOMPLETE = 0
private const val USER_SETUP_COMPLETE = 1
+ private const val TEST_PKG_NAME = "test.package"
}
@Mock lateinit var jobScheduler: JobScheduler
@Mock lateinit var context: Context
@Mock lateinit var userManager: UserManager
+ @Mock lateinit var packageManager: PackageManager
+ @Mock lateinit var telecomManager: TelecomManager
@Mock lateinit var contentResolver: ContentResolver
private lateinit var realContext: Context
@@ -83,6 +99,7 @@ class HibernationPolicyTest {
private lateinit var sharedPreferences: SharedPreferences
private lateinit var mockitoSession: MockitoSession
private lateinit var filesDir: File
+ private lateinit var userHandle: UserHandle
@Before
fun setup() {
@@ -100,11 +117,15 @@ class HibernationPolicyTest {
`when`(Settings.Secure.getUriFor(any())).thenReturn(Mockito.mock(Uri::class.java))
realContext = ApplicationProvider.getApplicationContext()
+ userHandle = UserHandle.getUserHandleForUid(Binder.getCallingUid())
sharedPreferences =
PreferenceManager.getDefaultSharedPreferences(realContext.applicationContext)
`when`(context.getSharedPreferences(anyString(), anyInt())).thenReturn(sharedPreferences)
`when`(context.getSystemService(UserManager::class.java)).thenReturn(userManager)
+ `when`(application.getSystemService(TelecomManager::class.java)).thenReturn(telecomManager)
+ `when`(application.packageManager).thenReturn(packageManager)
+ `when`(application.applicationContext).thenReturn(context)
filesDir = realContext.cacheDir
`when`(application.filesDir).thenReturn(filesDir)
@@ -207,6 +228,30 @@ class HibernationPolicyTest {
.isNotEqualTo(systemTimeSnapshot)
}
+ @Test
+ @Ignore("b/371061181")
+ // This method under test initializes several SmartAsyncMediatorLiveData classes which run code
+ // on GlobalScope which the unit test has no control over. This can lead to the code running
+ // during other tests which may not have the right static mocks.
+ // Until this is fixed, this test should be ignored to prevent flaky test faliures.
+ fun isPackageExemptBySystem_isCallingApp_returnsTrue() = runBlocking<Unit> {
+ val pkgInfo = makePackageInfo(TEST_PKG_NAME)
+
+ `when`(context.checkPermission(
+ eq(Manifest.permission.MANAGE_OWN_CALLS), anyInt(), eq(pkgInfo.uid)))
+ .thenReturn(PERMISSION_GRANTED)
+ `when`(context.checkPermission(
+ eq(Manifest.permission.RECORD_AUDIO), anyInt(), eq(pkgInfo.uid)))
+ .thenReturn(PERMISSION_GRANTED)
+ `when`(context.checkPermission(
+ eq(Manifest.permission.WRITE_CALL_LOG), anyInt(), eq(pkgInfo.uid)))
+ .thenReturn(PERMISSION_GRANTED)
+ `when`(telecomManager.selfManagedPhoneAccounts).thenReturn(
+ listOf(PhoneAccountHandle(ComponentName(TEST_PKG_NAME, "Service"), "id")))
+
+ assertThat(isPackageHibernationExemptBySystem(pkgInfo, userHandle)).isTrue()
+ }
+
private fun assertAdjustedTime(systemTimeSnapshot: Long, realtimeSnapshot: Long) {
val newStartTimeOfUnusedAppTracking =
sharedPreferences.getLong(
@@ -223,4 +268,23 @@ class HibernationPolicyTest {
assertThat(newRealtimeSnapshot).isNotEqualTo(SNAPSHOT_UNINITIALIZED)
assertThat(newRealtimeSnapshot).isAtLeast(realtimeSnapshot)
}
+
+ private fun makePackageInfo(packageName: String): LightPackageInfo {
+ return LightPackageInfo(
+ packageName,
+ emptyList(),
+ emptyList(),
+ emptyList(),
+ 0 /* uid */,
+ Build.VERSION_CODES.CUR_DEVELOPMENT,
+ false /* isInstantApp */,
+ true /* enabled */,
+ 0 /* appFlags */,
+ 0 /* firstInstallTime */,
+ 0 /* lastUpdateTime */,
+ false /* areAttributionsUserVisible */,
+ emptyMap() /* attributionTagsToLabels */,
+ ContextCompat.DEVICE_ID_DEFAULT
+ )
+ }
}
diff --git a/PermissionController/tests/mocking/src/com/android/permissioncontroller/tests/mocking/permission/domain/usecase/GetPermissionGroupUsageDetailsUseCaseTest.kt b/PermissionController/tests/mocking/src/com/android/permissioncontroller/tests/mocking/permission/domain/usecase/GetPermissionGroupUsageDetailsUseCaseTest.kt
index da2d05f63..e6a1c15c2 100644
--- a/PermissionController/tests/mocking/src/com/android/permissioncontroller/tests/mocking/permission/domain/usecase/GetPermissionGroupUsageDetailsUseCaseTest.kt
+++ b/PermissionController/tests/mocking/src/com/android/permissioncontroller/tests/mocking/permission/domain/usecase/GetPermissionGroupUsageDetailsUseCaseTest.kt
@@ -34,6 +34,7 @@ import com.android.permissioncontroller.permission.domain.model.v31.PermissionTi
import com.android.permissioncontroller.permission.domain.model.v31.PermissionTimelineUsageModelWrapper
import com.android.permissioncontroller.permission.domain.usecase.v31.GetPermissionGroupUsageDetailsUseCase
import com.android.permissioncontroller.permission.domain.usecase.v31.TELECOM_PACKAGE
+import com.android.permissioncontroller.permission.utils.LocationUtils
import com.android.permissioncontroller.pm.data.model.v31.PackageAttributionModel
import com.android.permissioncontroller.pm.data.model.v31.PackageInfoModel
import com.android.permissioncontroller.pm.data.repository.v31.PackageRepository
@@ -56,9 +57,11 @@ import kotlinx.coroutines.test.runTest
import org.junit.After
import org.junit.Assume
import org.junit.Before
+import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
+import org.mockito.Mockito
import org.mockito.Mockito.`when` as whenever
import org.mockito.MockitoAnnotations
import org.mockito.MockitoSession
@@ -88,11 +91,13 @@ class GetPermissionGroupUsageDetailsUseCaseTest {
mockitoSession =
ExtendedMockito.mockitoSession()
.mockStatic(PermissionControllerApplication::class.java)
+ .mockStatic(LocationUtils::class.java)
.strictness(Strictness.LENIENT)
.startMocking()
whenever(PermissionControllerApplication.get()).thenReturn(application)
whenever(application.applicationContext).thenReturn(context)
+ whenever(LocationUtils.isLocationProvider(Mockito.any(), Mockito.any())).thenReturn(false)
packageInfos =
mapOf(
@@ -102,7 +107,7 @@ class GetPermissionGroupUsageDetailsUseCaseTest {
systemPackageName to
getPackageInfoModel(
systemPackageName,
- applicationFlags = ApplicationInfo.FLAG_SYSTEM
+ applicationFlags = ApplicationInfo.FLAG_SYSTEM,
),
)
.toMutableMap()
@@ -124,7 +129,7 @@ class GetPermissionGroupUsageDetailsUseCaseTest {
emit(
listOf(
DiscretePackageOpsModel(testPackageName, currentUser.identifier, appOpEvents),
- DiscretePackageOpsModel(guestUserPkgName, guestUser.identifier, appOpEvents)
+ DiscretePackageOpsModel(guestUserPkgName, guestUser.identifier, appOpEvents),
)
)
}
@@ -150,11 +155,7 @@ class GetPermissionGroupUsageDetailsUseCaseTest {
emit(
listOf(
DiscretePackageOpsModel(testPackageName, currentUser.identifier, appOpEvents),
- DiscretePackageOpsModel(
- testPackageName,
- privateProfile.identifier,
- appOpEvents
- ),
+ DiscretePackageOpsModel(testPackageName, privateProfile.identifier, appOpEvents),
)
)
}
@@ -168,8 +169,8 @@ class GetPermissionGroupUsageDetailsUseCaseTest {
currentUserProfiles =
listOf(currentUser.identifier, privateProfile.identifier),
quietUserProfiles = listOf(privateProfile.identifier),
- showInQuiteModeProfiles = listOf(privateProfile.identifier)
- )
+ showInQuiteModeProfiles = listOf(privateProfile.identifier),
+ ),
)
val permissionTimelineUsages = getResult(underTest, this)
@@ -190,11 +191,7 @@ class GetPermissionGroupUsageDetailsUseCaseTest {
emit(
listOf(
DiscretePackageOpsModel(testPackageName, currentUser.identifier, appOpEvents),
- DiscretePackageOpsModel(
- testPackageName,
- privateProfile.identifier,
- appOpEvents
- ),
+ DiscretePackageOpsModel(testPackageName, privateProfile.identifier, appOpEvents),
)
)
}
@@ -208,7 +205,7 @@ class GetPermissionGroupUsageDetailsUseCaseTest {
currentUserProfiles =
listOf(currentUser.identifier, privateProfile.identifier),
quietUserProfiles = listOf(privateProfile.identifier),
- )
+ ),
)
val permissionTimelineUsages = getResult(underTest, this)
@@ -254,7 +251,7 @@ class GetPermissionGroupUsageDetailsUseCaseTest {
val discretePackageOps = flow {
emit(
listOf(
- DiscretePackageOpsModel(testPackageName, currentUser.identifier, appOpEvents),
+ DiscretePackageOpsModel(testPackageName, currentUser.identifier, appOpEvents)
)
)
}
@@ -280,7 +277,7 @@ class GetPermissionGroupUsageDetailsUseCaseTest {
val discretePackageOps = flow {
emit(
listOf(
- DiscretePackageOpsModel(testPackageName, currentUser.identifier, appOpEvents),
+ DiscretePackageOpsModel(testPackageName, currentUser.identifier, appOpEvents)
)
)
}
@@ -303,23 +300,23 @@ class GetPermissionGroupUsageDetailsUseCaseTest {
DiscreteOpModel(
AppOpsManager.OPSTR_CAMERA,
MINUTES.toMillis(1),
- MINUTES.toMillis(1)
+ MINUTES.toMillis(1),
),
DiscreteOpModel(
AppOpsManager.OPSTR_CAMERA,
MINUTES.toMillis(2),
- MINUTES.toMillis(1)
+ MINUTES.toMillis(1),
),
DiscreteOpModel(
AppOpsManager.OPSTR_CAMERA,
MINUTES.toMillis(3),
- MINUTES.toMillis(2)
+ MINUTES.toMillis(2),
),
)
val discretePackageOps = flow {
emit(
listOf(
- DiscretePackageOpsModel(testPackageName, currentUser.identifier, appOpEvents),
+ DiscretePackageOpsModel(testPackageName, currentUser.identifier, appOpEvents)
)
)
}
@@ -344,23 +341,19 @@ class GetPermissionGroupUsageDetailsUseCaseTest {
DiscreteOpModel(
AppOpsManager.OPSTR_FINE_LOCATION,
hours3 + MINUTES.toMillis(59),
- -1
+ -1,
),
DiscreteOpModel(
AppOpsManager.OPSTR_FINE_LOCATION,
hours4 + MINUTES.toMillis(0),
- -1
- ),
- DiscreteOpModel(
- AppOpsManager.OPSTR_FINE_LOCATION,
- hours4 + MINUTES.toMillis(1),
- -1
+ -1,
),
+ DiscreteOpModel(AppOpsManager.OPSTR_FINE_LOCATION, hours4 + MINUTES.toMillis(1), -1),
)
val discretePackageOps = flow {
emit(
listOf(
- DiscretePackageOpsModel(testPackageName, currentUser.identifier, appOpEvents),
+ DiscretePackageOpsModel(testPackageName, currentUser.identifier, appOpEvents)
)
)
}
@@ -383,18 +376,18 @@ class GetPermissionGroupUsageDetailsUseCaseTest {
DiscreteOpModel(
AppOpsManager.OPSTR_CAMERA,
MINUTES.toMillis(1),
- MINUTES.toMillis(3)
+ MINUTES.toMillis(3),
),
DiscreteOpModel(
AppOpsManager.OPSTR_CAMERA,
MINUTES.toMillis(3),
- MINUTES.toMillis(2)
+ MINUTES.toMillis(2),
),
)
val discretePackageOps = flow {
emit(
listOf(
- DiscretePackageOpsModel(testPackageName, currentUser.identifier, appOpEvents),
+ DiscretePackageOpsModel(testPackageName, currentUser.identifier, appOpEvents)
)
)
}
@@ -420,7 +413,7 @@ class GetPermissionGroupUsageDetailsUseCaseTest {
val discretePackageOps = flow {
emit(
listOf(
- DiscretePackageOpsModel(testPackageName, currentUser.identifier, appOpEvents),
+ DiscretePackageOpsModel(testPackageName, currentUser.identifier, appOpEvents)
)
)
}
@@ -448,18 +441,18 @@ class GetPermissionGroupUsageDetailsUseCaseTest {
DiscreteOpModel(
AppOpsManager.OPSTR_CAMERA,
MINUTES.toMillis(1),
- MINUTES.toMillis(3)
+ MINUTES.toMillis(3),
),
DiscreteOpModel(
AppOpsManager.OPSTR_CAMERA,
MINUTES.toMillis(5),
- MINUTES.toMillis(5)
+ MINUTES.toMillis(5),
),
)
val discretePackageOps = flow {
emit(
listOf(
- DiscretePackageOpsModel(testPackageName, currentUser.identifier, appOpEvents),
+ DiscretePackageOpsModel(testPackageName, currentUser.identifier, appOpEvents)
)
)
}
@@ -483,13 +476,11 @@ class GetPermissionGroupUsageDetailsUseCaseTest {
@Test
fun singleDiscreteAccess() = runTest {
val appOpEvents =
- listOf(
- DiscreteOpModel(AppOpsManager.OPSTR_FINE_LOCATION, MINUTES.toMillis(1), -1),
- )
+ listOf(DiscreteOpModel(AppOpsManager.OPSTR_FINE_LOCATION, MINUTES.toMillis(1), -1))
val discretePackageOps = flow {
emit(
listOf(
- DiscretePackageOpsModel(testPackageName, currentUser.identifier, appOpEvents),
+ DiscretePackageOpsModel(testPackageName, currentUser.identifier, appOpEvents)
)
)
}
@@ -512,13 +503,13 @@ class GetPermissionGroupUsageDetailsUseCaseTest {
DiscreteOpModel(
AppOpsManager.OPSTR_CAMERA,
MINUTES.toMillis(1),
- MINUTES.toMillis(3)
- ),
+ MINUTES.toMillis(3),
+ )
)
val discretePackageOps = flow {
emit(
listOf(
- DiscretePackageOpsModel(testPackageName, currentUser.identifier, appOpEvents),
+ DiscretePackageOpsModel(testPackageName, currentUser.identifier, appOpEvents)
)
)
}
@@ -541,29 +532,29 @@ class GetPermissionGroupUsageDetailsUseCaseTest {
DiscreteOpModel(
AppOpsManager.OPSTR_CAMERA,
MINUTES.toMillis(1),
- MINUTES.toMillis(2)
+ MINUTES.toMillis(2),
),
// This entry says the camera was accessed for 15 minutes starting at minute 3
DiscreteOpModel(
AppOpsManager.OPSTR_CAMERA,
MINUTES.toMillis(3),
- MINUTES.toMillis(15)
+ MINUTES.toMillis(15),
),
DiscreteOpModel(
AppOpsManager.OPSTR_CAMERA,
MINUTES.toMillis(4),
- MINUTES.toMillis(1)
+ MINUTES.toMillis(1),
),
DiscreteOpModel(
AppOpsManager.OPSTR_CAMERA,
MINUTES.toMillis(6),
- MINUTES.toMillis(1)
+ MINUTES.toMillis(1),
),
)
val discretePackageOps = flow {
emit(
listOf(
- DiscretePackageOpsModel(testPackageName, currentUser.identifier, appOpEvents),
+ DiscretePackageOpsModel(testPackageName, currentUser.identifier, appOpEvents)
)
)
}
@@ -582,9 +573,7 @@ class GetPermissionGroupUsageDetailsUseCaseTest {
@Test
fun verifyUserSensitiveFlags() = runTest {
val appOpEvents =
- listOf(
- DiscreteOpModel(AppOpsManager.OPSTR_COARSE_LOCATION, MINUTES.toMillis(1), -1),
- )
+ listOf(DiscreteOpModel(AppOpsManager.OPSTR_COARSE_LOCATION, MINUTES.toMillis(1), -1))
val discretePackageOps = flow {
emit(
listOf(
@@ -620,13 +609,11 @@ class GetPermissionGroupUsageDetailsUseCaseTest {
@Test
fun verifyNotUserSensitiveFlagsForSystemPackage() = runTest {
val appOpEvents =
- listOf(
- DiscreteOpModel(AppOpsManager.OPSTR_COARSE_LOCATION, MINUTES.toMillis(1), -1),
- )
+ listOf(DiscreteOpModel(AppOpsManager.OPSTR_COARSE_LOCATION, MINUTES.toMillis(1), -1))
val discretePackageOps = flow {
emit(
listOf(
- DiscretePackageOpsModel(systemPackageName, currentUser.identifier, appOpEvents),
+ DiscretePackageOpsModel(systemPackageName, currentUser.identifier, appOpEvents)
)
)
}
@@ -651,14 +638,12 @@ class GetPermissionGroupUsageDetailsUseCaseTest {
@Test
fun verifyCameraUserSensitiveFlagsForTelecomPackage() = runTest {
val appOpEvents =
- listOf(
- DiscreteOpModel(AppOpsManager.OPSTR_CAMERA, MINUTES.toMillis(1), -1),
- )
+ listOf(DiscreteOpModel(AppOpsManager.OPSTR_CAMERA, MINUTES.toMillis(1), -1))
packageInfos[TELECOM_PACKAGE] = getPackageInfoModel(TELECOM_PACKAGE)
val discretePackageOps = flow {
emit(
listOf(
- DiscretePackageOpsModel(TELECOM_PACKAGE, currentUser.identifier, appOpEvents),
+ DiscretePackageOpsModel(TELECOM_PACKAGE, currentUser.identifier, appOpEvents)
)
)
}
@@ -683,14 +668,12 @@ class GetPermissionGroupUsageDetailsUseCaseTest {
@Test
fun verifyLocationUserSensitiveFlagsForTelecomPackage() = runTest {
val appOpEvents =
- listOf(
- DiscreteOpModel(AppOpsManager.OPSTR_COARSE_LOCATION, MINUTES.toMillis(1), -1),
- )
+ listOf(DiscreteOpModel(AppOpsManager.OPSTR_COARSE_LOCATION, MINUTES.toMillis(1), -1))
packageInfos[TELECOM_PACKAGE] = getPackageInfoModel(TELECOM_PACKAGE)
val discretePackageOps = flow {
emit(
listOf(
- DiscretePackageOpsModel(TELECOM_PACKAGE, currentUser.identifier, appOpEvents),
+ DiscretePackageOpsModel(TELECOM_PACKAGE, currentUser.identifier, appOpEvents)
)
)
}
@@ -714,7 +697,75 @@ class GetPermissionGroupUsageDetailsUseCaseTest {
}
@Test
- fun verifyAttributionTagsAreGroupedAndClustered() = runTest {
+ @RequiresFlagsEnabled(
+ com.android.permission.flags.Flags.FLAG_PERMISSION_TIMELINE_ATTRIBUTION_LABEL_FIX
+ )
+ @Ignore("b/365004787")
+ fun verifyAccessIsNotGroupedByAttributionLabelAndClustered() = runTest {
+ // The package is not a location provider.
+ val appOpEvents =
+ listOf(
+ // These entries should be clustered even though they have
+ // different attribution labels.
+ DiscreteOpModel(
+ AppOpsManager.OPSTR_COARSE_LOCATION,
+ MINUTES.toMillis(1),
+ -1,
+ "tag1",
+ ),
+ DiscreteOpModel(
+ AppOpsManager.OPSTR_COARSE_LOCATION,
+ MINUTES.toMillis(2),
+ -1,
+ "tag1",
+ ),
+ DiscreteOpModel(
+ AppOpsManager.OPSTR_COARSE_LOCATION,
+ MINUTES.toMillis(3),
+ -1,
+ "tag3",
+ ),
+ DiscreteOpModel(
+ AppOpsManager.OPSTR_COARSE_LOCATION,
+ MINUTES.toMillis(4),
+ -1,
+ "tag2",
+ ),
+ )
+ val discretePackageOps = flow {
+ emit(
+ listOf(
+ DiscretePackageOpsModel(testPackageName, currentUser.identifier, appOpEvents)
+ )
+ )
+ }
+ val packageAttributions = mutableMapOf<String, PackageAttributionModel>()
+ // tag1 and tag3 refers to same attribution label.
+ val attributionTagToLabelRes = mapOf("tag1" to 100, "tag2" to 200, "tag3" to 100)
+ val attributionsMap = mapOf(100 to "Tag1 Label", 200 to "Tag2 Label")
+ packageAttributions[testPackageName] =
+ PackageAttributionModel(
+ testPackageName,
+ true,
+ attributionTagToLabelRes,
+ attributionsMap,
+ )
+
+ val underTest =
+ getPermissionGroupUsageDetailsUseCase(
+ LOCATION_PERMISSION_GROUP,
+ discretePackageOps,
+ packageRepository = FakePackageRepository(packageInfos, packageAttributions),
+ attributionLabelFix = true,
+ )
+ val permissionTimelineUsages = getResult(underTest, this)
+
+ Truth.assertThat(permissionTimelineUsages.size).isEqualTo(1)
+ }
+
+ @Test
+ fun verifyAccessIsGroupedByAttributionLabelAndClustered() = runTest {
+ whenever(LocationUtils.isLocationProvider(Mockito.any(), Mockito.any())).thenReturn(true)
val appOpEvents =
listOf(
// These 3 entries should be grouped and clustered.
@@ -722,38 +773,38 @@ class GetPermissionGroupUsageDetailsUseCaseTest {
AppOpsManager.OPSTR_COARSE_LOCATION,
MINUTES.toMillis(1),
-1,
- "tag1"
+ "tag1",
),
DiscreteOpModel(
AppOpsManager.OPSTR_COARSE_LOCATION,
MINUTES.toMillis(2),
-1,
- "tag1"
+ "tag1",
),
DiscreteOpModel(
AppOpsManager.OPSTR_COARSE_LOCATION,
MINUTES.toMillis(3),
-1,
- "tag3"
+ "tag3",
),
// The access at minute 4 should not be grouped or clustered.
DiscreteOpModel(
AppOpsManager.OPSTR_COARSE_LOCATION,
MINUTES.toMillis(4),
-1,
- "tag2"
+ "tag2",
),
DiscreteOpModel(
AppOpsManager.OPSTR_COARSE_LOCATION,
MINUTES.toMillis(8),
-1,
- "tag2"
+ "tag2",
),
)
val discretePackageOps = flow {
emit(
listOf(
- DiscretePackageOpsModel(testPackageName, currentUser.identifier, appOpEvents),
+ DiscretePackageOpsModel(testPackageName, currentUser.identifier, appOpEvents)
)
)
}
@@ -766,14 +817,14 @@ class GetPermissionGroupUsageDetailsUseCaseTest {
testPackageName,
true,
attributionTagToLabelRes,
- attributionsMap
+ attributionsMap,
)
val underTest =
getPermissionGroupUsageDetailsUseCase(
LOCATION_PERMISSION_GROUP,
discretePackageOps,
- packageRepository = FakePackageRepository(packageInfos, packageAttributions)
+ packageRepository = FakePackageRepository(packageInfos, packageAttributions),
)
val permissionTimelineUsages = getResult(underTest, this)
@@ -794,6 +845,7 @@ class GetPermissionGroupUsageDetailsUseCaseTest {
}
@Test
+ @Ignore("b/365004787")
@RequiresFlagsEnabled(Flags.FLAG_LOCATION_BYPASS_PRIVACY_DASHBOARD_ENABLED)
fun emergencyAccessesAreNotClusteredWithRegularAccesses() = runTest {
Assume.assumeTrue(SdkLevel.isAtLeastV())
@@ -805,7 +857,7 @@ class GetPermissionGroupUsageDetailsUseCaseTest {
val discretePackageOps = flow {
emit(
listOf(
- DiscretePackageOpsModel(testPackageName, currentUser.identifier, appOpEvents),
+ DiscretePackageOpsModel(testPackageName, currentUser.identifier, appOpEvents)
)
)
}
@@ -829,7 +881,7 @@ class GetPermissionGroupUsageDetailsUseCaseTest {
val discretePackageOps = flow {
emit(
listOf(
- DiscretePackageOpsModel(testPackageName, currentUser.identifier, appOpEvents),
+ DiscretePackageOpsModel(testPackageName, currentUser.identifier, appOpEvents)
)
)
}
@@ -843,7 +895,7 @@ class GetPermissionGroupUsageDetailsUseCaseTest {
private fun TestScope.getResult(
useCase: GetPermissionGroupUsageDetailsUseCase,
- coroutineScope: CoroutineScope
+ coroutineScope: CoroutineScope,
): List<PermissionTimelineUsageModel> {
val usages by collectLastValue(useCase(coroutineScope))
return (usages as PermissionTimelineUsageModelWrapper.Success).timelineUsageModels
@@ -854,7 +906,8 @@ class GetPermissionGroupUsageDetailsUseCaseTest {
discreteUsageFlow: Flow<List<DiscretePackageOpsModel>>,
permissionFlags: Map<String, Int> = emptyMap(),
userRepository: UserRepository = FakeUserRepository(listOf(currentUser.identifier)),
- packageRepository: PackageRepository = FakePackageRepository(packageInfos)
+ packageRepository: PackageRepository = FakePackageRepository(packageInfos),
+ attributionLabelFix: Boolean = false,
): GetPermissionGroupUsageDetailsUseCase {
val permissionRepository = FakePermissionRepository(permissionFlags)
val appOpUsageRepository = FakeAppOpRepository(emptyFlow(), discreteUsageFlow)
@@ -865,7 +918,8 @@ class GetPermissionGroupUsageDetailsUseCaseTest {
permissionRepository,
appOpUsageRepository,
roleRepository,
- userRepository
+ userRepository,
+ attributionLabelFix,
)
}
@@ -877,7 +931,7 @@ class GetPermissionGroupUsageDetailsUseCaseTest {
listOf(
PackageInfo.REQUESTED_PERMISSION_GRANTED,
PackageInfo.REQUESTED_PERMISSION_GRANTED,
- PackageInfo.REQUESTED_PERMISSION_GRANTED
+ PackageInfo.REQUESTED_PERMISSION_GRANTED,
),
applicationFlags: Int = 0,
) = PackageInfoModel(packageName, requestedPermissions, permissionsFlags, applicationFlags)
diff --git a/PermissionController/tests/mocking/src/com/android/permissioncontroller/tests/mocking/permission/ui/model/PermissionUsageDetailsViewModelTest.kt b/PermissionController/tests/mocking/src/com/android/permissioncontroller/tests/mocking/permission/ui/model/PermissionUsageDetailsViewModelTest.kt
index 31e81e588..edaea9aba 100644
--- a/PermissionController/tests/mocking/src/com/android/permissioncontroller/tests/mocking/permission/ui/model/PermissionUsageDetailsViewModelTest.kt
+++ b/PermissionController/tests/mocking/src/com/android/permissioncontroller/tests/mocking/permission/ui/model/PermissionUsageDetailsViewModelTest.kt
@@ -32,6 +32,7 @@ import com.android.permissioncontroller.appops.data.model.v31.DiscretePackageOps
import com.android.permissioncontroller.permission.domain.usecase.v31.GetPermissionGroupUsageDetailsUseCase
import com.android.permissioncontroller.permission.ui.model.v31.PermissionUsageDetailsViewModel.PermissionUsageDetailsUiState
import com.android.permissioncontroller.permission.ui.model.v31.PermissionUsageDetailsViewModelV2
+import com.android.permissioncontroller.permission.utils.LocationUtils
import com.android.permissioncontroller.permission.utils.StringUtils
import com.android.permissioncontroller.permission.utils.Utils
import com.android.permissioncontroller.pm.data.model.v31.PackageInfoModel
@@ -93,6 +94,7 @@ class PermissionUsageDetailsViewModelTest {
.mockStatic(DeviceUtils::class.java)
.mockStatic(StringUtils::class.java)
.mockStatic(Flags::class.java)
+ .mockStatic(LocationUtils::class.java)
.strictness(Strictness.LENIENT)
.startMocking()
@@ -109,6 +111,7 @@ class PermissionUsageDetailsViewModelTest {
)
)
.thenReturn("Duration Summary")
+ whenever(LocationUtils.isLocationProvider(any(), any())).thenReturn(false)
packageInfos =
mapOf(
@@ -500,7 +503,8 @@ class PermissionUsageDetailsViewModelTest {
permissionRepository,
appOpUsageRepository,
roleRepository,
- userRepository
+ userRepository,
+ false,
)
}
diff --git a/PermissionController/tests/mocking/src/com/android/permissioncontroller/tests/mocking/role/model/RoleParserTest.kt b/PermissionController/tests/mocking/src/com/android/permissioncontroller/tests/mocking/role/model/RoleParserTest.kt
index ec6ab4eff..da1430c0a 100644
--- a/PermissionController/tests/mocking/src/com/android/permissioncontroller/tests/mocking/role/model/RoleParserTest.kt
+++ b/PermissionController/tests/mocking/src/com/android/permissioncontroller/tests/mocking/role/model/RoleParserTest.kt
@@ -16,9 +16,17 @@
package com.android.permissioncontroller.tests.mocking.role.model
+import android.app.AppOpsManager
+import android.content.pm.PackageManager
+import android.content.pm.PermissionInfo
+import android.os.Process
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import com.android.permissioncontroller.role.model.RoleParserInitializer
+import com.android.role.controller.model.AppOp
+import com.android.role.controller.model.Permission
+import com.android.role.controller.model.PermissionSet
+import com.android.role.controller.model.Role
import com.android.role.controller.model.RoleParser
import org.junit.BeforeClass
import org.junit.Test
@@ -37,9 +45,10 @@ class RoleParserTest {
private val instrumentation = InstrumentationRegistry.getInstrumentation()
private val uiAutomation = instrumentation.uiAutomation
private val targetContext = instrumentation.targetContext
+ private val packageManager = targetContext.packageManager
@Test
- fun testParseRolesWithValidation() {
+ fun parseRoles() {
// We may need to call privileged APIs to determine availability of things.
uiAutomation.adoptShellPermissionIdentity()
try {
@@ -48,4 +57,129 @@ class RoleParserTest {
uiAutomation.dropShellPermissionIdentity()
}
}
+
+ @Test
+ fun validateRoles() {
+ // We may need to call privileged APIs to determine availability of things.
+ uiAutomation.adoptShellPermissionIdentity()
+ try {
+ val xml = RoleParser(targetContext).parseRolesXml()
+ requireNotNull(xml)
+ validateRoles(xml.first, xml.second)
+ } finally {
+ uiAutomation.dropShellPermissionIdentity()
+ }
+ }
+
+ fun validateRoles(permissionSets: Map<String, PermissionSet>, roles: Map<String, Role>) {
+ for (permissionSet in permissionSets.values) {
+ for (permission in permissionSet.permissions) {
+ validatePermission(permission)
+ }
+ }
+
+ for (role in roles.values) {
+ if (!role.isAvailableByFeatureFlagAndSdkVersion) {
+ continue
+ }
+
+ for (requiredComponent in role.requiredComponents) {
+ val permission = requiredComponent.permission
+ if (permission != null) {
+ validatePermission(permission)
+ }
+ }
+
+ for (permission in role.permissions) {
+ validatePermission(permission)
+ }
+
+ for (appOp in role.appOps) {
+ validateAppOp(appOp)
+ }
+
+ for (appOpPermission in role.appOpPermissions) {
+ validateAppOpPermission(appOpPermission)
+ }
+
+ for (preferredActivity in role.preferredActivities) {
+ require(preferredActivity.activity in role.requiredComponents) {
+ "<activity> of <preferred-activity> not required in <required-components>," +
+ " role: ${role.name}, preferred activity: $preferredActivity"
+ }
+ }
+ }
+ }
+
+ private fun validatePermission(permission: Permission) {
+ if (!permission.isAvailableAsUser(Process.myUserHandle(), targetContext)) {
+ return
+ }
+ validatePermission(permission.name, true)
+ }
+
+ private fun validatePermission(permissionName: String) {
+ validatePermission(permissionName, false)
+ }
+
+ private fun validatePermission(permissionName: String, enforceIsRuntimeOrRole: Boolean) {
+ val isAutomotive = packageManager.hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE)
+ // Skip validation for car permissions which may not be available on all build targets.
+ if (!isAutomotive && permissionName.startsWith("android.car")) {
+ return
+ }
+
+ val permissionInfo =
+ try {
+ packageManager.getPermissionInfo(permissionName, 0)
+ } catch (e: PackageManager.NameNotFoundException) {
+ throw IllegalArgumentException("Unknown permission: $permissionName", e)
+ }
+
+ if (enforceIsRuntimeOrRole) {
+ require(
+ permissionInfo.protection == PermissionInfo.PROTECTION_DANGEROUS ||
+ permissionInfo.protectionFlags and PermissionInfo.PROTECTION_FLAG_ROLE ==
+ PermissionInfo.PROTECTION_FLAG_ROLE
+ ) {
+ "Permission is not a runtime or role permission: $permissionName"
+ }
+ }
+ }
+
+ private fun validateAppOpPermission(appOpPermission: Permission) {
+ if (!appOpPermission.isAvailableAsUser(Process.myUserHandle(), targetContext)) {
+ return
+ }
+ validateAppOpPermission(appOpPermission.name)
+ }
+
+ private fun validateAppOpPermission(permissionName: String) {
+ val permissionInfo =
+ try {
+ packageManager.getPermissionInfo(permissionName, 0)
+ } catch (e: PackageManager.NameNotFoundException) {
+ throw IllegalArgumentException("Unknown app op permission: $permissionName", e)
+ }
+ require(
+ permissionInfo.protectionFlags and PermissionInfo.PROTECTION_FLAG_APPOP ==
+ PermissionInfo.PROTECTION_FLAG_APPOP
+ ) {
+ "Permission is not an app op permission: $permissionName"
+ }
+ }
+
+ private fun validateAppOp(appOp: AppOp) {
+ if (!appOp.isAvailableByFeatureFlagAndSdkVersion) {
+ return
+ }
+ // This throws IllegalArgumentException if app op is unknown.
+ val permissionName = AppOpsManager.opToPermission(appOp.name)
+ if (permissionName != null) {
+ val permissionInfo = packageManager.getPermissionInfo(permissionName, 0)
+ require(permissionInfo.protection != PermissionInfo.PROTECTION_DANGEROUS) {
+ "App op has an associated runtime permission: ${appOp.name}"
+ }
+ }
+ }
}
diff --git a/PermissionController/tests/permissionui/src/com/android/permissioncontroller/permissionui/ui/HealthConnectAllAppPermissionFragmentTest.kt b/PermissionController/tests/permissionui/src/com/android/permissioncontroller/permissionui/ui/HealthConnectAllAppPermissionFragmentTest.kt
index 0f4f3841a..ecc7e161f 100644
--- a/PermissionController/tests/permissionui/src/com/android/permissioncontroller/permissionui/ui/HealthConnectAllAppPermissionFragmentTest.kt
+++ b/PermissionController/tests/permissionui/src/com/android/permissioncontroller/permissionui/ui/HealthConnectAllAppPermissionFragmentTest.kt
@@ -90,26 +90,6 @@ class HealthConnectAllAppPermissionFragmentTest : BasePermissionUiTest() {
}
}
- @Test
- fun invalidGrantedUsedHealthConnectPermissionsAreListed() {
- installInvalidTestAppThatUsesHealthConnectPermission()
- grantTestAppPermission(HEALTH_CONNECT_PERMISSION_READ_FLOORS_CLIMBED)
-
- startManageAppPermissionsActivity()
-
- // Ensure that Health Connect permission group permissions are present if a single one is
- // already granted, regardless of whether the intent filters are incorrectly or not setup
- // for the app
- eventually {
- waitFindObject(By.text(HEALTH_CONNECT_LABEL))
- waitFindObject(By.text(HEALTH_CONNECT_PERMISSION_READ_FLOORS_CLIMBED_LABEL))
-
- // READ_STEPS is not granted, but should still be present due to READ_FLOORS_CLIMBED
- // being granted
- waitFindObject(By.text(HEALTH_CONNECT_PERMISSION_READ_STEPS_LABEL))
- }
- }
-
private fun startManageAppPermissionsActivity() {
uiDevice.performActionAndWait(
{
diff --git a/PermissionController/tests/permissionui/src/com/android/permissioncontroller/permissionui/ui/HealthConnectAppPermissionFragmentTest.kt b/PermissionController/tests/permissionui/src/com/android/permissioncontroller/permissionui/ui/HealthConnectAppPermissionFragmentTest.kt
index 04dbac39f..b2d47a7d7 100644
--- a/PermissionController/tests/permissionui/src/com/android/permissioncontroller/permissionui/ui/HealthConnectAppPermissionFragmentTest.kt
+++ b/PermissionController/tests/permissionui/src/com/android/permissioncontroller/permissionui/ui/HealthConnectAppPermissionFragmentTest.kt
@@ -70,16 +70,6 @@ class HealthConnectAppPermissionFragmentTest : BasePermissionUiTest() {
waitUntilObjectGone(By.text(HEALTH_CONNECT_LABEL), TIMEOUT_SHORT)
}
- @Test
- fun invalidGrantedUsedHealthConnectPermissionsAreListed() {
- installInvalidTestAppThatUsesHealthConnectPermission()
- grantTestAppPermission(HEALTH_CONNECT_PERMISSION_READ_FLOORS_CLIMBED)
-
- startManageAppPermissionsActivity()
-
- eventually { waitFindObject(By.text(HEALTH_CONNECT_LABEL)) }
- }
-
private fun startManageAppPermissionsActivity() {
runWithShellPermissionIdentity {
instrumentationContext.startActivity(
diff --git a/PermissionController/tests/permissionui/src/com/android/permissioncontroller/permissionui/ui/handheld/ManageCustomPermissionsFragmentTest.kt b/PermissionController/tests/permissionui/src/com/android/permissioncontroller/permissionui/ui/handheld/ManageCustomPermissionsFragmentTest.kt
index eb7be564b..b38f5f40a 100644
--- a/PermissionController/tests/permissionui/src/com/android/permissioncontroller/permissionui/ui/handheld/ManageCustomPermissionsFragmentTest.kt
+++ b/PermissionController/tests/permissionui/src/com/android/permissioncontroller/permissionui/ui/handheld/ManageCustomPermissionsFragmentTest.kt
@@ -26,10 +26,12 @@ import androidx.test.uiautomator.By
import com.android.compatibility.common.util.SystemUtil.eventually
import com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity
import com.android.compatibility.common.util.UiAutomatorUtils2.waitFindObject
+import com.android.compatibility.common.util.UiAutomatorUtils2.waitFindObjectOrNull
import com.android.permissioncontroller.permissionui.getUsageCountsFromUi
import com.android.permissioncontroller.permissionui.wakeUpScreen
import com.google.common.truth.Truth.assertThat
import org.junit.After
+import org.junit.Assert.assertNotNull
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@@ -47,6 +49,7 @@ class ManageCustomPermissionsFragmentTest : BaseHandheldPermissionUiTest() {
private val PERM_LABEL = "Permission A"
private val PERM = "com.android.permissioncontroller.tests.A"
private val ADDITIONAL_PERMISSIONS_LABEL = "Additional permissions"
+ private val BODY_SENSORS_LABEL = "Body sensors"
@Before
fun setup() {
@@ -92,6 +95,14 @@ class ManageCustomPermissionsFragmentTest : BaseHandheldPermissionUiTest() {
eventually { assertThat(getUsageCountsFromUi(PERM_LABEL)).isEqualTo(original) }
}
+ @Test
+ fun bodySensorsEitherDisplayedInMainPageOrInAdditional() {
+ if (waitFindObjectOrNull(By.textContains(BODY_SENSORS_LABEL)) == null) {
+ waitFindObject(By.textContains(ADDITIONAL_PERMISSIONS_LABEL)).click()
+ assertNotNull(waitFindObjectOrNull(By.textContains(BODY_SENSORS_LABEL)))
+ }
+ }
+
@After
fun tearDown() {
uninstallApp(DEFINER_PKG)
diff --git a/PermissionController/tests/permissionui/src/com/android/permissioncontroller/permissionui/ui/handheld/ManageStandardPermissionsFragmentTest.kt b/PermissionController/tests/permissionui/src/com/android/permissioncontroller/permissionui/ui/handheld/ManageStandardPermissionsFragmentTest.kt
index 1ad876245..fcce09450 100644
--- a/PermissionController/tests/permissionui/src/com/android/permissioncontroller/permissionui/ui/handheld/ManageStandardPermissionsFragmentTest.kt
+++ b/PermissionController/tests/permissionui/src/com/android/permissioncontroller/permissionui/ui/handheld/ManageStandardPermissionsFragmentTest.kt
@@ -23,6 +23,8 @@ import android.permission.cts.PermissionUtils.grantPermission
import android.permission.cts.PermissionUtils.install
import android.permission.cts.PermissionUtils.revokePermission
import android.permission.cts.PermissionUtils.uninstallApp
+import android.platform.test.annotations.RequiresFlagsEnabled
+import android.platform.test.flag.junit.DeviceFlagsValueProvider
import android.util.Log
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.uiautomator.By
@@ -30,17 +32,23 @@ import com.android.compatibility.common.util.SystemUtil.eventually
import com.android.compatibility.common.util.SystemUtil.getEventually
import com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity
import com.android.compatibility.common.util.UiAutomatorUtils2.waitFindObjectOrNull
+import com.android.permission.flags.Flags
import com.android.permissioncontroller.permissionui.getUsageCountsFromUi
import com.android.permissioncontroller.permissionui.wakeUpScreen
import com.google.common.truth.Truth.assertThat
import org.junit.After
+import org.junit.Assert.assertNull
import org.junit.Before
+import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
/** Simple tests for {@link ManageStandardPermissionsFragment} */
@RunWith(AndroidJUnit4::class)
class ManageStandardPermissionsFragmentTest : BaseHandheldPermissionUiTest() {
+
+ @JvmField @Rule val checkFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule()
+
@Before
fun setup() {
wakeUpScreen()
@@ -117,7 +125,7 @@ class ManageStandardPermissionsFragmentTest : BaseHandheldPermissionUiTest() {
assertThat(afterInstall.granted).isEqualTo(original.granted)
assertThat(afterInstall.total).isEqualTo(original.total + 1)
},
- TIMEOUT
+ TIMEOUT,
)
}
@@ -127,13 +135,13 @@ class ManageStandardPermissionsFragmentTest : BaseHandheldPermissionUiTest() {
install(LOCATION_USER_APK)
eventually(
{ assertThat(getUsageCountsFromUi(LOCATION_GROUP_LABEL)).isNotEqualTo(original) },
- TIMEOUT
+ TIMEOUT,
)
uninstallApp(LOCATION_USER_PKG)
eventually(
{ assertThat(getUsageCountsFromUi(LOCATION_GROUP_LABEL)).isEqualTo(original) },
- TIMEOUT
+ TIMEOUT,
)
}
@@ -147,7 +155,7 @@ class ManageStandardPermissionsFragmentTest : BaseHandheldPermissionUiTest() {
assertThat(getUsageCountsFromUi(LOCATION_GROUP_LABEL).total)
.isEqualTo(original.total + 1)
},
- TIMEOUT
+ TIMEOUT,
)
grantPermission(LOCATION_USER_PKG, ACCESS_COARSE_LOCATION)
@@ -170,7 +178,7 @@ class ManageStandardPermissionsFragmentTest : BaseHandheldPermissionUiTest() {
assertThat(getUsageCountsFromUi(LOCATION_GROUP_LABEL).granted)
.isNotEqualTo(original.granted)
},
- TIMEOUT
+ TIMEOUT,
)
revokePermission(LOCATION_USER_PKG, ACCESS_COARSE_LOCATION)
@@ -190,7 +198,7 @@ class ManageStandardPermissionsFragmentTest : BaseHandheldPermissionUiTest() {
{
assertThat(getAdditionalPermissionCount()).isEqualTo(additionalPermissionBefore + 1)
},
- TIMEOUT
+ TIMEOUT,
)
}
@@ -202,13 +210,13 @@ class ManageStandardPermissionsFragmentTest : BaseHandheldPermissionUiTest() {
install(ADDITIONAL_USER_APK)
eventually(
{ assertThat(getAdditionalPermissionCount()).isNotEqualTo(additionalPermissionBefore) },
- TIMEOUT
+ TIMEOUT,
)
uninstallApp(ADDITIONAL_USER_PKG)
eventually(
{ assertThat(getAdditionalPermissionCount()).isEqualTo(additionalPermissionBefore) },
- TIMEOUT
+ TIMEOUT,
)
}
@@ -220,16 +228,23 @@ class ManageStandardPermissionsFragmentTest : BaseHandheldPermissionUiTest() {
install(ADDITIONAL_USER_APK)
eventually(
{ assertThat(getAdditionalPermissionCount()).isNotEqualTo(additionalPermissionBefore) },
- TIMEOUT
+ TIMEOUT,
)
uninstallApp(ADDITIONAL_DEFINER_PKG)
eventually(
{ assertThat(getAdditionalPermissionCount()).isEqualTo(additionalPermissionBefore) },
- TIMEOUT
+ TIMEOUT,
)
}
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_DECLUTTERED_PERMISSION_MANAGER_ENABLED)
+ fun noUnusedPermissionGroupDisplayedInTheMainPage() {
+ eventually { assertNull(waitFindObjectOrNull(By.hasChild(By.textStartsWith("0 of 0")))) }
+ eventually { assertNull(waitFindObjectOrNull(By.hasChild(By.textStartsWith("0/0")))) }
+ }
+
companion object {
private val LOG_TAG = ManageStandardPermissionsFragmentTest::class.java.simpleName
diff --git a/SafetyCenter/Resources/res/raw-v36/safety_center_config.xml b/SafetyCenter/Resources/res/raw-v36/safety_center_config.xml
new file mode 100644
index 000000000..de033ac44
--- /dev/null
+++ b/SafetyCenter/Resources/res/raw-v36/safety_center_config.xml
@@ -0,0 +1,158 @@
+<!--
+ ~ 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.
+ -->
+
+<safety-center-config>
+ <safety-sources-config>
+ <safety-sources-group
+ id="AndroidLockScreenSources"
+ title="@com.android.safetycenter.resources:string/lock_screen_sources_title"
+ summary="@com.android.safetycenter.resources:string/lock_screen_sources_summary">
+ <dynamic-safety-source
+ id="AndroidLockScreen"
+ packageName="com.android.settings"
+ profile="primary_profile_only"
+ title="@com.android.safetycenter.resources:string/lock_screen_title"
+ summary="@com.android.safetycenter.resources:string/lock_screen_summary_disabled"
+ searchTerms="@com.android.safetycenter.resources:string/lock_screen_search_terms"
+ initialDisplayState="disabled"
+ notificationsAllowed="true"/>
+ <dynamic-safety-source
+ id="AndroidBiometrics"
+ packageName="com.android.settings"
+ profile="all_profiles"
+ title="@com.android.safetycenter.resources:string/biometrics_title"
+ titleForWork="@com.android.safetycenter.resources:string/biometrics_title_for_work"
+ titleForPrivateProfile="@com.android.safetycenter.resources:string/biometrics_title_for_private_profile"
+ searchTerms="@com.android.safetycenter.resources:string/biometrics_search_terms"
+ initialDisplayState="hidden"/>
+ </safety-sources-group>
+ <safety-sources-group
+ id="AndroidCellularNetworkSecuritySources"
+ title="@com.android.safetycenter.resources:string/cellular_network_security_title"
+ summary="@com.android.safetycenter.resources:string/cellular_network_security_summary">
+ <dynamic-safety-source
+ id="AndroidCellularNetworkSecurity"
+ packageName="com.android.phone"
+ profile="primary_profile_only"
+ notificationsAllowed="true"
+ initialDisplayState="hidden"/>
+ </safety-sources-group>
+ <safety-sources-group
+ id="AndroidPrivacySources"
+ title="@com.android.safetycenter.resources:string/privacy_sources_title"
+ summary="@com.android.safetycenter.resources:string/privacy_sources_summary"
+ statelessIconType="privacy">
+ <static-safety-source
+ id="AndroidPermissionManager"
+ profile="primary_profile_only"
+ intentAction="android.intent.action.MANAGE_PERMISSIONS"
+ title="@com.android.safetycenter.resources:string/permission_manager_title"
+ summary="@com.android.safetycenter.resources:string/permission_manager_summary"
+ searchTerms="@com.android.safetycenter.resources:string/permission_manager_search_terms"/>
+ <dynamic-safety-source
+ id="AndroidHealthConnect"
+ profile="primary_profile_only"
+ packageName="com.android.healthconnect.controller"
+ initialDisplayState="hidden"
+ refreshOnPageOpenAllowed="false"
+ title="@com.android.safetycenter.resources:string/health_connect_title"
+ searchTerms="@com.android.safetycenter.resources:string/health_connect_search_terms"/>
+ <dynamic-safety-source
+ id="AndroidPrivacyAppDataSharingUpdates"
+ packageName="com.android.permissioncontroller"
+ profile="primary_profile_only"
+ initialDisplayState="hidden"
+ refreshOnPageOpenAllowed="true"
+ title="@com.android.safetycenter.resources:string/app_data_sharing_updates_title"
+ searchTerms="@com.android.safetycenter.resources:string/app_data_sharing_updates_search_terms"/>
+ <static-safety-source
+ id="AndroidPrivacyControls"
+ profile="primary_profile_only"
+ intentAction="android.settings.PRIVACY_CONTROLS"
+ title="@com.android.safetycenter.resources:string/privacy_controls_title"
+ summary="@com.android.safetycenter.resources:string/privacy_controls_summary"
+ searchTerms="@com.android.safetycenter.resources:string/privacy_controls_search_terms"/>
+ <issue-only-safety-source
+ id="AndroidAccessibility"
+ packageName="com.android.permissioncontroller"
+ profile="all_profiles"
+ notificationsAllowed="true"
+ refreshOnPageOpenAllowed="true"/>
+ <issue-only-safety-source
+ id="AndroidNotificationListener"
+ packageName="com.android.permissioncontroller"
+ profile="primary_profile_only"
+ notificationsAllowed="true"
+ refreshOnPageOpenAllowed="true"/>
+ <issue-only-safety-source
+ id="AndroidBackgroundLocation"
+ packageName="com.android.permissioncontroller"
+ profile="all_profiles"
+ notificationsAllowed="true"
+ refreshOnPageOpenAllowed="true"/>
+ <issue-only-safety-source
+ id="AndroidPermissionAutoRevoke"
+ packageName="com.android.permissioncontroller"
+ profile="all_profiles"
+ notificationsAllowed="true"
+ refreshOnPageOpenAllowed="true"/>
+ <issue-only-safety-source
+ id="AndroidCertificateTransparency"
+ packageName="android"
+ profile="primary_profile_only"
+ notificationsAllowed="false"
+ refreshOnPageOpenAllowed="true"/>
+ </safety-sources-group>
+ <safety-sources-group
+ id="AndroidPrivacySourcesAdditional"
+ title="@com.android.safetycenter.resources:string/privacy_additional_title">
+ <static-safety-source
+ id="AndroidPermissionUsage"
+ profile="primary_profile_only"
+ intentAction="android.intent.action.REVIEW_PERMISSION_USAGE"
+ title="@com.android.safetycenter.resources:string/permission_usage_title"
+ summary="@com.android.safetycenter.resources:string/permission_usage_summary"
+ searchTerms="@com.android.safetycenter.resources:string/permission_usage_search_terms"/>
+ <dynamic-safety-source
+ id="AndroidPrivateSpace"
+ packageName="com.android.settings"
+ profile="primary_profile_only"
+ title="@com.android.safetycenter.resources:string/private_space_title"
+ summary="@com.android.safetycenter.resources:string/private_space_summary"
+ searchTerms="@com.android.safetycenter.resources:string/private_space_search_terms"
+ initialDisplayState="hidden"
+ maxSeverityLevel="0"/>
+ </safety-sources-group>
+ <safety-sources-group
+ id="AndroidAdvancedSources"
+ title="@com.android.safetycenter.resources:string/advanced_title">
+ <dynamic-safety-source
+ id="AndroidWorkPolicyInfo"
+ packageName="com.android.permissioncontroller"
+ profile="primary_profile_only"
+ title="@com.android.safetycenter.resources:string/work_policy_title"
+ initialDisplayState="hidden"
+ refreshOnPageOpenAllowed="true"/>
+ <static-safety-source
+ id="AndroidMoreSettings"
+ profile="primary_profile_only"
+ intentAction="com.android.settings.MORE_SECURITY_PRIVACY_SETTINGS"
+ title="@com.android.safetycenter.resources:string/more_settings_title"
+ summary="@com.android.safetycenter.resources:string/more_settings_summary"
+ searchTerms="@com.android.safetycenter.resources:string/more_settings_search_terms"/>
+ </safety-sources-group>
+ </safety-sources-config>
+</safety-center-config>
diff --git a/SafetyCenter/Resources/res/values-fa-v35/strings.xml b/SafetyCenter/Resources/res/values-fa-v35/strings.xml
index 878c10bb4..40072433d 100644
--- a/SafetyCenter/Resources/res/values-fa-v35/strings.xml
+++ b/SafetyCenter/Resources/res/values-fa-v35/strings.xml
@@ -21,7 +21,7 @@
<string name="cellular_network_security_summary" msgid="7319307247487475572">"نوع شبکه، رمزگذاری، کنترل‌های اعلان"</string>
<string name="biometrics_title_for_private_profile" msgid="542819107383037820"></string>
<string name="privacy_title" msgid="7047524783080782769">"حریم خصوصی"</string>
- <string name="privacy_sources_title" msgid="309304028326660956">"تنظیمات حریم خصوصی"</string>
+ <string name="privacy_sources_title" msgid="309304028326660956">"کنترل‌های حریم خصوصی"</string>
<string name="privacy_sources_summary" msgid="2165270848857537278">"اجازه‌ها، کنترل‌ها"</string>
<string name="privacy_additional_title" msgid="4239060639056083649"></string>
<string name="private_space_title" msgid="6158245041481535879">"فضای خصوصی"</string>
diff --git a/SafetyCenter/Resources/res/values-fa/strings.xml b/SafetyCenter/Resources/res/values-fa/strings.xml
index bdf2063f1..01835a8e5 100644
--- a/SafetyCenter/Resources/res/values-fa/strings.xml
+++ b/SafetyCenter/Resources/res/values-fa/strings.xml
@@ -33,9 +33,9 @@
<string name="permission_manager_title" msgid="5277347862821255015">"مدیر اجازه‌ها"</string>
<string name="permission_manager_summary" msgid="8099852107340970790">"کنترل دسترسی برنامه به داده‌های شما"</string>
<string name="permission_manager_search_terms" msgid="2895147613099694722">"اجازه‌ها، مدیر اجازه‌ها"</string>
- <string name="privacy_controls_title" msgid="5322875777945432395">"تنظیمات حریم خصوصی"</string>
+ <string name="privacy_controls_title" msgid="5322875777945432395">"کنترل‌های حریم خصوصی"</string>
<string name="privacy_controls_summary" msgid="2402066941190435424">"کنترل دسترسی دستگاه به میکروفون، دوربین، و غیره"</string>
- <string name="privacy_controls_search_terms" msgid="3774472175934304165">"حریم خصوصی، تنظیمات حریم خصوصی"</string>
+ <string name="privacy_controls_search_terms" msgid="3774472175934304165">"حریم خصوصی، کنترل‌های حریم خصوصی"</string>
<string name="advanced_title" msgid="8745436380690561172">"تنظیمات بیشتر"</string>
<string name="advanced_security_title" msgid="1126833338772188155">"تنظیمات ایمنی بیشتر"</string>
<string name="advanced_security_summary" msgid="6172253327022425123">"رمزگذاری، اطلاعات اعتباری، و غیره"</string>
diff --git a/SafetyCenter/Resources/res/values-hy-v35/strings.xml b/SafetyCenter/Resources/res/values-hy-v35/strings.xml
index dd5c9e01b..d5d9f39c4 100644
--- a/SafetyCenter/Resources/res/values-hy-v35/strings.xml
+++ b/SafetyCenter/Resources/res/values-hy-v35/strings.xml
@@ -24,7 +24,7 @@
<string name="privacy_sources_title" msgid="309304028326660956">"Գաղտնիության կարգավորումներ"</string>
<string name="privacy_sources_summary" msgid="2165270848857537278">"Թույլտվություններ, կարգավորումներ"</string>
<string name="privacy_additional_title" msgid="4239060639056083649"></string>
- <string name="private_space_title" msgid="6158245041481535879">"Անձնական տարածք"</string>
+ <string name="private_space_title" msgid="6158245041481535879">"Մասնավոր տարածք"</string>
<string name="private_space_summary" msgid="529869826714610294">"Կարգավորեք մասնավոր տարածքը և ավելին"</string>
- <string name="private_space_search_terms" msgid="4820808478299116258">"Անձնական տարածք"</string>
+ <string name="private_space_search_terms" msgid="4820808478299116258">"Մասնավոր տարածք"</string>
</resources>
diff --git a/SafetyCenter/Resources/res/values-ja/strings.xml b/SafetyCenter/Resources/res/values-ja/strings.xml
index b31b1690a..6e67998f4 100644
--- a/SafetyCenter/Resources/res/values-ja/strings.xml
+++ b/SafetyCenter/Resources/res/values-ja/strings.xml
@@ -30,9 +30,9 @@
<string name="permission_usage_title" msgid="3633779688945350407">"プライバシー ダッシュボード"</string>
<string name="permission_usage_summary" msgid="5323079206029964468">"権限を最近使用したアプリが表示されます"</string>
<string name="permission_usage_search_terms" msgid="3852343592870257104">"プライバシー, プライバシー ダッシュボード"</string>
- <string name="permission_manager_title" msgid="5277347862821255015">"権限マネージャ"</string>
+ <string name="permission_manager_title" msgid="5277347862821255015">"権限マネージャー"</string>
<string name="permission_manager_summary" msgid="8099852107340970790">"アプリのデータアクセスを管理します"</string>
- <string name="permission_manager_search_terms" msgid="2895147613099694722">"権限, 権限マネージャ"</string>
+ <string name="permission_manager_search_terms" msgid="2895147613099694722">"権限, 権限マネージャー"</string>
<string name="privacy_controls_title" msgid="5322875777945432395">"プライバシー管理"</string>
<string name="privacy_controls_summary" msgid="2402066941190435424">"マイク、カメラなどへのデバイス アクセスを管理します"</string>
<string name="privacy_controls_search_terms" msgid="3774472175934304165">"プライバシー, プライバシー管理"</string>
diff --git a/SafetyCenter/Resources/res/values-ka-v35/strings.xml b/SafetyCenter/Resources/res/values-ka-v35/strings.xml
index d5a444deb..6d434bca0 100644
--- a/SafetyCenter/Resources/res/values-ka-v35/strings.xml
+++ b/SafetyCenter/Resources/res/values-ka-v35/strings.xml
@@ -24,7 +24,7 @@
<string name="privacy_sources_title" msgid="309304028326660956">"კონფიდენციალურობის მართვის პარამეტრები"</string>
<string name="privacy_sources_summary" msgid="2165270848857537278">"ნებართვები, მართვის საშუალებები"</string>
<string name="privacy_additional_title" msgid="4239060639056083649"></string>
- <string name="private_space_title" msgid="6158245041481535879">"პირადი სივრცე"</string>
+ <string name="private_space_title" msgid="6158245041481535879">"კერძო სივრცე"</string>
<string name="private_space_summary" msgid="529869826714610294">"დააყენეთ პირადი სივრცე და ა.შ."</string>
- <string name="private_space_search_terms" msgid="4820808478299116258">"პირადი სივრცე"</string>
+ <string name="private_space_search_terms" msgid="4820808478299116258">"კერძო სივრცე"</string>
</resources>
diff --git a/SafetyCenter/Resources/res/values-kn-v35/strings.xml b/SafetyCenter/Resources/res/values-kn-v35/strings.xml
index 339e522d9..c054b2fa1 100644
--- a/SafetyCenter/Resources/res/values-kn-v35/strings.xml
+++ b/SafetyCenter/Resources/res/values-kn-v35/strings.xml
@@ -24,7 +24,7 @@
<string name="privacy_sources_title" msgid="309304028326660956">"ಗೌಪ್ಯತೆ ನಿಯಂತ್ರಣಗಳು"</string>
<string name="privacy_sources_summary" msgid="2165270848857537278">"ಅನುಮತಿಗಳು, ನಿಯಂತ್ರಣಗಳು"</string>
<string name="privacy_additional_title" msgid="4239060639056083649"></string>
- <string name="private_space_title" msgid="6158245041481535879">"ಖಾಸಗಿ ಸ್ಪೇಸ್"</string>
+ <string name="private_space_title" msgid="6158245041481535879">"ಪ್ರೈವೆಟ್ ಸ್ಪೇಸ್"</string>
<string name="private_space_summary" msgid="529869826714610294">"ಖಾಸಗಿ ಸ್ಪೇಸ್ ಅನ್ನು ಸೆಟಪ್ ಮಾಡಿ ಹಾಗೂ ಇನ್ನಷ್ಟನ್ನು ಮಾಡಿ"</string>
- <string name="private_space_search_terms" msgid="4820808478299116258">"ಖಾಸಗಿ ಸ್ಪೇಸ್"</string>
+ <string name="private_space_search_terms" msgid="4820808478299116258">"ಪ್ರೈವೆಟ್ ಸ್ಪೇಸ್"</string>
</resources>
diff --git a/SafetyCenter/Resources/res/values-lv-v35/strings.xml b/SafetyCenter/Resources/res/values-lv-v35/strings.xml
index f26fcccc3..e3cf556bf 100644
--- a/SafetyCenter/Resources/res/values-lv-v35/strings.xml
+++ b/SafetyCenter/Resources/res/values-lv-v35/strings.xml
@@ -24,7 +24,7 @@
<string name="privacy_sources_title" msgid="309304028326660956">"Konfidencialitātes vadīklas"</string>
<string name="privacy_sources_summary" msgid="2165270848857537278">"Atļaujas, vadīklas"</string>
<string name="privacy_additional_title" msgid="4239060639056083649"></string>
- <string name="private_space_title" msgid="6158245041481535879">"Privātā mape"</string>
+ <string name="private_space_title" msgid="6158245041481535879">"Privātā telpa"</string>
<string name="private_space_summary" msgid="529869826714610294">"Privātās mapes iestatīšana un citas iespējas"</string>
- <string name="private_space_search_terms" msgid="4820808478299116258">"Privātā mape"</string>
+ <string name="private_space_search_terms" msgid="4820808478299116258">"Privātā telpa"</string>
</resources>
diff --git a/SafetyCenter/Resources/res/values-or-v35/strings.xml b/SafetyCenter/Resources/res/values-or-v35/strings.xml
index 5023efd4d..4a0ba87a5 100644
--- a/SafetyCenter/Resources/res/values-or-v35/strings.xml
+++ b/SafetyCenter/Resources/res/values-or-v35/strings.xml
@@ -21,7 +21,7 @@
<string name="cellular_network_security_summary" msgid="7319307247487475572">"ନେଟୱାର୍କ ପ୍ରକାର, ଏନକ୍ରିପସନ, ବିଜ୍ଞପ୍ତି ନିୟନ୍ତ୍ରଣଗୁଡ଼ିକ"</string>
<string name="biometrics_title_for_private_profile" msgid="542819107383037820"></string>
<string name="privacy_title" msgid="7047524783080782769">"ଗୋପନୀୟତା"</string>
- <string name="privacy_sources_title" msgid="309304028326660956">"ଗୋପନୀୟତା ନିୟନ୍ତ୍ରଣଗୁଡ଼ିକ"</string>
+ <string name="privacy_sources_title" msgid="309304028326660956">"ଗୋପନୀୟତା ନିୟନ୍ତ୍ରଣ"</string>
<string name="privacy_sources_summary" msgid="2165270848857537278">"ଅନୁମତି, ନିୟନ୍ତ୍ରଣଗୁଡ଼ିକ"</string>
<string name="privacy_additional_title" msgid="4239060639056083649"></string>
<string name="private_space_title" msgid="6158245041481535879">"ପ୍ରାଇଭେଟ ସ୍ପେସ"</string>
diff --git a/SafetyCenter/Resources/res/values-si-v35/strings.xml b/SafetyCenter/Resources/res/values-si-v35/strings.xml
index bcff52b2a..41025a3a4 100644
--- a/SafetyCenter/Resources/res/values-si-v35/strings.xml
+++ b/SafetyCenter/Resources/res/values-si-v35/strings.xml
@@ -24,7 +24,7 @@
<string name="privacy_sources_title" msgid="309304028326660956">"පෞද්ගලිකත්ව පාලන"</string>
<string name="privacy_sources_summary" msgid="2165270848857537278">"අවසර, පාලන"</string>
<string name="privacy_additional_title" msgid="4239060639056083649"></string>
- <string name="private_space_title" msgid="6158245041481535879">"පෞද්ගලික ඉඩ"</string>
+ <string name="private_space_title" msgid="6158245041481535879">"රහසිගත අවකාශය"</string>
<string name="private_space_summary" msgid="529869826714610294">"පෞද්ගලික ඉඩ, සහ තවත් දේ පිහිටුවන්න"</string>
- <string name="private_space_search_terms" msgid="4820808478299116258">"පෞද්ගලික ඉඩ"</string>
+ <string name="private_space_search_terms" msgid="4820808478299116258">"රහසිගත අවකාශය"</string>
</resources>
diff --git a/SafetyCenter/Resources/res/values-sv-v35/strings.xml b/SafetyCenter/Resources/res/values-sv-v35/strings.xml
index 77a6de01f..974916040 100644
--- a/SafetyCenter/Resources/res/values-sv-v35/strings.xml
+++ b/SafetyCenter/Resources/res/values-sv-v35/strings.xml
@@ -22,9 +22,9 @@
<string name="biometrics_title_for_private_profile" msgid="542819107383037820"></string>
<string name="privacy_title" msgid="7047524783080782769">"Integritet"</string>
<string name="privacy_sources_title" msgid="309304028326660956">"Integritetsinställningar"</string>
- <string name="privacy_sources_summary" msgid="2165270848857537278">"Behörigheter, inställningar"</string>
+ <string name="privacy_sources_summary" msgid="2165270848857537278">"Behörigheter och inställningar"</string>
<string name="privacy_additional_title" msgid="4239060639056083649"></string>
- <string name="private_space_title" msgid="6158245041481535879">"Privat rum"</string>
+ <string name="private_space_title" msgid="6158245041481535879">"Privat utrymme"</string>
<string name="private_space_summary" msgid="529869826714610294">"Ställ in privat rum med mera"</string>
- <string name="private_space_search_terms" msgid="4820808478299116258">"Privat rum"</string>
+ <string name="private_space_search_terms" msgid="4820808478299116258">"Privat utrymme"</string>
</resources>
diff --git a/SafetyCenter/Resources/res/values-tr-v35/strings.xml b/SafetyCenter/Resources/res/values-tr-v35/strings.xml
index 89a3a3e0c..038bd9680 100644
--- a/SafetyCenter/Resources/res/values-tr-v35/strings.xml
+++ b/SafetyCenter/Resources/res/values-tr-v35/strings.xml
@@ -24,7 +24,7 @@
<string name="privacy_sources_title" msgid="309304028326660956">"Gizlilik denetimleri"</string>
<string name="privacy_sources_summary" msgid="2165270848857537278">"İzinler, denetimler"</string>
<string name="privacy_additional_title" msgid="4239060639056083649"></string>
- <string name="private_space_title" msgid="6158245041481535879">"Gizli Alan"</string>
+ <string name="private_space_title" msgid="6158245041481535879">"Özel Alan"</string>
<string name="private_space_summary" msgid="529869826714610294">"Gizli alan yapılandırma ve daha fazlası"</string>
- <string name="private_space_search_terms" msgid="4820808478299116258">"Gizli Alan"</string>
+ <string name="private_space_search_terms" msgid="4820808478299116258">"Özel Alan"</string>
</resources>
diff --git a/SafetyCenter/Resources/res/values-ur-v35/strings.xml b/SafetyCenter/Resources/res/values-ur-v35/strings.xml
index d4184bda3..d21e73368 100644
--- a/SafetyCenter/Resources/res/values-ur-v35/strings.xml
+++ b/SafetyCenter/Resources/res/values-ur-v35/strings.xml
@@ -24,7 +24,7 @@
<string name="privacy_sources_title" msgid="309304028326660956">"رازداری سے متعلق کنٹرولز"</string>
<string name="privacy_sources_summary" msgid="2165270848857537278">"اجازتیں، کنٹرولز"</string>
<string name="privacy_additional_title" msgid="4239060639056083649"></string>
- <string name="private_space_title" msgid="6158245041481535879">"نجی اسپیس"</string>
+ <string name="private_space_title" msgid="6158245041481535879">"پرائیویٹ اسپیس"</string>
<string name="private_space_summary" msgid="529869826714610294">"نجی اسپیس اور بھی بہت کچھ سیٹ اپ کریں"</string>
- <string name="private_space_search_terms" msgid="4820808478299116258">"نجی اسپیس"</string>
+ <string name="private_space_search_terms" msgid="4820808478299116258">"پرائیویٹ اسپیس"</string>
</resources>
diff --git a/SafetyCenter/Resources/res/values-zh-rTW-v34/strings.xml b/SafetyCenter/Resources/res/values-zh-rTW-v34/strings.xml
index 54bbf0f1c..dc90b3fc3 100644
--- a/SafetyCenter/Resources/res/values-zh-rTW-v34/strings.xml
+++ b/SafetyCenter/Resources/res/values-zh-rTW-v34/strings.xml
@@ -18,7 +18,7 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="lock_screen_sources_title" msgid="5493678510117489865">"裝置解鎖"</string>
- <string name="biometrics_title_for_work" msgid="1842284049407771568">"工作應用程式的生物特徵辨識選項"</string>
+ <string name="biometrics_title_for_work" msgid="1842284049407771568">"工作應用程式的生物辨識選項"</string>
<string name="privacy_sources_summary" msgid="4083646673569677049">"權限、資訊主頁、控制選項"</string>
<string name="health_connect_title" msgid="8318152190040327804">"健康資料同步"</string>
<string name="health_connect_search_terms" msgid="4998970586245680829">"健康, 健康資料同步"</string>
diff --git a/SafetyCenter/Resources/res/values-zh-rTW/strings.xml b/SafetyCenter/Resources/res/values-zh-rTW/strings.xml
index 5c44eb33a..6e6e41a38 100644
--- a/SafetyCenter/Resources/res/values-zh-rTW/strings.xml
+++ b/SafetyCenter/Resources/res/values-zh-rTW/strings.xml
@@ -23,7 +23,7 @@
<string name="lock_screen_title" msgid="4069104894527169877">"螢幕鎖定"</string>
<string name="lock_screen_summary_disabled" msgid="354071230916616692">"目前還沒有任何資訊"</string>
<string name="lock_screen_search_terms" msgid="2678486357779794826">"裝置鎖定,螢幕鎖定,將螢幕鎖定,鎖定螢幕,密碼,PIN 碼,圖案"</string>
- <string name="biometrics_title" msgid="5859504610285212938">"生物特徵辨識"</string>
+ <string name="biometrics_title" msgid="5859504610285212938">"生物辨識"</string>
<string name="biometrics_search_terms" msgid="6040319118762671981">"指紋,手指,新增指紋,人臉解鎖,人臉"</string>
<string name="privacy_sources_title" msgid="4061110826457365957">"隱私權"</string>
<string name="privacy_sources_summary" msgid="4089719981155120864">"資訊主頁、權限、控制選項"</string>
diff --git a/flags/Android.bp b/flags/Android.bp
index aba1e44a9..d22da26c3 100644
--- a/flags/Android.bp
+++ b/flags/Android.bp
@@ -36,6 +36,7 @@ java_aconfig_library {
libs: ["framework-configinfrastructure.stubs.module_lib"],
visibility: [
"//packages/modules/Permission:__subpackages__",
+ "//vendor:__subpackages__",
],
apex_available: [
"com.android.permission",
diff --git a/flags/flags.aconfig b/flags/flags.aconfig
index c94614654..4cb084988 100644
--- a/flags/flags.aconfig
+++ b/flags/flags.aconfig
@@ -104,7 +104,52 @@ flag {
name: "app_permission_fragment_uses_preferences"
is_exported: true
namespace: "permissions"
- description: "This flag enables AppPermissionFragment rather than LegacyAppPermissionFragment (to support BC25)"
+ description: "This flag enables AppPermissionFragment rather than LegacyAppPermissionFragment"
bug: "349675008"
is_fixed_read_only: true
}
+
+flag {
+ name: "cross_user_role_enabled"
+ is_exported: true
+ namespace: "permissions"
+ description: "This flag enables cross-user roles support and API"
+ bug: "367732307"
+ is_fixed_read_only: true
+}
+
+flag {
+ name: "permission_timeline_attribution_label_fix"
+ is_exported: true
+ namespace: "permissions"
+ description: "This flag is used for the attribution label fix on permission timeline page"
+ bug: "369606734"
+ is_fixed_read_only: true
+}
+
+flag {
+ name: "wear_compose_material3"
+ is_exported: true
+ namespace: "permissions"
+ description: "This flag enables material3 design system for wear ui components"
+ bug: "370489422"
+ is_fixed_read_only: true
+}
+
+flag {
+ name: "decluttered_permission_manager_enabled"
+ is_exported: true
+ namespace: "permissions"
+ description: "Enables displaying unused permission groups in the additional page, instead of displaying them in the main permission manager page"
+ bug: "365823624"
+ is_fixed_read_only: true
+}
+
+flag {
+ name: "expressive_design_enabled"
+ is_exported: true
+ namespace: "permissions"
+ description: "This flag is used to enable Expressive Design for Settings pages inside PermissionController"
+ bug: "375480184"
+ is_fixed_read_only: true
+} \ No newline at end of file
diff --git a/framework-s/api/system-current.txt b/framework-s/api/system-current.txt
index e15887576..2f6bbf154 100644
--- a/framework-s/api/system-current.txt
+++ b/framework-s/api/system-current.txt
@@ -6,6 +6,7 @@ package android.app.ecm {
method @NonNull public android.content.Intent createRestrictedSettingDialogIntent(@NonNull String, @NonNull String) throws android.content.pm.PackageManager.NameNotFoundException;
method @RequiresPermission(android.Manifest.permission.MANAGE_ENHANCED_CONFIRMATION_STATES) public boolean isClearRestrictionAllowed(@NonNull String) throws android.content.pm.PackageManager.NameNotFoundException;
method @RequiresPermission(android.Manifest.permission.MANAGE_ENHANCED_CONFIRMATION_STATES) public boolean isRestricted(@NonNull String, @NonNull String) throws android.content.pm.PackageManager.NameNotFoundException;
+ method @FlaggedApi("android.permission.flags.enhanced_confirmation_in_call_apis_enabled") @RequiresPermission(android.Manifest.permission.MANAGE_ENHANCED_CONFIRMATION_STATES) public boolean isUnknownCallOngoing();
method @RequiresPermission(android.Manifest.permission.MANAGE_ENHANCED_CONFIRMATION_STATES) public void setClearRestrictionAllowed(@NonNull String) throws android.content.pm.PackageManager.NameNotFoundException;
field public static final String ACTION_SHOW_ECM_RESTRICTED_SETTING_DIALOG = "android.app.ecm.action.SHOW_ECM_RESTRICTED_SETTING_DIALOG";
}
@@ -37,6 +38,7 @@ package android.app.role {
method @RequiresPermission(android.Manifest.permission.MANAGE_ROLE_HOLDERS) public void addRoleHolderAsUser(@NonNull String, @NonNull String, int, @NonNull android.os.UserHandle, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Boolean>);
method @Deprecated @RequiresPermission("com.android.permissioncontroller.permission.MANAGE_ROLES_FROM_CONTROLLER") public boolean addRoleHolderFromController(@NonNull String, @NonNull String);
method @RequiresPermission(android.Manifest.permission.MANAGE_ROLE_HOLDERS) public void clearRoleHoldersAsUser(@NonNull String, int, @NonNull android.os.UserHandle, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Boolean>);
+ method @FlaggedApi("com.android.permission.flags.cross_user_role_enabled") @Nullable @RequiresPermission(allOf={android.Manifest.permission.INTERACT_ACROSS_USERS_FULL, android.Manifest.permission.MANAGE_ROLE_HOLDERS, android.Manifest.permission.MANAGE_DEFAULT_APPLICATIONS}, conditional=true) public android.os.UserHandle getActiveUserForRole(@NonNull String);
method @Nullable @RequiresPermission(android.Manifest.permission.MANAGE_DEFAULT_APPLICATIONS) public String getDefaultApplication(@NonNull String);
method @Deprecated @NonNull @RequiresPermission("com.android.permissioncontroller.permission.MANAGE_ROLES_FROM_CONTROLLER") public java.util.List<java.lang.String> getHeldRolesFromController(@NonNull String);
method @NonNull @RequiresPermission(android.Manifest.permission.MANAGE_ROLE_HOLDERS) public java.util.List<java.lang.String> getRoleHolders(@NonNull String);
@@ -48,6 +50,7 @@ package android.app.role {
method @RequiresPermission(android.Manifest.permission.OBSERVE_ROLE_HOLDERS) public void removeOnRoleHoldersChangedListenerAsUser(@NonNull android.app.role.OnRoleHoldersChangedListener, @NonNull android.os.UserHandle);
method @RequiresPermission(android.Manifest.permission.MANAGE_ROLE_HOLDERS) public void removeRoleHolderAsUser(@NonNull String, @NonNull String, int, @NonNull android.os.UserHandle, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Boolean>);
method @Deprecated @RequiresPermission("com.android.permissioncontroller.permission.MANAGE_ROLES_FROM_CONTROLLER") public boolean removeRoleHolderFromController(@NonNull String, @NonNull String);
+ method @FlaggedApi("com.android.permission.flags.cross_user_role_enabled") @RequiresPermission(allOf={android.Manifest.permission.INTERACT_ACROSS_USERS_FULL, android.Manifest.permission.MANAGE_ROLE_HOLDERS, android.Manifest.permission.MANAGE_DEFAULT_APPLICATIONS}, conditional=true) public void setActiveUserForRole(@NonNull String, @NonNull android.os.UserHandle, int);
method @RequiresPermission(android.Manifest.permission.BYPASS_ROLE_QUALIFICATION) public void setBypassingRoleQualification(boolean);
method @RequiresPermission(android.Manifest.permission.MANAGE_DEFAULT_APPLICATIONS) public void setDefaultApplication(@NonNull String, @Nullable String, int, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Boolean>);
method @FlaggedApi("android.permission.flags.system_server_role_controller_enabled") @RequiresPermission(android.Manifest.permission.MANAGE_ROLE_HOLDERS) public void setRoleFallbackEnabled(@NonNull String, boolean);
@@ -55,6 +58,7 @@ package android.app.role {
field public static final int MANAGE_HOLDERS_FLAG_DONT_KILL_APP = 1; // 0x1
field public static final String ROLE_DEVICE_POLICY_MANAGEMENT = "android.app.role.DEVICE_POLICY_MANAGEMENT";
field public static final String ROLE_FINANCED_DEVICE_KIOSK = "android.app.role.FINANCED_DEVICE_KIOSK";
+ field @FlaggedApi("com.android.permission.flags.cross_user_role_enabled") public static final String ROLE_RESERVED_FOR_TESTING_PROFILE_GROUP_EXCLUSIVITY = "android.app.role.RESERVED_FOR_TESTING_PROFILE_GROUP_EXCLUSIVITY";
field public static final String ROLE_SYSTEM_ACTIVITY_RECOGNIZER = "android.app.role.SYSTEM_ACTIVITY_RECOGNIZER";
field public static final String ROLE_SYSTEM_CALL_STREAMING = "android.app.role.SYSTEM_CALL_STREAMING";
field public static final String ROLE_SYSTEM_SUPERVISION = "android.app.role.SYSTEM_SUPERVISION";
diff --git a/framework-s/java/android/app/ecm/EnhancedConfirmationManager.java b/framework-s/java/android/app/ecm/EnhancedConfirmationManager.java
index 74062165e..8eaa9354d 100644
--- a/framework-s/java/android/app/ecm/EnhancedConfirmationManager.java
+++ b/framework-s/java/android/app/ecm/EnhancedConfirmationManager.java
@@ -20,6 +20,7 @@ import static android.annotation.SdkConstant.SdkConstantType.BROADCAST_INTENT_AC
import android.annotation.FlaggedApi;
import android.annotation.IntDef;
+import android.annotation.NonNull;
import android.annotation.RequiresPermission;
import android.annotation.SdkConstant;
import android.annotation.SystemApi;
@@ -35,8 +36,6 @@ import android.os.RemoteException;
import android.permission.flags.Flags;
import android.util.ArraySet;
-import androidx.annotation.NonNull;
-
import java.lang.annotation.Retention;
/**
@@ -329,6 +328,23 @@ public final class EnhancedConfirmationManager {
}
/**
+ * Returns whether the enhanced confirmation system thinks a call with an unknown party is
+ * occurring
+ *
+ * @hide
+ */
+ @SystemApi
+ @FlaggedApi(Flags.FLAG_ENHANCED_CONFIRMATION_IN_CALL_APIS_ENABLED)
+ @RequiresPermission(android.Manifest.permission.MANAGE_ENHANCED_CONFIRMATION_STATES)
+ public boolean isUnknownCallOngoing() {
+ try {
+ return mService.isUntrustedCallOngoing();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
* Gets an intent that will open the "Restricted setting" dialog for the specified package
* and setting.
*
diff --git a/framework-s/java/android/app/ecm/IEnhancedConfirmationManager.aidl b/framework-s/java/android/app/ecm/IEnhancedConfirmationManager.aidl
index 5149daa49..833485890 100644
--- a/framework-s/java/android/app/ecm/IEnhancedConfirmationManager.aidl
+++ b/framework-s/java/android/app/ecm/IEnhancedConfirmationManager.aidl
@@ -30,4 +30,7 @@ interface IEnhancedConfirmationManager {
boolean isClearRestrictionAllowed(in String packageName, int userId);
void setClearRestrictionAllowed(in String packageName, int userId);
+
+ boolean isUntrustedCallOngoing();
+
}
diff --git a/framework-s/java/android/app/role/IRoleManager.aidl b/framework-s/java/android/app/role/IRoleManager.aidl
index 522967630..cd0079e08 100644
--- a/framework-s/java/android/app/role/IRoleManager.aidl
+++ b/framework-s/java/android/app/role/IRoleManager.aidl
@@ -45,6 +45,10 @@ interface IRoleManager {
void setDefaultApplicationAsUser(in String roleName, in String packageName, int flags,
int userId, in RemoteCallback callback);
+ int getActiveUserForRoleAsUser(in String roleName, int userId);
+
+ void setActiveUserForRoleAsUser(in String roleName, int activeUserId, int flags, int userId);
+
void addOnRoleHoldersChangedListenerAsUser(IOnRoleHoldersChangedListener listener, int userId);
void removeOnRoleHoldersChangedListenerAsUser(IOnRoleHoldersChangedListener listener,
diff --git a/framework-s/java/android/app/role/RoleManager.java b/framework-s/java/android/app/role/RoleManager.java
index 4b8c9b388..6f62fdd76 100644
--- a/framework-s/java/android/app/role/RoleManager.java
+++ b/framework-s/java/android/app/role/RoleManager.java
@@ -40,6 +40,7 @@ import android.os.RemoteCallback;
import android.os.RemoteException;
import android.os.UserHandle;
import android.permission.flags.Flags;
+import android.permission.internal.compat.UserHandleCompat;
import android.util.ArrayMap;
import android.util.SparseArray;
@@ -211,6 +212,16 @@ public final class RoleManager {
"android.app.role.SYSTEM_CALL_STREAMING";
/**
+ * The name of the role used for testing cross-user roles.
+ *
+ * @hide
+ */
+ @FlaggedApi(com.android.permission.flags.Flags.FLAG_CROSS_USER_ROLE_ENABLED)
+ @SystemApi
+ public static final String ROLE_RESERVED_FOR_TESTING_PROFILE_GROUP_EXCLUSIVITY =
+ "android.app.role.RESERVED_FOR_TESTING_PROFILE_GROUP_EXCLUSIVITY";
+
+ /**
* @hide
*/
@IntDef(flag = true, value = { MANAGE_HOLDERS_FLAG_DONT_KILL_APP })
@@ -574,6 +585,87 @@ public final class RoleManager {
}
}
+ /**
+ * Get the {@link UserHandle} of the user who that is the active user for the specified role.
+ * <p>
+ * Only profile-group exclusive roles can be used with this method, and they will
+ * have one active user within a profile group.
+ * <p>
+ * <strong>Note:</strong> Using this API requires holding
+ * {@code android.permission.INTERACT_ACROSS_USERS_FULL} and one of
+ * {@code android.permission.MANAGE_ROLE_HOLDERS} or
+ * {@code android.permission.MANAGE_DEFAULT_APPLICATIONS}.
+ *
+ * @param roleName the name of the role to get the active user for
+ *
+ * @return a {@link UserHandle} of the active user for the specified role
+ *
+ * @see #setActiveUserForRole(String, UserHandle, int)
+ *
+ * @hide
+ */
+ @RequiresPermission(allOf = {Manifest.permission.INTERACT_ACROSS_USERS_FULL,
+ Manifest.permission.MANAGE_ROLE_HOLDERS,
+ Manifest.permission.MANAGE_DEFAULT_APPLICATIONS},
+ conditional = true)
+ @RequiresApi(Build.VERSION_CODES.BAKLAVA)
+ @SystemApi
+ @UserHandleAware
+ @FlaggedApi(com.android.permission.flags.Flags.FLAG_CROSS_USER_ROLE_ENABLED)
+ @Nullable
+ public UserHandle getActiveUserForRole(@NonNull String roleName) {
+ Preconditions.checkStringNotEmpty(roleName, "roleName cannot be null or empty");
+ try {
+ int userId = mService.getActiveUserForRoleAsUser(roleName,
+ mContext.getUser().getIdentifier());
+ return userId == UserHandleCompat.USER_NULL ? null : UserHandle.of(userId);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Set a specific user as active user for a role.
+ * <p>
+ * Only profile-group exclusive roles can be used with this method, and they will have
+ * one active user within a profile group.
+ * <p>
+ * <strong>Note:</strong> Using this API requires holding
+ * {@code android.permission.INTERACT_ACROSS_USERS_FULL} and one of
+ * {@code android.permission.MANAGE_ROLE_HOLDERS} or
+ * {@code android.permission.MANAGE_DEFAULT_APPLICATIONS}.
+ *
+ * @param roleName the name of the role to set the active user for
+ * @param user the user to set as active user for specified role
+ * @param flags optional behavior flags
+ *
+ * @see #getActiveUserForRole(String)
+ *
+ * @hide
+ */
+ @RequiresPermission(allOf = {Manifest.permission.INTERACT_ACROSS_USERS_FULL,
+ Manifest.permission.MANAGE_ROLE_HOLDERS,
+ Manifest.permission.MANAGE_DEFAULT_APPLICATIONS},
+ conditional = true)
+ @RequiresApi(Build.VERSION_CODES.BAKLAVA)
+ @SystemApi
+ @UserHandleAware
+ @FlaggedApi(com.android.permission.flags.Flags.FLAG_CROSS_USER_ROLE_ENABLED)
+ // The user handle parameter is a value to be set by this method, while the context user of the
+ // operation is indeed read from the context
+ @SuppressLint("UserHandle")
+ public void setActiveUserForRole(
+ @NonNull String roleName, @NonNull UserHandle user, @ManageHoldersFlags int flags) {
+ Preconditions.checkStringNotEmpty(roleName, "roleName cannot be null or empty");
+ Objects.requireNonNull(user, "user cannot be null");
+ try {
+ mService.setActiveUserForRoleAsUser(roleName, user.getIdentifier(), flags,
+ mContext.getUser().getIdentifier());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
@NonNull
private static RemoteCallback createRemoteCallback(@NonNull Executor executor,
@NonNull Consumer<Boolean> callback) {
diff --git a/service/java/com/android/permission/compat/UserHandleCompat.java b/framework-s/java/android/permission/internal/compat/UserHandleCompat.java
index 1901aa997..8a3ec444d 100644
--- a/service/java/com/android/permission/compat/UserHandleCompat.java
+++ b/framework-s/java/android/permission/internal/compat/UserHandleCompat.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.permission.compat;
+package android.permission.internal.compat;
import android.annotation.UserIdInt;
import android.os.UserHandle;
@@ -29,6 +29,13 @@ public final class UserHandleCompat {
public static final int USER_ALL = UserHandle.ALL.getIdentifier();
/**
+ * A user ID to indicate an undefined user of the device.
+ *
+ * @see UserHandle#USER_NULL
+ */
+ public static final @UserIdInt int USER_NULL = -10000;
+
+ /**
* A user ID to indicate the "system" user of the device.
*/
public static final int USER_SYSTEM = UserHandle.SYSTEM.getIdentifier();
diff --git a/service/java/com/android/permission/compat/package-info.java b/framework-s/java/android/permission/internal/compat/package-info.java
index c89cc8eab..b78aac878 100644
--- a/service/java/com/android/permission/compat/package-info.java
+++ b/framework-s/java/android/permission/internal/compat/package-info.java
@@ -19,4 +19,4 @@
* TODO(b/146466118) remove this javadoc tag
*/
@android.annotation.Hide
-package com.android.permission.compat;
+package android.permission.internal.compat;
diff --git a/service/Android.bp b/service/Android.bp
index 6f851c4d2..b6e85b0fc 100644
--- a/service/Android.bp
+++ b/service/Android.bp
@@ -101,6 +101,7 @@ java_sdk_library {
"modules-utils-backgroundthread",
"modules-utils-build",
"modules-utils-os",
+ // framework-permission-s already includes com.android.permission.flags-aconfig-java
"role-controller",
"safety-center-config",
"safety-center-internal-data",
diff --git a/service/api/system-server-current.txt b/service/api/system-server-current.txt
index 30fbab484..a3db89370 100644
--- a/service/api/system-server-current.txt
+++ b/service/api/system-server-current.txt
@@ -1,4 +1,18 @@
// Signature format: 2.0
+package com.android.ecm {
+
+ @FlaggedApi("android.permission.flags.enhanced_confirmation_in_call_apis_enabled") public class EnhancedConfirmationCallTrackerService extends android.telecom.InCallService {
+ ctor public EnhancedConfirmationCallTrackerService();
+ }
+
+ @FlaggedApi("android.permission.flags.enhanced_confirmation_in_call_apis_enabled") public interface EnhancedConfirmationManagerLocal {
+ method public void addOngoingCall(@NonNull android.telecom.Call);
+ method public void clearOngoingCalls();
+ method public void removeOngoingCall(@NonNull String);
+ }
+
+}
+
package com.android.permission.persistence {
public interface RuntimePermissionsPersistence {
diff --git a/service/jarjar-rules.txt b/service/jarjar-rules.txt
index 4d4d6e050..ef6971b11 100644
--- a/service/jarjar-rules.txt
+++ b/service/jarjar-rules.txt
@@ -10,6 +10,10 @@ rule android.companion.virtualdevice.flags.*FeatureFlags* com.android.permission
rule android.companion.virtualdevice.flags.FeatureFlags* com.android.permission.jarjar.@0
rule android.companion.virtualdevice.flags.FeatureFlags com.android.permission.jarjar.@0
rule android.companion.virtualdevice.flags.Flags com.android.permission.jarjar.@0
+rule android.content.pm.*FeatureFlags* com.android.permission.jarjar.@0
+rule android.content.pm.FeatureFlags* com.android.permission.jarjar.@0
+rule android.content.pm.FeatureFlags com.android.permission.jarjar.@0
+rule android.content.pm.Flags com.android.permission.jarjar.@0
rule android.os.*FeatureFlags* com.android.permission.jarjar.@0
rule android.os.FeatureFlags* com.android.permission.jarjar.@0
rule android.os.FeatureFlags com.android.permission.jarjar.@0
diff --git a/service/java/com/android/ecm/EnhancedConfirmationCallTrackerService.java b/service/java/com/android/ecm/EnhancedConfirmationCallTrackerService.java
new file mode 100644
index 000000000..407d56f70
--- /dev/null
+++ b/service/java/com/android/ecm/EnhancedConfirmationCallTrackerService.java
@@ -0,0 +1,80 @@
+/*
+ * 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.ecm;
+
+import android.annotation.FlaggedApi;
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+import android.annotation.TargetApi;
+import android.content.Intent;
+import android.os.Build;
+import android.permission.flags.Flags;
+import android.telecom.Call;
+import android.telecom.InCallService;
+
+import com.android.server.LocalManagerRegistry;
+
+/**
+ * @hide
+ *
+ * This InCallService tracks called (both incoming and outgoing), and sends their information to the
+ * EnhancedConfirmationService
+ *
+ **/
+@FlaggedApi(Flags.FLAG_ENHANCED_CONFIRMATION_IN_CALL_APIS_ENABLED)
+@SystemApi(client = SystemApi.Client.SYSTEM_SERVER)
+@TargetApi(Build.VERSION_CODES.BAKLAVA)
+public class EnhancedConfirmationCallTrackerService extends InCallService {
+ private EnhancedConfirmationManagerLocal mEnhancedConfirmationManagerLocal;
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ if (Flags.enhancedConfirmationInCallApisEnabled()) {
+ mEnhancedConfirmationManagerLocal =
+ LocalManagerRegistry.getManager(EnhancedConfirmationManagerLocal.class);
+ }
+ }
+
+ @Override
+ public void onCallAdded(@Nullable Call call) {
+ if (mEnhancedConfirmationManagerLocal == null || call == null) {
+ return;
+ }
+
+ mEnhancedConfirmationManagerLocal.addOngoingCall(call);
+ }
+
+ @Override
+ public void onCallRemoved(@Nullable Call call) {
+ if (mEnhancedConfirmationManagerLocal == null || call == null) {
+ return;
+ }
+
+ mEnhancedConfirmationManagerLocal.removeOngoingCall(call.getDetails().getId());
+ }
+
+ /**
+ * When unbound, we should assume all calls have finished. Notify the system of such.
+ */
+ public boolean onUnbind(@Nullable Intent intent) {
+ if (mEnhancedConfirmationManagerLocal != null) {
+ mEnhancedConfirmationManagerLocal.clearOngoingCalls();
+ }
+ return super.onUnbind(intent);
+ }
+}
diff --git a/service/java/com/android/ecm/EnhancedConfirmationManagerLocal.java b/service/java/com/android/ecm/EnhancedConfirmationManagerLocal.java
new file mode 100644
index 000000000..483071716
--- /dev/null
+++ b/service/java/com/android/ecm/EnhancedConfirmationManagerLocal.java
@@ -0,0 +1,56 @@
+/*
+ * 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.ecm;
+
+import android.annotation.FlaggedApi;
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.annotation.TargetApi;
+import android.os.Build;
+import android.permission.flags.Flags;
+import android.telecom.Call;
+
+/**
+ * @hide
+ *
+ * In-process API for the Enhanced Confirmation Service
+ */
+@FlaggedApi(Flags.FLAG_ENHANCED_CONFIRMATION_IN_CALL_APIS_ENABLED)
+@SystemApi(client = SystemApi.Client.SYSTEM_SERVER)
+@TargetApi(Build.VERSION_CODES.BAKLAVA)
+public interface EnhancedConfirmationManagerLocal {
+ /**
+ * Inform the enhanced confirmation service of an ongoing call
+ *
+ * @param call The call to potentially track
+ *
+ */
+ void addOngoingCall(@NonNull Call call);
+
+ /**
+ * Inform the enhanced confirmation service that a call has ended
+ *
+ * @param callId The ID of the call to stop tracking
+ *
+ */
+ void removeOngoingCall(@NonNull String callId);
+
+ /**
+ * Informs the enhanced confirmation service it should clear out any ongoing calls
+ */
+ void clearOngoingCalls();
+}
diff --git a/service/java/com/android/ecm/EnhancedConfirmationManagerLocalImpl.java b/service/java/com/android/ecm/EnhancedConfirmationManagerLocalImpl.java
new file mode 100644
index 000000000..a5c6d3c36
--- /dev/null
+++ b/service/java/com/android/ecm/EnhancedConfirmationManagerLocalImpl.java
@@ -0,0 +1,59 @@
+/*
+ * 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.ecm;
+
+import android.annotation.NonNull;
+import android.annotation.TargetApi;
+import android.os.Build;
+import android.permission.flags.Flags;
+import android.telecom.Call;
+
+/** @hide */
+@TargetApi(Build.VERSION_CODES.BAKLAVA)
+class EnhancedConfirmationManagerLocalImpl implements EnhancedConfirmationManagerLocal {
+
+ private final EnhancedConfirmationService mService;
+
+ EnhancedConfirmationManagerLocalImpl(EnhancedConfirmationService service) {
+ if (Flags.enhancedConfirmationInCallApisEnabled()) {
+ mService = service;
+ } else {
+ mService = null;
+ }
+ }
+
+ @Override
+ public void addOngoingCall(@NonNull Call call) {
+ if (mService != null) {
+ mService.addOngoingCall(call);
+ }
+ }
+
+ @Override
+ public void removeOngoingCall(@NonNull String callId) {
+ if (mService != null) {
+ mService.removeOngoingCall(callId);
+ }
+ }
+
+ @Override
+ public void clearOngoingCalls() {
+ if (mService != null) {
+ mService.clearOngoingCalls();
+ }
+ }
+}
diff --git a/service/java/com/android/ecm/EnhancedConfirmationService.java b/service/java/com/android/ecm/EnhancedConfirmationService.java
index 708884e85..1bbba1079 100644
--- a/service/java/com/android/ecm/EnhancedConfirmationService.java
+++ b/service/java/com/android/ecm/EnhancedConfirmationService.java
@@ -19,11 +19,15 @@ package com.android.ecm;
import android.Manifest;
import android.annotation.FlaggedApi;
import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SuppressLint;
import android.annotation.UserIdInt;
import android.app.AppOpsManager;
import android.app.ecm.EnhancedConfirmationManager;
import android.app.ecm.IEnhancedConfirmationManager;
import android.app.role.RoleManager;
+import android.content.ContentResolver;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.InstallSourceInfo;
@@ -31,25 +35,33 @@ import android.content.pm.PackageInstaller;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.SignedPackage;
+import android.database.Cursor;
+import android.net.Uri;
import android.os.Binder;
import android.os.Build;
import android.os.SystemConfigManager;
import android.os.UserHandle;
import android.permission.flags.Flags;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.CommonDataKinds.StructuredName;
+import android.provider.ContactsContract.PhoneLookup;
+import android.telecom.Call;
+import android.telecom.PhoneAccount;
+import android.telephony.TelephonyManager;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.Log;
import androidx.annotation.Keep;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import com.android.internal.util.Preconditions;
import com.android.permission.util.UserUtils;
+import com.android.server.LocalManagerRegistry;
import com.android.server.SystemService;
import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@@ -66,16 +78,35 @@ import java.util.Set;
@Keep
@FlaggedApi(Flags.FLAG_ENHANCED_CONFIRMATION_MODE_APIS_ENABLED)
@RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+@SuppressLint("MissingPermission")
public class EnhancedConfirmationService extends SystemService {
private static final String LOG_TAG = EnhancedConfirmationService.class.getSimpleName();
private Map<String, List<byte[]>> mTrustedPackageCertDigests;
private Map<String, List<byte[]>> mTrustedInstallerCertDigests;
+ // A map of call ID to call type
+ private final Map<String, Integer> mOngoingCalls = new ArrayMap<>();
+
+ private static final int CALL_TYPE_UNTRUSTED = 0;
+ private static final int CALL_TYPE_TRUSTED = 1;
+ private static final int CALL_TYPE_EMERGENCY = 2;
+ @IntDef(flag = true, value = {
+ CALL_TYPE_UNTRUSTED,
+ CALL_TYPE_TRUSTED,
+ CALL_TYPE_EMERGENCY
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ @interface CallType {}
public EnhancedConfirmationService(@NonNull Context context) {
super(context);
+ LocalManagerRegistry.addManager(EnhancedConfirmationManagerLocal.class,
+ new EnhancedConfirmationManagerLocalImpl(this));
}
+ private ContentResolver mContentResolver;
+ private TelephonyManager mTelephonyManager;
+
@Override
public void onStart() {
Context context = getContext();
@@ -87,6 +118,8 @@ public class EnhancedConfirmationService extends SystemService {
systemConfigManager.getEnhancedConfirmationTrustedInstallers());
publishBinderService(Context.ECM_ENHANCED_CONFIRMATION_SERVICE, new Stub());
+ mContentResolver = getContext().getContentResolver();
+ mTelephonyManager = getContext().getSystemService(TelephonyManager.class);
}
private Map<String, List<byte[]>> toTrustedPackageMap(Set<SignedPackage> signedPackages) {
@@ -99,6 +132,90 @@ public class EnhancedConfirmationService extends SystemService {
return trustedPackageMap;
}
+ void addOngoingCall(Call call) {
+ if (!Flags.enhancedConfirmationInCallApisEnabled()) {
+ return;
+ }
+ if (call.getDetails() == null) {
+ return;
+ }
+ mOngoingCalls.put(call.getDetails().getId(), getCallType(call));
+ }
+
+ void removeOngoingCall(String callId) {
+ if (!Flags.enhancedConfirmationInCallApisEnabled()) {
+ return;
+ }
+ Integer returned = mOngoingCalls.remove(callId);
+ if (returned == null) {
+ // TODO b/379941144: Capture a bug report whenever this happens.
+ }
+ }
+
+ void clearOngoingCalls() {
+ mOngoingCalls.clear();
+ }
+
+ private @CallType int getCallType(Call call) {
+ String number = getPhoneNumber(call);
+ if (number != null && mTelephonyManager.isEmergencyNumber(number)) {
+ return CALL_TYPE_EMERGENCY;
+ } else if (number != null) {
+ return hasContactWithPhoneNumber(number) ? CALL_TYPE_TRUSTED : CALL_TYPE_UNTRUSTED;
+ } else {
+ return hasContactWithDisplayName(call.getDetails().getCallerDisplayName())
+ ? CALL_TYPE_TRUSTED : CALL_TYPE_UNTRUSTED;
+ }
+ }
+
+ private String getPhoneNumber(Call call) {
+ Uri handle = call.getDetails().getHandle();
+ if (handle == null || handle.getScheme() == null) {
+ return null;
+ }
+ if (!handle.getScheme().equals(PhoneAccount.SCHEME_TEL)) {
+ return null;
+ }
+ return handle.getSchemeSpecificPart();
+ }
+
+ private boolean hasContactWithPhoneNumber(String phoneNumber) {
+ if (phoneNumber == null) {
+ return false;
+ }
+ Uri uri = Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI,
+ Uri.encode(phoneNumber));
+ String[] projection = new String[]{
+ PhoneLookup.DISPLAY_NAME,
+ ContactsContract.PhoneLookup._ID
+ };
+ try (Cursor res = mContentResolver.query(uri, projection, null, null)) {
+ return res != null && res.getCount() > 0;
+ }
+ }
+
+ private boolean hasContactWithDisplayName(String displayName) {
+ if (displayName == null) {
+ return false;
+ }
+ Uri uri = ContactsContract.Data.CONTENT_URI;
+ String[] projection = new String[]{PhoneLookup._ID};
+ String selection = StructuredName.DISPLAY_NAME + " = ?";
+ String[] selectionArgs = new String[]{displayName};
+ try (Cursor res = mContentResolver.query(uri, projection, selection, selectionArgs, null)) {
+ return res != null && res.getCount() > 0;
+ }
+ }
+
+ private boolean hasCallOfType(@CallType int callType) {
+ for (int ongoingCallType : mOngoingCalls.values()) {
+ if (ongoingCallType == callType) {
+ return true;
+ }
+ }
+ return false;
+ }
+
private class Stub extends IEnhancedConfirmationManager.Stub {
/** A map of ECM states to their corresponding app op states */
@@ -227,8 +344,20 @@ public class EnhancedConfirmationService extends SystemService {
}
}
+ @Override
+ public boolean isUntrustedCallOngoing() {
+ enforcePermissions("isUntrustedCallOngoing",
+ UserHandle.getUserHandleForUid(Binder.getCallingUid()).getIdentifier());
+ if (hasCallOfType(CALL_TYPE_EMERGENCY)) {
+ // If we have an emergency call, return false always.
+ return false;
+ }
+ return hasCallOfType(CALL_TYPE_UNTRUSTED);
+ }
+
private void enforcePermissions(@NonNull String methodName, @UserIdInt int userId) {
- UserUtils.enforceCrossUserPermission(userId, false, methodName, mContext);
+ UserUtils.enforceCrossUserPermission(userId, /* allowAll= */ false,
+ /* enforceForProfileGroup= */ false, methodName, mContext);
mContext.enforceCallingOrSelfPermission(
android.Manifest.permission.MANAGE_ENHANCED_CONFIRMATION_STATES, methodName);
}
@@ -322,6 +451,7 @@ public class EnhancedConfirmationService extends SystemService {
return (applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0;
}
+ @SuppressLint("WrongConstant")
private void setAppEcmState(@NonNull String packageName, @EcmState int ecmState,
@UserIdInt int userId) throws NameNotFoundException {
int packageUid = getPackageUid(packageName, userId);
diff --git a/service/java/com/android/permission/util/UserUtils.java b/service/java/com/android/permission/util/UserUtils.java
index 33389a88f..c69afb199 100644
--- a/service/java/com/android/permission/util/UserUtils.java
+++ b/service/java/com/android/permission/util/UserUtils.java
@@ -17,18 +17,21 @@
package com.android.permission.util;
import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.annotation.UserIdInt;
+import android.app.admin.DevicePolicyManager;
import android.content.Context;
import android.os.Binder;
import android.os.Process;
import android.os.UserHandle;
import android.os.UserManager;
+import android.permission.internal.compat.UserHandleCompat;
import com.android.internal.util.Preconditions;
import com.android.modules.utils.build.SdkLevel;
-import com.android.permission.compat.UserHandleCompat;
import com.android.permission.flags.Flags;
+import java.util.ArrayList;
import java.util.List;
/** Utility class to deal with Android users. */
@@ -40,11 +43,12 @@ public final class UserUtils {
public static void enforceCrossUserPermission(
@UserIdInt int userId,
boolean allowAll,
+ boolean enforceForProfileGroup,
@NonNull String message,
@NonNull Context context) {
final int callingUid = Binder.getCallingUid();
final int callingUserId = UserHandleCompat.getUserId(callingUid);
- if (userId == callingUserId) {
+ if (userId == callingUserId && !enforceForProfileGroup) {
return;
}
Preconditions.checkArgument(
@@ -53,13 +57,40 @@ public final class UserUtils {
"Invalid user " + userId);
context.enforceCallingOrSelfPermission(
android.Manifest.permission.INTERACT_ACROSS_USERS_FULL, message);
- if (callingUid != Process.SHELL_UID || userId < UserHandleCompat.USER_SYSTEM) {
+ if (callingUid != Process.SHELL_UID || userId == UserHandleCompat.USER_ALL) {
return;
}
+
+ if (enforceForProfileGroup) {
+ DevicePolicyManager devicePolicyManager =
+ context.getSystemService(DevicePolicyManager.class);
+ if (!devicePolicyManager.isOrganizationOwnedDeviceWithManagedProfile()) {
+ // For profileGroup exclusive roles users on BYOD are free to choose personal o
+ // work profile app regardless of DISALLOW_DEBUGGING_FEATURES
+ return;
+ }
+
+ Context userContext = UserUtils.getUserContext(userId, context);
+ List<UserHandle> profiles = getUserProfiles(userContext, true);
+ final int profilesSize = profiles.size();
+ for (int i = 0; i < profilesSize; i++) {
+ int profileId = profiles.get(i).getIdentifier();
+ if (profileId == callingUserId) {
+ continue;
+ }
+ enforceShellRestriction(profileId, context);
+ }
+ } else {
+ enforceShellRestriction(userId, context);
+ }
+ }
+
+ private static void enforceShellRestriction(int userId, @NonNull Context context) {
UserManager userManager = context.getSystemService(UserManager.class);
if (userManager.hasUserRestrictionForUser(
UserManager.DISALLOW_DEBUGGING_FEATURES, UserHandle.of(userId))) {
- throw new SecurityException("Shell does not have permission to access user " + userId);
+ throw new SecurityException(
+ "Shell does not have permission to access user " + userId);
}
}
@@ -81,6 +112,68 @@ public final class UserUtils {
}
}
+ /** Returns all the enabled user profiles on the device. */
+ @NonNull
+ public static List<UserHandle> getUserProfiles(@NonNull Context context) {
+ return getUserProfiles(context, false);
+ }
+
+ /**
+ * Returns all the enabled user profiles on the device
+ *
+ * @param context the {@link Context}
+ * @param excludePrivate {@code true} to exclude private profiles from returned list of users
+ */
+ @NonNull
+ public static List<UserHandle> getUserProfiles(@NonNull Context context,
+ boolean excludePrivate) {
+ UserManager userManager = context.getSystemService(UserManager.class);
+ // This call requires the QUERY_USERS permission.
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ List<UserHandle> profiles = userManager.getUserProfiles();
+ if (!excludePrivate) {
+ return profiles;
+ }
+ List<UserHandle> filteredProfiles = new ArrayList<>();
+ final int profilesSize = profiles.size();
+ for (int i = 0; i < profilesSize; i++) {
+ UserHandle user = profiles.get(i);
+ if (!isPrivateProfile(user.getIdentifier(), context)) {
+ filteredProfiles.add(user);
+ }
+ }
+ return filteredProfiles;
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+
+ /**
+ * Returns the parent of a given user, or userId if it has no parent (e.g. it is the primary
+ * profile)
+ */
+ @UserIdInt
+ public static int getProfileParentIdOrSelf(@UserIdInt int userId, @NonNull Context context) {
+ UserHandle profileParent = getProfileParent(userId, context);
+ // If profile parent userhandle is null, then original user id is the parent
+ return profileParent != null ? profileParent.getIdentifier() : userId;
+ }
+
+ /** Returns the parent of a given user. */
+ @Nullable
+ private static UserHandle getProfileParent(@UserIdInt int userId, @NonNull Context context) {
+ Context userContext = getUserContext(userId, context);
+ UserManager userManager = userContext.getSystemService(UserManager.class);
+ // This call requires the INTERACT_ACROSS_USERS permission.
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ return userManager.getProfileParent(UserHandle.of(userId));
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+
/** Returns whether a given {@code userId} corresponds to a managed profile. */
public static boolean isManagedProfile(@UserIdInt int userId, @NonNull Context context) {
UserManager userManager = context.getSystemService(UserManager.class);
@@ -107,8 +200,7 @@ public final class UserUtils {
// MANAGE_USERS, QUERY_USERS, or INTERACT_ACROSS_USERS.
final long identity = Binder.clearCallingIdentity();
try {
- Context userContext = context
- .createContextAsUser(UserHandle.of(userId), /* flags= */ 0);
+ Context userContext = getUserContext(userId, context);
UserManager userManager = userContext.getSystemService(UserManager.class);
return userManager != null && userManager.isPrivateProfile();
} finally {
@@ -141,4 +233,13 @@ public final class UserUtils {
Binder.restoreCallingIdentity(identity);
}
}
+
+ @NonNull
+ public static Context getUserContext(@UserIdInt int userId, @NonNull Context context) {
+ if (SdkLevel.isAtLeastS() && context.getUser().getIdentifier() == userId) {
+ return context;
+ } else {
+ return context.createContextAsUser(UserHandle.of(userId), 0);
+ }
+ }
}
diff --git a/service/java/com/android/role/RoleService.java b/service/java/com/android/role/RoleService.java
index 20250b4f6..1145f273d 100644
--- a/service/java/com/android/role/RoleService.java
+++ b/service/java/com/android/role/RoleService.java
@@ -18,6 +18,7 @@ package com.android.role;
import android.Manifest;
import android.annotation.AnyThread;
+import android.annotation.ChecksSdkIntAtLeast;
import android.annotation.MainThread;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -46,6 +47,7 @@ import android.os.RemoteException;
import android.os.UserHandle;
import android.os.UserManager;
import android.permission.flags.Flags;
+import android.permission.internal.compat.UserHandleCompat;
import android.provider.Settings;
import android.text.TextUtils;
import android.util.ArraySet;
@@ -62,13 +64,14 @@ import com.android.internal.infra.AndroidFuture;
import com.android.internal.util.Preconditions;
import com.android.internal.util.dump.DualDumpOutputStream;
import com.android.modules.utils.build.SdkLevel;
-import com.android.permission.compat.UserHandleCompat;
import com.android.permission.util.ArrayUtils;
import com.android.permission.util.CollectionUtils;
import com.android.permission.util.ForegroundThread;
import com.android.permission.util.PackageUtils;
import com.android.permission.util.ThrottledRunnable;
import com.android.permission.util.UserUtils;
+import com.android.role.controller.model.Role;
+import com.android.role.controller.model.Roles;
import com.android.server.LocalManagerRegistry;
import com.android.server.SystemService;
import com.android.server.role.RoleServicePlatformHelper;
@@ -77,6 +80,7 @@ import java.io.FileDescriptor;
import java.io.FileOutputStream;
import java.io.PrintWriter;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
@@ -176,6 +180,7 @@ public class RoleService extends SystemService implements RoleUserState.Callback
registerUserRemovedReceiver();
}
+ // TODO(b/375029649): enforce single active user for all cross-user roles
private void registerUserRemovedReceiver() {
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(Intent.ACTION_USER_REMOVED);
@@ -191,6 +196,7 @@ public class RoleService extends SystemService implements RoleUserState.Callback
}, intentFilter, null, null);
}
+ // TODO(b/375029649): enforce single active user for all cross-user roles
@Override
public void onStart() {
publishBinderService(Context.ROLE_SERVICE, new Stub());
@@ -464,8 +470,8 @@ public class RoleService extends SystemService implements RoleUserState.Callback
@Override
public boolean isRoleAvailableAsUser(@NonNull String roleName, @UserIdInt int userId) {
- UserUtils.enforceCrossUserPermission(userId, false, "isRoleAvailableAsUser",
- getContext());
+ UserUtils.enforceCrossUserPermission(userId, /* allowAll= */ false,
+ /* enforceForProfileGroup= */ false, "isRoleAvailableAsUser", getContext());
if (!UserUtils.isUserExistent(userId, getContext())) {
Log.e(LOG_TAG, "user " + userId + " does not exist");
return false;
@@ -481,7 +487,8 @@ public class RoleService extends SystemService implements RoleUserState.Callback
@UserIdInt int userId) {
mAppOpsManager.checkPackage(getCallingUid(), packageName);
- UserUtils.enforceCrossUserPermission(userId, false, "isRoleHeldAsUser", getContext());
+ UserUtils.enforceCrossUserPermission(userId, /* allowAll= */ false,
+ /* enforceForProfileGroup= */ false, "isRoleHeldAsUser", getContext());
if (!UserUtils.isUserExistent(userId, getContext())) {
Log.e(LOG_TAG, "user " + userId + " does not exist");
return false;
@@ -500,8 +507,8 @@ public class RoleService extends SystemService implements RoleUserState.Callback
@NonNull
@Override
public List<String> getRoleHoldersAsUser(@NonNull String roleName, @UserIdInt int userId) {
- UserUtils.enforceCrossUserPermission(userId, false, "getRoleHoldersAsUser",
- getContext());
+ UserUtils.enforceCrossUserPermission(userId, /* allowAll= */ false,
+ /* enforceForProfileGroup= */ false, "getRoleHoldersAsUser", getContext());
if (!UserUtils.isUserExistent(userId, getContext())) {
Log.e(LOG_TAG, "user " + userId + " does not exist");
return Collections.emptyList();
@@ -523,8 +530,8 @@ public class RoleService extends SystemService implements RoleUserState.Callback
public void addRoleHolderAsUser(@NonNull String roleName, @NonNull String packageName,
@RoleManager.ManageHoldersFlags int flags, @UserIdInt int userId,
@NonNull RemoteCallback callback) {
- UserUtils.enforceCrossUserPermission(userId, false, "addRoleHolderAsUser",
- getContext());
+ UserUtils.enforceCrossUserPermission(userId, /* allowAll= */ false,
+ /* enforceForProfileGroup= */ false, "addRoleHolderAsUser", getContext());
if (!UserUtils.isUserExistent(userId, getContext())) {
Log.e(LOG_TAG, "user " + userId + " does not exist");
return;
@@ -544,8 +551,8 @@ public class RoleService extends SystemService implements RoleUserState.Callback
public void removeRoleHolderAsUser(@NonNull String roleName, @NonNull String packageName,
@RoleManager.ManageHoldersFlags int flags, @UserIdInt int userId,
@NonNull RemoteCallback callback) {
- UserUtils.enforceCrossUserPermission(userId, false, "removeRoleHolderAsUser",
- getContext());
+ UserUtils.enforceCrossUserPermission(userId, /* allowAll= */ false,
+ /* enforceForProfileGroup= */ false, "removeRoleHolderAsUser", getContext());
if (!UserUtils.isUserExistent(userId, getContext())) {
Log.e(LOG_TAG, "user " + userId + " does not exist");
return;
@@ -566,8 +573,8 @@ public class RoleService extends SystemService implements RoleUserState.Callback
public void clearRoleHoldersAsUser(@NonNull String roleName,
@RoleManager.ManageHoldersFlags int flags, @UserIdInt int userId,
@NonNull RemoteCallback callback) {
- UserUtils.enforceCrossUserPermission(userId, false, "clearRoleHoldersAsUser",
- getContext());
+ UserUtils.enforceCrossUserPermission(userId, /* allowAll= */ false,
+ /* enforceForProfileGroup= */ false, "clearRoleHoldersAsUser", getContext());
if (!UserUtils.isUserExistent(userId, getContext())) {
Log.e(LOG_TAG, "user " + userId + " does not exist");
return;
@@ -585,7 +592,8 @@ public class RoleService extends SystemService implements RoleUserState.Callback
@Override
@Nullable
public String getDefaultApplicationAsUser(@NonNull String roleName, @UserIdInt int userId) {
- UserUtils.enforceCrossUserPermission(userId, false, "getDefaultApplicationAsUser",
+ UserUtils.enforceCrossUserPermission(userId, /* allowAll= */ false,
+ /* enforceForProfileGroup= */ false, "getDefaultApplicationAsUser",
getContext());
if (!UserUtils.isUserExistent(userId, getContext())) {
Log.e(LOG_TAG, "user " + userId + " does not exist");
@@ -610,7 +618,8 @@ public class RoleService extends SystemService implements RoleUserState.Callback
public void setDefaultApplicationAsUser(@NonNull String roleName,
@Nullable String packageName, @RoleManager.ManageHoldersFlags int flags,
@UserIdInt int userId, @NonNull RemoteCallback callback) {
- UserUtils.enforceCrossUserPermission(userId, false, "setDefaultApplicationAsUser",
+ UserUtils.enforceCrossUserPermission(userId, /* allowAll= */ false,
+ /* enforceForProfileGroup= */ false, "setDefaultApplicationAsUser",
getContext());
if (!UserUtils.isUserExistent(userId, getContext())) {
Log.e(LOG_TAG, "user " + userId + " does not exist");
@@ -633,10 +642,101 @@ public class RoleService extends SystemService implements RoleUserState.Callback
}
@Override
+ public int getActiveUserForRoleAsUser(@NonNull String roleName, @UserIdInt int userId) {
+ Preconditions.checkState(isProfileGroupExclusivityAvailable(),
+ "getActiveUserForRoleAsUser not available");
+ enforceProfileGroupExclusiveRole(roleName);
+
+ UserUtils.enforceCrossUserPermission(userId, /* allowAll= */ false,
+ /* enforceForProfileGroup= */ true, "getActiveUserForRole", getContext());
+ if (!UserUtils.isUserExistent(userId, getContext())) {
+ Log.e(LOG_TAG, "user " + userId + " does not exist");
+ return UserHandleCompat.USER_NULL;
+ }
+
+ enforceCallingOrSelfAnyPermissions(new String[] {
+ Manifest.permission.MANAGE_DEFAULT_APPLICATIONS,
+ Manifest.permission.MANAGE_ROLE_HOLDERS
+ }, "getActiveUserForRole");
+
+ int profileParentId = UserUtils.getProfileParentIdOrSelf(userId, getContext());
+ RoleUserState userState = getOrCreateUserState(profileParentId);
+ return userState.getActiveUserForRole(roleName);
+ }
+
+ @Override
+ public void setActiveUserForRoleAsUser(@NonNull String roleName,
+ @UserIdInt int activeUserId, @RoleManager.ManageHoldersFlags int flags,
+ @UserIdInt int userId) {
+ Preconditions.checkState(isProfileGroupExclusivityAvailable(),
+ "setActiveUserForRoleAsUser not available");
+ enforceProfileGroupExclusiveRole(roleName);
+
+ UserUtils.enforceCrossUserPermission(userId, /* allowAll= */ false,
+ /* enforceForProfileGroup= */ true, "setActiveUserForRole", getContext());
+ if (!UserUtils.isUserExistent(userId, getContext())) {
+ Log.e(LOG_TAG, "user " + userId + " does not exist");
+ return;
+ }
+ if (!UserUtils.isUserExistent(activeUserId, getContext())) {
+ Log.e(LOG_TAG, "user " + activeUserId + " does not exist");
+ return;
+ }
+ if (UserUtils.isPrivateProfile(activeUserId, getContext())) {
+ Log.e(LOG_TAG, "Cannot set private profile " + activeUserId + " as active user"
+ + " for role");
+ return;
+ }
+ Context userContext = UserUtils.getUserContext(userId, getContext());
+ List<UserHandle> profiles = UserUtils.getUserProfiles(userContext, true);
+ if (!profiles.contains(UserHandle.of(activeUserId))) {
+ Log.e(LOG_TAG, "User " + activeUserId + " is not in the same profile-group as "
+ + userId);
+ return;
+ }
+
+ enforceCallingOrSelfAnyPermissions(new String[] {
+ Manifest.permission.MANAGE_DEFAULT_APPLICATIONS,
+ Manifest.permission.MANAGE_ROLE_HOLDERS
+ }, "setDefaultApplicationAsUser");
+
+ int profileParentId = UserUtils.getProfileParentIdOrSelf(userId, getContext());
+ RoleUserState userState = getOrCreateUserState(profileParentId);
+
+ if (!userState.setActiveUserForRole(roleName, activeUserId)) {
+ Log.i(LOG_TAG, "User " + activeUserId + " is already the active user for role");
+ return;
+ }
+
+ final int profilesSize = profiles.size();
+ for (int i = 0; i < profilesSize; i++) {
+ final AndroidFuture<Void> future = new AndroidFuture<>();
+ final RemoteCallback callback = new RemoteCallback(result -> {
+ boolean successful = result != null;
+ if (successful) {
+ future.complete(null);
+ } else {
+ future.completeExceptionally(new RuntimeException());
+ }
+ });
+ int profilesUserId = profiles.get(i).getIdentifier();
+ getOrCreateController(profilesUserId)
+ .onClearRoleHolders(roleName, flags, callback);
+ try {
+ future.get(5, TimeUnit.SECONDS);
+ } catch (InterruptedException | ExecutionException | TimeoutException e) {
+ Log.e(LOG_TAG, "Exception while clearing role holders for non-active"
+ + "user: " + profilesUserId, e);
+ }
+ }
+ }
+
+ @Override
public void addOnRoleHoldersChangedListenerAsUser(
@NonNull IOnRoleHoldersChangedListener listener, @UserIdInt int userId) {
- UserUtils.enforceCrossUserPermission(userId, true,
- "addOnRoleHoldersChangedListenerAsUser", getContext());
+ UserUtils.enforceCrossUserPermission(userId, /* allowAll= */ true,
+ /* enforceForProfileGroup= */ false, "addOnRoleHoldersChangedListenerAsUser",
+ getContext());
if (userId != UserHandleCompat.USER_ALL && !UserUtils.isUserExistent(userId,
getContext())) {
Log.e(LOG_TAG, "user " + userId + " does not exist");
@@ -656,7 +756,8 @@ public class RoleService extends SystemService implements RoleUserState.Callback
@Override
public void removeOnRoleHoldersChangedListenerAsUser(
@NonNull IOnRoleHoldersChangedListener listener, @UserIdInt int userId) {
- UserUtils.enforceCrossUserPermission(userId, true,
+ UserUtils.enforceCrossUserPermission(userId, /* allowAll= */ true,
+ /* enforceForProfileGroup= */ false,
"removeOnRoleHoldersChangedListenerAsUser", getContext());
if (userId != UserHandleCompat.USER_ALL && !UserUtils.isUserExistent(userId,
getContext())) {
@@ -709,7 +810,8 @@ public class RoleService extends SystemService implements RoleUserState.Callback
@Override
public boolean isRoleFallbackEnabledAsUser(@NonNull String roleName,
@UserIdInt int userId) {
- UserUtils.enforceCrossUserPermission(userId, false, "isRoleFallbackEnabledAsUser",
+ UserUtils.enforceCrossUserPermission(userId, /* allowAll= */ false,
+ /* enforceForProfileGroup= */ false, "isRoleFallbackEnabledAsUser",
getContext());
if (!UserUtils.isUserExistent(userId, getContext())) {
Log.e(LOG_TAG, "user " + userId + " does not exist");
@@ -727,7 +829,8 @@ public class RoleService extends SystemService implements RoleUserState.Callback
@Override
public void setRoleFallbackEnabledAsUser(@NonNull String roleName, boolean fallbackEnabled,
@UserIdInt int userId) {
- UserUtils.enforceCrossUserPermission(userId, false, "setRoleFallbackEnabledAsUser",
+ UserUtils.enforceCrossUserPermission(userId, /* allowAll= */ false,
+ /* enforceForProfileGroup= */ false, "setRoleFallbackEnabledAsUser",
getContext());
if (!UserUtils.isUserExistent(userId, getContext())) {
Log.e(LOG_TAG, "user " + userId + " does not exist");
@@ -745,7 +848,8 @@ public class RoleService extends SystemService implements RoleUserState.Callback
@Override
public void setRoleNamesFromControllerAsUser(@NonNull List<String> roleNames,
@UserIdInt int userId) {
- UserUtils.enforceCrossUserPermission(userId, false, "setRoleNamesFromControllerAsUser",
+ UserUtils.enforceCrossUserPermission(userId, /* allowAll= */ false,
+ /* enforceForProfileGroup= */ false, "setRoleNamesFromControllerAsUser",
getContext());
if (!UserUtils.isUserExistent(userId, getContext())) {
Log.e(LOG_TAG, "user " + userId + " does not exist");
@@ -764,8 +868,9 @@ public class RoleService extends SystemService implements RoleUserState.Callback
@Override
public boolean addRoleHolderFromControllerAsUser(@NonNull String roleName,
@NonNull String packageName, @UserIdInt int userId) {
- UserUtils.enforceCrossUserPermission(userId, false,
- "addRoleHolderFromControllerAsUser", getContext());
+ UserUtils.enforceCrossUserPermission(userId, /* allowAll= */ false,
+ /* enforceForProfileGroup= */ false, "addRoleHolderFromControllerAsUser",
+ getContext());
if (!UserUtils.isUserExistent(userId, getContext())) {
Log.e(LOG_TAG, "user " + userId + " does not exist");
return false;
@@ -784,8 +889,9 @@ public class RoleService extends SystemService implements RoleUserState.Callback
@Override
public boolean removeRoleHolderFromControllerAsUser(@NonNull String roleName,
@NonNull String packageName, @UserIdInt int userId) {
- UserUtils.enforceCrossUserPermission(userId, false,
- "removeRoleHolderFromControllerAsUser", getContext());
+ UserUtils.enforceCrossUserPermission(userId, /* allowAll= */ false,
+ /* enforceForProfileGroup= */ false, "removeRoleHolderFromControllerAsUser",
+ getContext());
if (!UserUtils.isUserExistent(userId, getContext())) {
Log.e(LOG_TAG, "user " + userId + " does not exist");
return false;
@@ -804,8 +910,9 @@ public class RoleService extends SystemService implements RoleUserState.Callback
@Override
public List<String> getHeldRolesFromControllerAsUser(@NonNull String packageName,
@UserIdInt int userId) {
- UserUtils.enforceCrossUserPermission(userId, false,
- "getHeldRolesFromControllerAsUser", getContext());
+ UserUtils.enforceCrossUserPermission(userId, /* allowAll= */ false,
+ /* enforceForProfileGroup= */ false, "getHeldRolesFromControllerAsUser",
+ getContext());
if (!UserUtils.isUserExistent(userId, getContext())) {
Log.e(LOG_TAG, "user " + userId + " does not exist");
return Collections.emptyList();
@@ -914,7 +1021,8 @@ public class RoleService extends SystemService implements RoleUserState.Callback
@Override
public String getSmsRoleHolder(int userId) {
final Context context = getContext();
- UserUtils.enforceCrossUserPermission(userId, false, "getSmsRoleHolder", context);
+ UserUtils.enforceCrossUserPermission(userId, /* allowAll= */ false,
+ /* enforceForProfileGroup= */ false, "getSmsRoleHolder", context);
if (!UserUtils.isUserExistent(userId, getContext())) {
Log.e(LOG_TAG, "user " + userId + " does not exist");
return null;
@@ -938,7 +1046,8 @@ public class RoleService extends SystemService implements RoleUserState.Callback
@Override
public String getEmergencyRoleHolder(int userId) {
final Context context = getContext();
- UserUtils.enforceCrossUserPermission(userId, false, "getEmergencyRoleHolder", context);
+ UserUtils.enforceCrossUserPermission(userId, /* allowAll= */ false,
+ /* enforceForProfileGroup= */ false, "getEmergencyRoleHolder", context);
if (!UserUtils.isUserExistent(userId, getContext())) {
Log.e(LOG_TAG, "user " + userId + " does not exist");
return null;
@@ -964,8 +1073,8 @@ public class RoleService extends SystemService implements RoleUserState.Callback
@Override
public boolean isRoleVisibleAsUser(@NonNull String roleName, @UserIdInt int userId) {
- UserUtils.enforceCrossUserPermission(userId, false, "isRoleVisibleAsUser",
- getContext());
+ UserUtils.enforceCrossUserPermission(userId, /* allowAll= */ false,
+ /* enforceForProfileGroup= */ false, "isRoleVisibleAsUser", getContext());
if (!UserUtils.isUserExistent(userId, getContext())) {
Log.e(LOG_TAG, "user " + userId + " does not exist");
return false;
@@ -982,8 +1091,9 @@ public class RoleService extends SystemService implements RoleUserState.Callback
@Override
public boolean isApplicationVisibleForRoleAsUser(@NonNull String roleName,
@NonNull String packageName, @UserIdInt int userId) {
- UserUtils.enforceCrossUserPermission(userId, false,
- "isApplicationVisibleForRoleAsUser", getContext());
+ UserUtils.enforceCrossUserPermission(userId, /* allowAll= */ false,
+ /* enforceForProfileGroup= */ false, "isApplicationVisibleForRoleAsUser",
+ getContext());
if (!UserUtils.isUserExistent(userId, getContext())) {
Log.e(LOG_TAG, "user " + userId + " does not exist");
return false;
@@ -1040,6 +1150,36 @@ public class RoleService extends SystemService implements RoleUserState.Callback
return true;
}
}
+
+ private void enforceCallingOrSelfAnyPermissions(@NonNull String[] permissions,
+ @NonNull String message) {
+ for (String permission : permissions) {
+ if (getContext().checkCallingOrSelfPermission(permission)
+ == PackageManager.PERMISSION_GRANTED) {
+ return;
+ }
+ }
+
+ throw new SecurityException(message + ": Neither user " + Binder.getCallingUid()
+ + " nor current process has at least one of" + Arrays.toString(permissions)
+ + ".");
+ }
+
+ private void enforceProfileGroupExclusiveRole(@NonNull String roleName) {
+ Preconditions.checkStringNotEmpty(roleName, "roleName cannot be null or empty");
+ Role role = Roles.get(getContext()).get(roleName);
+ Objects.requireNonNull(role, "Unknown role: " + roleName);
+ Preconditions.checkArgument(
+ role.getExclusivity() == Role.EXCLUSIVITY_PROFILE_GROUP,
+ roleName + " is not a profile-group exclusive role");
+ }
+
+ @ChecksSdkIntAtLeast(api = Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ private boolean isProfileGroupExclusivityAvailable() {
+ // TODO(b/372743073): change to isAtLeastB once available
+ return SdkLevel.isAtLeastV()
+ && com.android.permission.flags.Flags.crossUserRoleEnabled();
+ }
}
private class Local implements RoleManagerLocal {
diff --git a/service/java/com/android/role/RoleShellCommand.java b/service/java/com/android/role/RoleShellCommand.java
index 808a64cb4..538e62f47 100644
--- a/service/java/com/android/role/RoleShellCommand.java
+++ b/service/java/com/android/role/RoleShellCommand.java
@@ -22,11 +22,11 @@ import android.app.role.IRoleManager;
import android.os.Build;
import android.os.RemoteCallback;
import android.os.RemoteException;
+import android.permission.internal.compat.UserHandleCompat;
import androidx.annotation.RequiresApi;
import com.android.modules.utils.BasicShellCommandHandler;
-import com.android.permission.compat.UserHandleCompat;
import java.io.PrintWriter;
import java.util.List;
@@ -87,6 +87,10 @@ class RoleShellCommand extends BasicShellCommandHandler {
return runClearRoleHolders();
case "set-bypassing-role-qualification":
return runSetBypassingRoleQualification();
+ case "get-active-user-for-role":
+ return runGetActiveUserForRole();
+ case "set-active-user-for-role":
+ return runSetActiveUserForRole();
default:
return handleDefaultCommands(cmd);
}
@@ -162,6 +166,27 @@ class RoleShellCommand extends BasicShellCommandHandler {
return 0;
}
+ private int runGetActiveUserForRole() throws RemoteException {
+ int userId = getUserIdMaybe();
+ String roleName = getNextArgRequired();
+
+ int activeUserId = mRoleManager.getActiveUserForRoleAsUser(roleName, userId);
+ if (activeUserId != UserHandleCompat.USER_NULL) {
+ getOutPrintWriter().println(activeUserId);
+ }
+ return 0;
+ }
+
+ private int runSetActiveUserForRole() throws RemoteException {
+ int userId = getUserIdMaybe();
+ String roleName = getNextArgRequired();
+ int activeUserId = Integer.parseInt(getNextArgRequired());
+ int flags = getFlagsMaybe();
+
+ mRoleManager.setActiveUserForRoleAsUser(roleName, activeUserId, flags, userId);
+ return 0;
+ }
+
@Override
public void onHelp() {
PrintWriter pw = getOutPrintWriter();
@@ -174,6 +199,8 @@ class RoleShellCommand extends BasicShellCommandHandler {
pw.println(" remove-role-holder [--user USER_ID] ROLE PACKAGE [FLAGS]");
pw.println(" clear-role-holders [--user USER_ID] ROLE [FLAGS]");
pw.println(" set-bypassing-role-qualification true|false");
+ pw.println(" get-active-user-for-role [--user USER_ID] ROLE");
+ pw.println(" set-active-user-for-role [--user USER_ID] ROLE ACTIVE_USER_ID [FLAGS]");
pw.println();
}
}
diff --git a/service/java/com/android/role/RoleUserState.java b/service/java/com/android/role/RoleUserState.java
index 81007d65e..c94b58826 100644
--- a/service/java/com/android/role/RoleUserState.java
+++ b/service/java/com/android/role/RoleUserState.java
@@ -24,6 +24,7 @@ import android.annotation.WorkerThread;
import android.os.Build;
import android.os.Handler;
import android.os.UserHandle;
+import android.permission.internal.compat.UserHandleCompat;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.Log;
@@ -39,6 +40,7 @@ import com.android.role.persistence.RolesState;
import com.android.server.role.RoleServicePlatformHelper;
import java.util.ArrayList;
+import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@@ -96,6 +98,10 @@ class RoleUserState {
private ArraySet<String> mFallbackEnabledRoles = new ArraySet<>();
@GuardedBy("mLock")
+ @NonNull
+ private ArrayMap<String, Integer> mActiveUserIds = new ArrayMap<>();
+
+ @GuardedBy("mLock")
private boolean mWriteScheduled;
@GuardedBy("mLock")
@@ -423,6 +429,42 @@ class RoleUserState {
}
/**
+ * Return the active user for the role
+ *
+ * @param roleName the name of the role to get the active user for
+ */
+ public int getActiveUserForRole(@NonNull String roleName) {
+ synchronized (mLock) {
+ return mActiveUserIds.getOrDefault(roleName, UserHandleCompat.USER_NULL);
+ }
+ }
+
+ /**
+ * Set the active user for the role
+ *
+ * @param roleName the name of the role to set the active user for
+ * @param userId User id to set as active for this role
+ * @return whether any changes were made
+ */
+ public boolean setActiveUserForRole(@NonNull String roleName, @UserIdInt int userId) {
+ if (!com.android.permission.flags.Flags.crossUserRoleEnabled()) {
+ return false;
+ }
+ synchronized (mLock) {
+ Integer currentActiveUserId = mActiveUserIds.get(roleName);
+ // If we have pre-existing roles that weren't profile group exclusive and don't have an
+ // active user, ensure we set and write value, and return modified, otherwise other
+ // users might not have role holder revoked.
+ if (currentActiveUserId != null && currentActiveUserId == userId) {
+ return false;
+ }
+ mActiveUserIds.put(roleName, userId);
+ scheduleWriteFileLocked();
+ return true;
+ }
+ }
+
+ /**
* Schedule writing the state to file.
*/
@GuardedBy("mLock")
@@ -449,9 +491,15 @@ class RoleUserState {
// Force a reconciliation on next boot if we are bypassing role qualification now.
String packagesHash = mBypassingRoleQualification ? null : mPackagesHash;
- roles = new RolesState(mVersion, packagesHash,
- (Map<String, Set<String>>) (Map<String, ?>) snapshotRolesLocked(),
- snapshotFallbackEnabledRoles());
+ if (com.android.permission.flags.Flags.crossUserRoleEnabled()) {
+ roles = new RolesState(mVersion, packagesHash,
+ (Map<String, Set<String>>) (Map<String, ?>) snapshotRolesLocked(),
+ snapshotFallbackEnabledRoles(), snapshotActiveUserIds());
+ } else {
+ roles = new RolesState(mVersion, packagesHash,
+ (Map<String, Set<String>>) (Map<String, ?>) snapshotRolesLocked(),
+ snapshotFallbackEnabledRoles());
+ }
}
mPersistence.writeForUser(roles, UserHandle.of(mUserId));
@@ -463,14 +511,17 @@ class RoleUserState {
Map<String, Set<String>> roles;
Set<String> fallbackEnabledRoles;
+ Map<String, Integer> activeUserIds;
if (roleState != null) {
mVersion = roleState.getVersion();
mPackagesHash = roleState.getPackagesHash();
roles = roleState.getRoles();
fallbackEnabledRoles = roleState.getFallbackEnabledRoles();
+ activeUserIds = roleState.getActiveUserIds();
} else {
roles = mPlatformHelper.getLegacyRoleState(mUserId);
fallbackEnabledRoles = roles.keySet();
+ activeUserIds = Collections.emptyMap();
}
mRoles.clear();
for (Map.Entry<String, Set<String>> entry : roles.entrySet()) {
@@ -480,6 +531,10 @@ class RoleUserState {
}
mFallbackEnabledRoles.clear();
mFallbackEnabledRoles.addAll(fallbackEnabledRoles);
+ mActiveUserIds.clear();
+ if (com.android.permission.flags.Flags.crossUserRoleEnabled()) {
+ mActiveUserIds.putAll(activeUserIds);
+ }
if (roleState == null) {
scheduleWriteFileLocked();
}
@@ -496,12 +551,14 @@ class RoleUserState {
int version;
String packagesHash;
ArrayMap<String, ArraySet<String>> roles;
+ ArrayMap<String, Integer> activeUserIds;
ArraySet<String> fallbackEnabledRoles;
synchronized (mLock) {
version = mVersion;
packagesHash = mPackagesHash;
roles = snapshotRolesLocked();
fallbackEnabledRoles = snapshotFallbackEnabledRoles();
+ activeUserIds = snapshotActiveUserIds();
}
long fieldToken = dumpOutputStream.start(fieldName, fieldId);
@@ -514,10 +571,14 @@ class RoleUserState {
String roleName = roles.keyAt(rolesIndex);
ArraySet<String> roleHolders = roles.valueAt(rolesIndex);
boolean fallbackEnabled = fallbackEnabledRoles.contains(roleName);
+ Integer activeUserId = activeUserIds.get(roleName);
long rolesToken = dumpOutputStream.start("roles", RoleUserStateProto.ROLES);
dumpOutputStream.write("name", RoleProto.NAME, roleName);
dumpOutputStream.write("fallback_enabled", RoleProto.FALLBACK_ENABLED, fallbackEnabled);
+ if (activeUserId != null) {
+ dumpOutputStream.write("active_user_id", RoleProto.ACTIVE_USER_ID, activeUserId);
+ }
int roleHoldersSize = roleHolders.size();
for (int roleHoldersIndex = 0; roleHoldersIndex < roleHoldersSize; roleHoldersIndex++) {
String roleHolder = roleHolders.valueAt(roleHoldersIndex);
@@ -563,6 +624,12 @@ class RoleUserState {
return new ArraySet<>(mFallbackEnabledRoles);
}
+ @GuardedBy("mLock")
+ @NonNull
+ private ArrayMap<String, Integer> snapshotActiveUserIds() {
+ return new ArrayMap<>(mActiveUserIds);
+ }
+
/**
* Destroy this user state and delete the corresponding file. Any pending writes to the file
* will be cancelled, and any future interaction with this state will throw an exception.
diff --git a/service/java/com/android/role/persistence/RolesPersistenceImpl.java b/service/java/com/android/role/persistence/RolesPersistenceImpl.java
index 220a8440b..8382d3632 100644
--- a/service/java/com/android/role/persistence/RolesPersistenceImpl.java
+++ b/service/java/com/android/role/persistence/RolesPersistenceImpl.java
@@ -67,6 +67,7 @@ public class RolesPersistenceImpl implements RolesPersistence {
private static final String ATTRIBUTE_VERSION = "version";
private static final String ATTRIBUTE_NAME = "name";
private static final String ATTRIBUTE_FALLBACK_ENABLED = "fallbackEnabled";
+ private static final String ATTRIBUTE_ACTIVE_USER_ID = "activeUserId";
private static final String ATTRIBUTE_PACKAGES_HASH = "packagesHash";
@VisibleForTesting
@@ -144,6 +145,7 @@ public class RolesPersistenceImpl implements RolesPersistence {
Map<String, Set<String>> roles = new ArrayMap<>();
Set<String> fallbackEnabledRoles = new ArraySet<>();
+ Map<String, Integer> activeUserIds = new ArrayMap<>();
int type;
int depth;
int innerDepth = parser.getDepth() + 1;
@@ -159,12 +161,23 @@ public class RolesPersistenceImpl implements RolesPersistence {
if (Boolean.parseBoolean(fallbackEnabled)) {
fallbackEnabledRoles.add(roleName);
}
+ if (com.android.permission.flags.Flags.crossUserRoleEnabled()) {
+ String activeUserId = parser.getAttributeValue(null, ATTRIBUTE_ACTIVE_USER_ID);
+ if (activeUserId != null) {
+ activeUserIds.put(roleName, Integer.parseInt(activeUserId));
+ }
+ }
Set<String> roleHolders = parseRoleHolders(parser);
roles.put(roleName, roleHolders);
}
}
- return new RolesState(version, packagesHash, roles, fallbackEnabledRoles);
+ if (com.android.permission.flags.Flags.crossUserRoleEnabled()) {
+ return new RolesState(version, packagesHash, roles, fallbackEnabledRoles,
+ activeUserIds);
+ } else {
+ return new RolesState(version, packagesHash, roles, fallbackEnabledRoles);
+ }
}
@NonNull
@@ -244,15 +257,22 @@ public class RolesPersistenceImpl implements RolesPersistence {
}
Set<String> fallbackEnabledRoles = roles.getFallbackEnabledRoles();
+ Map<String, Integer> activeUserIds = roles.getActiveUserIds();
for (Map.Entry<String, Set<String>> entry : roles.getRoles().entrySet()) {
String roleName = entry.getKey();
Set<String> roleHolders = entry.getValue();
boolean isFallbackEnabled = fallbackEnabledRoles.contains(roleName);
+ Integer activeUserId = com.android.permission.flags.Flags.crossUserRoleEnabled()
+ ? activeUserIds.get(roleName) : null;
serializer.startTag(null, TAG_ROLE);
serializer.attribute(null, ATTRIBUTE_NAME, roleName);
serializer.attribute(null, ATTRIBUTE_FALLBACK_ENABLED,
Boolean.toString(isFallbackEnabled));
+ if (activeUserId != null) {
+ serializer.attribute(
+ null, ATTRIBUTE_ACTIVE_USER_ID, Integer.toString(activeUserId));
+ }
serializeRoleHolders(serializer, roleHolders);
serializer.endTag(null, TAG_ROLE);
}
diff --git a/service/java/com/android/role/persistence/RolesState.java b/service/java/com/android/role/persistence/RolesState.java
index a189dd4c2..f1b3d8dfa 100644
--- a/service/java/com/android/role/persistence/RolesState.java
+++ b/service/java/com/android/role/persistence/RolesState.java
@@ -23,12 +23,13 @@ import android.annotation.SystemApi;
import android.annotation.SystemApi.Client;
import android.permission.flags.Flags;
+import java.util.Collections;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
/**
- * State of all roles.
+ * State of all roles for a user.
*
* TODO(b/147914847): Remove @hide when it becomes the default.
* @hide
@@ -59,6 +60,12 @@ public final class RolesState {
private final Set<String> mFallbackEnabledRoles;
/**
+ * The active users for cross user roles.
+ */
+ @NonNull
+ private final Map<String, Integer> mActiveUserIds;
+
+ /**
* Create a new instance of this class.
*
* @param version the version of the roles
@@ -81,10 +88,27 @@ public final class RolesState {
@FlaggedApi(Flags.FLAG_SYSTEM_SERVER_ROLE_CONTROLLER_ENABLED)
public RolesState(int version, @Nullable String packagesHash,
@NonNull Map<String, Set<String>> roles, @NonNull Set<String> fallbackEnabledRoles) {
+ this(version, packagesHash, roles, fallbackEnabledRoles, Collections.emptyMap());
+ }
+
+ /**
+ * Create a new instance of this class.
+ *
+ * @param version the version of the roles
+ * @param packagesHash the hash of all packages in the system
+ * @param roles the roles
+ * @param fallbackEnabledRoles the roles with fallback enabled
+ * @param activeUserIds the active users for cross user roles
+ * @hide
+ */
+ public RolesState(int version, @Nullable String packagesHash,
+ @NonNull Map<String, Set<String>> roles, @NonNull Set<String> fallbackEnabledRoles,
+ @NonNull Map<String, Integer> activeUserIds) {
mVersion = version;
mPackagesHash = packagesHash;
mRoles = roles;
mFallbackEnabledRoles = fallbackEnabledRoles;
+ mActiveUserIds = activeUserIds;
}
/**
@@ -127,6 +151,17 @@ public final class RolesState {
return mFallbackEnabledRoles;
}
+ /**
+ * Get the active users for cross user roles.
+ *
+ * @return active users for cross user roles
+ * @hide
+ */
+ @NonNull
+ public Map<String, Integer> getActiveUserIds() {
+ return mActiveUserIds;
+ }
+
@Override
public boolean equals(Object object) {
if (this == object) {
@@ -139,11 +174,12 @@ public final class RolesState {
return mVersion == that.mVersion
&& Objects.equals(mPackagesHash, that.mPackagesHash)
&& Objects.equals(mRoles, that.mRoles)
- && Objects.equals(mFallbackEnabledRoles, that.mFallbackEnabledRoles);
+ && Objects.equals(mFallbackEnabledRoles, that.mFallbackEnabledRoles)
+ && Objects.equals(mActiveUserIds, that.mActiveUserIds);
}
@Override
public int hashCode() {
- return Objects.hash(mVersion, mPackagesHash, mRoles, mFallbackEnabledRoles);
+ return Objects.hash(mVersion, mPackagesHash, mRoles, mFallbackEnabledRoles, mActiveUserIds);
}
}
diff --git a/service/java/com/android/safetycenter/SafetyCenterService.java b/service/java/com/android/safetycenter/SafetyCenterService.java
index 250be5f25..6df4184e1 100644
--- a/service/java/com/android/safetycenter/SafetyCenterService.java
+++ b/service/java/com/android/safetycenter/SafetyCenterService.java
@@ -694,7 +694,8 @@ public final class SafetyCenterService extends SystemService {
/** Enforces cross user permission and returns whether the user is valid. */
private boolean enforceCrossUserPermission(String message, @UserIdInt int userId) {
UserUtils.enforceCrossUserPermission(
- userId, /* allowAll= */ false, message, getContext());
+ userId, /* allowAll= */ false, /* enforceForProfileGroup= */ false, message,
+ getContext());
if (!UserUtils.isUserExistent(userId, getContext())) {
Log.w(
TAG,
diff --git a/service/java/com/android/safetycenter/UserProfileGroup.java b/service/java/com/android/safetycenter/UserProfileGroup.java
index 46a440bf7..1f5258437 100644
--- a/service/java/com/android/safetycenter/UserProfileGroup.java
+++ b/service/java/com/android/safetycenter/UserProfileGroup.java
@@ -21,15 +21,12 @@ import static java.util.Objects.requireNonNull;
import android.annotation.IntDef;
import android.annotation.UserIdInt;
import android.content.Context;
-import android.content.pm.PackageManager;
import android.os.Binder;
-import android.os.Process;
import android.os.UserHandle;
import android.os.UserManager;
+import android.permission.internal.compat.UserHandleCompat;
import android.util.Log;
-import androidx.annotation.Nullable;
-
import com.android.permission.util.UserUtils;
import java.lang.annotation.Retention;
@@ -49,8 +46,6 @@ import java.util.Objects;
public final class UserProfileGroup {
private static final String TAG = "UserProfileGroup";
- // UserHandle#USER_NULL is a @TestApi so it cannot be accessed from the mainline module.
- public static final @UserIdInt int USER_NULL = -10000;
@UserIdInt private final int mProfileParentUserId;
private final int[] mManagedProfilesUserIds;
@@ -134,20 +129,16 @@ public final class UserProfileGroup {
* is disabled.
*/
public static UserProfileGroup fromUser(Context context, @UserIdInt int userId) {
- UserManager userManager = getUserManagerForUser(userId, context);
- List<UserHandle> userProfiles = getEnabledUserProfiles(userManager);
- UserHandle profileParent = getProfileParent(userManager, userId);
- int profileParentUserId = userId;
- if (profileParent != null) {
- profileParentUserId = profileParent.getIdentifier();
- }
+ Context userContext = UserUtils.getUserContext(userId, context);
+ List<UserHandle> userProfiles = UserUtils.getUserProfiles(userContext);
+ int profileParentUserId = UserUtils.getProfileParentIdOrSelf(userId, userContext);
int[] managedProfilesUserIds = new int[userProfiles.size()];
int[] managedRunningProfilesUserIds = new int[userProfiles.size()];
int managedProfilesUserIdsLen = 0;
int managedRunningProfilesUserIdsLen = 0;
- int privateProfileUserId = USER_NULL;
+ int privateProfileUserId = UserHandleCompat.USER_NULL;
boolean privateProfileRunning = false;
for (int i = 0; i < userProfiles.size(); i++) {
@@ -192,23 +183,10 @@ public final class UserProfileGroup {
}
private static UserManager getUserManagerForUser(@UserIdInt int userId, Context context) {
- Context userContext = getUserContext(context, UserHandle.of(userId));
+ Context userContext = UserUtils.getUserContext(userId, context);
return requireNonNull(userContext.getSystemService(UserManager.class));
}
- private static Context getUserContext(Context context, UserHandle userHandle) {
- if (Process.myUserHandle().equals(userHandle)) {
- return context;
- } else {
- try {
- return context.createPackageContextAsUser(
- context.getPackageName(), /* flags= */ 0, userHandle);
- } catch (PackageManager.NameNotFoundException doesNotHappen) {
- throw new IllegalStateException(doesNotHappen);
- }
- }
- }
-
private static boolean isProfile(@UserIdInt int userId, Context context) {
// This call requires the INTERACT_ACROSS_USERS permission.
final long callingId = Binder.clearCallingIdentity();
@@ -220,27 +198,6 @@ public final class UserProfileGroup {
}
}
- private static List<UserHandle> getEnabledUserProfiles(UserManager userManager) {
- // This call requires the QUERY_USERS permission.
- final long callingId = Binder.clearCallingIdentity();
- try {
- return userManager.getUserProfiles();
- } finally {
- Binder.restoreCallingIdentity(callingId);
- }
- }
-
- @Nullable
- private static UserHandle getProfileParent(UserManager userManager, @UserIdInt int userId) {
- // This call requires the INTERACT_ACROSS_USERS permission.
- final long callingId = Binder.clearCallingIdentity();
- try {
- return userManager.getProfileParent(UserHandle.of(userId));
- } finally {
- Binder.restoreCallingIdentity(callingId);
- }
- }
-
/** Returns the profile parent user id of the {@link UserProfileGroup}. */
public int getProfileParentUserId() {
return mProfileParentUserId;
@@ -262,7 +219,7 @@ public final class UserProfileGroup {
/* destPos= */ 1,
mManagedProfilesUserIds.length);
- if (mPrivateProfileUserId != USER_NULL) {
+ if (mPrivateProfileUserId != UserHandleCompat.USER_NULL) {
allProfileIds[allProfileIds.length - 1] = mPrivateProfileUserId;
}
@@ -303,7 +260,7 @@ public final class UserProfileGroup {
case PROFILE_TYPE_MANAGED:
return mManagedProfilesUserIds;
case PROFILE_TYPE_PRIVATE:
- return mPrivateProfileUserId != USER_NULL
+ return mPrivateProfileUserId != UserHandleCompat.USER_NULL
? new int[]{mPrivateProfileUserId} : new int[]{};
default:
Log.w(TAG, "profiles requested for unexpected profile type " + profileType);
@@ -342,7 +299,7 @@ public final class UserProfileGroup {
private int getNumProfiles() {
return 1
+ mManagedProfilesUserIds.length
- + (mPrivateProfileUserId == USER_NULL ? 0 : 1);
+ + (mPrivateProfileUserId == UserHandleCompat.USER_NULL ? 0 : 1);
}
/**
@@ -395,7 +352,8 @@ public final class UserProfileGroup {
}
}
- return USER_NULL != mPrivateProfileUserId && userId == mPrivateProfileUserId;
+ return UserHandleCompat.USER_NULL != mPrivateProfileUserId
+ && userId == mPrivateProfileUserId;
}
@Override
diff --git a/service/lint-baseline.xml b/service/lint-baseline.xml
index b10928320..6aaf3da8c 100644
--- a/service/lint-baseline.xml
+++ b/service/lint-baseline.xml
@@ -35,17 +35,6 @@
</issue>
<issue
- id="NewApi"
- message="Call requires API level 31 (current min is 30): `android.os.UserHandle#getUid`"
- errorLine1=" return UserHandle.of(userId).getUid(appId);"
- errorLine2=" ~~~~~~">
- <location
- file="packages/modules/Permission/service/java/com/android/permission/compat/UserHandleCompat.java"
- line="57"
- column="38"/>
- </issue>
-
- <issue
id="FlaggedApi"
message="Method `RolesState()` is a flagged API and should be inside an `if (Flags.systemServerRoleControllerEnabled())` check (or annotate the surrounding method `writeFile` with `@FlaggedApi(Flags.FLAG_SYSTEM_SERVER_ROLE_CONTROLLER_ENABLED) to transfer requirement to caller`)"
errorLine1=" roles = new RolesState(mVersion, packagesHash,"
diff --git a/service/proto/role_service.proto b/service/proto/role_service.proto
index f982ead5b..fb1866d83 100644
--- a/service/proto/role_service.proto
+++ b/service/proto/role_service.proto
@@ -56,4 +56,7 @@ message RoleProto {
// Whether fallback holders are enabled for this role.
optional bool fallback_enabled = 3;
+
+ // The active user id of this cross user role.
+ optional int32 active_user_id = 4;
}
diff --git a/tests/apex/Android.bp b/tests/apex/Android.bp
index 18f1bea75..9dfbdf589 100644
--- a/tests/apex/Android.bp
+++ b/tests/apex/Android.bp
@@ -31,6 +31,8 @@ android_test {
"androidx.test.rules",
"androidx.test.ext.junit",
"androidx.test.ext.truth",
+ "com.android.permission.flags-aconfig-java",
+ "flag-junit",
"mockito-target-extended-minus-junit4",
],
jni_libs: [
diff --git a/tests/apex/java/com/android/role/persistence/RolesPersistenceTest.kt b/tests/apex/java/com/android/role/persistence/RolesPersistenceTest.kt
index 6500b3926..e9c93a33a 100644
--- a/tests/apex/java/com/android/role/persistence/RolesPersistenceTest.kt
+++ b/tests/apex/java/com/android/role/persistence/RolesPersistenceTest.kt
@@ -20,12 +20,18 @@ import android.content.ApexEnvironment
import android.content.Context
import android.os.Process
import android.os.UserHandle
+import android.platform.test.annotations.RequiresFlagsDisabled
+import android.platform.test.annotations.RequiresFlagsEnabled
+import android.platform.test.flag.junit.DeviceFlagsValueProvider
import androidx.test.platform.app.InstrumentationRegistry
import com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession
+import com.android.permission.flags.Flags
import com.google.common.truth.Truth.assertThat
import java.io.File
import org.junit.After
+import org.junit.Assume.assumeFalse
import org.junit.Before
+import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
@@ -49,11 +55,22 @@ class RolesPersistenceTest {
private val persistence = RolesPersistenceImpl {}
private val defaultRoles = mapOf(ROLE_NAME to setOf(HOLDER_1, HOLDER_2))
+ private val activeUserIds = mapOf(ROLE_NAME to USER_ID)
private val stateVersionUndefined = RolesState(VERSION_UNDEFINED, PACKAGE_HASH, defaultRoles)
private val stateVersionFallbackMigrated =
RolesState(VERSION_FALLBACK_MIGRATED, PACKAGE_HASH, defaultRoles, setOf(ROLE_NAME))
+ private val stateVersionActiveUserIds =
+ RolesState(
+ VERSION_ACTIVE_USER_IDS,
+ PACKAGE_HASH,
+ defaultRoles,
+ setOf(ROLE_NAME),
+ activeUserIds,
+ )
private val user = Process.myUserHandle()
+ @get:Rule val flagsRule = DeviceFlagsValueProvider.createCheckFlagsRule()
+
@Before
fun setUp() {
createMockDataDirectory()
@@ -84,16 +101,41 @@ class RolesPersistenceTest {
mockitoSession.finishMocking()
}
+ @RequiresFlagsDisabled(Flags.FLAG_CROSS_USER_ROLE_ENABLED)
@Test
fun testWriteRead() {
+ assumeFalse(stateVersion == StateVersion.VERSION_ACTIVE_USER_IDS)
persistence.writeForUser(state, user)
val persistedState = persistence.readForUser(user)
assertThat(persistedState).isEqualTo(state)
}
+ @RequiresFlagsEnabled(Flags.FLAG_CROSS_USER_ROLE_ENABLED)
+ @Test
+ fun testWriteRead_supportsActiveUser() {
+ persistence.writeForUser(state, user)
+ val persistedState = persistence.readForUser(user)
+
+ assertThat(persistedState).isEqualTo(state)
+ }
+
+ @RequiresFlagsDisabled(Flags.FLAG_CROSS_USER_ROLE_ENABLED)
@Test
fun testWriteCorruptReadFromReserveCopy() {
+ assumeFalse(stateVersion == StateVersion.VERSION_ACTIVE_USER_IDS)
+ persistence.writeForUser(state, user)
+ // Corrupt the primary file.
+ RolesPersistenceImpl.getFile(user)
+ .writeText("<roles version=\"-1\"><role name=\"com.foo.bar\"><holder")
+ val persistedState = persistence.readForUser(user)
+
+ assertThat(persistedState).isEqualTo(state)
+ }
+
+ @RequiresFlagsEnabled(Flags.FLAG_CROSS_USER_ROLE_ENABLED)
+ @Test
+ fun testWriteCorruptReadFromReserveCopy_supportsActiveUser() {
persistence.writeForUser(state, user)
// Corrupt the primary file.
RolesPersistenceImpl.getFile(user)
@@ -116,11 +158,13 @@ class RolesPersistenceTest {
when (stateVersion) {
StateVersion.VERSION_UNDEFINED -> stateVersionUndefined
StateVersion.VERSION_FALLBACK_MIGRATED -> stateVersionFallbackMigrated
+ StateVersion.VERSION_ACTIVE_USER_IDS -> stateVersionActiveUserIds
}
enum class StateVersion {
VERSION_UNDEFINED,
- VERSION_FALLBACK_MIGRATED
+ VERSION_FALLBACK_MIGRATED,
+ VERSION_ACTIVE_USER_IDS,
}
companion object {
@@ -130,10 +174,12 @@ class RolesPersistenceTest {
private const val VERSION_UNDEFINED = -1
private const val VERSION_FALLBACK_MIGRATED = 1
+ private const val VERSION_ACTIVE_USER_IDS = 2
private const val APEX_MODULE_NAME = "com.android.permission"
private const val PACKAGE_HASH = "packagesHash"
private const val ROLE_NAME = "roleName"
private const val HOLDER_1 = "holder1"
private const val HOLDER_2 = "holder2"
+ private const val USER_ID = 10
}
}
diff --git a/tests/cts/permission/src/android/permission/cts/BackgroundPermissionsTest.java b/tests/cts/permission/src/android/permission/cts/BackgroundPermissionsTest.java
index f3f47631c..f4bed4ada 100644
--- a/tests/cts/permission/src/android/permission/cts/BackgroundPermissionsTest.java
+++ b/tests/cts/permission/src/android/permission/cts/BackgroundPermissionsTest.java
@@ -24,6 +24,7 @@ import static android.app.AppOpsManager.MODE_FOREGROUND;
import static android.app.AppOpsManager.MODE_IGNORED;
import static android.content.pm.PermissionInfo.PROTECTION_DANGEROUS;
import static android.content.pm.PermissionInfo.PROTECTION_INTERNAL;
+import static android.health.connect.HealthPermissions.HEALTH_PERMISSION_GROUP;
import static android.permission.cts.PermissionUtils.getAppOp;
import static android.permission.cts.PermissionUtils.grantPermission;
import static android.permission.cts.PermissionUtils.install;
@@ -31,6 +32,7 @@ import static android.permission.cts.PermissionUtils.uninstallApp;
import static com.android.compatibility.common.util.SystemUtil.eventually;
+import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
import static org.junit.Assert.assertNotEquals;
@@ -43,14 +45,21 @@ import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PermissionInfo;
+import android.os.Build;
+import android.permission.flags.Flags;
import android.platform.test.annotations.AppModeFull;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
import android.util.ArrayMap;
import android.util.Log;
+import androidx.test.filters.SdkSuppress;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.runner.AndroidJUnit4;
import org.junit.After;
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -72,6 +81,9 @@ public class BackgroundPermissionsTest {
private static final UiAutomation sUiAutomation =
InstrumentationRegistry.getInstrumentation().getUiAutomation();
+ @Rule
+ public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+
@After
public void uninstallTestApp() {
uninstallApp(APP_PKG);
@@ -79,9 +91,24 @@ public class BackgroundPermissionsTest {
@Test
@AppModeFull(reason = "Instant apps cannot read properties of other packages")
- public void verifybackgroundPermissionsProperties() throws Exception {
+ public void verifyBackgroundPropertiesForPlatformPermissions() throws Exception {
+ verifyBackgroundPermissionsProperties("android");
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.BAKLAVA, codeName = "Baklava")
+ @RequiresFlagsEnabled({Flags.FLAG_REPLACE_BODY_SENSOR_PERMISSION_ENABLED})
+ @Test
+ @AppModeFull(reason = "Instant apps cannot read properties of other packages")
+ public void verifyBackgroundPropertiesForHealthPermissions() throws Exception {
+ String healthPackageName = sContext.getPackageManager().getPermissionGroupInfo(
+ HEALTH_PERMISSION_GROUP, /* flags= */ 0).packageName;
+ verifyBackgroundPermissionsProperties(healthPackageName);
+ }
+
+ private void verifyBackgroundPermissionsProperties(String packageName)
+ throws Exception {
PackageInfo pkg = sContext.getPackageManager().getPackageInfo(
- "android", PackageManager.GET_PERMISSIONS);
+ packageName, PackageManager.GET_PERMISSIONS);
ArrayMap<String, String> potentialBackgroundPermissionsToGroup = new ArrayMap<>();
int numPermissions = pkg.permissions.length;
@@ -97,11 +124,13 @@ public class BackgroundPermissionsTest {
}
}
+ int backgroundPermissionCount = 0;
for (int i = 0; i < numPermissions; i++) {
PermissionInfo permission = pkg.permissions[i];
String backgroundPermissionName = permission.backgroundPermission;
if (backgroundPermissionName != null) {
+ backgroundPermissionCount += 1;
Log.i(LOG_TAG, permission.name + "->" + backgroundPermissionName);
// foreground permissions must be dangerous
@@ -115,6 +144,8 @@ public class BackgroundPermissionsTest {
.containsKey(backgroundPermissionName));
}
}
+ // Tested packages must have at least one permission linked with a background permission.
+ assertThat(backgroundPermissionCount).isGreaterThan(0);
}
/**
diff --git a/tests/cts/permission/src/android/permission/cts/NoWifiStatePermissionTest.java b/tests/cts/permission/src/android/permission/cts/NoWifiStatePermissionTest.java
index a0637827c..9fff22747 100644
--- a/tests/cts/permission/src/android/permission/cts/NoWifiStatePermissionTest.java
+++ b/tests/cts/permission/src/android/permission/cts/NoWifiStatePermissionTest.java
@@ -30,9 +30,6 @@ import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import androidx.test.platform.app.InstrumentationRegistry;
-import com.android.compatibility.common.util.UserHelper;
-
-import org.junit.Assume;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -217,11 +214,6 @@ public class NoWifiStatePermissionTest {
*/
@Test(expected = SecurityException.class)
public void testSetWifiEnabled() {
- // Skip the test for passenger on Multi-user-multi-display devices for Automotive
- UserHelper userHelper = new UserHelper(sContext);
- Assume.assumeFalse(
- "Skipped for visible background User as wifi is disabled for visible background "
- + "user.", userHelper.isVisibleBackgroundUser());
mWifiManager.setWifiEnabled(true);
}
}
diff --git a/tests/cts/permission/src/android/permission/cts/OneTimePermissionTest.java b/tests/cts/permission/src/android/permission/cts/OneTimePermissionTest.java
index 2692c6e7c..291633aab 100644
--- a/tests/cts/permission/src/android/permission/cts/OneTimePermissionTest.java
+++ b/tests/cts/permission/src/android/permission/cts/OneTimePermissionTest.java
@@ -290,34 +290,18 @@ public class OneTimePermissionTest {
}
private void exitApp() {
- boolean[] hasExited = {false};
- try {
- new Thread(() -> {
- while (!hasExited[0]) {
- DreamManager mDreamManager = mContext.getSystemService(DreamManager.class);
- mUiDevice.pressBack();
- runWithShellPermissionIdentity(() -> {
- if (mDreamManager.isDreaming()) {
- mDreamManager.stopDream();
- }
- });
- try {
- Thread.sleep(1000);
- } catch (InterruptedException e) {
- }
+ eventually(() -> {
+ mUiDevice.pressBack();
+ runWithShellPermissionIdentity(() -> {
+ DreamManager mDreamManager = mContext.getSystemService(DreamManager.class);
+ if (mDreamManager.isDreaming()) {
+ mDreamManager.stopDream();
}
- }).start();
- eventually(() -> {
- runWithShellPermissionIdentity(() -> {
- if (mActivityManager.getPackageImportance(APP_PKG_NAME)
- <= IMPORTANCE_FOREGROUND) {
- throw new AssertionError("Unable to exit application");
- }
- });
+ Assert.assertFalse("Unable to exit application",
+ mActivityManager.getPackageImportance(APP_PKG_NAME)
+ <= IMPORTANCE_FOREGROUND);
});
- } finally {
- hasExited[0] = true;
- }
+ });
}
private void clickOneTimeButton() throws Throwable {
diff --git a/tests/cts/permission/src/android/permission/cts/SplitPermissionsSystemTest.java b/tests/cts/permission/src/android/permission/cts/SplitPermissionsSystemTest.java
index 776a1065e..f2d59cbe7 100755
--- a/tests/cts/permission/src/android/permission/cts/SplitPermissionsSystemTest.java
+++ b/tests/cts/permission/src/android/permission/cts/SplitPermissionsSystemTest.java
@@ -45,9 +45,15 @@ import static org.junit.Assert.assertEquals;
import static org.junit.Assume.assumeTrue;
import android.content.Context;
+import android.health.connect.HealthPermissions;
import android.os.Build;
import android.permission.PermissionManager;
import android.permission.PermissionManager.SplitPermissionInfo;
+import android.permission.flags.Flags;
+import android.platform.test.annotations.RequiresFlagsDisabled;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
import androidx.test.InstrumentationRegistry;
import androidx.test.filters.SdkSuppress;
@@ -56,6 +62,7 @@ import androidx.test.runner.AndroidJUnit4;
import com.android.compatibility.common.util.ApiLevelUtil;
import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -71,6 +78,9 @@ public class SplitPermissionsSystemTest {
private List<SplitPermissionInfo> mSplitPermissions;
+ @Rule
+ public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+
@Before
public void before() {
Context context = InstrumentationRegistry.getContext();
@@ -87,9 +97,14 @@ public class SplitPermissionsSystemTest {
for (SplitPermissionInfo split : mSplitPermissions) {
String splitPermission = split.getSplitPermission();
- boolean isAndroid = splitPermission.startsWith("android");
- if (!isAndroid) {
+ // Due to limitation with accessing flag values in tests, BODY_SENSORS relevant splits
+ // are handled in its dedicated tests.
+ boolean shouldSkip =
+ !splitPermission.startsWith("android")
+ || splitPermission.equals(BODY_SENSORS)
+ || splitPermission.equals(BODY_SENSORS_BACKGROUND);
+ if (shouldSkip) {
continue;
}
@@ -149,9 +164,6 @@ public class SplitPermissionsSystemTest {
case BLUETOOTH_SCAN:
assertSplit(split, Build.VERSION_CODES.S, BLUETOOTH, BLUETOOTH_ADMIN);
break;
- case BODY_SENSORS:
- assertSplit(split, Build.VERSION_CODES.TIRAMISU, BODY_SENSORS_BACKGROUND);
- break;
case ACCESS_MEDIA_LOCATION:
case READ_MEDIA_IMAGES:
case READ_MEDIA_VIDEO:
@@ -160,7 +172,56 @@ public class SplitPermissionsSystemTest {
}
}
- assertEquals(24, seenSplits.size());
+ assertEquals(23, seenSplits.size());
+ }
+
+ @RequiresFlagsDisabled({Flags.FLAG_REPLACE_BODY_SENSOR_PERMISSION_ENABLED})
+ @Test
+ public void
+ validateBodySensors_beforeGranularHealthPermissions_isSplitToBodySensorsBackground() {
+ assumeTrue(ApiLevelUtil.isAtLeast(Build.VERSION_CODES.Q));
+
+ mSplitPermissions.stream()
+ .filter(split -> split.getSplitPermission().equals(BODY_SENSORS))
+ .findFirst()
+ .ifPresent(
+ split ->
+ assertSplit(
+ split,
+ Build.VERSION_CODES.TIRAMISU,
+ BODY_SENSORS_BACKGROUND));
+ }
+
+ @RequiresFlagsEnabled({Flags.FLAG_REPLACE_BODY_SENSOR_PERMISSION_ENABLED})
+ @Test
+ public void validateBodySensors_afterGranularHealthPermissions_isSplitToReadHeartRate() {
+ // TODO: Change this to Baklava when available.
+ assumeTrue(ApiLevelUtil.isAtLeast(36));
+
+ SplitPermissionInfo legacyBodySensorPermissionInfo = null;
+ SplitPermissionInfo readHeartRatePermissionInfo = null;
+ SplitPermissionInfo bodySensorBackgroundPermissionInfo = null;
+ for (SplitPermissionInfo split : mSplitPermissions) {
+ if (split.getSplitPermission().equals(BODY_SENSORS)
+ && split.getNewPermissions().contains(BODY_SENSORS_BACKGROUND)) {
+ legacyBodySensorPermissionInfo = split;
+ } else if (split.getSplitPermission().equals(BODY_SENSORS)
+ && split.getNewPermissions().contains(HealthPermissions.READ_HEART_RATE)) {
+ readHeartRatePermissionInfo = split;
+ } else if (split.getSplitPermission().equals(BODY_SENSORS_BACKGROUND)) {
+ bodySensorBackgroundPermissionInfo = split;
+ }
+ }
+ // Assert BODY_SENSORS is split to BODY_SENSORS_BACKGROUND and READ_HEART_RATE.
+ assertSplit(
+ legacyBodySensorPermissionInfo,
+ Build.VERSION_CODES.TIRAMISU,
+ BODY_SENSORS_BACKGROUND);
+ assertSplit(readHeartRatePermissionInfo, HealthPermissions.READ_HEART_RATE);
+ // Assert BODY_SENSORS_BACKGROUND is split to READ_HEALTH_DATA_IN_BACKGROUND.
+ assertSplit(
+ bodySensorBackgroundPermissionInfo,
+ HealthPermissions.READ_HEALTH_DATA_IN_BACKGROUND);
}
private void assertSplit(SplitPermissionInfo split, int targetSdk, String... permission) {
diff --git a/tests/cts/permissionmultidevice/src/android/permissionmultidevice/cts/DeviceAwarePermissionGrantTest.kt b/tests/cts/permissionmultidevice/src/android/permissionmultidevice/cts/DeviceAwarePermissionGrantTest.kt
index f0a0e3fc1..687234582 100644
--- a/tests/cts/permissionmultidevice/src/android/permissionmultidevice/cts/DeviceAwarePermissionGrantTest.kt
+++ b/tests/cts/permissionmultidevice/src/android/permissionmultidevice/cts/DeviceAwarePermissionGrantTest.kt
@@ -21,6 +21,7 @@ import android.app.Instrumentation
import android.companion.virtual.VirtualDeviceManager
import android.companion.virtual.VirtualDeviceParams
import android.companion.virtual.VirtualDeviceParams.DEVICE_POLICY_CUSTOM
+import android.companion.virtual.VirtualDeviceParams.DEVICE_POLICY_DEFAULT
import android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_CAMERA
import android.content.ComponentName
import android.content.Intent
@@ -39,7 +40,9 @@ import android.permissionmultidevice.cts.UiAutomatorUtils.click
import android.permissionmultidevice.cts.UiAutomatorUtils.findTextForView
import android.permissionmultidevice.cts.UiAutomatorUtils.waitFindObject
import android.platform.test.annotations.AppModeFull
+import android.platform.test.annotations.RequiresFlagsDisabled
import android.platform.test.annotations.RequiresFlagsEnabled
+import android.provider.Settings
import android.view.Display
import android.virtualdevice.cts.common.VirtualDeviceRule
import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -47,7 +50,6 @@ import androidx.test.filters.SdkSuppress
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.By
import com.android.compatibility.common.util.SystemUtil
-import com.google.common.truth.Truth
import com.google.common.truth.Truth.assertThat
import java.util.concurrent.CompletableFuture
import java.util.concurrent.TimeUnit
@@ -146,6 +148,7 @@ class DeviceAwarePermissionGrantTest {
Flags.FLAG_DEVICE_AWARE_PERMISSION_APIS_ENABLED,
Flags.FLAG_DEVICE_AWARE_PERMISSIONS_ENABLED
)
+ @RequiresFlagsDisabled(Flags.FLAG_ALLOW_HOST_PERMISSION_DIALOGS_ON_VIRTUAL_DEVICES)
@Test
fun onRemoteDevice_requestPermissionForHostDevice_shouldShowWarningDialog() {
requestPermissionOnDevice(virtualDisplay.display.displayId, DEVICE_ID_DEFAULT)
@@ -156,6 +159,32 @@ class DeviceAwarePermissionGrantTest {
@RequiresFlagsEnabled(
Flags.FLAG_DEVICE_AWARE_PERMISSION_APIS_ENABLED,
+ Flags.FLAG_DEVICE_AWARE_PERMISSIONS_ENABLED,
+ Flags.FLAG_ALLOW_HOST_PERMISSION_DIALOGS_ON_VIRTUAL_DEVICES
+ )
+ @Test
+ fun onRemoteDevice_requestPermissionForHostDevice_shouldGrantPermission() {
+ // Create a virtual device with default policy, so that camera permission request will
+ // correspond to default device camera access.
+ virtualDevice =
+ virtualDeviceRule.createManagedVirtualDevice(
+ VirtualDeviceParams.Builder()
+ .setDevicePolicy(POLICY_TYPE_CAMERA, DEVICE_POLICY_DEFAULT)
+ .build()
+ )
+ testGrantPermissionForDevice(
+ virtualDisplay.display.displayId,
+ virtualDevice.deviceId,
+ true,
+ Settings.Global.getString(defaultDeviceContext.contentResolver,
+ Settings.Global.DEVICE_NAME),
+ expectPermissionGrantedOnDefaultDevice = true,
+ expectPermissionGrantedOnRemoteDevice = false
+ )
+ }
+
+ @RequiresFlagsEnabled(
+ Flags.FLAG_DEVICE_AWARE_PERMISSION_APIS_ENABLED,
Flags.FLAG_DEVICE_AWARE_PERMISSIONS_ENABLED
)
@Test
@@ -199,7 +228,7 @@ class DeviceAwarePermissionGrantTest {
TestConstants.PERMISSION_RESULT_KEY_PERMISSIONS
)
)
- .isEqualTo(arrayOf(DEVICE_AWARE_PERMISSION))
+ .isEqualTo(arrayOf(PERMISSION))
assertThat(
grantPermissionResult.getIntArray(TestConstants.PERMISSION_RESULT_KEY_GRANT_RESULTS)
)
@@ -237,7 +266,7 @@ class DeviceAwarePermissionGrantTest {
private fun assertPermissionMessageContainsDeviceName(displayId: Int, deviceName: String) {
waitFindObject(By.displayId(displayId).res(PERMISSION_MESSAGE_ID))
val text = findTextForView(By.displayId(displayId).res(PERMISSION_MESSAGE_ID))
- Truth.assertThat(text).contains(deviceName)
+ assertThat(text).contains(deviceName)
}
private fun assertAppHasPermissionForDevice(deviceId: Int, expectPermissionGranted: Boolean) {
@@ -245,7 +274,7 @@ class DeviceAwarePermissionGrantTest {
defaultDeviceContext
.createDeviceContext(deviceId)
.packageManager
- .checkPermission(DEVICE_AWARE_PERMISSION, APP_PACKAGE_NAME)
+ .checkPermission(PERMISSION, APP_PACKAGE_NAME)
if (expectPermissionGranted) {
Assert.assertEquals(PackageManager.PERMISSION_GRANTED, checkPermissionResult)
@@ -269,7 +298,7 @@ class DeviceAwarePermissionGrantTest {
"com.android.permissioncontroller:id/permission_allow_foreground_only_button"
const val DEVICE_ID_DEFAULT = 0
const val PERSISTENT_DEVICE_ID_DEFAULT = VirtualDeviceManager.PERSISTENT_DEVICE_ID_DEFAULT
- const val DEVICE_AWARE_PERMISSION = Manifest.permission.CAMERA
+ const val PERMISSION = Manifest.permission.CAMERA
const val TIMEOUT = 5000L
private const val DISPLAY_HEIGHT = 1920
private const val DISPLAY_WIDTH = 1080
diff --git a/tests/cts/permissionmultiuser/Android.bp b/tests/cts/permissionmultiuser/Android.bp
index b86b02205..23aafb7e0 100644
--- a/tests/cts/permissionmultiuser/Android.bp
+++ b/tests/cts/permissionmultiuser/Android.bp
@@ -33,6 +33,7 @@ android_test {
"compatibility-device-util-axt",
"ctstestrunner-axt",
"Harrier",
+ "bedstead-multiuser",
"modules-utils-build_system",
"Nene",
],
diff --git a/tests/cts/permissionmultiuser/AndroidTest.xml b/tests/cts/permissionmultiuser/AndroidTest.xml
index 10fd4e7a5..f6834036b 100644
--- a/tests/cts/permissionmultiuser/AndroidTest.xml
+++ b/tests/cts/permissionmultiuser/AndroidTest.xml
@@ -63,8 +63,8 @@
<test class="com.android.tradefed.testtype.AndroidJUnitTest" >
<option name="package" value="android.permissionmultiuser.cts" />
- <option name="exclude-annotation" value="com.android.bedstead.harrier.annotations.RequireRunOnWorkProfile" />
- <option name="exclude-annotation" value="com.android.bedstead.harrier.annotations.RequireRunOnSecondaryUser" />
+ <option name="exclude-annotation" value="com.android.bedstead.enterprise.annotations.RequireRunOnWorkProfile" />
+ <option name="exclude-annotation" value="com.android.bedstead.multiuser.annotations.RequireRunOnSecondaryUser" />
<option name="runtime-hint" value="5m" />
</test>
diff --git a/tests/cts/permissionmultiuser/src/android/permissionmultiuser/cts/AppDataSharingUpdatesTest.kt b/tests/cts/permissionmultiuser/src/android/permissionmultiuser/cts/AppDataSharingUpdatesTest.kt
index 2169f0f72..f3309bd3c 100644
--- a/tests/cts/permissionmultiuser/src/android/permissionmultiuser/cts/AppDataSharingUpdatesTest.kt
+++ b/tests/cts/permissionmultiuser/src/android/permissionmultiuser/cts/AppDataSharingUpdatesTest.kt
@@ -50,15 +50,16 @@ import android.support.test.uiautomator.UiObject2
import android.util.Log
import androidx.test.filters.SdkSuppress
import androidx.test.platform.app.InstrumentationRegistry
+import com.android.bedstead.enterprise.annotations.RequireRunOnWorkProfile
import com.android.bedstead.harrier.BedsteadJUnit4
import com.android.bedstead.harrier.DeviceState
import com.android.bedstead.permissions.annotations.EnsureHasPermission
import com.android.bedstead.harrier.annotations.EnsureSecureSettingSet
import com.android.bedstead.harrier.annotations.RequireDoesNotHaveFeature
import com.android.bedstead.harrier.annotations.RequireNotWatch
-import com.android.bedstead.harrier.annotations.RequireRunOnAdditionalUser
-import com.android.bedstead.harrier.annotations.RequireRunOnWorkProfile
import com.android.bedstead.harrier.annotations.RequireSdkVersion
+import com.android.bedstead.multiuser.additionalUser
+import com.android.bedstead.multiuser.annotations.RequireRunOnAdditionalUser
import com.android.bedstead.permissions.CommonPermissions.INTERACT_ACROSS_USERS
import com.android.compatibility.common.util.ApiTest
import com.android.compatibility.common.util.DeviceConfigStateChangerRule
diff --git a/tests/cts/permissionpolicy/Android.bp b/tests/cts/permissionpolicy/Android.bp
index 4249f3c9d..07fde8bff 100644
--- a/tests/cts/permissionpolicy/Android.bp
+++ b/tests/cts/permissionpolicy/Android.bp
@@ -37,6 +37,7 @@ android_test {
"permission-test-util-lib",
"androidx.test.rules",
"flag-junit",
+ "android.app.flags-aconfig",
"android.permission.flags-aconfig-java-export",
],
srcs: [
diff --git a/tests/cts/permissionpolicy/res/raw/android_manifest.xml b/tests/cts/permissionpolicy/res/raw/android_manifest.xml
index 9609ea1fe..5df9c944e 100644
--- a/tests/cts/permissionpolicy/res/raw/android_manifest.xml
+++ b/tests/cts/permissionpolicy/res/raw/android_manifest.xml
@@ -898,13 +898,26 @@
android:featureFlag="android.provider.user_keys" />
<!-- Allows an application to set default account for new contacts.
- <p> This permission is only granted to system applications fulfilling the Contacts app role.
+ <p>This permission is only granted to system applications fulfilling the Contacts app role.
<p>Protection level: internal|role
@SystemApi
@hide
-->
<permission android:name="android.permission.SET_DEFAULT_ACCOUNT_FOR_CONTACTS"
- android:protectionLevel="internal|role" />
+ android:protectionLevel="internal|role"
+ android:featureFlag="!android.provider.new_default_account_api_enabled"/>
+
+ <!-- Allows an application to set default account for new contacts.
+ <p>This permission is only granted to system applications fulfilling the Contacts app role
+ and the application with known signers.
+ <p>Protection level: internal|role|knownSigner
+ @SystemApi
+ @hide
+ -->
+ <permission android:name="android.permission.SET_DEFAULT_ACCOUNT_FOR_CONTACTS"
+ android:protectionLevel="internal|role|knownSigner"
+ android:knownCerts="@array/config_setContactsDefaultAccountKnownSigners"
+ android:featureFlag="android.provider.new_default_account_api_enabled"/>
<!-- ====================================================================== -->
<!-- Permissions for accessing user's calendar -->
@@ -2574,6 +2587,22 @@
android:label="@string/permlab_getAccounts" />
<uses-permission android:name="android.permission.GET_ACCOUNTS"/>
+ <!-- @SystemApi Allows access to remove an account.
+ @FlaggedApi("android.app.admin.flags.split_create_managed_profile_enabled")
+ <p>Not for use by third-party applications.
+ @hide -->
+ <permission android:name="android.permission.REMOVE_ACCOUNTS"
+ android:protectionLevel="signature|role"
+ android:featureFlag="android.app.admin.flags.split_create_managed_profile_enabled" />
+
+ <!-- @SystemApi Allows access to copy an account to another user.
+ @FlaggedApi("android.app.admin.flags.split_create_managed_profile_enabled")
+ <p>Not for use by third-party applications.
+ @hide -->
+ <permission android:name="android.permission.COPY_ACCOUNTS"
+ android:protectionLevel="signature|role"
+ android:featureFlag="android.app.admin.flags.split_create_managed_profile_enabled" />
+
<!-- Allows applications to call into AccountAuthenticators.
<p>Not for use by third-party applications. -->
<permission android:name="android.permission.ACCOUNT_MANAGER"
@@ -2616,12 +2645,22 @@
<!-- @SystemApi Allows access to perform vendor effects in the vibrator.
<p>Protection level: signature
+ @FlaggedApi("android.os.vibrator.vendor_vibration_effects")
@hide
-->
<permission android:name="android.permission.VIBRATE_VENDOR_EFFECTS"
android:protectionLevel="signature|privileged"
android:featureFlag="android.os.vibrator.vendor_vibration_effects" />
+ <!-- @SystemApi Allows access to start a vendor vibration session.
+ <p>Protection level: signature
+ @FlaggedApi("android.os.vibrator.vendor_vibration_effects")
+ @hide
+ -->
+ <permission android:name="android.permission.START_VIBRATION_SESSIONS"
+ android:protectionLevel="signature|privileged"
+ android:featureFlag="android.os.vibrator.vendor_vibration_effects" />
+
<!-- @SystemApi Allows access to the vibrator state.
<p>Protection level: signature
@hide
@@ -3925,6 +3964,51 @@
android:protectionLevel="signature|installer" />
<uses-permission android:name="android.permission.MANAGE_ENHANCED_CONFIRMATION_STATES" />
+ <!-- Allows an application to toggle the device's advanced protection mode status.
+ @FlaggedApi("android.security.aapm_api")
+ @SystemApi
+ @hide -->
+ <permission android:name="android.permission.MANAGE_ADVANCED_PROTECTION_MODE"
+ android:protectionLevel="signature|privileged"
+ android:featureFlag="android.security.aapm_api"/>
+
+ <!-- Allows an application to query the device's advanced protection mode status.
+ @FlaggedApi("android.security.aapm_api") -->
+ <permission android:name="android.permission.QUERY_ADVANCED_PROTECTION_MODE"
+ android:protectionLevel="normal"
+ android:featureFlag="android.security.aapm_api"/>
+
+ <!-- Allows an application to read the state of the IntrusionDetectionService
+ @FlaggedApi(android.security.Flags.FLAG_AFL_API)
+ @SystemApi
+ @hide -->
+ <permission android:name="android.permission.READ_INTRUSION_DETECTION_STATE"
+ android:featureFlag="android.security.afl_api"
+ android:protectionLevel="signature|privileged" />
+ <uses-permission android:name="android.permission.READ_INTRUSION_DETECTION_STATE"
+ android:featureFlag="android.security.afl_api"/>
+
+ <!-- Allows an application to change the state of the IntrusionDetectionService
+ @FlaggedApi(android.security.Flags.FLAG_AFL_API)
+ @SystemApi
+ @hide -->
+ <permission android:name="android.permission.MANAGE_INTRUSION_DETECTION_STATE"
+ android:featureFlag="android.security.afl_api"
+ android:protectionLevel="signature|privileged" />
+ <uses-permission android:name="android.permission.MANAGE_INTRUSION_DETECTION_STATE"
+ android:featureFlag="android.security.afl_api"/>
+
+ <!-- Must be required by any IntrusionDetectionEventTransportService to ensure that
+ only the system can bind to it.
+ @FlaggedApi(android.security.Flags.FLAG_AFL_API)
+ @SystemApi
+ @hide -->
+ <permission android:name="android.permission.BIND_INTRUSION_DETECTION_EVENT_TRANSPORT_SERVICE"
+ android:featureFlag="android.security.afl_api"
+ android:protectionLevel="signature" />
+ <uses-permission android:name="android.permission.BIND_INTRUSION_DETECTION_EVENT_TRANSPORT_SERVICE"
+ android:featureFlag="android.security.afl_api"/>
+
<!-- @SystemApi @hide Allows an application to set a device owner on retail demo devices.-->
<permission android:name="android.permission.PROVISION_DEMO_DEVICE"
android:protectionLevel="signature|setup|knownSigner"
@@ -4068,6 +4152,52 @@
android:protectionLevel="signature" />
<!-- ================================== -->
+ <!-- Permissions associated with picture profiles and processing -->
+ <!-- ================================== -->
+ <eat-comment />
+
+ <!-- @FlaggedApi(android.media.tv.flags.Flags.apply_picture_profiles)
+ Allows an app to apply a {@link MediaQualityManager.PictureProfile} to a layer via
+ {@link MediaCodec.PARAMETER_KEY_PICTURE_PROFILE} and, additionally, system apps via
+ {@link SurfaceControl.Transaction#setPictureProfileHandle}.
+ -->
+ <permission android:name="android.permission.APPLY_PICTURE_PROFILE"
+ android:protectionLevel="normal"
+ android:featureFlag="android.media.tv.flags.apply_picture_profiles"/>
+
+ <!-- @hide
+ Allows MediaQualityManager to observe any {@link MediaQualityManager.PictureProfile}
+ applied to any layer in the system by apps via
+ {@link MediaCodec.PARAMETER_KEY_PICTURE_PROFILE} and by system apps via
+ {@link SurfaceControl.Transaction#setPictureProfileHandle}.
+ -->
+ <permission android:name="android.permission.OBSERVE_PICTURE_PROFILES"
+ android:protectionLevel="signature|privileged"
+ android:featureFlag="android.media.tv.flags.apply_picture_profiles"/>
+
+ <!--
+ @SystemApi
+ @FlaggedApi("android.media.tv.flags.media_quality_fw")
+ Allows an application to access its picture profile from the media quality database.
+ <p> Protection level: signature|privileged|vendor privileged
+ @hide
+ -->
+ <permission android:name="android.permission.MANAGE_GLOBAL_PICTURE_QUALITY_SERVICE"
+ android:protectionLevel="signature|privileged|vendorPrivileged"
+ android:featureFlag="android.media.tv.flags.media_quality_fw"/>
+
+ <!--
+ @SystemApi
+ @FlaggedApi("android.media.tv.flags.media_quality_fw")
+ Allows an application to access its sound profile from the media quality database.
+ <p> Protection level: signature|privileged|vendor privileged
+ @hide
+ -->
+ <permission android:name="android.permission.MANAGE_GLOBAL_SOUND_QUALITY_SERVICE"
+ android:protectionLevel="signature|privileged|vendorPrivileged"
+ android:featureFlag="android.media.tv.flags.media_quality_fw"/>
+
+ <!-- ================================== -->
<!-- Permissions affecting the display of other applications -->
<!-- ================================== -->
<eat-comment />
@@ -4244,6 +4374,18 @@
android:description="@string/permdesc_hideOverlayWindows"
android:protectionLevel="normal" />
+ <!-- Allows an app to enter Picture-in-Picture mode when the user is not explicitly requesting
+ it. This includes using {@link PictureInPictureParams.Builder#setAutoEnterEnabled} as well
+ as lifecycle methods such as {@link Activity#onUserLeaveHint} and {@link Activity#onPause}
+ to enter PiP when the user leaves the app.
+ This permission should only be used for certain PiP
+ <a href="{@docRoot}training/tv/get-started/multitasking#usage-types">usage types</a>.
+ @FlaggedApi("android.app.enable_tv_implicit_enter_pip_restriction")
+ -->
+ <permission android:name="android.permission.TV_IMPLICIT_ENTER_PIP"
+ android:protectionLevel="normal"
+ android:featureFlag="android.app.enable_tv_implicit_enter_pip_restriction" />
+
<!-- ================================== -->
<!-- Permissions affecting the system wallpaper -->
<!-- ================================== -->
@@ -4750,6 +4892,27 @@
<permission android:name="android.permission.PROVIDE_REMOTE_CREDENTIALS"
android:protectionLevel="signature|privileged|role" />
+ <!-- @FlaggedApi(com.android.settingslib.flags.Flags.FLAG_SETTINGS_CATALYST)
+ Allows an application to access the Settings Preference services to read settings exposed
+ by the system Settings app and system apps that contribute settings surfaced by the
+ Settings app.
+ <p>This allows the calling application to read settings values through the host
+ application, agnostic of underlying storage. -->
+ <permission android:name="android.permission.READ_SYSTEM_PREFERENCES"
+ android:protectionLevel="signature|privileged|role"
+ android:featureFlag="com.android.settingslib.flags.settings_catalyst" />
+
+ <!-- @FlaggedApi(com.android.settingslib.flags.Flags.FLAG_WRITE_SYSTEM_PREFERENCE_PERMISSION_ENABLED)
+ Allows an application to access the Settings Preference services to write settings
+ values exposed by the system Settings app and system apps that contribute settings surfaced
+ in the Settings app.
+ <p>This allows the calling application to write settings values
+ through the host application, agnostic of underlying storage.
+ <p>Protection Level: signature|privileged|appop -->
+ <permission android:name="android.permission.WRITE_SYSTEM_PREFERENCES"
+ android:protectionLevel="signature|privileged|appop"
+ android:featureFlag="com.android.settingslib.flags.write_system_preference_permission_enabled" />
+
<!-- ========================================= -->
<!-- Permissions for special development tools -->
<!-- ========================================= -->
@@ -5538,6 +5701,17 @@
<permission android:name="android.permission.LOCK_DEVICE"
android:protectionLevel="signature|privileged" />
+ <!-- @SystemApi
+ @FlaggedApi(android.security.Flags.FLAG_SECURE_LOCKDOWN)
+ Allows an application to lock down the device into an enhanced security state.
+ <p>Not for use by third-party applications.
+ <p>Protection level: signature|privileged
+ @hide
+ -->
+ <permission android:name="android.permission.MANAGE_SECURE_LOCK_DEVICE"
+ android:protectionLevel="signature|privileged"
+ android:featureFlag="android.security.secure_lockdown" />
+
<!-- @SystemApi Allows low-level access to setting the orientation (actually
rotation) of the screen.
<p>Not for use by third-party applications.
@@ -6148,6 +6322,15 @@
<permission android:name="android.permission.CAPTURE_VOICE_COMMUNICATION_OUTPUT"
android:protectionLevel="signature|privileged|role" />
+ <!-- @SystemApi Allows an application to bypass concurrency restrictions while
+ recording audio. For example, apps with this permission can continue to record
+ while a voice call is active.</p>
+ @FlaggedApi(android.media.audio.Flags.FLAG_CONCURRENT_AUDIO_RECORD_BYPASS_PERMISSION)
+ @hide -->
+ <permission android:name="android.permission.BYPASS_CONCURRENT_RECORD_AUDIO_RESTRICTION"
+ android:featureFlag="android.media.audio.concurrent_audio_record_bypass_permission"
+ android:protectionLevel="signature|privileged" />
+
<!-- @SystemApi Allows an application to capture audio for hotword detection.
<p>Not for use by third-party applications.</p>
@hide -->
@@ -7591,7 +7774,31 @@
<!-- @SystemApi Allows an application to access shared libraries.
@hide -->
<permission android:name="android.permission.ACCESS_SHARED_LIBRARIES"
- android:protectionLevel="signature|installer" />
+ android:protectionLevel="signature|installer"
+ android:featureFlag="!android.content.pm.sdk_dependency_installer" />
+
+ <!-- @SystemApi Allows an application to access shared libraries.
+ @hide -->
+ <permission android:name="android.permission.ACCESS_SHARED_LIBRARIES"
+ android:protectionLevel="signature|installer|role"
+ android:featureFlag="android.content.pm.sdk_dependency_installer" />
+
+ <!-- @SystemApi Permission held by the system to allow binding to the dependency installer role
+ holder.
+ @FlaggedApi(android.content.pm.Flags.FLAG_SDK_DEPENDENCY_INSTALLER)
+ @hide -->
+ <permission android:name="android.permission.BIND_DEPENDENCY_INSTALLER"
+ android:protectionLevel="signature"
+ android:featureFlag="android.content.pm.sdk_dependency_installer" />
+
+ <!-- @SystemApi Allows an application to install shared libraries of types
+ {@link android.content.pm.SharedLibraryInfo#TYPE_STATIC} or
+ {@link android.content.pm.SharedLibraryInfo#TYPE_SDK_PACKAGE}.
+ @FlaggedApi(android.content.pm.Flags.FLAG_SDK_DEPENDENCY_INSTALLER)
+ @hide -->
+ <permission android:name="android.permission.INSTALL_DEPENDENCY_SHARED_LIBRARIES"
+ android:protectionLevel="signature|role"
+ android:featureFlag="android.content.pm.sdk_dependency_installer" />
<!-- Allows an app to log compat change usage.
@hide <p>Not for use by third-party applications.</p> -->
@@ -7666,6 +7873,15 @@
<permission android:name="android.permission.ADD_ALWAYS_UNLOCKED_DISPLAY"
android:protectionLevel="signature|role"/>
+ <!-- Allows an application to create displays that mirror other displays' content.
+ <p>Not for use by third-party applications.
+ <p>Protection level: internal|role
+ @FlaggedApi(android.companion.virtualdevice.flags.Flags.FLAG_ENABLE_LIMITED_VDM_ROLE)
+ @hide @SystemApi -->
+ <permission android:name="android.permission.ADD_MIRROR_DISPLAY"
+ android:protectionLevel="internal|role"
+ android:featureFlag="android.companion.virtualdevice.flags.enable_limited_vdm_role" />
+
<!-- @hide @SystemApi Allows an application to access locusId events in the usage stats. -->
<permission android:name="android.permission.ACCESS_LOCUS_ID_USAGE_STATS"
android:protectionLevel="signature|role" />
@@ -8082,6 +8298,26 @@
android:protectionLevel="signature|knownSigner"
android:knownCerts="@array/config_healthConnectMigrationKnownSigners" />
+ <!-- @hide @SystemApi Allows permitted apps to back up Health Connect data and settings.
+ <p>Protection level: signature|knownSigner
+ @FlaggedApi("android.permission.flags.health_connect_backup_restore_permission_enabled")
+ -->
+ <permission
+ android:name="android.permission.BACKUP_HEALTH_CONNECT_DATA_AND_SETTINGS"
+ android:protectionLevel="signature|knownSigner"
+ android:knownCerts="@array/config_backupHealthConnectDataAndSettingsKnownSigners"
+ android:featureFlag="android.permission.flags.health_connect_backup_restore_permission_enabled" />
+
+ <!-- @hide @SystemApi Allows permitted apps to restore Health Connect data and settings.
+ <p>Protection level: signature|knownSigner
+ @FlaggedApi("android.permission.flags.health_connect_backup_restore_permission_enabled")
+ -->
+ <permission
+ android:name="android.permission.RESTORE_HEALTH_CONNECT_DATA_AND_SETTINGS"
+ android:protectionLevel="signature|knownSigner"
+ android:knownCerts="@array/config_restoreHealthConnectDataAndSettingsKnownSigners"
+ android:featureFlag="android.permission.flags.health_connect_backup_restore_permission_enabled" />
+
<!-- @SystemApi Allows an app to query apps in clone profile. The permission is
bidirectional in nature, i.e. cloned apps would be able to query apps in root user.
The permission is not meant for 3P apps as of now.
@@ -8274,6 +8510,52 @@
<permission android:name="android.permission.RESERVED_FOR_TESTING_SIGNATURE"
android:protectionLevel="signature"/>
+ <!-- Allows app to enter trade-in-mode.
+ <p>Protection level: signature|privileged
+ @hide
+ -->
+ <permission android:name="android.permission.ENTER_TRADE_IN_MODE"
+ android:protectionLevel="signature|privileged"
+ android:featureFlag="com.android.tradeinmode.flags.enable_trade_in_mode" />
+
+ <!-- @SystemApi
+ @FlaggedApi(com.android.art.flags.Flags.FLAG_EXECUTABLE_METHOD_FILE_OFFSETS)
+ Ability to read program metadata and attach dynamic instrumentation.
+ <p>Protection level: signature
+ @hide
+ -->
+ <permission android:name="android.permission.DYNAMIC_INSTRUMENTATION"
+ android:protectionLevel="signature"
+ android:featureFlag="com.android.art.flags.executable_method_file_offsets" />
+
+ <!-- @SystemApi
+ @FlaggedApi("android.media.tv.flags.kids_mode_tvdb_sharing")
+ This permission is required when accessing information related to
+ singleUser-ed TIS session.
+ <p>This should only be used by OEM.
+ <p>Protection level: signature|privileged|vendorPrivileged
+ @hide
+ -->
+ <permission android:name="android.permission.SINGLE_USER_TIS_ACCESS"
+ android:protectionLevel="signature|privileged|vendorPrivileged"
+ android:featureFlag="android.media.tv.flags.kids_mode_tvdb_sharing"/>
+
+ <!--
+ This permission allows the system to receive PACKAGE_CHANGED broadcasts when the component
+ state of a non-exported component has been changed.
+ <p>Not for use by third-party applications. </p>
+ <p>Protection level: internal
+ @hide
+ -->
+ <permission
+ android:name="android.permission.INTERNAL_RECEIVE_PACKAGE_CHANGED_BROADCAST_ON_COMPONENT_STATE_CHANGED"
+ android:protectionLevel="internal"
+ android:featureFlag="android.content.pm.reduce_broadcasts_for_component_state_changes"/>
+
+ <uses-permission
+ android:name="android.permission.INTERNAL_RECEIVE_PACKAGE_CHANGED_BROADCAST_ON_COMPONENT_STATE_CHANGED"
+ android:featureFlag="android.content.pm.reduce_broadcasts_for_component_state_changes"/>
+
<!-- Attribution for Geofencing service. -->
<attribution android:tag="GeofencingService" android:label="@string/geofencing_service"/>
<!-- Attribution for Country Detector. -->
@@ -8860,6 +9142,17 @@
android:permission="android.permission.BIND_JOB_SERVICE" >
</service>
+ <service android:name="android.app.ecm.EnhancedConfirmationCallTrackerService"
+ android:permission="android.permission.BIND_INCALL_SERVICE"
+ android:featureFlag="android.permission.flags.enhanced_confirmation_in_call_apis_enabled"
+ android:exported="true">
+ <meta-data android:name="android.telecom.INCLUDE_SELF_MANAGED_CALLS"
+ android:value="true" />
+ <intent-filter>
+ <action android:name="android.telecom.InCallService"/>
+ </intent-filter>
+ </service>
+
<service android:name="com.android.server.companion.datatransfer.contextsync.CallMetadataSyncInCallService"
android:permission="android.permission.BIND_INCALL_SERVICE"
android:exported="true">
diff --git a/tests/cts/permissionpolicy/res/raw/automotive_android_manifest.xml b/tests/cts/permissionpolicy/res/raw/automotive_android_manifest.xml
index 783cd7f6b..e123f13ce 100644
--- a/tests/cts/permissionpolicy/res/raw/automotive_android_manifest.xml
+++ b/tests/cts/permissionpolicy/res/raw/automotive_android_manifest.xml
@@ -620,4 +620,16 @@
android:protectionLevel="signature|privileged"
android:label="@string/car_permission_label_bind_app_card_provider"
android:description="@string/car_permission_desc_bind_app_card_provider" />
+ <permission
+ android:name="android.car.permission.RECORD_VEHICLE_PROPERTIES"
+ android:protectionLevel="signature"
+ android:label="@string/car_permission_label_record_vehicle_properties"
+ android:description="@string/car_permission_desc_record_vehicle_properties"
+ android:featureFlag="android.car.feature.car_power_cancel_shell_command" />
+ <permission
+ android:name="android.car.permission.INJECT_VEHICLE_PROPERTIES"
+ android:protectionLevel="signature"
+ android:label="@string/car_permission_label_inject_vehicle_properties"
+ android:description="@string/car_permission_desc_inject_vehicle_properties"
+ android:featureFlag="android.car.feature.car_power_cancel_shell_command" />
</manifest>
diff --git a/tests/cts/permissionpolicy/src/android/permissionpolicy/cts/RuntimePermissionProperties.kt b/tests/cts/permissionpolicy/src/android/permissionpolicy/cts/RuntimePermissionProperties.kt
index 70832b6ba..2ce48af44 100644
--- a/tests/cts/permissionpolicy/src/android/permissionpolicy/cts/RuntimePermissionProperties.kt
+++ b/tests/cts/permissionpolicy/src/android/permissionpolicy/cts/RuntimePermissionProperties.kt
@@ -59,6 +59,7 @@ import android.app.AppOpsManager.permissionToOp
import android.content.pm.PackageManager.GET_PERMISSIONS
import android.content.pm.PermissionInfo.PROTECTION_DANGEROUS
import android.content.pm.PermissionInfo.PROTECTION_FLAG_APPOP
+import android.health.connect.HealthPermissions
import android.os.Build
import android.permission.flags.Flags
import android.permission.PermissionManager
@@ -195,6 +196,18 @@ class RuntimePermissionProperties {
expectedPerms.add(RANGING)
}
- assertThat(expectedPerms).containsExactlyElementsIn(platformRuntimePerms.map { it.name })
+ // Separately check health permissions.
+ if (Flags.replaceBodySensorPermissionEnabled()) {
+ assertThat(expectedPerms).contains(HealthPermissions.READ_HEART_RATE);
+ assertThat(expectedPerms).contains(HealthPermissions.READ_HEALTH_DATA_IN_BACKGROUND);
+
+ // Remove these from the expected list once we've confirmed their
+ // present. These are not permissions owned by "android" so won't be
+ // in the list of platform runtime permissions.
+ expectedPerms.remove(HealthPermissions.READ_HEART_RATE);
+ expectedPerms.remove(HealthPermissions.READ_HEALTH_DATA_IN_BACKGROUND);
+ }
+
+ assertThat(platformRuntimePerms.map { it.name }).containsExactlyElementsIn(expectedPerms)
}
}
diff --git a/tests/cts/permissionui/AndroidManifest.xml b/tests/cts/permissionui/AndroidManifest.xml
index 3b80b8d8b..b5c9e2ad0 100644
--- a/tests/cts/permissionui/AndroidManifest.xml
+++ b/tests/cts/permissionui/AndroidManifest.xml
@@ -25,6 +25,7 @@
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
+ <uses-permission android:name="android.permission.MANAGE_OWN_CALLS" />
<application>
@@ -78,6 +79,23 @@
<meta-data android:name="android.accessibilityservice"
android:resource="@xml/test_accessibilityservice"/>
</service>
+ <service android:name=".VoipHelperTestConnectionService"
+ android:exported="true"
+ android:permission="android.permission.BIND_TELECOM_CONNECTION_SERVICE">
+ <intent-filter>
+ <action android:name="android.telecom.ConnectionService" />
+ </intent-filter>
+ </service>
+
+ <service android:name=".EcmInCallTestInCallService"
+ android:permission="android.permission.BIND_INCALL_SERVICE"
+ android:exported="true">
+ <meta-data android:name="android.telecom.INCLUDE_SELF_MANAGED_CALLS"
+ android:value="true" />
+ <intent-filter>
+ <action android:name="android.telecom.InCallService"/>
+ </intent-filter>
+ </service>
</application>
diff --git a/tests/cts/permissionui/src/android/permissionui/cts/CameraMicIndicatorsPermissionTest.kt b/tests/cts/permissionui/src/android/permissionui/cts/CameraMicIndicatorsPermissionTest.kt
index e3197599c..19b67e729 100644
--- a/tests/cts/permissionui/src/android/permissionui/cts/CameraMicIndicatorsPermissionTest.kt
+++ b/tests/cts/permissionui/src/android/permissionui/cts/CameraMicIndicatorsPermissionTest.kt
@@ -28,6 +28,7 @@ import android.os.Build
import android.os.Process
import android.os.SystemClock
import android.os.SystemProperties
+import android.os.UserManager
import android.permission.PermissionManager
import android.permission.cts.MtsIgnore
import android.platform.test.annotations.AsbSecurityTest
@@ -161,6 +162,8 @@ class CameraMicIndicatorsPermissionTest : StsExtraBusinessLogicTestCase {
@Before
fun setUp() {
+ // Camera and Mic are not supported for secondary user visible as a background user.
+ assumeFalse(isAutomotiveWithVisibleBackgroundUser())
runWithShellPermissionIdentity {
screenTimeoutBeforeTest =
Settings.System.getLong(context.contentResolver, Settings.System.SCREEN_OFF_TIMEOUT)
@@ -209,6 +212,9 @@ class CameraMicIndicatorsPermissionTest : StsExtraBusinessLogicTestCase {
@After
fun tearDown() {
+ if (isAutomotiveWithVisibleBackgroundUser()) {
+ return
+ }
uninstall()
if (isCar) {
// Deselect the indicator since it persists otherwise
@@ -775,4 +781,10 @@ class CameraMicIndicatorsPermissionTest : StsExtraBusinessLogicTestCase {
private fun byOneOfText(vararg textValues: String) =
By.text(Pattern.compile(textValues.joinToString(separator = "|") { Pattern.quote(it) }))
+
+ fun isAutomotiveWithVisibleBackgroundUser(): Boolean {
+ val userManager = context.getSystemService(UserManager::class.java)
+ return packageManager.hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE) &&
+ userManager.isVisibleBackgroundUsersSupported()
+ }
}
diff --git a/tests/cts/permissionui/src/android/permissionui/cts/EnhancedConfirmationInCallTest.kt b/tests/cts/permissionui/src/android/permissionui/cts/EnhancedConfirmationInCallTest.kt
new file mode 100644
index 000000000..cbf4734d5
--- /dev/null
+++ b/tests/cts/permissionui/src/android/permissionui/cts/EnhancedConfirmationInCallTest.kt
@@ -0,0 +1,231 @@
+/*
+ * 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 android.permissionui.cts
+
+import android.Manifest
+import android.app.Instrumentation
+import android.app.ecm.EnhancedConfirmationManager
+import android.content.ContentProviderOperation
+import android.content.Context
+import android.content.pm.PackageManager
+import android.net.Uri
+import android.os.Build
+import android.permission.flags.Flags
+import android.platform.test.annotations.AppModeFull
+import android.platform.test.annotations.RequiresFlagsEnabled
+import android.platform.test.flag.junit.CheckFlagsRule
+import android.platform.test.flag.junit.DeviceFlagsValueProvider
+import android.provider.ContactsContract
+import android.provider.ContactsContract.CommonDataKinds
+import android.provider.ContactsContract.Data
+import android.provider.ContactsContract.RawContacts
+import androidx.test.filters.SdkSuppress
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.compatibility.common.util.SystemUtil.callWithShellPermissionIdentity
+import com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity
+import java.util.concurrent.Callable
+import org.junit.After
+import org.junit.AfterClass
+import org.junit.Assert
+import org.junit.Assume
+import org.junit.Before
+import org.junit.BeforeClass
+import org.junit.Rule
+import org.junit.Test
+
+/**
+ * This test verifies the behavior of the Enhanced Confirmation Manager APIs that deal with unknown
+ * callers
+ */
+@AppModeFull(reason = "Instant apps cannot install packages")
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.BAKLAVA, codeName = "Baklava")
+@RequiresFlagsEnabled(Flags.FLAG_ENHANCED_CONFIRMATION_IN_CALL_APIS_ENABLED)
+// @CddTest(requirement = "TBD")
+class EnhancedConfirmationInCallTest {
+ private val ecm = context.getSystemService(EnhancedConfirmationManager::class.java)!!
+ private val packageManager = context.packageManager
+ private val addedContacts = mutableMapOf<String, List<Uri>>()
+
+ @JvmField
+ @Rule
+ val checkFlagsRule: CheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule()
+
+ @Before
+ fun assumeNotAutoOrTv() {
+ Assume.assumeFalse(packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK))
+ Assume.assumeFalse(packageManager.hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE))
+ }
+
+ companion object {
+ private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation()
+ private val context: Context = instrumentation.targetContext
+ private lateinit var voipService: VoipCallHelper
+
+ @JvmStatic
+ @BeforeClass
+ fun setupVoipService() {
+ voipService = VoipCallHelper(context)
+ voipService.registerPhoneAccount()
+ }
+
+ @JvmStatic
+ @AfterClass
+ fun tearDownVoipService() {
+ voipService.removePhoneAccount()
+ }
+
+ const val CONTACT_DISPLAY_NAME = "Alice Bobson"
+ const val NON_CONTACT_DISPLAY_NAME = "Eve McEve"
+ const val CONTACT_PHONE_NUMBER = "8888888888"
+ const val NON_CONTACT_PHONE_NUMBER = "1111111111"
+ }
+
+ private fun addContact(displayName: String, phoneNumber: String) {
+ runWithShellPermissionIdentity {
+ val ops: ArrayList<ContentProviderOperation> = ArrayList()
+ ops.add(
+ ContentProviderOperation.newInsert(RawContacts.CONTENT_URI)
+ .withValue(RawContacts.ACCOUNT_TYPE, "test type")
+ .withValue(RawContacts.ACCOUNT_NAME, "test account")
+ .build()
+ )
+ ops.add(
+ ContentProviderOperation.newInsert(Data.CONTENT_URI)
+ .withValueBackReference(Data.RAW_CONTACT_ID, 0)
+ .withValue(Data.MIMETYPE, CommonDataKinds.Phone.CONTENT_ITEM_TYPE)
+ .withValue(CommonDataKinds.StructuredName.DISPLAY_NAME, displayName)
+ .build()
+ )
+ ops.add(
+ ContentProviderOperation.newInsert(Data.CONTENT_URI)
+ .withValueBackReference(Data.RAW_CONTACT_ID, 0)
+ .withValue(Data.MIMETYPE, CommonDataKinds.Phone.CONTENT_ITEM_TYPE)
+ .withValue(CommonDataKinds.Phone.NUMBER, phoneNumber)
+ .build()
+ )
+ val results = context.contentResolver.applyBatch(ContactsContract.AUTHORITY, ops)
+ val resultsForDisplayName = mutableListOf<Uri>()
+ results.forEach { resultsForDisplayName.add(it.uri!!) }
+ addedContacts[displayName] = resultsForDisplayName
+ }
+ }
+
+ private fun removeContact(displayName: String) {
+ runWithShellPermissionIdentity {
+ var totalRowsRemoved = 0
+ for (data in addedContacts[displayName] ?: emptyList()) {
+ totalRowsRemoved += context.contentResolver.delete(data, null)
+ }
+ // There are multiple contacts tables, and removing from the raw_contacts table
+ // can cause row removals from the data table, so we may get some uris that don't
+ // report a delete, but we should get at least one, and not more than the number of uris
+ Assert.assertNotEquals(
+ "Expected at least one contact row to be removed",
+ 0,
+ totalRowsRemoved,
+ )
+ Assert.assertTrue(
+ "Unexpectedly large number of contact rows removed",
+ totalRowsRemoved <= (addedContacts[displayName]?.size ?: 0),
+ )
+ addedContacts.remove(displayName)
+ }
+ }
+
+ @After
+ fun tearDown() {
+ voipService.endCallAndWaitForInactive()
+ addedContacts.keys.forEach { removeContact(it) }
+ }
+
+ private fun getInUnknownCallState(): Boolean {
+ return callWithShellPermissionIdentity { ecm.isUnknownCallOngoing }
+ }
+
+ @Test
+ fun testCannotReadOngoingState_WithoutPermission() {
+ try {
+ ecm.isUnknownCallOngoing
+ Assert.fail()
+ } catch (expected: SecurityException) {
+ Assert.assertTrue(
+ expected.message?.contains(
+ Manifest.permission.MANAGE_ENHANCED_CONFIRMATION_STATES
+ ) == true
+ )
+ }
+
+ val unexpectedException =
+ callWithShellPermissionIdentity(
+ Callable {
+ try {
+ ecm.isUnknownCallOngoing
+ null
+ } catch (unexpected: SecurityException) {
+ // Catching the exception, because exceptions thrown inside
+ // run/callWithShellPermissionIdentity are obscured by the rethrow
+ // from run/call.
+ unexpected
+ }
+ },
+ Manifest.permission.MANAGE_ENHANCED_CONFIRMATION_STATES,
+ )
+ Assert.assertNull(unexpectedException)
+ }
+
+ @Test
+ fun testIncomingCall_NonContact() {
+ voipService.createCallAndWaitForActive(NON_CONTACT_DISPLAY_NAME, NON_CONTACT_PHONE_NUMBER)
+ Assert.assertTrue(getInUnknownCallState())
+ voipService.endCallAndWaitForInactive()
+ Assert.assertFalse(getInUnknownCallState())
+ }
+
+ @Test
+ fun testIncomingCall_Contact_DisplayNameMatches_PhoneNotGiven() {
+ addContact(CONTACT_DISPLAY_NAME, CONTACT_PHONE_NUMBER)
+ // If no phone number is given, the display name will be checked
+ voipService.createCallAndWaitForActive(CONTACT_DISPLAY_NAME, CONTACT_PHONE_NUMBER)
+ Assert.assertFalse(getInUnknownCallState())
+ voipService.endCallAndWaitForInactive()
+ Assert.assertFalse(getInUnknownCallState())
+ }
+
+ @Test
+ fun testIncomingCall_Contact_PhoneNumberMatches() {
+ addContact(CONTACT_DISPLAY_NAME, CONTACT_PHONE_NUMBER)
+ // If the phone number matches, the display name is not checked
+ voipService.createCallAndWaitForActive(NON_CONTACT_DISPLAY_NAME, CONTACT_PHONE_NUMBER)
+ Assert.assertFalse(getInUnknownCallState())
+ voipService.endCallAndWaitForInactive()
+ Assert.assertFalse(getInUnknownCallState())
+ }
+
+ @Test
+ fun testCall_DoesntBecomeTrustedIfCallerAddedDuringCall() {
+ val tempContactDisplay = "TEMP CONTACT"
+ val tempContactPhone = "999-999-9999"
+ voipService.createCallAndWaitForActive(tempContactDisplay, tempContactPhone)
+ addContact(tempContactDisplay, tempContactPhone)
+ // State should not be recomputed just because the contact is newly added
+ Assert.assertTrue(getInUnknownCallState())
+ voipService.endCallAndWaitForInactive()
+ voipService.createCallAndWaitForActive(tempContactDisplay, tempContactPhone)
+ // A new call should recognize our contact, and mark the call as trusted
+ Assert.assertFalse(getInUnknownCallState())
+ }
+}
diff --git a/tests/cts/permissionui/src/android/permissionui/cts/PermissionSplitTest.kt b/tests/cts/permissionui/src/android/permissionui/cts/PermissionSplitTest.kt
index d509add3a..da70fc186 100644
--- a/tests/cts/permissionui/src/android/permissionui/cts/PermissionSplitTest.kt
+++ b/tests/cts/permissionui/src/android/permissionui/cts/PermissionSplitTest.kt
@@ -17,15 +17,22 @@
package android.permissionui.cts
import android.os.Build
+import android.permission.flags.Flags
+import android.platform.test.annotations.RequiresFlagsDisabled
+import android.platform.test.flag.junit.DeviceFlagsValueProvider
import androidx.test.filters.FlakyTest
import androidx.test.filters.SdkSuppress
import org.junit.Assume.assumeFalse
import org.junit.Before
+import org.junit.Rule
import org.junit.Test
/** Runtime permission behavior tests for permission splits. */
@FlakyTest
class PermissionSplitTest : BaseUsePermissionTest() {
+
+ @Rule @JvmField val mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule()
+
@Before
fun assumeNotTv() {
assumeFalse(isTv)
@@ -56,6 +63,7 @@ class PermissionSplitTest : BaseUsePermissionTest() {
}
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.TIRAMISU, codeName = "Tiramisu")
+ @RequiresFlagsDisabled(Flags.FLAG_REPLACE_BODY_SENSOR_PERMISSION_ENABLED)
@Test
fun testBodySensorSplit() {
installPackage(APP_APK_PATH_31)
@@ -63,6 +71,7 @@ class PermissionSplitTest : BaseUsePermissionTest() {
}
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.TIRAMISU, codeName = "Tiramisu")
+ @RequiresFlagsDisabled(Flags.FLAG_REPLACE_BODY_SENSOR_PERMISSION_ENABLED)
@Test
fun testBodySensorSplit32() {
installPackage(APP_APK_PATH_32)
@@ -70,6 +79,7 @@ class PermissionSplitTest : BaseUsePermissionTest() {
}
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.TIRAMISU, codeName = "Tiramisu")
+ @RequiresFlagsDisabled(Flags.FLAG_REPLACE_BODY_SENSOR_PERMISSION_ENABLED)
@Test
fun testBodySensorNonSplit() {
installPackage(APP_APK_PATH_LATEST)
diff --git a/tests/cts/permissionui/src/android/permissionui/cts/ReviewAccessibilityServicesTest.kt b/tests/cts/permissionui/src/android/permissionui/cts/ReviewAccessibilityServicesTest.kt
index e434e9c70..55f028e17 100644
--- a/tests/cts/permissionui/src/android/permissionui/cts/ReviewAccessibilityServicesTest.kt
+++ b/tests/cts/permissionui/src/android/permissionui/cts/ReviewAccessibilityServicesTest.kt
@@ -190,7 +190,7 @@ class ReviewAccessibilityServicesTest {
!uiDevice.performActionAndWait(
{ block() },
Until.newWindow(),
- NEW_WINDOW_TIMEOUT_MILLIS
+ NEW_WINDOW_TIMEOUT_MILLIS,
)
if (timeoutOccurred) {
@@ -212,17 +212,11 @@ class ReviewAccessibilityServicesTest {
private fun waitForSettingsButtonToDisappear() {
SystemUtil.eventually {
- findPCObjectByClassAndText(false,
- "android.widget.Button",
- "Settings"
- )
+ findPCObjectByClassAndText(false, "android.widget.Button", "Settings")
}
}
- private fun findObjectByTextWithoutRetry(
- shouldBePresent: Boolean,
- text: String,
- ): UiObject2? {
+ private fun findObjectByTextWithoutRetry(shouldBePresent: Boolean, text: String): UiObject2? {
val containsWithoutCaseSelector =
By.text(Pattern.compile(".*$text.*", Pattern.CASE_INSENSITIVE))
val view =
@@ -235,7 +229,7 @@ class ReviewAccessibilityServicesTest {
assertEquals(
"Expected to find view with text $text: $shouldBePresent",
shouldBePresent,
- view != null
+ view != null,
)
return view
}
@@ -251,15 +245,16 @@ class ReviewAccessibilityServicesTest {
private fun findPCObjectByClassAndText(
shouldBePresent: Boolean,
className: String,
- text: String
+ text: String,
): UiObject2? {
- val selector = By.pkg(packageName)
- .clazz(className)
- .text(text)
+ val selector = By.pkg(packageName).clazz(className).text(text)
val view = waitFindObjectOrNull(selector)
assertEquals(
"Expected to find view with packageName '$packageName' className '$className' " +
- "text '$text' : $shouldBePresent", shouldBePresent, view != null)
+ "text '$text' : $shouldBePresent",
+ shouldBePresent,
+ view != null,
+ )
return view
}
}
diff --git a/tests/cts/permissionui/src/android/permissionui/cts/VoipCallHelper.kt b/tests/cts/permissionui/src/android/permissionui/cts/VoipCallHelper.kt
new file mode 100644
index 000000000..480d7bff3
--- /dev/null
+++ b/tests/cts/permissionui/src/android/permissionui/cts/VoipCallHelper.kt
@@ -0,0 +1,171 @@
+/*
+ * 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 android.permissionui.cts
+
+import android.content.ComponentName
+import android.content.Context
+import android.net.Uri
+import android.os.Bundle
+import android.os.Process
+import android.permissionui.cts.VoipCallHelper.Companion.EXTRA_DISPLAY_NAME
+import android.permissionui.cts.VoipCallHelper.Companion.awaitingCallStateLatch
+import android.permissionui.cts.VoipCallHelper.Companion.currentActiveConnection
+import android.telecom.Connection
+import android.telecom.ConnectionRequest
+import android.telecom.ConnectionService
+import android.telecom.DisconnectCause
+import android.telecom.PhoneAccount
+import android.telecom.PhoneAccountHandle
+import android.telecom.TelecomManager
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
+import org.junit.Assert
+
+/** A helper class which can register a phone account, and make/end VOIP phone calls */
+class VoipCallHelper(val context: Context) {
+ private val telecomManager by lazy { context.getSystemService(TelecomManager::class.java) }
+ private lateinit var phoneAccount: PhoneAccount
+ private val accountHandle =
+ PhoneAccountHandle(
+ ComponentName(context, VoipHelperTestConnectionService::class.java),
+ "cts-voip-helper-test",
+ Process.myUserHandle(),
+ )
+
+ init {
+ registerPhoneAccount()
+ }
+
+ companion object {
+ var currentActiveConnection: VoIPConnection? = null
+ var awaitingCallStateLatch: CallPlacedLatch? = null
+
+ const val EXTRA_DISPLAY_NAME = "display_name"
+ const val CUSTOM_ADDRESS_SCHEMA = "custom_schema"
+ const val CALL_STATE_WAIT_MS = 1000L
+ const val CALL_TIMEOUT_MS = 10000L
+ }
+
+ fun registerPhoneAccount() {
+ val phoneAccountBuilder = PhoneAccount.builder(accountHandle, "CTS VOIP HELPER")
+ phoneAccountBuilder.setCapabilities(PhoneAccount.CAPABILITY_SELF_MANAGED)
+ // see b/343674176. Some OEMs expect the PhoneAccount.getExtras() to be non-null
+ val defaultBundle = Bundle()
+ phoneAccountBuilder.setExtras(defaultBundle)
+
+ // build and register the PhoneAccount via the Platform API
+ phoneAccount = phoneAccountBuilder.build()
+ telecomManager.registerPhoneAccount(phoneAccount)
+ }
+
+ fun removePhoneAccount() {
+ telecomManager.unregisterPhoneAccount(phoneAccount.accountHandle)
+ }
+
+ fun createCallAndWaitForActive(displayName: String?, phoneNumber: String?) {
+ val extras = Bundle()
+
+ val phoneUri =
+ if (phoneNumber != null) {
+ Uri.fromParts(PhoneAccount.SCHEME_TEL, phoneNumber, null)
+ } else {
+ // If we don't have a phone number, provide a custom address URI, like many VOIP
+ // apps that aren't tied to a phone number do
+ Uri.fromParts(CUSTOM_ADDRESS_SCHEMA, "custom_address", null)
+ }
+ if (displayName != null) {
+ extras.putString(EXTRA_DISPLAY_NAME, displayName)
+ }
+ extras.putParcelable(TelecomManager.EXTRA_INCOMING_CALL_ADDRESS, phoneUri)
+ awaitingCallStateLatch = CallPlacedLatch(phoneUri, displayName)
+ telecomManager.addNewIncomingCall(phoneAccount.accountHandle, extras)
+ Assert.assertTrue(
+ "Timed out waiting for call to start",
+ awaitingCallStateLatch!!.await(CALL_TIMEOUT_MS, TimeUnit.MILLISECONDS),
+ )
+ // TODO b/379941144: Replace wait with waiting until a test InCallService gets a callback
+ Thread.sleep(CALL_STATE_WAIT_MS)
+ }
+
+ fun endCallAndWaitForInactive() {
+ currentActiveConnection?.let { connection ->
+ connection.setDisconnected(DisconnectCause(DisconnectCause.LOCAL))
+ connection.destroy()
+ // TODO b/379941144: Replace wait with waiting until a test InCallService gets a
+ // callback
+ Thread.sleep(CALL_STATE_WAIT_MS)
+ }
+ currentActiveConnection = null
+ }
+}
+
+class CallPlacedLatch(val address: Uri?, val displayName: String?) : CountDownLatch(1) {
+ fun nameAndNumberMatch(connection: Connection): Boolean {
+ return connection.address == address && connection.callerDisplayName == displayName
+ }
+}
+
+class VoIPConnection : Connection() {
+ init {
+ setConnectionProperties(PROPERTY_SELF_MANAGED)
+ setAudioModeIsVoip(true)
+ setActive()
+ }
+
+ override fun onShowIncomingCallUi() {
+ super.onShowIncomingCallUi()
+ setActive()
+ currentActiveConnection = this
+ if (awaitingCallStateLatch?.nameAndNumberMatch(this) == true) {
+ awaitingCallStateLatch?.countDown()
+ }
+ }
+}
+
+class VoipHelperTestConnectionService : ConnectionService() {
+ override fun onCreateOutgoingConnection(
+ connectionManagerPhoneAccount: PhoneAccountHandle,
+ request: ConnectionRequest,
+ ): Connection {
+ return createConnection(request)
+ }
+
+ override fun onCreateIncomingConnection(
+ connectionManagerPhoneAccount: PhoneAccountHandle?,
+ request: ConnectionRequest?,
+ ): Connection {
+ return createConnection(request)
+ }
+
+ private fun createConnection(request: ConnectionRequest?): Connection {
+ val connection = VoIPConnection()
+ if (request?.extras?.containsKey(EXTRA_DISPLAY_NAME) == true) {
+ connection.setCallerDisplayName(
+ request.extras.getString(EXTRA_DISPLAY_NAME),
+ TelecomManager.PRESENTATION_ALLOWED,
+ )
+ connection.setAddress(
+ request.extras.getParcelable(
+ TelecomManager.EXTRA_INCOMING_CALL_ADDRESS,
+ Uri::class.java,
+ ),
+ TelecomManager.PRESENTATION_ALLOWED,
+ )
+ }
+ return connection
+ }
+}
diff --git a/tests/cts/role/Android.bp b/tests/cts/role/Android.bp
index f0095b7dd..9f1e6cff6 100644
--- a/tests/cts/role/Android.bp
+++ b/tests/cts/role/Android.bp
@@ -30,9 +30,12 @@ android_test {
static_libs: [
"android.permission.flags-aconfig-java-export",
"androidx.test.rules",
+ "com.android.permission.flags-aconfig-java-export",
"compatibility-device-util-axt",
"ctstestrunner-axt",
"Harrier",
+ "bedstead-multiuser",
+ "flag-junit",
"platform-test-annotations",
"truth",
],
diff --git a/tests/cts/role/src/android/app/role/cts/RoleManagerTest.java b/tests/cts/role/src/android/app/role/cts/RoleManagerTest.java
index e31659dbc..9f89140d7 100644
--- a/tests/cts/role/src/android/app/role/cts/RoleManagerTest.java
+++ b/tests/cts/role/src/android/app/role/cts/RoleManagerTest.java
@@ -16,6 +16,7 @@
package android.app.role.cts;
+import static com.android.bedstead.multiuser.MultiUserDeviceStateExtensionsKt.privateProfile;
import static com.android.compatibility.common.util.SystemUtil.callWithShellPermissionIdentity;
import static com.android.compatibility.common.util.SystemUtil.runShellCommand;
import static com.android.compatibility.common.util.SystemUtil.runShellCommandOrThrow;
@@ -66,7 +67,7 @@ import androidx.test.uiautomator.Until;
import com.android.bedstead.harrier.BedsteadJUnit4;
import com.android.bedstead.harrier.DeviceState;
-import com.android.bedstead.harrier.annotations.EnsureHasPrivateProfile;
+import com.android.bedstead.multiuser.annotations.EnsureHasPrivateProfile;
import com.android.bedstead.nene.types.OptionalBoolean;
import com.android.compatibility.common.util.DisableAnimationRule;
import com.android.compatibility.common.util.FreezeRotationRule;
@@ -103,6 +104,8 @@ public class RoleManagerTest {
private static final String ROLE_NAME = RoleManager.ROLE_BROWSER;
private static final String ROLE_PHONE_NAME = RoleManager.ROLE_DIALER;
private static final String ROLE_SMS_NAME = RoleManager.ROLE_SMS;
+ private static final String PROFILE_GROUP_EXCLUSIVE_ROLE_NAME =
+ RoleManager.ROLE_RESERVED_FOR_TESTING_PROFILE_GROUP_EXCLUSIVITY;
private static final String ROLE_SHORT_LABEL = "Browser app";
private static final String APP_APK_PATH = "/data/local/tmp/cts-role/CtsRoleTestApp.apk";
@@ -184,6 +187,7 @@ public class RoleManagerTest {
@Before
public void setUp() throws Exception {
+ assumeTrue(RoleManagerUtil.INSTANCE.isCddCompliantScreenSize());
saveRoleHolder();
installApp();
wakeUpScreen();
@@ -864,7 +868,7 @@ public class RoleManagerTest {
return;
}
- UserHandle privateProfile = sDeviceState.privateProfile().userHandle();
+ UserHandle privateProfile = privateProfile(sDeviceState).userHandle();
assertThat(privateProfile).isNotNull();
installPackage(APP_APK_PATH, privateProfile);
installPackage(APP_CLONE_APK_PATH, privateProfile);
@@ -1347,6 +1351,51 @@ public class RoleManagerTest {
});
}
+ @RequiresFlagsEnabled(com.android.permission.flags.Flags.FLAG_CROSS_USER_ROLE_ENABLED)
+ @Test
+ public void cannotGetActiveUserForRoleWithoutPermission() throws Exception {
+ assertThrows(SecurityException.class, ()->
+ sRoleManager.getActiveUserForRole(PROFILE_GROUP_EXCLUSIVE_ROLE_NAME));
+ }
+
+ @RequiresFlagsEnabled(com.android.permission.flags.Flags.FLAG_CROSS_USER_ROLE_ENABLED)
+ @Test
+ public void cannotGetActiveUserForNonProfileGroupExclusiveRole() throws Exception {
+ runWithShellPermissionIdentity(() ->
+ assertThrows(IllegalArgumentException.class, () ->
+ sRoleManager.getActiveUserForRole(
+ RoleManager.ROLE_SYSTEM_ACTIVITY_RECOGNIZER)));
+ }
+
+ @RequiresFlagsEnabled(com.android.permission.flags.Flags.FLAG_CROSS_USER_ROLE_ENABLED)
+ @Test
+ public void cannotSetActiveUserForRoleWithoutPermission() throws Exception {
+ assertThrows(SecurityException.class, ()->
+ sRoleManager.setActiveUserForRole(PROFILE_GROUP_EXCLUSIVE_ROLE_NAME,
+ Process.myUserHandle(), 0));
+ }
+
+ @RequiresFlagsEnabled(com.android.permission.flags.Flags.FLAG_CROSS_USER_ROLE_ENABLED)
+ @Test
+ public void cannotSetActiveUserForNonProfileGroupExclusiveRole() throws Exception {
+ runWithShellPermissionIdentity(() ->
+ assertThrows(IllegalArgumentException.class, () ->
+ sRoleManager.setActiveUserForRole(
+ RoleManager.ROLE_SYSTEM_ACTIVITY_RECOGNIZER, Process.myUserHandle(),
+ 0)));
+ }
+
+ @RequiresFlagsEnabled(com.android.permission.flags.Flags.FLAG_CROSS_USER_ROLE_ENABLED)
+ @Test
+ public void setAndGetActiveUserForRole() throws Exception {
+ runWithShellPermissionIdentity(() -> {
+ sRoleManager.setActiveUserForRole(PROFILE_GROUP_EXCLUSIVE_ROLE_NAME,
+ Process.myUserHandle(), 0);
+ assertThat(sRoleManager.getActiveUserForRole(PROFILE_GROUP_EXCLUSIVE_ROLE_NAME))
+ .isEqualTo(Process.myUserHandle());
+ });
+ }
+
@NonNull
private List<String> getRoleHolders(@NonNull String roleName) throws Exception {
return callWithShellPermissionIdentity(() -> sRoleManager.getRoleHolders(roleName));
diff --git a/tests/cts/role/src/android/app/role/cts/RoleManagerUtil.kt b/tests/cts/role/src/android/app/role/cts/RoleManagerUtil.kt
new file mode 100644
index 000000000..10a3834a2
--- /dev/null
+++ b/tests/cts/role/src/android/app/role/cts/RoleManagerUtil.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 android.app.role.cts
+
+import android.content.res.Configuration
+import android.content.res.Resources
+import android.util.Log
+
+object RoleManagerUtil {
+ private val TAG = RoleManagerUtil::class.java.getSimpleName()
+
+ /**
+ * This method checks for the minimum screen size described in CDD {@see
+ * https://source.android.com/docs/compatibility/14/android-14-cdd#7111_screen_size_and_shape}
+ */
+ fun isCddCompliantScreenSize(): Boolean {
+ if (
+ Resources.getSystem().configuration.uiMode and Configuration.UI_MODE_TYPE_MASK ==
+ Configuration.UI_MODE_TYPE_WATCH
+ ) {
+ Log.d(TAG, "UI mode is UI_MODE_TYPE_WATCH, skipping the min dp check")
+ return true
+ }
+
+ val screenSize =
+ Resources.getSystem().configuration.screenLayout and
+ Configuration.SCREENLAYOUT_SIZE_MASK
+ return when (screenSize) {
+ Configuration.SCREENLAYOUT_SIZE_SMALL -> hasMinScreenSize(426, 320)
+ Configuration.SCREENLAYOUT_SIZE_NORMAL -> hasMinScreenSize(480, 320)
+ Configuration.SCREENLAYOUT_SIZE_LARGE -> hasMinScreenSize(640, 480)
+ Configuration.SCREENLAYOUT_SIZE_XLARGE -> hasMinScreenSize(960, 720)
+ else -> {
+ Log.e(TAG, "Unknown screen size: $screenSize")
+ true
+ }
+ }
+ }
+
+ private fun hasMinScreenSize(minWidthDp: Int, minHeightDp: Int): Boolean {
+ val dpi = Resources.getSystem().displayMetrics.densityDpi
+ val widthDp = (160f / dpi) * Resources.getSystem().displayMetrics.widthPixels
+ val heightDp = (160f / dpi) * Resources.getSystem().displayMetrics.heightPixels
+
+ // CDD does seem to follow width & height convention correctly, hence checking both ways
+ return (widthDp >= minWidthDp && heightDp >= minHeightDp) ||
+ (widthDp >= minHeightDp && heightDp >= minWidthDp)
+ }
+}
diff --git a/tests/cts/role/src/android/app/role/cts/RoleShellCommandTest.kt b/tests/cts/role/src/android/app/role/cts/RoleShellCommandTest.kt
index 83d4f78ad..f615f0f4b 100644
--- a/tests/cts/role/src/android/app/role/cts/RoleShellCommandTest.kt
+++ b/tests/cts/role/src/android/app/role/cts/RoleShellCommandTest.kt
@@ -19,15 +19,19 @@ package android.app.role.cts
import android.app.role.RoleManager
import android.os.Build
import android.os.UserHandle
+import android.platform.test.annotations.RequiresFlagsEnabled
+import android.platform.test.flag.junit.DeviceFlagsValueProvider
import androidx.test.InstrumentationRegistry
import androidx.test.filters.SdkSuppress
import androidx.test.runner.AndroidJUnit4
import com.android.compatibility.common.util.SystemUtil.callWithShellPermissionIdentity
import com.android.compatibility.common.util.SystemUtil.runShellCommandOrThrow
+import com.android.permission.flags.Flags
import com.google.common.truth.Truth.assertThat
import org.junit.After
import org.junit.Assert.assertThrows
import org.junit.Before
+import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -43,6 +47,8 @@ class RoleShellCommandTest {
private var roleHolder: String? = null
private var wasBypassingRoleQualification: Boolean = false
+ @get:Rule val flagsRule = DeviceFlagsValueProvider.createCheckFlagsRule()
+
@Before
public fun setUp() {
saveRoleHolder()
@@ -156,6 +162,28 @@ class RoleShellCommandTest {
assertThat(isBypassingRoleQualification()).isFalse()
}
+ @RequiresFlagsEnabled(Flags.FLAG_CROSS_USER_ROLE_ENABLED)
+ @Test
+ fun setActiveUserForProfileGroupExclusiveRoleAsUser() {
+ val activeUser = userId
+ setActiveUserForRole(PROFILE_GROUP_EXCLUSIVITY_ROLE_NAME, activeUser)
+
+ val currentActiveUserId = getActiveUserForRole(PROFILE_GROUP_EXCLUSIVITY_ROLE_NAME)
+ assertThat(currentActiveUserId).isEqualTo(activeUser)
+ }
+
+ @RequiresFlagsEnabled(Flags.FLAG_CROSS_USER_ROLE_ENABLED)
+ @Test
+ fun setActiveUserForNonProfileGroupExclusiveRoleThenFails() {
+ assertThrows(AssertionError::class.java) { setActiveUserForRole(ROLE_NAME, userId) }
+ }
+
+ @RequiresFlagsEnabled(Flags.FLAG_CROSS_USER_ROLE_ENABLED)
+ @Test
+ fun getActiveUserForNonProfileGroupExclusiveRoleThenFails() {
+ assertThrows(AssertionError::class.java) { getActiveUserForRole(ROLE_NAME) }
+ }
+
private fun addRoleHolder(packageName: String = APP_PACKAGE_NAME) {
runShellCommandOrThrow("cmd role add-role-holder --user $userId $ROLE_NAME $packageName")
}
@@ -204,8 +232,22 @@ class RoleShellCommandTest {
callWithShellPermissionIdentity { roleManager.setBypassingRoleQualification(value) }
}
+ private fun getActiveUserForRole(roleName: String): Int? {
+ return runShellCommandOrThrow("cmd role get-active-user-for-role --user $userId $roleName")
+ .trim()
+ .toIntOrNull()
+ }
+
+ private fun setActiveUserForRole(roleName: String, activeUserId: Int) {
+ runShellCommandOrThrow(
+ "cmd role set-active-user-for-role --user $userId $roleName $activeUserId"
+ )
+ }
+
companion object {
private const val ROLE_NAME = RoleManager.ROLE_BROWSER
+ private const val PROFILE_GROUP_EXCLUSIVITY_ROLE_NAME =
+ RoleManager.ROLE_RESERVED_FOR_TESTING_PROFILE_GROUP_EXCLUSIVITY
private const val APP_APK_PATH = "/data/local/tmp/cts-role/CtsRoleTestApp.apk"
private const val APP_PACKAGE_NAME = "android.app.role.cts.app"
private const val APP_CLONE_APK_PATH = "/data/local/tmp/cts-role/CtsRoleTestAppClone.apk"
diff --git a/tests/cts/rolemultiuser/Android.bp b/tests/cts/rolemultiuser/Android.bp
new file mode 100644
index 000000000..13fbf28ff
--- /dev/null
+++ b/tests/cts/rolemultiuser/Android.bp
@@ -0,0 +1,47 @@
+// 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 {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test {
+ name: "CtsRoleMultiUserTestCases",
+ defaults: ["mts-target-sdk-version-current"],
+ sdk_version: "test_current",
+ min_sdk_version: "30",
+
+ srcs: [
+ "src/**/*.java",
+ "src/**/*.kt",
+ ],
+
+ static_libs: [
+ "bedstead-flags",
+ "bedstead-multiuser",
+ "com.android.permission.flags-aconfig-java-export",
+ "ctstestrunner-axt",
+ "Harrier",
+ "flag-junit",
+ "Nene",
+ "truth",
+ ],
+
+ test_suites: [
+ "cts",
+ "general-tests",
+ "mts-permission",
+ "mcts-permission",
+ ],
+}
diff --git a/tests/cts/rolemultiuser/AndroidManifest.xml b/tests/cts/rolemultiuser/AndroidManifest.xml
new file mode 100644
index 000000000..1524c5703
--- /dev/null
+++ b/tests/cts/rolemultiuser/AndroidManifest.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+ * 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.
+ -->
+
+<manifest
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ package="android.app.rolemultiuser.cts">
+
+ <application>
+ <uses-library android:name="android.test.runner" />
+ </application>
+
+ <instrumentation
+ android:name="androidx.test.runner.AndroidJUnitRunner"
+ android:targetPackage="android.app.rolemultiuser.cts"
+ android:label="CTS multi-user tests of android.app.role">
+ </instrumentation>
+</manifest>
diff --git a/tests/cts/rolemultiuser/AndroidTest.xml b/tests/cts/rolemultiuser/AndroidTest.xml
new file mode 100644
index 000000000..00f4c0993
--- /dev/null
+++ b/tests/cts/rolemultiuser/AndroidTest.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+ ~ 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.
+ -->
+
+<configuration description="Config for CTS role multi-user test cases">
+
+ <option name="test-suite-tag" value="cts" />
+ <option name="config-descriptor:metadata" key="component" value="permissions" />
+ <option name="config-descriptor:metadata" key="parameter" value="all_foldable_states" />
+ <option name="config-descriptor:metadata" key="parameter" value="not_instant_app" />
+ <option name="config-descriptor:metadata" key="parameter" value="not_multi_abi" />
+ <option name="config-descriptor:metadata" key="parameter" value="multiuser" />
+ <option name="config-descriptor:metadata" key="parameter" value="secondary_user" />
+ <option name="config-descriptor:metadata" key="parameter" value="run_on_sdk_sandbox" />
+ <option name="config-descriptor:metadata" key="mainline-param" value="com.google.android.permission.apex" />
+ <object type="module_controller" class="com.android.tradefed.testtype.suite.module.Sdk30ModuleController" />
+
+ <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+ <option name="cleanup-apks" value="true" />
+ <option name="test-file-name" value="CtsRoleMultiUserTestCases.apk" />
+ </target_preparer>
+
+ <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
+ <option name="run-command" value="mkdir -p /data/local/tmp/cts-role" />
+ <option name="teardown-command" value="rm -rf /data/local/tmp/cts-role"/>
+ </target_preparer>
+
+ <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+ <option name="package" value="android.app.rolemultiuser.cts" />
+ <option name="exclude-annotation" value="com.android.bedstead.enterprise.annotations.RequireRunOnWorkProfile" />
+ <option name="exclude-annotation" value="com.android.bedstead.multiuser.annotations.RequireRunOnSecondaryUser" />
+ <option name="runtime-hint" value="5m" />
+ </test>
+</configuration>
diff --git a/tests/cts/rolemultiuser/TEST_MAPPING b/tests/cts/rolemultiuser/TEST_MAPPING
new file mode 100644
index 000000000..323e3094c
--- /dev/null
+++ b/tests/cts/rolemultiuser/TEST_MAPPING
@@ -0,0 +1,12 @@
+{
+ "postsubmit": [
+ {
+ "name": "CtsRoleMultiUserTestCases"
+ }
+ ],
+ "mainline-postsubmit": [
+ {
+ "name": "CtsRoleMultiUserTestCases[com.google.android.permission.apex]"
+ }
+ ]
+}
diff --git a/tests/cts/rolemultiuser/src/android/app/rolemultiuser/cts/RoleManagerMultiUserTest.kt b/tests/cts/rolemultiuser/src/android/app/rolemultiuser/cts/RoleManagerMultiUserTest.kt
new file mode 100644
index 000000000..d1bf5284c
--- /dev/null
+++ b/tests/cts/rolemultiuser/src/android/app/rolemultiuser/cts/RoleManagerMultiUserTest.kt
@@ -0,0 +1,227 @@
+/*
+ * 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 android.app.rolemultiuser.cts
+
+import android.app.role.RoleManager
+import android.content.Context
+import android.os.Build
+import android.os.Process
+import androidx.test.filters.SdkSuppress
+import com.android.bedstead.enterprise.annotations.EnsureHasWorkProfile
+import com.android.bedstead.enterprise.workProfile
+import com.android.bedstead.flags.annotations.RequireFlagsEnabled
+import com.android.bedstead.harrier.BedsteadJUnit4
+import com.android.bedstead.harrier.DeviceState
+import com.android.bedstead.multiuser.annotations.EnsureHasAdditionalUser
+import com.android.bedstead.multiuser.annotations.EnsureHasPrivateProfile
+import com.android.bedstead.multiuser.annotations.EnsureHasSecondaryUser
+import com.android.bedstead.multiuser.annotations.RequireRunNotOnSecondaryUser
+import com.android.bedstead.multiuser.privateProfile
+import com.android.bedstead.multiuser.secondaryUser
+import com.android.bedstead.nene.TestApis.context
+import com.android.bedstead.nene.TestApis.users
+import com.android.bedstead.nene.types.OptionalBoolean
+import com.android.bedstead.permissions.CommonPermissions.INTERACT_ACROSS_USERS_FULL
+import com.android.bedstead.permissions.CommonPermissions.MANAGE_DEFAULT_APPLICATIONS
+import com.android.bedstead.permissions.CommonPermissions.MANAGE_ROLE_HOLDERS
+import com.android.bedstead.permissions.annotations.EnsureDoesNotHavePermission
+import com.android.bedstead.permissions.annotations.EnsureHasPermission
+import com.google.common.truth.Truth.assertThat
+import org.junit.Assert.assertThrows
+import org.junit.Assume.assumeFalse
+import org.junit.ClassRule
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM)
+@RunWith(BedsteadJUnit4::class)
+class RoleManagerMultiUserTest {
+ @RequireFlagsEnabled(com.android.permission.flags.Flags.FLAG_CROSS_USER_ROLE_ENABLED)
+ @EnsureHasPermission(INTERACT_ACROSS_USERS_FULL, MANAGE_ROLE_HOLDERS)
+ @Test
+ @Throws(Exception::class)
+ fun cannotGetActiveUserForNonCrossUserRole() {
+ assertThrows(IllegalArgumentException::class.java) {
+ roleManager.getActiveUserForRole(RoleManager.ROLE_SYSTEM_ACTIVITY_RECOGNIZER)
+ }
+ }
+
+ @RequireFlagsEnabled(com.android.permission.flags.Flags.FLAG_CROSS_USER_ROLE_ENABLED)
+ @EnsureHasPermission(MANAGE_ROLE_HOLDERS)
+ @EnsureDoesNotHavePermission(INTERACT_ACROSS_USERS_FULL)
+ @Test
+ @Throws(Exception::class)
+ fun cannotGetActiveUserForRoleWithoutInteractAcrossUserPermission() {
+ assertThrows(SecurityException::class.java) {
+ roleManager.getActiveUserForRole(PROFILE_GROUP_EXCLUSIVITY_ROLE_NAME)
+ }
+ }
+
+ @RequireFlagsEnabled(com.android.permission.flags.Flags.FLAG_CROSS_USER_ROLE_ENABLED)
+ @EnsureHasPermission(INTERACT_ACROSS_USERS_FULL)
+ @EnsureDoesNotHavePermission(MANAGE_ROLE_HOLDERS, MANAGE_DEFAULT_APPLICATIONS)
+ @Test
+ @Throws(Exception::class)
+ fun cannotGetActiveUserForRoleWithoutManageRoleAndManageDefaultApplicationsPermission() {
+ assertThrows(SecurityException::class.java) {
+ roleManager.getActiveUserForRole(PROFILE_GROUP_EXCLUSIVITY_ROLE_NAME)
+ }
+ }
+
+ @RequireFlagsEnabled(com.android.permission.flags.Flags.FLAG_CROSS_USER_ROLE_ENABLED)
+ @EnsureHasPermission(INTERACT_ACROSS_USERS_FULL, MANAGE_ROLE_HOLDERS)
+ @Test
+ @Throws(Exception::class)
+ fun cannotSetActiveUserForNonCrossUserRole() {
+ assertThrows(IllegalArgumentException::class.java) {
+ roleManager.setActiveUserForRole(
+ RoleManager.ROLE_SYSTEM_ACTIVITY_RECOGNIZER,
+ Process.myUserHandle(),
+ 0,
+ )
+ }
+ }
+
+ @RequireFlagsEnabled(com.android.permission.flags.Flags.FLAG_CROSS_USER_ROLE_ENABLED)
+ @EnsureHasPermission(MANAGE_ROLE_HOLDERS)
+ @EnsureDoesNotHavePermission(INTERACT_ACROSS_USERS_FULL)
+ @Test
+ @Throws(Exception::class)
+ fun cannotSetActiveUserForRoleWithoutInteractAcrossUserPermission() {
+ assertThrows(SecurityException::class.java) {
+ roleManager.setActiveUserForRole(
+ PROFILE_GROUP_EXCLUSIVITY_ROLE_NAME,
+ Process.myUserHandle(),
+ 0,
+ )
+ }
+ }
+
+ @RequireFlagsEnabled(com.android.permission.flags.Flags.FLAG_CROSS_USER_ROLE_ENABLED)
+ @EnsureHasPermission(INTERACT_ACROSS_USERS_FULL)
+ @EnsureDoesNotHavePermission(MANAGE_ROLE_HOLDERS, MANAGE_DEFAULT_APPLICATIONS)
+ @Test
+ @Throws(Exception::class)
+ fun cannotSetActiveUserForRoleWithoutManageRoleAndManageDefaultApplicationsPermission() {
+ assertThrows(SecurityException::class.java) {
+ roleManager.setActiveUserForRole(
+ PROFILE_GROUP_EXCLUSIVITY_ROLE_NAME,
+ Process.myUserHandle(),
+ 0,
+ )
+ }
+ }
+
+ @RequireFlagsEnabled(com.android.permission.flags.Flags.FLAG_CROSS_USER_ROLE_ENABLED)
+ @EnsureHasPermission(INTERACT_ACROSS_USERS_FULL, MANAGE_ROLE_HOLDERS)
+ @Test
+ @Throws(Exception::class)
+ fun cannotSetActiveUserForRoleToNonExistentUser() {
+ val targetActiveUser = users().nonExisting().userHandle()
+ roleManager.setActiveUserForRole(PROFILE_GROUP_EXCLUSIVITY_ROLE_NAME, targetActiveUser, 0)
+
+ assertThat(roleManager.getActiveUserForRole(PROFILE_GROUP_EXCLUSIVITY_ROLE_NAME))
+ .isNotEqualTo(targetActiveUser)
+ }
+
+ @RequireFlagsEnabled(com.android.permission.flags.Flags.FLAG_CROSS_USER_ROLE_ENABLED)
+ @EnsureHasPermission(INTERACT_ACROSS_USERS_FULL, MANAGE_ROLE_HOLDERS)
+ @EnsureHasPrivateProfile(installInstrumentedApp = OptionalBoolean.TRUE)
+ @Test
+ @Throws(Exception::class)
+ fun cannotSetActiveUserForRoleToPrivateProfileUser() {
+ val targetActiveUser = deviceState.privateProfile().userHandle()
+ roleManager.setActiveUserForRole(PROFILE_GROUP_EXCLUSIVITY_ROLE_NAME, targetActiveUser, 0)
+
+ assertThat(roleManager.getActiveUserForRole(PROFILE_GROUP_EXCLUSIVITY_ROLE_NAME))
+ .isNotEqualTo(targetActiveUser)
+ }
+
+ @RequireFlagsEnabled(com.android.permission.flags.Flags.FLAG_CROSS_USER_ROLE_ENABLED)
+ @EnsureHasPermission(INTERACT_ACROSS_USERS_FULL, MANAGE_ROLE_HOLDERS)
+ @EnsureHasAdditionalUser(installInstrumentedApp = OptionalBoolean.TRUE)
+ @EnsureHasSecondaryUser
+ @RequireRunNotOnSecondaryUser
+ @Test
+ @Throws(Exception::class)
+ fun cannotSetActiveUserForRoleToUserNotInProfileGroup() {
+ val targetActiveUser = deviceState.secondaryUser().userHandle()
+ roleManager.setActiveUserForRole(PROFILE_GROUP_EXCLUSIVITY_ROLE_NAME, targetActiveUser, 0)
+
+ assertThat(roleManager.getActiveUserForRole(PROFILE_GROUP_EXCLUSIVITY_ROLE_NAME))
+ .isNotEqualTo(targetActiveUser)
+ }
+
+ @RequireFlagsEnabled(com.android.permission.flags.Flags.FLAG_CROSS_USER_ROLE_ENABLED)
+ @EnsureHasPermission(INTERACT_ACROSS_USERS_FULL, MANAGE_ROLE_HOLDERS)
+ @EnsureDoesNotHavePermission(MANAGE_DEFAULT_APPLICATIONS)
+ @EnsureHasWorkProfile(installInstrumentedApp = OptionalBoolean.TRUE)
+ @Test
+ @Throws(Exception::class)
+ fun setAndGetActiveUserForRoleSetCurrentUserWithManageRoleHoldersPermission() {
+ assumeFalse(
+ "setActiveUser not supported for private profile",
+ users().current().type().name() == PRIVATE_PROFILE_TYPE_NAME,
+ )
+
+ val targetActiveUser = users().current().userHandle()
+ roleManager.setActiveUserForRole(PROFILE_GROUP_EXCLUSIVITY_ROLE_NAME, targetActiveUser, 0)
+ assertThat(roleManager.getActiveUserForRole(PROFILE_GROUP_EXCLUSIVITY_ROLE_NAME))
+ .isEqualTo(targetActiveUser)
+ }
+
+ @RequireFlagsEnabled(com.android.permission.flags.Flags.FLAG_CROSS_USER_ROLE_ENABLED)
+ @EnsureHasPermission(INTERACT_ACROSS_USERS_FULL, MANAGE_DEFAULT_APPLICATIONS)
+ @EnsureDoesNotHavePermission(MANAGE_ROLE_HOLDERS)
+ @EnsureHasWorkProfile(installInstrumentedApp = OptionalBoolean.TRUE)
+ @Test
+ @Throws(Exception::class)
+ fun setAndGetActiveUserForRoleSetCurrentUserWithManageDefaultApplicationPermission() {
+ assumeFalse(
+ "setActiveUser not supported for private profile",
+ users().current().type().name() == PRIVATE_PROFILE_TYPE_NAME,
+ )
+
+ val targetActiveUser = users().current().userHandle()
+ roleManager.setActiveUserForRole(PROFILE_GROUP_EXCLUSIVITY_ROLE_NAME, targetActiveUser, 0)
+ assertThat(roleManager.getActiveUserForRole(PROFILE_GROUP_EXCLUSIVITY_ROLE_NAME))
+ .isEqualTo(targetActiveUser)
+ }
+
+ @RequireFlagsEnabled(com.android.permission.flags.Flags.FLAG_CROSS_USER_ROLE_ENABLED)
+ @EnsureHasPermission(INTERACT_ACROSS_USERS_FULL, MANAGE_ROLE_HOLDERS)
+ @EnsureHasWorkProfile(installInstrumentedApp = OptionalBoolean.TRUE)
+ @Test
+ @Throws(Exception::class)
+ fun setAndGetActiveUserForRoleSetWorkProfile() {
+ val targetActiveUser = deviceState.workProfile().userHandle()
+ roleManager.setActiveUserForRole(PROFILE_GROUP_EXCLUSIVITY_ROLE_NAME, targetActiveUser, 0)
+
+ assertThat(roleManager.getActiveUserForRole(PROFILE_GROUP_EXCLUSIVITY_ROLE_NAME))
+ .isEqualTo(targetActiveUser)
+ }
+
+ companion object {
+ private const val PROFILE_GROUP_EXCLUSIVITY_ROLE_NAME =
+ RoleManager.ROLE_RESERVED_FOR_TESTING_PROFILE_GROUP_EXCLUSIVITY
+ private const val PRIVATE_PROFILE_TYPE_NAME = "android.os.usertype.profile.PRIVATE"
+ private val context: Context = context().instrumentedContext()
+ private val roleManager: RoleManager = context.getSystemService(RoleManager::class.java)
+
+ @JvmField @ClassRule @Rule val deviceState = DeviceState()
+ }
+}
diff --git a/tests/functional/safetycenter/multiusers/Android.bp b/tests/functional/safetycenter/multiusers/Android.bp
index 30024221b..745e763f0 100644
--- a/tests/functional/safetycenter/multiusers/Android.bp
+++ b/tests/functional/safetycenter/multiusers/Android.bp
@@ -36,6 +36,7 @@ android_test {
"Harrier",
"Nene",
"TestApp",
+ "bedstead-enterprise",
"com.android.permission.flags-aconfig-java-export",
],
test_suites: [
diff --git a/tests/functional/safetycenter/multiusers/src/android/safetycenter/functional/multiusers/SafetyCenterMultiUsersTest.kt b/tests/functional/safetycenter/multiusers/src/android/safetycenter/functional/multiusers/SafetyCenterMultiUsersTest.kt
index ff5dcc1f6..82ca0365c 100644
--- a/tests/functional/safetycenter/multiusers/src/android/safetycenter/functional/multiusers/SafetyCenterMultiUsersTest.kt
+++ b/tests/functional/safetycenter/multiusers/src/android/safetycenter/functional/multiusers/SafetyCenterMultiUsersTest.kt
@@ -43,14 +43,18 @@ import androidx.test.core.app.ApplicationProvider
import androidx.test.filters.LargeTest
import com.android.bedstead.harrier.BedsteadJUnit4
import com.android.bedstead.harrier.DeviceState
-import com.android.bedstead.harrier.annotations.EnsureHasAdditionalUser
-import com.android.bedstead.harrier.annotations.EnsureHasCloneProfile
+import com.android.bedstead.multiuser.annotations.EnsureHasAdditionalUser
+import com.android.bedstead.multiuser.annotations.EnsureHasCloneProfile
import com.android.bedstead.enterprise.annotations.EnsureHasNoWorkProfile
-import com.android.bedstead.harrier.annotations.EnsureHasPrivateProfile
-import com.android.bedstead.harrier.annotations.EnsureHasNoPrivateProfile
+import com.android.bedstead.multiuser.annotations.EnsureHasPrivateProfile
+import com.android.bedstead.multiuser.annotations.EnsureHasNoPrivateProfile
import com.android.bedstead.enterprise.annotations.EnsureHasWorkProfile
import com.android.bedstead.enterprise.annotations.EnsureHasDeviceOwner
import com.android.bedstead.enterprise.annotations.EnsureHasNoDeviceOwner
+import com.android.bedstead.enterprise.workProfile
+import com.android.bedstead.multiuser.additionalUser
+import com.android.bedstead.multiuser.cloneProfile
+import com.android.bedstead.multiuser.privateProfile
import com.android.bedstead.nene.TestApis
import com.android.bedstead.nene.types.OptionalBoolean.TRUE
import com.android.compatibility.common.util.DisableAnimationRule
diff --git a/tests/functional/safetycenter/singleuser/AndroidTest.xml b/tests/functional/safetycenter/singleuser/AndroidTest.xml
index 3aa173508..af040eb6f 100644
--- a/tests/functional/safetycenter/singleuser/AndroidTest.xml
+++ b/tests/functional/safetycenter/singleuser/AndroidTest.xml
@@ -47,6 +47,10 @@
<!-- Disable syncing to prevent overwriting flags during testing. -->
<option name="run-command" value="device_config set_sync_disabled_for_tests persistent" />
<option name="teardown-command" value="device_config set_sync_disabled_for_tests none" />
+ <!-- TODO(b/379928062): Ensure device not on lockscreen. Reassess when keyguard bug is
+ closed -->
+ <option name="run-command" value="input keyevent KEYCODE_WAKEUP" />
+ <option name="run-command" value="wm dismiss-keyguard" />
<!-- Dismiss any system dialogs (e.g. crashes, ANR). -->
<option name="run-command" value="am broadcast -a android.intent.action.CLOSE_SYSTEM_DIALOGS --receiver-foreground" />
</target_preparer>
diff --git a/tests/utils/safetycenter/java/com/android/safetycenter/testing/SafetyCenterFlags.kt b/tests/utils/safetycenter/java/com/android/safetycenter/testing/SafetyCenterFlags.kt
index 912ea44ad..f6bef747d 100644
--- a/tests/utils/safetycenter/java/com/android/safetycenter/testing/SafetyCenterFlags.kt
+++ b/tests/utils/safetycenter/java/com/android/safetycenter/testing/SafetyCenterFlags.kt
@@ -17,6 +17,7 @@
package com.android.safetycenter.testing
import android.Manifest.permission.READ_DEVICE_CONFIG
+import android.Manifest.permission.WRITE_ALLOWLISTED_DEVICE_CONFIG
import android.Manifest.permission.WRITE_DEVICE_CONFIG
import android.annotation.TargetApi
import android.app.job.JobInfo
@@ -532,7 +533,7 @@ object SafetyCenterFlags {
}
private fun writeDeviceConfigProperty(name: String, stringValue: String?) {
- callWithShellPermissionIdentity(WRITE_DEVICE_CONFIG) {
+ callWithShellPermissionIdentity(WRITE_DEVICE_CONFIG, WRITE_ALLOWLISTED_DEVICE_CONFIG) {
val valueWasSet =
DeviceConfig.setProperty(
NAMESPACE_PRIVACY,