diff options
Diffstat (limited to 'PermissionController')
121 files changed, 4795 insertions, 1673 deletions
diff --git a/PermissionController/Android.bp b/PermissionController/Android.bp index 37a94cc61..397870c20 100644 --- a/PermissionController/Android.bp +++ b/PermissionController/Android.bp @@ -159,6 +159,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..74303a439 100644 --- a/PermissionController/jarjar-rules.txt +++ b/PermissionController/jarjar-rules.txt @@ -2,6 +2,10 @@ # RoleParser.applyJarjarTransform(), by adding NO_IFTTT=reason to your commit # message. # LINT.IfChange +rule android.app.admin.flags.*FeatureFlags* com.android.permissioncontroller.jarjar.@0 +rule android.app.admin.flags.FeatureFlags* com.android.permissioncontroller.jarjar.@0 +rule android.app.admin.flags.FeatureFlags com.android.permissioncontroller.jarjar.@0 +rule android.app.admin.flags.Flags com.android.permissioncontroller.jarjar.@0 rule android.app.appfunctions.flags.*FeatureFlags* com.android.permissioncontroller.jarjar.@0 rule android.app.appfunctions.flags.FeatureFlags* com.android.permissioncontroller.jarjar.@0 rule android.app.appfunctions.flags.FeatureFlags com.android.permissioncontroller.jarjar.@0 @@ -22,4 +26,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-af-v33/strings.xml b/PermissionController/res/values-af-v33/strings.xml index 6d6a118cd..8bfc6821d 100644 --- a/PermissionController/res/values-af-v33/strings.xml +++ b/PermissionController/res/values-af-v33/strings.xml @@ -16,8 +16,8 @@ <resources xmlns:android="http://schemas.android.com/apk/res/android" xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> - <string name="role_dialer_request_description" msgid="6188305064871543419">"Hierdie program sal toegelaat word om vir jou kennisgewings te stuur, en sal toegang tot jou kamera, kontakte, mikrofoon, foon en SMS\'e kry"</string> - <string name="role_sms_request_description" msgid="1506966389698625395">"Hierdie program sal toegelaat word om vir jou kennisgewings te stuur, en sal toegang kry tot jou kamera, kontakte, lêers, mikrofoon, foon en SMS\'e"</string> + <string name="role_dialer_request_description" msgid="6188305064871543419">"Hierdie app sal toegelaat word om vir jou kennisgewings te stuur, en sal toegang tot jou kamera, kontakte, mikrofoon, foon en SMS\'e kry"</string> + <string name="role_sms_request_description" msgid="1506966389698625395">"Hierdie app sal toegelaat word om vir jou kennisgewings te stuur, en sal toegang kry tot jou kamera, kontakte, lêers, mikrofoon, foon en SMS\'e"</string> <string name="permission_description_summary_storage" msgid="1917071243213043858">"Programme met hierdie toestemming het toegang tot alle lêers op hierdie toestel"</string> <string name="work_policy_title" msgid="832967780713677409">"Jou werkbeleidinligting"</string> <string name="work_policy_summary" msgid="3886113358084963931">"Instellings wat deur jou IT-admin bestuur word"</string> diff --git a/PermissionController/res/values-af/strings.xml b/PermissionController/res/values-af/strings.xml index 728ef11ef..dbaaeffdc 100644 --- a/PermissionController/res/values-af/strings.xml +++ b/PermissionController/res/values-af/strings.xml @@ -81,8 +81,8 @@ <string name="app_permissions_info_button_label" msgid="7633312050729974623">"Maak programinligting oop"</string> <string name="additional_permissions_more" msgid="5681220714755304407">"{count,plural, =1{Nog #}other{Nog #}}"</string> <string name="old_sdk_deny_warning" msgid="2382236998845153919">"Hierdie program is vir \'n ouer weergawe van Android ontwerp. As toestemming geweier word, kan dit veroorsaak dat dit dalk nie meer soos bedoel werk nie."</string> - <string name="storage_supergroup_warning_allow" msgid="103093462784523190">"Hierdie program is vir \'n ouer weergawe van Android ontwerp. As jy hierdie toestemming toelaat, sal toegang tot alle berging (insluitend foto\'s, video\'s, musiek, oudio en ander lêers) toegelaat word."</string> - <string name="storage_supergroup_warning_deny" msgid="6420765672683284347">"Hierdie program is vir \'n ouer weergawe van Android ontwerp. As jy hierdie toestemming weier, sal toegang tot alle berging (insluitend foto\'s, video\'s, musiek, oudio en ander lêers) geweier word."</string> + <string name="storage_supergroup_warning_allow" msgid="103093462784523190">"Hierdie app is vir \'n ouer weergawe van Android ontwerp. As jy hierdie toestemming toelaat, sal toegang tot alle berging (insluitend foto\'s, video\'s, musiek, oudio en ander lêers) toegelaat word."</string> + <string name="storage_supergroup_warning_deny" msgid="6420765672683284347">"Hierdie app is vir \'n ouer weergawe van Android ontwerp. As jy hierdie toestemming weier, sal toegang tot alle berging (insluitend foto\'s, video\'s, musiek, oudio en ander lêers) geweier word."</string> <string name="default_permission_description" msgid="4624464917726285203">"voer \'n onbekende handeling uit"</string> <string name="app_permissions_group_summary" msgid="8788419008958284002">"<xliff:g id="COUNT_0">%1$d</xliff:g> van <xliff:g id="COUNT_1">%2$d</xliff:g> programme toegelaat"</string> <string name="app_permissions_group_summary2" msgid="4329922444840521150">"<xliff:g id="COUNT_0">%1$d</xliff:g>/<xliff:g id="COUNT_1">%2$d</xliff:g> programme toegelaat"</string> @@ -95,8 +95,8 @@ <string name="location_settings" msgid="3624412509133422562">"Ligginginstellings"</string> <string name="location_warning" msgid="2381649060929040962">"<xliff:g id="APP_NAME">%1$s</xliff:g> is \'n verskaffer van liggingdienste vir hierdie toestel. Liggingtoegang kan vanuit ligginginstellings gewysig word."</string> <string name="system_warning" msgid="1173400963234358816">"As jy hierdie toestemming weier, sal basiese kenmerke van jou toestel dalk nie meer soos bedoel werk nie."</string> - <string name="deny_read_media_visual_warning" msgid="3982586279917232827">"Hierdie program is vir \'n ouer weergawe van Android ontwerp. As jy nie vir hierdie program toegang tot foto\'s en video\'s gee nie, sal dit ook nie toegang tot musiek en ander oudio hê nie."</string> - <string name="deny_read_media_aural_warning" msgid="8928699919508646732">"Hierdie program is vir \'n ouer weergawe van Android ontwerp. As jy nie vir hierdie program toegang tot musiek en ander oudio gee nie, sal dit ook nie toegang tot foto\'s en video\'s hê nie."</string> + <string name="deny_read_media_visual_warning" msgid="3982586279917232827">"Hierdie app is vir \'n ouer weergawe van Android ontwerp. As jy nie vir hierdie app toegang tot foto\'s en video\'s gee nie, sal dit ook nie toegang tot musiek en ander oudio hê nie."</string> + <string name="deny_read_media_aural_warning" msgid="8928699919508646732">"Hierdie app is vir \'n ouer weergawe van Android ontwerp. As jy nie vir hierdie app toegang tot musiek en ander oudio gee nie, sal dit ook nie toegang tot foto\'s en video\'s hê nie."</string> <string name="cdm_profile_revoke_warning" msgid="4443893270719106700">"As jy hierdie toestemming weier, sal sommige kenmerke van jou toestel wat deur hierdie program bestuur word dalk nie meer soos bedoel werk nie."</string> <string name="permission_summary_enforced_by_policy" msgid="4443598170942950519">"Afgedwing deur beleid"</string> <string name="permission_summary_disabled_by_policy_background_only" msgid="221995005556362660">"Agtergrondtoegang is gedeaktiveer volgens beleid"</string> @@ -365,7 +365,7 @@ <string name="role_dialer_short_label" msgid="7186888549465352489">"Foonapp"</string> <string name="role_dialer_description" msgid="8768708633696539612">"Apps wat jou toelaat om telefoonoproepe op jou toestel te maak en te ontvang"</string> <string name="role_dialer_request_title" msgid="5959618560705912058">"Stel <xliff:g id="APP_NAME">%1$s</xliff:g> as jou verstekfoonprogram?"</string> - <string name="role_dialer_request_description" msgid="6288839625724909320">"Hierdie program sal toegang tot jou kamera, kontakte, mikrofoon, foon en SMS\'e kry"</string> + <string name="role_dialer_request_description" msgid="6288839625724909320">"Hierdie app sal toegang tot jou kamera, kontakte, mikrofoon, foon en SMS\'e kry"</string> <string name="role_dialer_search_keywords" msgid="3324448983559188087">"beller"</string> <string name="role_sms_label" msgid="8456999857547686640">"Verstek-SMS-app"</string> <string name="role_sms_short_label" msgid="4371444488034692243">"SMS-app"</string> @@ -532,8 +532,8 @@ <string name="permgroupupgraderequestdetail_sensors" msgid="6651914048792092835">"Dié app wil dalk deurentyd toegang tot sensordata oor jou lewenstekens hê, selfs wanneer jy nie die app gebruik nie. "<annotation id="link">"Gaan na instellings"</annotation>" om hierdie verandering te maak."</string> <string name="permgroupbackgroundrequest_sensors" msgid="5661924322018503886">"Gee <b><xliff:g id="APP_NAME">%1$s</xliff:g></b> toegang tot die sensordata oor jou lewenstekens?"</string> <string name="permgroupbackgroundrequest_device_aware_sensors" msgid="3687673359121603824">"Gee <b><xliff:g id="APP_NAME">%1$s</xliff:g></b> toegang tot die sensordata oor jou lewenstekens op <b><xliff:g id="DEVICE_NAME">%2$s</xliff:g></b>?"</string> - <string name="permgroupbackgroundrequestdetail_sensors" msgid="7726767635834043501"><annotation id="link">"Gaan na instellings"</annotation>" om altyd vir hierdie program toegang tot liggaamsensordata te gee, selfs wanneer jy nie die program gebruik nie."</string> - <string name="permgroupupgraderequest_sensors" msgid="7576527638411370468">"Gee <b><xliff:g id="APP_NAME">%1$s</xliff:g></b> steeds toegang tot liggaamsensordata terwyl die program gebruik word?"</string> + <string name="permgroupbackgroundrequestdetail_sensors" msgid="7726767635834043501"><annotation id="link">"Gaan na instellings"</annotation>" om altyd vir hierdie app toegang tot liggaamsensordata te gee, selfs wanneer jy nie die app gebruik nie."</string> + <string name="permgroupupgraderequest_sensors" msgid="7576527638411370468">"Gee <b><xliff:g id="APP_NAME">%1$s</xliff:g></b> steeds toegang tot liggaamsensordata terwyl die app gebruik word?"</string> <string name="permgroupupgraderequest_device_aware_sensors" msgid="5542771499929819675">"Gee <b><xliff:g id="APP_NAME">%1$s</xliff:g></b> steeds toegang tot liggaamsensordata op <b><xliff:g id="DEVICE_NAME">%2$s</xliff:g></b> terwyl die app gebruik word?"</string> <string name="permgrouprequest_notifications" msgid="6396739062335106181">"Laat <b><xliff:g id="APP_NAME">%1$s</xliff:g></b> toe om vir jou kennisgewings te stuur?"</string> <string name="permgrouprequest_device_aware_notifications" msgid="857671638951040514">"Gee <b><xliff:g id="APP_NAME">%1$s</xliff:g></b> toegang om vir jou kennisgewings te stuur op <b><xliff:g id="DEVICE_NAME">%2$s</xliff:g></b>?"</string> @@ -588,8 +588,8 @@ <string name="permissions_removed_qs" msgid="8957319130625294572">"Toestemming is verwyder"</string> <string name="camera_usage_qs" msgid="4394233566086665994">"Sien onlangse kameragebruik"</string> <string name="microphone_usage_qs" msgid="8527666682168170417">"Sien onlangse mikrofoongebruik"</string> - <string name="remove_camera_qs" msgid="3649996161066883350">"Verwyder toestemming vir hierdie program"</string> - <string name="remove_microphone_qs" msgid="1276551965129953198">"Verwyder toestemming vir hierdie program"</string> + <string name="remove_camera_qs" msgid="3649996161066883350">"Verwyder toestemming vir hierdie app"</string> + <string name="remove_microphone_qs" msgid="1276551965129953198">"Verwyder toestemming vir hierdie app"</string> <string name="manage_service_qs" msgid="7862555549364153805">"Bestuur diens"</string> <string name="manage_permissions_qs" msgid="3780541819763475434">"Bestuur toestemmings"</string> <string name="active_call_usage_qs" msgid="8559974395932523391">"Word tans gebruik deur foonoproep"</string> @@ -610,14 +610,14 @@ <string name="media_confirm_dialog_title_q_to_s_aural_deny" msgid="3128147568953297969">"Toegang tot foto\'s en video\'s sal ook nie toegelaat word"</string> <string name="media_confirm_dialog_title_q_to_s_visual_allow" msgid="6310682466493330434">"Toegang tot musiek- en oudiolêers sal ook toegelaat word"</string> <string name="media_confirm_dialog_title_q_to_s_visual_deny" msgid="1123845663785900471">"Toegang tot musiek- en oudiolêers sal ook nie toegelaat word nie"</string> - <string name="media_confirm_dialog_message_a_to_p_aural_allow" msgid="7865167246140107623">"Hierdie program steun nie die jongste weergawe van Android nie. As hierdie program toegang tot musiek- en oudiolêers het, sal dit ook toegelaat word om toegang tot foto\'s, video\'s en ander lêers te kry."</string> - <string name="media_confirm_dialog_message_a_to_p_aural_deny" msgid="287502523664804786">"Hierdie program steun nie die jongste weergawe van Android nie. As hierdie program nie toegang tot musiek- en oudiolêers het nie, sal dit ook nie toegelaat word om toegang tot foto\'s, video\'s en ander lêers te kry nie."</string> - <string name="media_confirm_dialog_message_a_to_p_visual_allow" msgid="4952410892939590487">"Hierdie program steun nie die jongste weergawe van Android nie. As hierdie program toegang tot foto\'s en video\'s het, sal dit ook toegelaat word om toegang tot musiek-, oudio- en ander lêers te kry."</string> - <string name="media_confirm_dialog_message_a_to_p_visual_deny" msgid="6609500525590757681">"Hierdie program steun nie die jongste weergawe van Android nie. As hierdie program nie toegang tot foto\'s en video\'s het nie, sal dit ook nie toegelaat word om toegang tot musiek-, oudio- en ander lêers te kry nie."</string> - <string name="media_confirm_dialog_message_q_to_s_aural_allow" msgid="1702402580147536160">"Hierdie program steun nie die jongste weergawe van Android nie. As hierdie program toegang tot musiek en oudiolêers het, sal dit ook toegelaat word om toegang tot foto\'s en video\'s te kry."</string> - <string name="media_confirm_dialog_message_q_to_s_aural_deny" msgid="6832087393653561911">"Hierdie program steun nie die jongste weergawe van Android nie. As hierdie program nie toegang tot musiek- en oudiolêers het nie, sal dit ook nie toegelaat word om toegang tot foto\'s en video\'s te kry nie."</string> - <string name="media_confirm_dialog_message_q_to_s_visual_allow" msgid="3504335060843147760">"Hierdie program steun nie die jongste weergawe van Android nie. As hierdie program toegang tot foto\'s en video\'s het, sal dit ook toegelaat word toegang tot musiek-, oudio- en ander lêers te kry."</string> - <string name="media_confirm_dialog_message_q_to_s_visual_deny" msgid="2145973462806481992">"Hierdie program steun nie die jongste weergawe van Android nie. As hierdie program nie toegang tot musiek- en oudiolêers het nie, sal dit ook nie toegelaat word om toegang tot foto\'s en video\'s te kry nie."</string> + <string name="media_confirm_dialog_message_a_to_p_aural_allow" msgid="7865167246140107623">"Hierdie app steun nie die jongste weergawe van Android nie. As hierdie app toegang tot musiek- en oudiolêers het, sal dit ook toegelaat word om toegang tot foto\'s, video\'s en ander lêers te kry."</string> + <string name="media_confirm_dialog_message_a_to_p_aural_deny" msgid="287502523664804786">"Hierdie app steun nie die jongste weergawe van Android nie. As hierdie app nie toegang tot musiek- en oudiolêers het nie, sal dit ook nie toegelaat word om toegang tot foto\'s, video\'s en ander lêers te kry nie."</string> + <string name="media_confirm_dialog_message_a_to_p_visual_allow" msgid="4952410892939590487">"Hierdie app steun nie die jongste weergawe van Android nie. As hierdie app toegang tot foto\'s en video\'s het, sal dit ook toegelaat word om toegang tot musiek-, oudio- en ander lêers te kry."</string> + <string name="media_confirm_dialog_message_a_to_p_visual_deny" msgid="6609500525590757681">"Hierdie app steun nie die jongste weergawe van Android nie. As hierdie app nie toegang tot foto\'s en video\'s het nie, sal dit ook nie toegelaat word om toegang tot musiek-, oudio- en ander lêers te kry nie."</string> + <string name="media_confirm_dialog_message_q_to_s_aural_allow" msgid="1702402580147536160">"Hierdie app steun nie die jongste weergawe van Android nie. As hierdie app toegang tot musiek en oudiolêers het, sal dit ook toegelaat word om toegang tot foto\'s en video\'s te kry."</string> + <string name="media_confirm_dialog_message_q_to_s_aural_deny" msgid="6832087393653561911">"Hierdie app steun nie die jongste weergawe van Android nie. As hierdie app nie toegang tot musiek- en oudiolêers het nie, sal dit ook nie toegelaat word om toegang tot foto\'s en video\'s te kry nie."</string> + <string name="media_confirm_dialog_message_q_to_s_visual_allow" msgid="3504335060843147760">"Hierdie app steun nie die jongste weergawe van Android nie. As hierdie app toegang tot foto\'s en video\'s het, sal dit ook toegelaat word toegang tot musiek-, oudio- en ander lêers te kry."</string> + <string name="media_confirm_dialog_message_q_to_s_visual_deny" msgid="2145973462806481992">"Hierdie app steun nie die jongste weergawe van Android nie. As hierdie app nie toegang tot musiek- en oudiolêers het nie, sal dit ook nie toegelaat word om toegang tot foto\'s en video\'s te kry nie."</string> <string name="safety_center_background_location_access_notification_title" msgid="8933610618810588237">"Gaan app met agtergrondliggingtoegang na"</string> <string name="safety_center_background_location_access_reminder_notification_content" msgid="4066560182507301022">"<xliff:g id="APP_NAME">%s</xliff:g> het altyd toegang tot jou ligging, selfs wanneer die app toe is"</string> <string name="safety_center_background_location_access_reminder_title" msgid="5477847038103863843">"Gaan app met agtergrondliggingtoegang na"</string> diff --git a/PermissionController/res/values-bg/strings.xml b/PermissionController/res/values-bg/strings.xml index 9bd171db2..aad55e46f 100644 --- a/PermissionController/res/values-bg/strings.xml +++ b/PermissionController/res/values-bg/strings.xml @@ -676,7 +676,7 @@ <string name="enhanced_confirmation_dialog_desc" msgid="5921240234843839219">"От съображения за сигурност понастоящем тази настройка не е налице."</string> <string name="enhanced_confirmation_dialog_title_permission" msgid="2149144789394238266">"На приложението бе отказан достъп до <xliff:g id="PERMISSION_NAME">%1$s</xliff:g>"</string> <string name="enhanced_confirmation_dialog_desc_permission" msgid="3150778951946468945">"Приложението поиска разрешение за достъп до поверителни данни, което може да изложи на риск личната или финансовата ви информация.<xliff:g id="ID_1"><br><br></xliff:g>Възможно е приложението да не работи правилно без това ограничено разрешение. <a href=<xliff:g id="LEARN_MORE_LINK">%1$s</xliff:g>>Научете как да разрешите достъпа</a>"</string> - <string name="enhanced_confirmation_dialog_title_role" msgid="1737023798483772780">"На приложението бе забранено да изпълнява функциите на основно <xliff:g id="ROLE_NAME">%1$s</xliff:g>"</string> + <string name="enhanced_confirmation_dialog_title_role" msgid="1737023798483772780">"Приложението не получи разрешение да бъде по подразбиране: <xliff:g id="ROLE_NAME">%1$s</xliff:g>"</string> <string name="enhanced_confirmation_dialog_desc_role" msgid="6369601947905234551">"Приложението поиска разрешения за достъп до поверителни данни, които може да изложат на риск личната или финансовата ви информация.<xliff:g id="ID_1"><br><br></xliff:g>Възможно е приложението да не работи правилно без тези ограничени разрешения. <a href=<xliff:g id="LEARN_MORE_LINK">%1$s</xliff:g>>Научете как да разрешите достъпа</a>"</string> <string name="enhanced_confirmation_dialog_title_settings_default" msgid="1858092969721041576">"На приложението бе отказан достъп"</string> <string name="enhanced_confirmation_dialog_desc_settings_default" msgid="6911632348359332981">"Достъпът до това разрешение може да изложи на риск личната и финансовата ви информация.<xliff:g id="ID_1"><br><br></xliff:g>Възможно е приложението да не работи правилно без това ограничено разрешение. <a href=<xliff:g id="LEARN_MORE_LINK">%1$s</xliff:g>>Научете как да разрешите достъпа</a>"</string> diff --git a/PermissionController/res/values-ca/strings.xml b/PermissionController/res/values-ca/strings.xml index 1de8e07e6..c0bd6cb8a 100644 --- a/PermissionController/res/values-ca/strings.xml +++ b/PermissionController/res/values-ca/strings.xml @@ -679,7 +679,7 @@ <string name="enhanced_confirmation_dialog_title_role" msgid="1737023798483772780">"A l\'aplicació se li ha denegat l\'accés per ser <xliff:g id="ROLE_NAME">%1$s</xliff:g> de manera predeterminada"</string> <string name="enhanced_confirmation_dialog_desc_role" msgid="6369601947905234551">"L\'aplicació ha demanat accés a permisos sensibles, els quals poden posar en risc la teva informació personal o financera.<xliff:g id="ID_1"><br><br></xliff:g>És possible que l\'aplicació no funcioni correctament sense aquests permisos restringits. <a href=<xliff:g id="LEARN_MORE_LINK">%1$s</xliff:g>>Més informació sobre com pots permetre l\'accés</a>"</string> <string name="enhanced_confirmation_dialog_title_settings_default" msgid="1858092969721041576">"A l\'aplicació se li ha denegat l\'accés"</string> - <string name="enhanced_confirmation_dialog_desc_settings_default" msgid="6911632348359332981">"L\'accés a aquest permís pot posar en risc la teva informació personal i financera.<xliff:g id="ID_1"><br><br></xliff:g>És possible que l\'aplicació no funcioni correctament sense aquest permís. <a href=<xliff:g id="LEARN_MORE_LINK">%1$s</xliff:g>>Més informació sobre com pots permetre l\'accés</a>"</string> + <string name="enhanced_confirmation_dialog_desc_settings_default" msgid="6911632348359332981">"L\'accés a aquest permís pot posar en risc la teva informació personal i financera.<xliff:g id="ID_1"><br><br></xliff:g>És possible que l\'aplicació no funcioni correctament sense aquest permís restringit. <a href=<xliff:g id="LEARN_MORE_LINK">%1$s</xliff:g>>Més informació sobre com pots permetre l\'accés</a>"</string> <string name="enhanced_confirmation_dialog_learn_more" msgid="5226619861379095709">"Més informació"</string> <string name="enhanced_confirmation_dialog_ok" msgid="8560373821598619924">"D\'acord"</string> <string name="permission_grant_dialog_streaming_blocked_title" msgid="8905241017017043649">"S\'ha suprimit la sol·licitud de permís"</string> diff --git a/PermissionController/res/values-eu/strings.xml b/PermissionController/res/values-eu/strings.xml index 139f41668..6e9242d35 100644 --- a/PermissionController/res/values-eu/strings.xml +++ b/PermissionController/res/values-eu/strings.xml @@ -78,7 +78,7 @@ <string name="never_ask_again" msgid="4728762438198560329">"Ez galdetu berriro"</string> <string name="no_permissions" msgid="3881676756371148563">"Ez dago baimenik"</string> <string name="additional_permissions" msgid="5801285469338873430">"Baimen gehigarriak"</string> - <string name="app_permissions_info_button_label" msgid="7633312050729974623">"Ireki aplikazioaren informazioa"</string> + <string name="app_permissions_info_button_label" msgid="7633312050729974623">"Ireki aplikazioari buruzko informazioa"</string> <string name="additional_permissions_more" msgid="5681220714755304407">"{count,plural, =1{Beste #}other{Beste #}}"</string> <string name="old_sdk_deny_warning" msgid="2382236998845153919">"Android-en bertsio zaharrago baterako diseinatuta dago aplikazio hau. Baimena ukatzen baduzu, agian aurrerantzean ez du behar bezala funtzionatuko."</string> <string name="storage_supergroup_warning_allow" msgid="103093462784523190">"Android-en bertsio zaharrago baterako dago diseinatuta aplikazio hau. Baimena ematen baduzu, biltegi osoa erabiltzeko baimena emango da (argazkiak, musika, audioa eta bestelako fitxategiak atzitzekoa barne)."</string> @@ -263,7 +263,7 @@ <string name="hours" msgid="7302866489666950038">"{count,plural, =1{# ordu}other{# ordu}}"</string> <string name="minutes" msgid="4868414855445375753">"{count,plural, =1{# minutu}other{# minutu}}"</string> <string name="seconds" msgid="5893958182059842734">"{count,plural, =1{# segundo}other{# segundo}}"</string> - <string name="permission_reminders" msgid="6528257957664832636">"Baimenen abisuak"</string> + <string name="permission_reminders" msgid="6528257957664832636">"Baimenen gogorarazpenak"</string> <string name="auto_revoke_permission_reminder_notification_title_one" msgid="6690347469376854137">"Erabiltzen ez den 1 aplikazio"</string> <string name="auto_revoke_permission_reminder_notification_title_many" msgid="6062217713645069960">"Erabiltzen ez diren <xliff:g id="NUMBER_OF_APPS">%s</xliff:g> aplikazio"</string> <string name="auto_revoke_permission_reminder_notification_content" msgid="4492228990462107487">"Baimenak kendu egin dira zure pribatutasuna babesteko. Sakatu berrikusteko."</string> diff --git a/PermissionController/res/values-hr/strings.xml b/PermissionController/res/values-hr/strings.xml index 55cfcfa78..c0b9d935e 100644 --- a/PermissionController/res/values-hr/strings.xml +++ b/PermissionController/res/values-hr/strings.xml @@ -675,11 +675,11 @@ <string name="enhanced_confirmation_dialog_title" msgid="7562437438040966351">"Ograničena postavka"</string> <string name="enhanced_confirmation_dialog_desc" msgid="5921240234843839219">"Radi vaše sigurnosti ova postavka trenutačno nije dostupna."</string> <string name="enhanced_confirmation_dialog_title_permission" msgid="2149144789394238266">"Aplikaciji je odbijen pristup dopuštenju <xliff:g id="PERMISSION_NAME">%1$s</xliff:g>"</string> - <string name="enhanced_confirmation_dialog_desc_permission" msgid="3150778951946468945">"Aplikacija je zatražila pristup dopuštenju za osjetljive podatke koje može ugroziti vaše osobne i financijske podatke.<xliff:g id="ID_1"><br><br></xliff:g>Moguće je da aplikacija neće pravilno funkcionirati bez tog ograničenog dopuštenja. <a href=<xliff:g id="LEARN_MORE_LINK">%1$s</xliff:g>>Saznajte kako omogućiti pristup</a>"</string> - <string name="enhanced_confirmation_dialog_title_role" msgid="1737023798483772780">"Aplikaciji je uskraćeno da prema zadanim postavkama bude <xliff:g id="ROLE_NAME">%1$s</xliff:g>"</string> - <string name="enhanced_confirmation_dialog_desc_role" msgid="6369601947905234551">"Aplikacija je zatražila pristup dopuštenjima za osjetljive podatke koja mogu ugroziti vaše osobne i financijske podatke.<xliff:g id="ID_1"><br><br></xliff:g>Moguće je da aplikacija neće pravilno funkcionirati bez tih ograničenih dopuštenja. <a href=<xliff:g id="LEARN_MORE_LINK">%1$s</xliff:g>>Saznajte kako omogućiti pristup</a>"</string> + <string name="enhanced_confirmation_dialog_desc_permission" msgid="3150778951946468945">"Aplikacija je zatražila pristup dopuštenju za osjetljive podatke koje može ugroziti vaše osobne i financijske podatke.<xliff:g id="ID_1"><br><br></xliff:g>Moguće je da aplikacija neće pravilno funkcionirati bez tog uskraćenog dopuštenja. <a href=<xliff:g id="LEARN_MORE_LINK">%1$s</xliff:g>>Saznajte kako omogućiti pristup</a>"</string> + <string name="enhanced_confirmation_dialog_title_role" msgid="1737023798483772780">"Aplikaciji je uskraćeno da bude zadana <xliff:g id="ROLE_NAME">%1$s</xliff:g>"</string> + <string name="enhanced_confirmation_dialog_desc_role" msgid="6369601947905234551">"Aplikacija je zatražila pristup dopuštenjima za osjetljive podatke čime može ugroziti vaše osobne i financijske podatke.<xliff:g id="ID_1"><br><br></xliff:g>Moguće je da aplikacija neće pravilno funkcionirati bez tih ograničenih dopuštenja. <a href=<xliff:g id="LEARN_MORE_LINK">%1$s</xliff:g>>Saznajte kako omogućiti pristup</a>"</string> <string name="enhanced_confirmation_dialog_title_settings_default" msgid="1858092969721041576">"Aplikaciji je odbijen pristup"</string> - <string name="enhanced_confirmation_dialog_desc_settings_default" msgid="6911632348359332981">"Pristup tom dopuštenju može ugroziti vaše osobne i financijske podatke.<xliff:g id="ID_1"><br><br></xliff:g>Moguće je da aplikacija neće pravilno funkcionirati bez tog ograničenog dopuštenja. <a href=<xliff:g id="LEARN_MORE_LINK">%1$s</xliff:g>>Saznajte kako omogućiti pristup</a>"</string> + <string name="enhanced_confirmation_dialog_desc_settings_default" msgid="6911632348359332981">"Pristup tom dopuštenju može ugroziti vaše osobne i financijske podatke.<xliff:g id="ID_1"><br><br></xliff:g>Moguće je da aplikacija neće pravilno funkcionirati bez tog dopuštenja. <a href=<xliff:g id="LEARN_MORE_LINK">%1$s</xliff:g>>Saznajte kako omogućiti pristup</a>"</string> <string name="enhanced_confirmation_dialog_learn_more" msgid="5226619861379095709">"Saznajte više"</string> <string name="enhanced_confirmation_dialog_ok" msgid="8560373821598619924">"U redu"</string> <string name="permission_grant_dialog_streaming_blocked_title" msgid="8905241017017043649">"Upit za dopuštenje je spriječen"</string> diff --git a/PermissionController/res/values-ja/strings.xml b/PermissionController/res/values-ja/strings.xml index e7322b07b..d706f9174 100644 --- a/PermissionController/res/values-ja/strings.xml +++ b/PermissionController/res/values-ja/strings.xml @@ -185,7 +185,7 @@ <string name="app_permission_usage_summary" msgid="390383661936709672">"アクセス: <xliff:g id="NUM">%1$s</xliff:g> 回。合計時間: <xliff:g id="DURATION">%2$s</xliff:g>。最終使用: <xliff:g id="TIME">%3$s</xliff:g>前。"</string> <string name="app_permission_usage_summary_no_duration" msgid="3698475875179457400">"アクセス: <xliff:g id="NUM">%1$s</xliff:g> 回。最終使用: <xliff:g id="TIME">%2$s</xliff:g>前。"</string> <string name="app_permission_button_allow" msgid="5808039516494774647">"許可する"</string> - <string name="app_permission_button_allow_all_files" msgid="1792232272599018825">"すべてのファイルの管理を許可"</string> + <string name="app_permission_button_allow_all_files" msgid="1792232272599018825">"すべての管理を許可"</string> <string name="app_permission_button_allow_media_only" msgid="2834282724426046154">"アクセスのみ許可"</string> <string name="app_permission_button_allow_always" msgid="4573292371734011171">"常に許可"</string> <string name="app_permission_button_allow_foreground" msgid="1991570451498943207">"アプリの使用中のみ許可"</string> diff --git a/PermissionController/res/values-ms/strings.xml b/PermissionController/res/values-ms/strings.xml index 6b65594b4..39bf0b7b3 100644 --- a/PermissionController/res/values-ms/strings.xml +++ b/PermissionController/res/values-ms/strings.xml @@ -676,8 +676,8 @@ <string name="enhanced_confirmation_dialog_desc" msgid="5921240234843839219">"Untuk keselamatan anda, tetapan ini tidak tersedia pada masa ini."</string> <string name="enhanced_confirmation_dialog_title_permission" msgid="2149144789394238266">"Akses apl kepada <xliff:g id="PERMISSION_NAME">%1$s</xliff:g> telah ditolak"</string> <string name="enhanced_confirmation_dialog_desc_permission" msgid="3150778951946468945">"Apl meminta akses kepada kebenaran sensitif yang boleh mengakibatkan risiko terhadap maklumat peribadi dan kewangan anda.<xliff:g id="ID_1"><br><br></xliff:g>Apl tersebut mungkin tidak dapat berfungsi dengan betul tanpa kebenaran terhad ini. <a href=<xliff:g id="LEARN_MORE_LINK">%1$s</xliff:g>>Ketahui cara membenarkan akses</a>"</string> - <string name="enhanced_confirmation_dialog_title_role" msgid="1737023798483772780">"Akses apl untuk menjadi <xliff:g id="ROLE_NAME">%1$s</xliff:g> secara lalai telah ditolak"</string> - <string name="enhanced_confirmation_dialog_desc_role" msgid="6369601947905234551">"Apl meminta akses kepada kebenaran sensitif yang boleh mengakibatkan risiko terhadap maklumat peribadi dan kewangan anda.<xliff:g id="ID_1"><br><br></xliff:g>Apl tersebut mungkin tidak dapat berfungsi dengan betul tanpa kebenaran terhad ini. <a href=<xliff:g id="LEARN_MORE_LINK">%1$s</xliff:g>>Ketahui cara membenarkan akses</a>"</string> + <string name="enhanced_confirmation_dialog_title_role" msgid="1737023798483772780">"Akses apl untuk menjadi <xliff:g id="ROLE_NAME">%1$s</xliff:g> lalai telah ditolak"</string> + <string name="enhanced_confirmation_dialog_desc_role" msgid="6369601947905234551">"Apl meminta akses kepada kebenaran sensitif yang boleh mengakibatkan risiko terhadap maklumat peribadi dan kewangan anda.<xliff:g id="ID_1"><br><br></xliff:g>Apl mungkin tidak dapat berfungsi dengan betul tanpa kebenaran terhad ini. <a href=<xliff:g id="LEARN_MORE_LINK">%1$s</xliff:g>>Ketahui cara membenarkan akses</a>"</string> <string name="enhanced_confirmation_dialog_title_settings_default" msgid="1858092969721041576">"Akses apl telah ditolak"</string> <string name="enhanced_confirmation_dialog_desc_settings_default" msgid="6911632348359332981">"Akses kepada kebenaran ini boleh mengakibatkan risiko terhadap maklumat peribadi dan kewangan anda.<xliff:g id="ID_1"><br><br></xliff:g>Apl tersebut mungkin tidak dapat berfungsi dengan betul tanpa kebenaran terhad ini. <a href=<xliff:g id="LEARN_MORE_LINK">%1$s</xliff:g>>Ketahui cara membenarkan akses</a>"</string> <string name="enhanced_confirmation_dialog_learn_more" msgid="5226619861379095709">"Ketahui lebih lanjut"</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-ru/strings.xml b/PermissionController/res/values-ru/strings.xml index a96a76df3..07ad9f710 100644 --- a/PermissionController/res/values-ru/strings.xml +++ b/PermissionController/res/values-ru/strings.xml @@ -676,8 +676,8 @@ <string name="enhanced_confirmation_dialog_desc" msgid="5921240234843839219">"В целях безопасности эти настройки пока недоступны."</string> <string name="enhanced_confirmation_dialog_title_permission" msgid="2149144789394238266">"Для приложения заблокировано разрешение \"<xliff:g id="PERMISSION_NAME">%1$s</xliff:g>\""</string> <string name="enhanced_confirmation_dialog_desc_permission" msgid="3150778951946468945">"Приложение запрашивает разрешение на доступ к конфиденциальной информации. Если вы предоставите его, ваши личные и финансовые данные могут оказаться под угрозой.<xliff:g id="ID_1"><br><br></xliff:g>Без такого разрешения приложение может работать неправильно. Узнайте, <a href=<xliff:g id="LEARN_MORE_LINK">%1$s</xliff:g>>как предоставить доступ к данным</a>."</string> - <string name="enhanced_confirmation_dialog_title_role" msgid="1737023798483772780">"Запрещено использовать приложение в качестве стандартного в категории \"<xliff:g id="ROLE_NAME">%1$s</xliff:g>\""</string> - <string name="enhanced_confirmation_dialog_desc_role" msgid="6369601947905234551">"Приложение запрашивает разрешения на доступ к конфиденциальной информации. Если вы предоставите их, ваши личные и финансовые данные могут оказаться под угрозой.<xliff:g id="ID_1"><br><br></xliff:g>Без таких разрешений приложение может работать неправильно. Узнайте, <a href=<xliff:g id="LEARN_MORE_LINK">%1$s</xliff:g>>как предоставить доступ к данным</a>."</string> + <string name="enhanced_confirmation_dialog_title_role" msgid="1737023798483772780">"Запрос на использование в качестве приложения по умолчанию (<xliff:g id="ROLE_NAME">%1$s</xliff:g>) отклонен"</string> + <string name="enhanced_confirmation_dialog_desc_role" msgid="6369601947905234551">"Приложение запросило разрешения на доступ к конфиденциальной информации. Такие разрешения могут поставить ваши личные и финансовые данные под угрозу.<xliff:g id="ID_1"><br><br></xliff:g>Без них приложение может работать неправильно. Узнайте, <a href=<xliff:g id="LEARN_MORE_LINK">%1$s</xliff:g>>как предоставить доступ к данным</a>."</string> <string name="enhanced_confirmation_dialog_title_settings_default" msgid="1858092969721041576">"Доступ запрещен"</string> <string name="enhanced_confirmation_dialog_desc_settings_default" msgid="6911632348359332981">"Если вы предоставите это разрешение, ваши личные и финансовые данные могут оказаться под угрозой.<xliff:g id="ID_1"><br><br></xliff:g>Без него приложение может работать неправильно. Узнайте, <a href=<xliff:g id="LEARN_MORE_LINK">%1$s</xliff:g>>как предоставить доступ к данным</a>."</string> <string name="enhanced_confirmation_dialog_learn_more" msgid="5226619861379095709">"Подробнее"</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-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-rHK/strings.xml b/PermissionController/res/values-zh-rHK/strings.xml index 07a401ab3..c195fd722 100644 --- a/PermissionController/res/values-zh-rHK/strings.xml +++ b/PermissionController/res/values-zh-rHK/strings.xml @@ -676,7 +676,7 @@ <string name="enhanced_confirmation_dialog_desc" msgid="5921240234843839219">"為安全起見,系統目前不提供此設定。"</string> <string name="enhanced_confirmation_dialog_title_permission" msgid="2149144789394238266">"系統已拒絕授予應用程式「<xliff:g id="PERMISSION_NAME">%1$s</xliff:g>」存取權"</string> <string name="enhanced_confirmation_dialog_desc_permission" msgid="3150778951946468945">"應用程式要求存取敏感資料權限,授予此權限可能會危害你的個人和財務資料。<xliff:g id="ID_1"><br><br></xliff:g>如沒有此受限制權限,應用程式可能無法正常運作。<a href=<xliff:g id="LEARN_MORE_LINK">%1$s</xliff:g>>進一步瞭解如何授予存取權</a>"</string> - <string name="enhanced_confirmation_dialog_title_role" msgid="1737023798483772780">"系統已拒絕授予應用程式預設<xliff:g id="ROLE_NAME">%1$s</xliff:g>存取權"</string> + <string name="enhanced_confirmation_dialog_title_role" msgid="1737023798483772780">"系統已拒絕授予應用程式作為預設<xliff:g id="ROLE_NAME">%1$s</xliff:g>的存取權"</string> <string name="enhanced_confirmation_dialog_desc_role" msgid="6369601947905234551">"應用程式要求存取敏感資料權限,授予此權限可能會危害你的個人和財務資料。<xliff:g id="ID_1"><br><br></xliff:g>如沒有此受限制權限,應用程式可能無法正常運作。<a href=<xliff:g id="LEARN_MORE_LINK">%1$s</xliff:g>>進一步瞭解如何授予存取權</a>"</string> <string name="enhanced_confirmation_dialog_title_settings_default" msgid="1858092969721041576">"系統已拒絕授予應用程式存取權"</string> <string name="enhanced_confirmation_dialog_desc_settings_default" msgid="6911632348359332981">"授予此權限可能會危害你的個人和財務資料。<xliff:g id="ID_1"><br><br></xliff:g>如沒有此受限制權限,應用程式可能無法正常運作。<a href=<xliff:g id="LEARN_MORE_LINK">%1$s</xliff:g>>進一步瞭解如何授予存取權</a>"</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/values/strings.xml b/PermissionController/res/values/strings.xml index d67a3582d..82c7889c6 100644 --- a/PermissionController/res/values/strings.xml +++ b/PermissionController/res/values/strings.xml @@ -1243,6 +1243,17 @@ <string name="role_wallet_request_title">Set <xliff:g id="app_name" example="Super Wallet">%1$s</xliff:g> as your default wallet app?</string> <string name="role_wallet_request_description">No permissions needed</string> + <!-- Label for the RESERVED_FOR_TESTING_PROFILE_GROUP_EXCLUSIVITY role. [DO NOT TRANSLATE] --> + <string name="role_for_testing_profile_group_exclusivity_label" translatable="false">Default test profile group exclusive role app</string> + <!-- Short label for the RESERVED_FOR_TESTING_PROFILE_GROUP_EXCLUSIVITY role. [DO NOT TRANSLATE] --> + <string name="role_for_testing_profile_group_exclusivity_short_label" translatable="false">Test profile group exclusive role app</string> + <!-- Description for the RESERVED_FOR_TESTING_PROFILE_GROUP_EXCLUSIVITY role. [DO NOT TRANSLATE] --> + <string name="role_for_testing_profile_group_exclusivity_description" translatable="false">Test profile group exclusive role apps are for tests only and should not be held by production apps.</string> + <!-- Request title for the RESERVED_FOR_TESTING_PROFILE_GROUP_EXCLUSIVITY role. [DO NOT TRANSLATE] --> + <string name="role_for_testing_profile_group_exclusivity_request_title" translatable="false">Set <xliff:g id="app_name" example="Super test app">%1$s</xliff:g> as your default test profile group exclusive role app?</string> + <!-- Request description for the RESERVED_FOR_TESTING_PROFILE_GROUP_EXCLUSIVITY role. [DO NOT TRANSLATE] --> + <string name="role_for_testing_profile_group_exclusivity_request_description" translatable="false">No permissions needed</string> + <!-- Subtitle for the application that is the current default application [CHAR LIMIT=30] --> <string name="request_role_current_default">Current default</string> @@ -2005,6 +2016,13 @@ Allow <xliff:g id="app_name" example="Gmail">%4$s</xliff:g> to upload a bug repo <!--Content for dialog displayed to tell user that settings are blocked by setting restrictions [CHAR LIMIT=NONE] --> <string name="enhanced_confirmation_dialog_desc">For your security, this setting is currently unavailable.</string> + <!--Title for dialog displayed to tell user that settings are blocked due to the phone state (such as being in a call with an unknown number) [CHAR LIMIT=50] --> + <string name="enhanced_confirmation_phone_state_dialog_title">Action not available while on a phone call</string> + <!--Content for dialog displayed to tell user that settings are blocked due to the phone state (such as being in a call with an unknown number) [CHAR LIMIT=NONE] --> + <string name="enhanced_confirmation_phone_state_dialog_desc">Allowing apps to install other apps is not allowed during a phone call.\n\n + Scammers often request this type of action during phone call conversations, so it\u2019s blocked to protect you. If you are being guided to take this action + by someone you don\u2019t know, it might be a scam.</string> + <!--Title for dialog displayed to tell user that permissions are blocked by setting restrictions [CHAR LIMIT=50] --> <string name="enhanced_confirmation_dialog_title_permission">App was denied access to <xliff:g id="permission_name" example="contacts">%1$s</xliff:g></string> <!--Content for dialog displayed to tell user that settings are blocked by setting restrictions [CHAR LIMIT=NONE] --> 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..3d858a9c5 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.app.admin.flags.Flags.splitCreateManagedProfileEnabled" /> + <permission name="android.permission.REMOVE_ACCOUNTS" + featureFlag="android.app.admin.flags.Flags.splitCreateManagedProfileEnabled" /> </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" @@ -835,7 +855,8 @@ ~ In addition, packages MUST NOT: ~ - Request INTERNET permission. Instead packages MUST access the internet through ~ well-defined APIs in an open source project. - ~ - Perform direct binds to other applications, except the following system packages: + ~ - Perform direct binds to other applications, except the following system packages or + ~ other preloaded packages conforming with the requirements here: ~ - Bluetooth ~ - Contacts ~ - Media @@ -849,6 +870,7 @@ name="android.app.role.SYSTEM_UI_INTELLIGENCE" defaultHolders="config_systemUiIntelligence" exclusive="true" + exclusivity="user" minSdkVersion="31" static="true" systemOnly="true" @@ -904,6 +926,7 @@ name="android.app.role.SYSTEM_AMBIENT_AUDIO_INTELLIGENCE" defaultHolders="config_systemAmbientAudioIntelligence" exclusive="true" + exclusivity="user" minSdkVersion="31" static="true" systemOnly="true" @@ -950,6 +973,7 @@ name="android.app.role.SYSTEM_AUDIO_INTELLIGENCE" defaultHolders="config_systemAudioIntelligence" exclusive="true" + exclusivity="user" minSdkVersion="31" static="true" systemOnly="true" @@ -996,6 +1020,7 @@ name="android.app.role.SYSTEM_NOTIFICATION_INTELLIGENCE" defaultHolders="config_systemNotificationIntelligence" exclusive="true" + exclusivity="user" minSdkVersion="31" static="true" systemOnly="true" @@ -1038,6 +1063,7 @@ name="android.app.role.SYSTEM_TEXT_INTELLIGENCE" defaultHolders="config_systemTextIntelligence" exclusive="true" + exclusivity="user" minSdkVersion="31" static="true" systemOnly="true" @@ -1078,6 +1104,7 @@ name="android.app.role.SYSTEM_VISUAL_INTELLIGENCE" defaultHolders="config_systemVisualIntelligence" exclusive="true" + exclusivity="user" minSdkVersion="31" static="true" systemOnly="true" @@ -1103,6 +1130,7 @@ name="android.app.role.SYSTEM_DOCUMENT_MANAGER" behavior="v33.DocumentManagerRoleBehavior" exclusive="true" + exclusivity="user" minSdkVersion="33" static="true" systemOnly="true" @@ -1135,6 +1163,7 @@ allowBypassingQualification="true" defaultHolders="config_systemActivityRecognizer" exclusive="false" + exclusivity="none" static="true" systemOnly="true" visible="false"> @@ -1154,6 +1183,7 @@ name="android.app.role.SYSTEM_UI" defaultHolders="config_systemUi" exclusive="true" + exclusivity="user" minSdkVersion="31" static="true" systemOnly="true" @@ -1180,6 +1210,7 @@ behavior="v31.TelevisionRoleBehavior" defaultHolders="config_systemTelevisionRemoteService" exclusive="true" + exclusivity="user" minSdkVersion="31" static="true" systemOnly="true" @@ -1199,6 +1230,7 @@ behavior="v33.CompanionDeviceAppStreamingRoleBehavior" description="@string/role_app_streaming_description" exclusive="false" + exclusivity="none" minSdkVersion="33" systemOnly="true" visible="false"> @@ -1207,7 +1239,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 +1257,7 @@ behavior="v33.CompanionDeviceComputerRoleBehavior" description="@string/role_companion_device_computer_description" exclusive="false" + exclusivity="none" minSdkVersion="33" systemOnly="true" visible="false"> @@ -1242,6 +1277,7 @@ name="android.app.role.COMPANION_DEVICE_GLASSES" behavior="v34.CompanionDeviceGlassesRoleBehavior" exclusive="false" + exclusivity="none" minSdkVersion="34" systemOnly="false" visible="false"> @@ -1268,12 +1304,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 +1320,7 @@ name="android.app.role.SYSTEM_SUPERVISION" defaultHolders="config_systemSupervision" exclusive="true" + exclusivity="user" minSdkVersion="33" static="true" systemOnly="true" @@ -1291,6 +1331,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 +1383,7 @@ behavior="v33.DevicePolicyManagementRoleBehavior" defaultHolders="config_devicePolicyManagement" exclusive="true" + exclusivity="user" minSdkVersion="33" static="true" systemOnly="false" @@ -1408,6 +1489,12 @@ <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.MANAGE_DEVICE_POLICY_APP_FUNCTIONS" + featureFlag="android.app.appfunctions.flags.Flags.enableAppFunctionManager" /> + <permission name="android.permission.COPY_ACCOUNTS" + featureFlag="android.app.admin.flags.Flags.splitCreateManagedProfileEnabled" /> + <permission name="android.permission.REMOVE_ACCOUNTS" + featureFlag="android.app.admin.flags.Flags.splitCreateManagedProfileEnabled" /> </permissions> </role> @@ -1415,6 +1502,7 @@ name="android.app.role.SYSTEM_APP_PROTECTION_SERVICE" defaultHolders="config_systemAppProtectionService" exclusive="true" + exclusivity="user" minSdkVersion="33" static="true" systemOnly="true" @@ -1443,6 +1531,7 @@ behavior="v31.AutomotiveRoleBehavior" defaultHolders="config_systemAutomotiveCalendarSyncManager" exclusive="true" + exclusivity="user" minSdkVersion="33" static="true" systemOnly="true" @@ -1464,6 +1553,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 +1627,7 @@ name="android.app.role.SYSTEM_SETTINGS_INTELLIGENCE" defaultHolders="config_systemSettingsIntelligence" exclusive="true" + exclusivity="user" minSdkVersion="33" static="true" systemOnly="true" @@ -1554,6 +1645,7 @@ name="android.app.role.SYSTEM_BLUETOOTH_STACK" defaultHolders="config_systemBluetoothStack" exclusive="true" + exclusivity="user" minSdkVersion="33" static="true" systemOnly="true" @@ -1578,6 +1670,7 @@ <role name="android.app.role.FINANCED_DEVICE_KIOSK" exclusive="true" + exclusivity="user" minSdkVersion="34" visible="false"> <permissions> @@ -1593,6 +1686,7 @@ name="android.app.role.SYSTEM_FINANCED_DEVICE_CONTROLLER" defaultHolders="config_systemFinancedDeviceController" exclusive="true" + exclusivity="user" minSdkVersion="34" static="true" systemOnly="true" @@ -1620,6 +1714,7 @@ behavior="v33.SystemWearHealthServiceRoleBehavior" defaultHolders="config_systemWearHealthService" exclusive="true" + exclusivity="user" minSdkVersion="33" static="true" systemOnly="true" @@ -1629,6 +1724,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 +1742,7 @@ defaultHolders="config_defaultNotes" description="@string/role_notes_description" exclusive="true" + exclusivity="user" label="@string/role_notes_label" minSdkVersion="34" overrideUserWhenGranting="true" @@ -1682,6 +1784,7 @@ allowBypassingQualification="true" defaultHolders="config_systemCallStreaming" exclusive="true" + exclusivity="user" minSdkVersion="34" static="true" systemOnly="true" @@ -1704,6 +1807,7 @@ behavior="v35.RetailDemoRoleBehavior" defaultHolders="config_defaultRetailDemo" exclusive="true" + exclusivity="user" minSdkVersion="35" static="true" visible="false"> @@ -1730,6 +1834,7 @@ defaultHolders="config_defaultWallet" description="@string/role_wallet_description" exclusive="true" + exclusivity="user" label="@string/role_wallet_label" minSdkVersion="35" overrideUserWhenGranting="true" @@ -1740,5 +1845,85 @@ 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" + behavior="ReservedForTestingProfileGroupExclusivityRoleBehavior" + description="@string/role_for_testing_profile_group_exclusivity_description" + exclusive="true" + exclusivity="profileGroup" + fallBackToDefaultHolder="true" + featureFlag="com.android.permission.flags.Flags.crossUserRoleEnabled" + label="@string/role_for_testing_profile_group_exclusivity_label" + requestable="true" + requestDescription="@string/role_for_testing_profile_group_exclusivity_request_description" + requestTitle="@string/role_for_testing_profile_group_exclusivity_request_title" + shortLabel="@string/role_for_testing_profile_group_exclusivity_short_label" + showNone="true" + visible="true"/> + <!--- + ~ A role for the vendor package that provides privacy-preserving intelligent processor for + ~ vendor specific features. + ~ + ~ A package holding this role MUST comply with requirements outlined in the Android CDD + ~ section "9.8.6 Content Capture". + ~ Example link for Android 15: + ~ https://source.android.com/docs/compatibility/15/android-15-cdd#986_os-level_and_ambient_data + ~ + ~ In addition, packages MUST NOT: + ~ - Request INTERNET permission. Instead packages MUST access the internet through + ~ well-defined APIs in an open source project. + ~ - Perform direct binds to other applications, except the following system packages or + ~ other preloaded packages conforming with the requirements here: + ~ - Bluetooth + ~ - Contacts + ~ - Media + ~ - Telephony + ~ - System UI + ~ - Component providing internet APIs (see above) + ~ To achieve this packages MUST set up explicit <allow-association> configuration in the + ~ system config. + --> + <role + name="android.app.role.SYSTEM_VENDOR_INTELLIGENCE" + defaultHolders="config_systemVendorIntelligence" + exclusive="true" + exclusivity="user" + featureFlag="android.permission.flags.Flags.systemVendorIntelligenceRoleEnabled" + static="true" + systemOnly="true" + visible="false"> + <permissions> + <permission name="android.permission.READ_GLOBAL_APP_SEARCH_DATA" /> + <permission name="android.permission.EXECUTE_APP_FUNCTIONS_TRUSTED" + featureFlag="android.app.appfunctions.flags.Flags.enableAppFunctionManager" /> + </permissions> + </role> </roles> diff --git a/PermissionController/role-controller/Android.bp b/PermissionController/role-controller/Android.bp index 166823b08..9f217660a 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,13 +27,17 @@ java_library { ], libs: [ "androidx.annotation_annotation", + "com.android.permission.flags-aconfig-java", + "framework-annotations-lib", ], 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", + "device_policy_aconfig_flags_java_export", ], apex_available: [ "com.android.permission", diff --git a/PermissionController/role-controller/java/com/android/role/controller/behavior/ReservedForTestingProfileGroupExclusivityRoleBehavior.java b/PermissionController/role-controller/java/com/android/role/controller/behavior/ReservedForTestingProfileGroupExclusivityRoleBehavior.java new file mode 100644 index 000000000..a9be00806 --- /dev/null +++ b/PermissionController/role-controller/java/com/android/role/controller/behavior/ReservedForTestingProfileGroupExclusivityRoleBehavior.java @@ -0,0 +1,58 @@ +/* + * 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.role.controller.behavior; + +import android.app.role.RoleManager; +import android.content.Context; +import android.os.UserHandle; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.android.role.controller.model.Role; +import com.android.role.controller.model.RoleBehavior; +import com.android.role.controller.util.RoleFlags; +import com.android.role.controller.util.UserUtils; + +import java.util.List; + +public class ReservedForTestingProfileGroupExclusivityRoleBehavior implements RoleBehavior { + @Nullable + @Override + public List<String> getDefaultHoldersAsUser(@NonNull Role role, @NonNull UserHandle user, + @NonNull Context context) { + if (RoleFlags.isProfileGroupExclusivityAvailable()) { + Context userContext = UserUtils.getUserContext(context, user); + RoleManager roleManager = userContext.getSystemService(RoleManager.class); + return roleManager.getDefaultHoldersForTest(role.getName()); + } else { + return null; + } + } + + @Override + public boolean isVisibleAsUser(@NonNull Role role, @NonNull UserHandle user, + @NonNull Context context) { + if (RoleFlags.isProfileGroupExclusivityAvailable()) { + Context userContext = UserUtils.getUserContext(context, user); + RoleManager roleManager = userContext.getSystemService(RoleManager.class); + return roleManager.isRoleVisibleForTest(role.getName()); + } else { + return false; + } + } +} 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..02fa0d455 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,17 +37,23 @@ 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; import com.android.role.controller.util.PackageUtils; +import com.android.role.controller.util.RoleFlags; 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 +88,36 @@ 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, + RoleFlags.isProfileGroupExclusivityAvailable()); + } + /** * The name of this role. Must be unique. */ @@ -109,9 +145,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 +222,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 +277,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 +293,7 @@ public class Role { mBehavior = behavior; mDefaultHoldersResourceName = defaultHoldersResourceName; mDescriptionResource = descriptionResource; - mExclusive = exclusive; + mExclusivity = exclusivity; mFallBackToDefaultHolder = fallBackToDefaultHolder; mFeatureFlag = featureFlag; mLabelResource = labelResource; @@ -297,7 +334,29 @@ 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); + } + if (!mPreferredActivities.isEmpty() && exclusivity == EXCLUSIVITY_PROFILE_GROUP) { + throw new IllegalArgumentException( + "Role cannot have preferred activities when exclusivity is profileGroup"); + } + return exclusivity; + } + } + return mExclusivity; } @Nullable @@ -413,8 +472,25 @@ public class Role { if (!isAvailableByFeatureFlagAndSdkVersion()) { return false; } + + if (getExclusivity() == EXCLUSIVITY_PROFILE_GROUP + && UserUtils.isPrivateProfile(user, context)) { + return false; + } + if (mBehavior != null) { - return mBehavior.isAvailableAsUser(this, user, context); + boolean isAvailableAsUser = mBehavior.isAvailableAsUser(this, user, context); + // Ensure that cross-user role is only available if also available for + // the profile-group's full user + if (isAvailableAsUser && getExclusivity() == EXCLUSIVITY_PROFILE_GROUP) { + UserHandle profileParent = UserUtils.getProfileParentOrSelf(user, context); + if (!Objects.equals(profileParent, user) + && !mBehavior.isAvailableAsUser(this, profileParent, context)) { + throw new IllegalArgumentException("Role is not available for profile parent: " + + profileParent.getIdentifier()); + } + } + return isAvailableAsUser; } return true; } @@ -424,7 +500,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; } @@ -449,6 +526,12 @@ public class Role { @NonNull public List<String> getDefaultHoldersAsUser(@NonNull UserHandle user, @NonNull Context context) { + // Do not allow default role holder for non-active user if the role is exclusive to profile + // group + if (isNonActiveUserForProfileGroupExclusiveRole(user, context)) { + return Collections.emptyList(); + } + if (mBehavior != null) { List<String> defaultHolders = mBehavior.getDefaultHoldersAsUser(this, user, context); if (defaultHolders != null) { @@ -560,6 +643,10 @@ public class Role { if (!RoleManagerCompat.isRoleFallbackEnabledAsUser(this, user, context)) { return null; } + // Do not fall back for non-active user if the role is exclusive to profile group + if (isNonActiveUserForProfileGroupExclusiveRole(user, context)) { + return null; + } if (mFallBackToDefaultHolder) { return CollectionUtils.firstOrNull(getDefaultHoldersAsUser(user, context)); } @@ -569,6 +656,17 @@ public class Role { return null; } + private boolean isNonActiveUserForProfileGroupExclusiveRole(@NonNull UserHandle user, + @NonNull Context context) { + if (RoleFlags.isProfileGroupExclusivityAvailable() + && getExclusivity() == Role.EXCLUSIVITY_PROFILE_GROUP) { + Context userContext = UserUtils.getUserContext(context, user); + RoleManager userRoleManager = userContext.getSystemService(RoleManager.class); + return !Objects.equals(userRoleManager.getActiveUserForRole(mName), user); + } + return false; + } + /** * Check whether this role is allowed to bypass qualification, if enabled globally. * @@ -1039,7 +1137,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 +1200,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..4b05554e3 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; @@ -543,6 +580,12 @@ public class RoleParser { skipCurrentTag(parser); continue; } + if (exclusivity == Role.EXCLUSIVITY_PROFILE_GROUP) { + throwOrLogMessage("<preferred-activities> is not supported for a" + + " profile-group-exclusive role: " + name); + skipCurrentTag(parser); + continue; + } preferredActivities = parsePreferredActivities(parser); break; default: @@ -567,12 +610,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 +667,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 +694,7 @@ public class RoleParser { if (metaDataName == null) { continue; } - if (mValidationEnabled) { + if (mThrowOnError) { validateNoDuplicateElement(metaDataName, validationMetaDataNames, "meta data"); } @@ -668,7 +711,7 @@ public class RoleParser { RequiredMetaData requiredMetaData = new RequiredMetaData(metaDataName, metaDataValue, metaDataProhibited); metaData.add(requiredMetaData); - if (mValidationEnabled) { + if (mThrowOnError) { validationMetaDataNames.add(metaDataName); } break; @@ -1204,7 +1247,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 +1255,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 +1265,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/role-controller/java/com/android/role/controller/service/RoleControllerServiceImpl.java b/PermissionController/role-controller/java/com/android/role/controller/service/RoleControllerServiceImpl.java index bc7562c11..d00fd47af 100644 --- a/PermissionController/role-controller/java/com/android/role/controller/service/RoleControllerServiceImpl.java +++ b/PermissionController/role-controller/java/com/android/role/controller/service/RoleControllerServiceImpl.java @@ -16,6 +16,7 @@ package com.android.role.controller.service; +import android.annotation.UserIdInt; import android.app.role.RoleControllerService; import android.app.role.RoleManager; import android.content.Context; @@ -34,6 +35,7 @@ import com.android.role.controller.model.Roles; import com.android.role.controller.util.CollectionUtils; import com.android.role.controller.util.LegacyRoleFallbackEnabledUtils; import com.android.role.controller.util.PackageUtils; +import com.android.role.controller.util.RoleFlags; import com.android.role.controller.util.UserUtils; import java.util.ArrayList; @@ -49,11 +51,21 @@ public class RoleControllerServiceImpl extends RoleControllerService { private static final boolean DEBUG = false; + public static volatile SetActiveUserForRoleMethod sSetActiveUserForRoleMethod; private UserHandle mUser; private Context mContext; private RoleManager mUserRoleManager; + /** Method for setting active user from role controller */ + public interface SetActiveUserForRoleMethod { + /** + * Sets user as active for the given role. + * @see RoleManager#setActiveUserForRole(String, UserHandle, int) + */ + void setActiveUserForRole(@NonNull String roleName, @UserIdInt int userId, int flags); + } + public RoleControllerServiceImpl() {} public RoleControllerServiceImpl(@NonNull UserHandle user, @NonNull Context context) { @@ -121,6 +133,19 @@ public class RoleControllerServiceImpl extends RoleControllerService { String roleName = role.getName(); + if (RoleFlags.isProfileGroupExclusivityAvailable() + && role.getExclusivity() == Role.EXCLUSIVITY_PROFILE_GROUP) { + if (mUserRoleManager.getActiveUserForRole(roleName) == null) { + UserHandle profileParent = UserUtils.getProfileParentOrSelf(mUser, mContext); + if (Objects.equals(mUser, profileParent)) { + Log.i(LOG_TAG, "No active user for role: " + roleName + ", setting " + + "active user to user: " + mUser.getIdentifier()); + sSetActiveUserForRoleMethod.setActiveUserForRole(roleName, + mUser.getIdentifier(), 0); + } + } + } + // For each of the current holders, check if it is still qualified, redo grant if so, or // remove it otherwise. List<String> currentPackageNames = mUserRoleManager.getRoleHolders(roleName); @@ -232,6 +257,11 @@ public class RoleControllerServiceImpl extends RoleControllerService { boolean added = false; if (role.isExclusive()) { + if (role.getExclusivity() == Role.EXCLUSIVITY_PROFILE_GROUP) { + sSetActiveUserForRoleMethod.setActiveUserForRole(roleName, mUser.getIdentifier(), + flags); + } + List<String> currentPackageNames = mUserRoleManager.getRoleHolders(roleName); int currentPackageNamesSize = currentPackageNames.size(); for (int i = 0; i < currentPackageNamesSize; i++) { diff --git a/PermissionController/role-controller/java/com/android/role/controller/util/RoleFlags.java b/PermissionController/role-controller/java/com/android/role/controller/util/RoleFlags.java new file mode 100644 index 000000000..2c5a247b6 --- /dev/null +++ b/PermissionController/role-controller/java/com/android/role/controller/util/RoleFlags.java @@ -0,0 +1,45 @@ +/* + * 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.role.controller.util; + +import android.os.Build; + +import androidx.annotation.ChecksSdkIntAtLeast; + +import java.util.Objects; + +/** Util class for getting shared feature flag check logic. */ +public final class RoleFlags { + private RoleFlags() { /* cannot be instantiated */ } + + /** + * Returns whether profile group exclusive roles are available. Profile exclusive roles are + * available on B+ + */ + @ChecksSdkIntAtLeast(api = Build.VERSION_CODES.BAKLAVA) + public static boolean isProfileGroupExclusivityAvailable() { + // TODO(b/372743073): change to isAtLeastB once available + return isAtLeastB() && com.android.permission.flags.Flags.crossUserRoleEnabled(); + } + + // TODO(b/372743073): remove once SdkLevel.isAtLeastB available + @ChecksSdkIntAtLeast(api = 36 /* BUILD_VERSION_CODES.Baklava */) + public static boolean isAtLeastB() { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.BAKLAVA + || Objects.equals(Build.VERSION.CODENAME, "Baklava"); + } +} diff --git a/PermissionController/role-controller/java/com/android/role/controller/util/UserUtils.java b/PermissionController/role-controller/java/com/android/role/controller/util/UserUtils.java index 1b6926ef8..f3cb7926a 100644 --- a/PermissionController/role-controller/java/com/android/role/controller/util/UserUtils.java +++ b/PermissionController/role-controller/java/com/android/role/controller/util/UserUtils.java @@ -24,6 +24,7 @@ import android.os.UserHandle; import android.os.UserManager; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import com.android.modules.utils.build.SdkLevel; @@ -111,4 +112,24 @@ public final class UserUtils { return context.createContextAsUser(user, 0); } } + + /** + * Returns the parent of a given user, or user if it has no parent (e.g. it is the primary + * user) + */ + @NonNull + public static UserHandle getProfileParentOrSelf(@NonNull UserHandle user, + @NonNull Context context) { + UserHandle profileParent = getProfileParent(user, context); + // If profile parent user is null, then original user is the parent + return profileParent != null ? profileParent : user; + } + + /** Returns the parent of a given user. */ + @Nullable + private static UserHandle getProfileParent(UserHandle user, @NonNull Context context) { + Context userContext = getUserContext(context, user); + UserManager userManager = userContext.getSystemService(UserManager.class); + return userManager.getProfileParent(user); + } } diff --git a/PermissionController/src/com/android/permissioncontroller/ecm/EnhancedConfirmationDialogActivity.kt b/PermissionController/src/com/android/permissioncontroller/ecm/EnhancedConfirmationDialogActivity.kt index bc6f774ad..e6cf094e3 100644 --- a/PermissionController/src/com/android/permissioncontroller/ecm/EnhancedConfirmationDialogActivity.kt +++ b/PermissionController/src/com/android/permissioncontroller/ecm/EnhancedConfirmationDialogActivity.kt @@ -18,6 +18,7 @@ package com.android.permissioncontroller.ecm import android.annotation.SuppressLint import android.app.AlertDialog +import android.app.AppOpsManager import android.app.Dialog import android.app.ecm.EnhancedConfirmationManager import android.content.Context @@ -65,6 +66,7 @@ class EnhancedConfirmationDialogActivity : FragmentActivity() { finish() return } + if (savedInstanceState != null) { wasClearRestrictionAllowed = savedInstanceState.getBoolean(KEY_WAS_CLEAR_RESTRICTION_ALLOWED) @@ -79,11 +81,19 @@ class EnhancedConfirmationDialogActivity : FragmentActivity() { require(uid != Process.INVALID_UID) { "EXTRA_UID cannot be null or invalid" } require(!packageName.isNullOrEmpty()) { "EXTRA_PACKAGE_NAME cannot be null or empty" } require(!settingIdentifier.isNullOrEmpty()) { "EXTRA_SUBJECT cannot be null or empty" } - wasClearRestrictionAllowed = setClearRestrictionAllowed(packageName, UserHandle.getUserHandleForUid(uid)) val setting = Setting.fromIdentifier(this, settingIdentifier, isEcmInApp) + if ( + SettingType.fromIdentifier(this, settingIdentifier, isEcmInApp) == + SettingType.BLOCKED_DUE_TO_PHONE_STATE && + !Flags.unknownCallPackageInstallBlockingEnabled() + ) { + finish() + return + } + if (DeviceUtils.isWear(this)) { WearEnhancedConfirmationDialogFragment.newInstance(setting.title, setting.message) .show(supportFragmentManager, WearEnhancedConfirmationDialogFragment.TAG) @@ -116,7 +126,7 @@ class EnhancedConfirmationDialogActivity : FragmentActivity() { fun fromIdentifier( context: Context, settingIdentifier: String, - isEcmInApp: Boolean + isEcmInApp: Boolean, ): Setting { val settingType = SettingType.fromIdentifier(context, settingIdentifier, isEcmInApp) val label = @@ -124,7 +134,7 @@ class EnhancedConfirmationDialogActivity : FragmentActivity() { SettingType.PLATFORM_PERMISSION -> KotlinUtils.getPermGroupLabel( context, - PermissionMapping.getGroupOfPlatformPermission(settingIdentifier)!! + PermissionMapping.getGroupOfPlatformPermission(settingIdentifier)!!, ) SettingType.PLATFORM_PERMISSION_GROUP -> KotlinUtils.getPermGroupLabel(context, settingIdentifier) @@ -132,15 +142,22 @@ class EnhancedConfirmationDialogActivity : FragmentActivity() { context.getString( Roles.get(context)[settingIdentifier]!!.shortLabelResource ) + SettingType.BLOCKED_DUE_TO_PHONE_STATE, SettingType.OTHER -> null } - val url = - context.getString(R.string.help_url_action_disabled_by_restricted_settings) - return Setting( - title = settingType.titleRes?.let { context.getString(it, label) }, + var title: String? + var message: CharSequence? + if (settingType == SettingType.BLOCKED_DUE_TO_PHONE_STATE) { + title = settingType.titleRes?.let { context.getString(it) } + message = settingType.messageRes?.let { context.getString(it) } + } else { + val url = + context.getString(R.string.help_url_action_disabled_by_restricted_settings) + title = (settingType.titleRes?.let { context.getString(it, label) }) message = settingType.messageRes?.let { Html.fromHtml(context.getString(it, url), 0) } - ) + } + return Setting(title, message) } } } @@ -148,29 +165,35 @@ class EnhancedConfirmationDialogActivity : FragmentActivity() { private enum class SettingType(val titleRes: Int?, val messageRes: Int?) { PLATFORM_PERMISSION( R.string.enhanced_confirmation_dialog_title_permission, - R.string.enhanced_confirmation_dialog_desc_permission + R.string.enhanced_confirmation_dialog_desc_permission, ), PLATFORM_PERMISSION_GROUP( R.string.enhanced_confirmation_dialog_title_permission, - R.string.enhanced_confirmation_dialog_desc_permission + R.string.enhanced_confirmation_dialog_desc_permission, ), ROLE( R.string.enhanced_confirmation_dialog_title_role, - R.string.enhanced_confirmation_dialog_desc_role + R.string.enhanced_confirmation_dialog_desc_role, ), OTHER( R.string.enhanced_confirmation_dialog_title_settings_default, - R.string.enhanced_confirmation_dialog_desc_settings_default + R.string.enhanced_confirmation_dialog_desc_settings_default, + ), + BLOCKED_DUE_TO_PHONE_STATE( + R.string.enhanced_confirmation_phone_state_dialog_title, + R.string.enhanced_confirmation_phone_state_dialog_desc, ); companion object { fun fromIdentifier( context: Context, settingIdentifier: String, - isEcmInApp: Boolean + isEcmInApp: Boolean, ): SettingType { - if (!isEcmInApp) return SettingType.OTHER return when { + settingIdentifier == AppOpsManager.OPSTR_REQUEST_INSTALL_PACKAGES -> + BLOCKED_DUE_TO_PHONE_STATE + !isEcmInApp -> OTHER PermissionMapping.isRuntimePlatformPermission(settingIdentifier) && PermissionMapping.getGroupOfPlatformPermission(settingIdentifier) != null -> PLATFORM_PERMISSION @@ -178,7 +201,7 @@ class EnhancedConfirmationDialogActivity : FragmentActivity() { PLATFORM_PERMISSION_GROUP settingIdentifier.startsWith("android.app.role.") && Roles.get(context).containsKey(settingIdentifier) -> ROLE - else -> SettingType.OTHER + else -> OTHER } } } @@ -188,7 +211,7 @@ class EnhancedConfirmationDialogActivity : FragmentActivity() { this.dialogResult = dialogResult setResult( RESULT_OK, - Intent().apply { putExtra(Intent.EXTRA_RETURN_RESULT, dialogResult.statsLogValue) } + Intent().apply { putExtra(Intent.EXTRA_RETURN_RESULT, dialogResult.statsLogValue) }, ) finish() } @@ -200,7 +223,7 @@ class EnhancedConfirmationDialogActivity : FragmentActivity() { uid = intent.getIntExtra(Intent.EXTRA_UID, Process.INVALID_UID), settingIdentifier = intent.getStringExtra(Intent.EXTRA_SUBJECT)!!, firstShowForApp = !wasClearRestrictionAllowed, - dialogResult = dialogResult + dialogResult = dialogResult, ) } } @@ -249,7 +272,7 @@ class EnhancedConfirmationDialogActivity : FragmentActivity() { private fun createDialogView( context: Context, title: String?, - message: CharSequence? + message: CharSequence?, ): View = LayoutInflater.from(context) .inflate(R.layout.enhanced_confirmation_dialog, null) diff --git a/PermissionController/src/com/android/permissioncontroller/hibernation/HibernationPolicy.kt b/PermissionController/src/com/android/permissioncontroller/hibernation/HibernationPolicy.kt index d23225ed3..8b11036e8 100644 --- a/PermissionController/src/com/android/permissioncontroller/hibernation/HibernationPolicy.kt +++ b/PermissionController/src/com/android/permissioncontroller/hibernation/HibernationPolicy.kt @@ -73,6 +73,7 @@ import android.service.dreams.DreamService import android.service.notification.NotificationListenerService import android.service.voice.VoiceInteractionService import android.service.wallpaper.WallpaperService +import android.telecom.TelecomManager import android.telephony.TelephonyManager.CARRIER_PRIVILEGE_STATUS_HAS_ACCESS import android.telephony.TelephonyManager.CARRIER_PRIVILEGE_STATUS_NO_ACCESS import android.util.Log @@ -658,6 +659,36 @@ suspend fun isPackageHibernationExemptBySystem( return true } + // Note that it's fine to check permissions instead of app ops as all these permissions were + // introduced before auto-revoke / hibernation in R. + val hasCallRelatedPermissions = + context.checkPermission(Manifest.permission.MANAGE_OWN_CALLS, -1 /* pid */, pkg.uid) == + PERMISSION_GRANTED + && context.checkPermission(Manifest.permission.RECORD_AUDIO, -1 /* pid */, pkg.uid) == + PERMISSION_GRANTED + && context.checkPermission(Manifest.permission.WRITE_CALL_LOG, -1 /* pid */, pkg.uid) == + PERMISSION_GRANTED + if (hasCallRelatedPermissions) { + val phoneAccounts = context.getSystemService(TelecomManager::class.java)!! + .selfManagedPhoneAccounts + var hasRegisteredPhoneAccount = false + for (phoneAccount in phoneAccounts) { + if (pkg.packageName == phoneAccount.componentName.packageName) { + hasRegisteredPhoneAccount = true + break + } + } + if (hasRegisteredPhoneAccount) { + if (DEBUG_HIBERNATION_POLICY) { + DumpableLog.i( + LOG_TAG, + "Exempted ${pkg.packageName} - caller app" + ) + } + return true + } + } + if (SdkLevel.isAtLeastS()) { val hasInstallOrUpdatePermissions = context.checkPermission(Manifest.permission.INSTALL_PACKAGES, -1 /* pid */, pkg.uid) == diff --git a/PermissionController/src/com/android/permissioncontroller/hibernation/TEST_MAPPING b/PermissionController/src/com/android/permissioncontroller/hibernation/TEST_MAPPING index 69a8f74be..038b2f992 100644 --- a/PermissionController/src/com/android/permissioncontroller/hibernation/TEST_MAPPING +++ b/PermissionController/src/com/android/permissioncontroller/hibernation/TEST_MAPPING @@ -11,5 +11,15 @@ } ] } + ], + "postsubmit": [ + { + "name": "PermissionControllerMockingTests", + "options": [ + { + "include-filter": "com.android.permissioncontroller.tests.mocking.hibernation" + } + ] + } ] } diff --git a/PermissionController/src/com/android/permissioncontroller/permission/compat/AppOpsManagerCompat.java b/PermissionController/src/com/android/permissioncontroller/permission/compat/AppOpsManagerCompat.java new file mode 100644 index 000000000..05d69e998 --- /dev/null +++ b/PermissionController/src/com/android/permissioncontroller/permission/compat/AppOpsManagerCompat.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.permissioncontroller.permission.compat; + +import android.annotation.SuppressLint; +import android.app.AppOpsManager; +import android.permission.flags.Flags; + +import androidx.annotation.NonNull; + +/** + * Helper AppOpsManager compat class + */ +public class AppOpsManagerCompat { + + private AppOpsManagerCompat() {} + + /** + * For platform version <= V, call the deprecated unsafeCheckOpRawNoThrow. For newer platforms, + * call the new API checkOpRawNoThrow. + * + * @return the raw mode of the op + */ + // TODO: b/379749734 + @SuppressWarnings("deprecation") + @SuppressLint("NewApi") + public static int checkOpRawNoThrow(@NonNull AppOpsManager appOpsManager, @NonNull String op, + int uid, @NonNull String packageName) { + if (Flags.checkOpOverloadApiEnabled()) { + return appOpsManager.checkOpRawNoThrow(op, uid, packageName, null); + } else { + return appOpsManager.unsafeCheckOpRawNoThrow(op, uid, packageName); + } + } +} diff --git a/PermissionController/src/com/android/permissioncontroller/permission/compat/AppPermissionFragmentCompat.java b/PermissionController/src/com/android/permissioncontroller/permission/compat/AppPermissionFragmentCompat.java index 188e3a9d0..de7404ead 100644 --- a/PermissionController/src/com/android/permissioncontroller/permission/compat/AppPermissionFragmentCompat.java +++ b/PermissionController/src/com/android/permissioncontroller/permission/compat/AppPermissionFragmentCompat.java @@ -19,6 +19,7 @@ package com.android.permissioncontroller.permission.compat; import static com.android.permissioncontroller.Constants.EXTRA_SESSION_ID; import static com.android.permissioncontroller.permission.ui.ManagePermissionsActivity.EXTRA_CALLER_NAME; +import android.content.Context; import android.content.Intent; import android.os.Bundle; import android.os.UserHandle; @@ -29,6 +30,7 @@ import androidx.preference.PreferenceFragmentCompat; import com.android.modules.utils.build.SdkLevel; import com.android.permission.flags.Flags; +import com.android.permissioncontroller.R; import com.android.permissioncontroller.permission.ui.handheld.max35.LegacyAppPermissionFragment; import com.android.permissioncontroller.permission.ui.handheld.v36.AppPermissionFragment; @@ -41,8 +43,10 @@ public class AppPermissionFragmentCompat { * Create an instance of this fragment */ @NonNull - public static PreferenceFragmentCompat createFragment() { - if (SdkLevel.isAtLeastV() && Flags.appPermissionFragmentUsesPreferences()) { + public static PreferenceFragmentCompat createFragment(@NonNull Context context) { + if (SdkLevel.isAtLeastV() && (Flags.appPermissionFragmentUsesPreferences() + || context.getResources().getBoolean( + R.bool.config_usePreferenceForAppPermissionSettings))) { return new AppPermissionFragment(); } else { return new LegacyAppPermissionFragment(); diff --git a/PermissionController/src/com/android/permissioncontroller/permission/data/AppOpLiveData.kt b/PermissionController/src/com/android/permissioncontroller/permission/data/AppOpLiveData.kt index 1e44f16bd..3202f5b69 100644 --- a/PermissionController/src/com/android/permissioncontroller/permission/data/AppOpLiveData.kt +++ b/PermissionController/src/com/android/permissioncontroller/permission/data/AppOpLiveData.kt @@ -19,6 +19,7 @@ package com.android.permissioncontroller.permission.data import android.app.AppOpsManager import android.app.Application import com.android.permissioncontroller.PermissionControllerApplication +import com.android.permissioncontroller.permission.compat.AppOpsManagerCompat /** * A LiveData which represents the appop state @@ -36,13 +37,13 @@ private constructor( private val app: Application, private val packageName: String, private val op: String, - private val uid: Int + private val uid: Int, ) : SmartUpdateMediatorLiveData<Int>() { private val appOpsManager = app.getSystemService(AppOpsManager::class.java)!! override fun onUpdate() { - value = appOpsManager.unsafeCheckOpNoThrow(op, uid, packageName) + value = AppOpsManagerCompat.checkOpRawNoThrow(appOpsManager, op, uid, packageName) } override fun onActive() { @@ -62,7 +63,7 @@ private constructor( PermissionControllerApplication.get(), key.first, key.second, - key.third + key.third, ) } } diff --git a/PermissionController/src/com/android/permissioncontroller/permission/data/AppPermGroupUiInfoLiveData.kt b/PermissionController/src/com/android/permissioncontroller/permission/data/AppPermGroupUiInfoLiveData.kt index b17098a13..394cb3eb7 100644 --- a/PermissionController/src/com/android/permissioncontroller/permission/data/AppPermGroupUiInfoLiveData.kt +++ b/PermissionController/src/com/android/permissioncontroller/permission/data/AppPermGroupUiInfoLiveData.kt @@ -233,8 +233,8 @@ private constructor( * user */ private fun isUserSet(permissionState: Map<String, PermState>): Boolean { - val flagMask = - PackageManager.FLAG_PERMISSION_USER_SET or PackageManager.FLAG_PERMISSION_USER_FIXED + val flagMask = PackageManager.FLAG_PERMISSION_USER_SET or + PackageManager.FLAG_PERMISSION_USER_FIXED or PackageManager.FLAG_PERMISSION_ONE_TIME return permissionState.any { (it.value.permFlags and flagMask) != 0 } } diff --git a/PermissionController/src/com/android/permissioncontroller/permission/data/FullStoragePermissionAppsLiveData.kt b/PermissionController/src/com/android/permissioncontroller/permission/data/FullStoragePermissionAppsLiveData.kt index 4a2d3b68a..2b6d9728e 100644 --- a/PermissionController/src/com/android/permissioncontroller/permission/data/FullStoragePermissionAppsLiveData.kt +++ b/PermissionController/src/com/android/permissioncontroller/permission/data/FullStoragePermissionAppsLiveData.kt @@ -28,6 +28,7 @@ import android.app.Application import android.os.Build import android.os.UserHandle import com.android.permissioncontroller.PermissionControllerApplication +import com.android.permissioncontroller.permission.compat.AppOpsManagerCompat import com.android.permissioncontroller.permission.model.livedatatypes.LightPackageInfo import kotlinx.coroutines.Job @@ -46,7 +47,7 @@ object FullStoragePermissionAppsLiveData : val packageName: String, val user: UserHandle, val isLegacy: Boolean, - val isGranted: Boolean + val isGranted: Boolean, ) init { @@ -88,7 +89,7 @@ object FullStoragePermissionAppsLiveData : fun getFullStorageStateForPackage( appOpsManager: AppOpsManager, packageInfo: LightPackageInfo, - userHandle: UserHandle? = null + userHandle: UserHandle? = null, ): FullStoragePackageState? { val sdk = packageInfo.targetSdkVersion val user = userHandle ?: UserHandle.getUserHandleForUid(packageInfo.uid) @@ -97,29 +98,31 @@ object FullStoragePermissionAppsLiveData : packageInfo.packageName, user, isLegacy = true, - isGranted = true + isGranted = true, ) } else if ( sdk <= Build.VERSION_CODES.Q && - appOpsManager.unsafeCheckOpNoThrow( + AppOpsManagerCompat.checkOpRawNoThrow( + appOpsManager, OPSTR_LEGACY_STORAGE, packageInfo.uid, - packageInfo.packageName + packageInfo.packageName, ) == MODE_ALLOWED ) { return FullStoragePackageState( packageInfo.packageName, user, isLegacy = true, - isGranted = true + isGranted = true, ) } if (MANAGE_EXTERNAL_STORAGE in packageInfo.requestedPermissions) { val mode = - appOpsManager.unsafeCheckOpNoThrow( + AppOpsManagerCompat.checkOpRawNoThrow( + appOpsManager, OPSTR_MANAGE_EXTERNAL_STORAGE, packageInfo.uid, - packageInfo.packageName + packageInfo.packageName, ) val granted = mode == MODE_ALLOWED || @@ -130,7 +133,7 @@ object FullStoragePermissionAppsLiveData : packageInfo.packageName, user, isLegacy = false, - isGranted = granted + isGranted = granted, ) } return null diff --git a/PermissionController/src/com/android/permissioncontroller/permission/domain/usecase/v31/GetPermissionGroupUsageDetailsUseCase.kt b/PermissionController/src/com/android/permissioncontroller/permission/domain/usecase/v31/GetPermissionGroupUsageDetailsUseCase.kt index 16eaaf6f2..5ba649fd3 100644 --- a/PermissionController/src/com/android/permissioncontroller/permission/domain/usecase/v31/GetPermissionGroupUsageDetailsUseCase.kt +++ b/PermissionController/src/com/android/permissioncontroller/permission/domain/usecase/v31/GetPermissionGroupUsageDetailsUseCase.kt @@ -21,6 +21,7 @@ import android.app.AppOpsManager import android.os.UserHandle import android.permission.flags.Flags import com.android.modules.utils.build.SdkLevel +import com.android.permissioncontroller.PermissionControllerApplication import com.android.permissioncontroller.appops.data.model.v31.DiscretePackageOpsModel import com.android.permissioncontroller.appops.data.model.v31.DiscretePackageOpsModel.DiscreteOpModel import com.android.permissioncontroller.appops.data.repository.v31.AppOpRepository @@ -29,6 +30,7 @@ import com.android.permissioncontroller.permission.domain.model.v31.PermissionTi import com.android.permissioncontroller.permission.domain.model.v31.PermissionTimelineUsageModelWrapper import com.android.permissioncontroller.permission.ui.model.v31.PermissionUsageDetailsViewModel.Companion.CLUSTER_SPACING_MINUTES import com.android.permissioncontroller.permission.ui.model.v31.PermissionUsageDetailsViewModel.Companion.ONE_MINUTE_MS +import com.android.permissioncontroller.permission.utils.LocationUtils import com.android.permissioncontroller.permission.utils.PermissionMapping import com.android.permissioncontroller.pm.data.repository.v31.PackageRepository import com.android.permissioncontroller.role.data.repository.v31.RoleRepository @@ -48,6 +50,9 @@ class GetPermissionGroupUsageDetailsUseCase( private val appOpRepository: AppOpRepository, private val roleRepository: RoleRepository, private val userRepository: UserRepository, + // Allow tests to inject as on T- READ_DEVICE_CONFIG permission check is enforced. + private val attributionLabelFix: Boolean = + com.android.permission.flags.Flags.permissionTimelineAttributionLabelFix(), ) { operator fun invoke(coroutineScope: CoroutineScope): Flow<PermissionTimelineUsageModelWrapper> { val opNames = requireNotNull(permissionGroupToOpNames[permissionGroup]) @@ -72,7 +77,7 @@ class GetPermissionGroupUsageDetailsUseCase( permissionGroup, packageOps.userId, permissionRepository, - packageRepository + packageRepository, ) packageOps } @@ -86,43 +91,61 @@ class GetPermissionGroupUsageDetailsUseCase( } } + // show attribution on T+ for location provider only.. + private fun shouldShowAttributionLabel(packageName: String): Boolean { + return if (attributionLabelFix) { + SdkLevel.isAtLeastT() && + LocationUtils.isLocationProvider(PermissionControllerApplication.get(), packageName) + } else true + } + /** Group app op accesses by attribution label if it is available and user visible. */ private suspend fun List<DiscretePackageOpsModel>.groupByAttributionLabelIfNeeded() = map { packageOps -> - val attributionInfo = - packageRepository.getPackageAttributionInfo( - packageOps.packageName, - UserHandle.of(packageOps.userId) - ) - if (attributionInfo != null) { - if (attributionInfo.areUserVisible && attributionInfo.tagResourceMap != null) { - val attributionLabelOpsMap: Map<String?, List<DiscreteOpModel>> = - packageOps.appOpAccesses - .map { appOpEntry -> - val resourceId = - attributionInfo.tagResourceMap[appOpEntry.attributionTag] - val label = attributionInfo.resourceLabelMap?.get(resourceId) - label to appOpEntry - } - .groupBy { labelAppOpEntryPair -> labelAppOpEntryPair.first } - .mapValues { (_, values) -> - values.map { labelAppOpEntryPair -> labelAppOpEntryPair.second } - } + if (!shouldShowAttributionLabel(packageOps.packageName)) { + listOf(packageOps) + } else { + val attributionInfo = + packageRepository.getPackageAttributionInfo( + packageOps.packageName, + UserHandle.of(packageOps.userId), + ) + if (attributionInfo != null) { + if ( + attributionInfo.areUserVisible && attributionInfo.tagResourceMap != null + ) { + val attributionLabelOpsMap: Map<String?, List<DiscreteOpModel>> = + packageOps.appOpAccesses + .map { appOpEntry -> + val resourceId = + attributionInfo.tagResourceMap[ + appOpEntry.attributionTag] + val label = + attributionInfo.resourceLabelMap?.get(resourceId) + label to appOpEntry + } + .groupBy { labelAppOpEntryPair -> labelAppOpEntryPair.first } + .mapValues { (_, values) -> + values.map { labelAppOpEntryPair -> + labelAppOpEntryPair.second + } + } - attributionLabelOpsMap.map { labelAppOpsEntry -> - DiscretePackageOpsModel( - packageOps.packageName, - packageOps.userId, - appOpAccesses = labelAppOpsEntry.value, - attributionLabel = labelAppOpsEntry.key, - isUserSensitive = packageOps.isUserSensitive, - ) + attributionLabelOpsMap.map { labelAppOpsEntry -> + DiscretePackageOpsModel( + packageOps.packageName, + packageOps.userId, + appOpAccesses = labelAppOpsEntry.value, + attributionLabel = labelAppOpsEntry.key, + isUserSensitive = packageOps.isUserSensitive, + ) + } + } else { + listOf(packageOps) } } else { listOf(packageOps) } - } else { - listOf(packageOps) } } .flatten() @@ -147,7 +170,7 @@ class GetPermissionGroupUsageDetailsUseCase( packageOps.userId, currentCluster.toMutableList(), packageOps.attributionLabel, - packageOps.isUserSensitive + packageOps.isUserSensitive, ) ) currentCluster.clear() @@ -164,7 +187,7 @@ class GetPermissionGroupUsageDetailsUseCase( packageOps.userId, currentCluster.toMutableList(), packageOps.attributionLabel, - packageOps.isUserSensitive + packageOps.isUserSensitive, ) ) } @@ -220,7 +243,7 @@ class GetPermissionGroupUsageDetailsUseCase( private fun canAccessBeAddedToCluster( currentAccess: DiscreteOpModel, - clusteredAccesses: List<DiscreteOpModel> + clusteredAccesses: List<DiscreteOpModel>, ): Boolean { val clusterOp = clusteredAccesses.last().opName if ( @@ -282,7 +305,7 @@ class GetPermissionGroupUsageDetailsUseCase( listOf( Manifest.permission_group.CAMERA, Manifest.permission_group.LOCATION, - Manifest.permission_group.MICROPHONE + Manifest.permission_group.MICROPHONE, ) permissionGroups.forEach { permissionGroup -> val opNames = diff --git a/PermissionController/src/com/android/permissioncontroller/permission/service/BackupHelper.java b/PermissionController/src/com/android/permissioncontroller/permission/service/BackupHelper.java index 24aab174c..2fa809c6d 100644 --- a/PermissionController/src/com/android/permissioncontroller/permission/service/BackupHelper.java +++ b/PermissionController/src/com/android/permissioncontroller/permission/service/BackupHelper.java @@ -96,6 +96,7 @@ public class BackupHelper { private static final String ATTR_USER_SET = "set"; private static final String ATTR_USER_FIXED = "fixed"; private static final String ATTR_WAS_REVIEWED = "was-reviewed"; + private static final String ATTR_ONE_TIME = "one-time"; /** Flags of permissions to <u>not</u> back up */ private static final int SYSTEM_RUNTIME_GRANT_MASK = FLAG_PERMISSION_POLICY_FIXED @@ -452,19 +453,21 @@ public class BackupHelper { private final boolean mIsUserSet; private final boolean mIsUserFixed; private final boolean mWasReviewed; + private final boolean mIsOneTime; // Not persisted, used during parsing so explicitly defined state takes precedence private final boolean mIsAddedFromSplit; private BackupPermissionState(@NonNull String permissionName, boolean isGranted, boolean isUserSet, boolean isUserFixed, boolean wasReviewed, - boolean isAddedFromSplit) { + boolean isOneTime, boolean isAddedFromSplit) { mPermissionName = permissionName; mIsGranted = isGranted; mIsUserSet = isUserSet; mIsUserFixed = isUserFixed; mWasReviewed = wasReviewed; mIsAddedFromSplit = isAddedFromSplit; + mIsOneTime = isOneTime; } /** @@ -512,6 +515,7 @@ public class BackupHelper { "true".equals(parser.getAttributeValue(null, ATTR_USER_SET)), "true".equals(parser.getAttributeValue(null, ATTR_USER_FIXED)), "true".equals(parser.getAttributeValue(null, ATTR_WAS_REVIEWED)), + "true".equals(parser.getAttributeValue(null, ATTR_ONE_TIME)), /* isAddedFromSplit */ i > 0)); } @@ -519,7 +523,8 @@ public class BackupHelper { } /** - * Is the permission granted, also considering the app-op. + * Is the permission granted, also considering the app-op. Don't consider one time grant + * as a permission grant for backup/restore. * * <p>This does not consider the review-required state of the permission. * @@ -528,7 +533,8 @@ public class BackupHelper { * @return {@code true} iff the permission and app-op is granted */ private static boolean isPermGrantedIncludingAppOp(@NonNull Permission perm) { - return perm.isGranted() && (!perm.affectsAppOp() || perm.isAppOpAllowed()); + return perm.isGranted() && (!perm.affectsAppOp() || perm.isAppOpAllowed()) + && !perm.isOneTime(); } /** @@ -549,7 +555,7 @@ public class BackupHelper { return null; } - if (!perm.isUserSet() && perm.isGrantedByDefault()) { + if (!perm.isUserSet() && !perm.isOneTime() && perm.isGrantedByDefault()) { return null; } @@ -564,10 +570,10 @@ public class BackupHelper { } if (isNotInDefaultGrantState || perm.isUserSet() || perm.isUserFixed() - || permissionWasReviewed) { + || perm.isOneTime() || permissionWasReviewed) { return new BackupPermissionState(perm.getName(), isPermGrantedIncludingAppOp(perm), perm.isUserSet(), perm.isUserFixed(), permissionWasReviewed, - /* isAddedFromSplit */ false); + perm.isOneTime(), /* isAddedFromSplit */ false); } else { return null; } @@ -628,6 +634,10 @@ public class BackupHelper { serializer.attribute(null, ATTR_WAS_REVIEWED, "true"); } + if (mIsOneTime) { + serializer.attribute(null, ATTR_ONE_TIME, "true"); + } + serializer.endTag(null, TAG_PERMISSION); } @@ -671,6 +681,10 @@ public class BackupHelper { perm.setUserSet(mIsUserSet); } + + if (!perm.isOneTime()) { + perm.setOneTime(mIsOneTime); + } } } diff --git a/PermissionController/src/com/android/permissioncontroller/permission/service/RuntimePermissionsUpgradeController.kt b/PermissionController/src/com/android/permissioncontroller/permission/service/RuntimePermissionsUpgradeController.kt index 2734116dd..85145f346 100644 --- a/PermissionController/src/com/android/permissioncontroller/permission/service/RuntimePermissionsUpgradeController.kt +++ b/PermissionController/src/com/android/permissioncontroller/permission/service/RuntimePermissionsUpgradeController.kt @@ -566,10 +566,11 @@ object RuntimePermissionsUpgradeController { appPermGroup.permissions[permission.ACCESS_MEDIA_LOCATION] ?: continue if ( + !perm.isGranted && !perm.isUserSet && - !perm.isSystemFixed && - !perm.isPolicyFixed && - !perm.isGranted + !perm.isOneTime && + !perm.isSystemFixed && + !perm.isPolicyFixed ) { grants.add( Grant(false, appPermGroup, listOf(permission.ACCESS_MEDIA_LOCATION)) @@ -610,20 +611,21 @@ object RuntimePermissionsUpgradeController { // Upon upgrading to platform 33, for all targetSdk>=33 apps, do the following: // If STORAGE is granted, and the user has not set READ_MEDIA_AURAL or // READ_MEDIA_VISUAL, grant READ_MEDIA_AURAL and READ_MEDIA_VISUAL - val storageAppPermGroups = + val grantedStorageAppPermGroups = storageAndMediaAppPermGroups.filter { it.packageInfo.targetSdkVersion >= Build.VERSION_CODES.TIRAMISU && it.permGroupInfo.name == permission_group.STORAGE && it.isGranted && it.isUserSet } - for (storageAppPermGroup in storageAppPermGroups) { + for (storageAppPermGroup in grantedStorageAppPermGroups) { val pkgName = storageAppPermGroup.packageInfo.packageName val auralAppPermGroup = storageAndMediaAppPermGroups.firstOrNull { it.packageInfo.packageName == pkgName && it.permGroupInfo.name == permission_group.READ_MEDIA_AURAL && !it.isUserSet && + !it.isOneTime && !it.isUserFixed } val visualAppPermGroup = @@ -632,7 +634,8 @@ object RuntimePermissionsUpgradeController { it.permGroupInfo.name == permission_group.READ_MEDIA_VISUAL && !it.permissions .filter { it.key != permission.ACCESS_MEDIA_LOCATION } - .any { it.value.isUserSet || it.value.isUserFixed } + .any { it.value.isUserSet || it.value.isOneTime || + it.value.isUserFixed } } if (auralAppPermGroup != null) { diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/GrantPermissionsActivity.java b/PermissionController/src/com/android/permissioncontroller/permission/ui/GrantPermissionsActivity.java index a4f629d80..c1479caf2 100644 --- a/PermissionController/src/com/android/permissioncontroller/permission/ui/GrantPermissionsActivity.java +++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/GrantPermissionsActivity.java @@ -46,7 +46,6 @@ import android.app.KeyguardManager; import android.app.ecm.EnhancedConfirmationManager; import android.content.Context; import android.content.Intent; -import android.content.pm.PackageInfo; import android.content.pm.PackageItemInfo; import android.content.pm.PackageManager; import android.content.res.Resources; @@ -195,7 +194,7 @@ public class GrantPermissionsActivity extends SettingsActivity /** A list of permissions requested on an app's behalf by the system. Usually Implicitly * requested, although this isn't necessarily always the case. */ - private List<String> mSystemRequestedPermissions = new ArrayList<>(); + private final List<String> mSystemRequestedPermissions = new ArrayList<>(); /** A copy of the list of permissions originally requested in the intent to this activity */ private String[] mOriginalRequestedPermissions = new String[0]; @@ -209,7 +208,7 @@ public class GrantPermissionsActivity extends SettingsActivity * A list of other GrantPermissionActivities for the same package which passed their list of * permissions to this one. They need to be informed when this activity finishes. */ - private List<GrantPermissionsActivity> mFollowerActivities = new ArrayList<>(); + private final List<GrantPermissionsActivity> mFollowerActivities = new ArrayList<>(); /** Whether this activity has asked another GrantPermissionsActivity to show on its behalf */ private boolean mDelegated; @@ -235,7 +234,7 @@ public class GrantPermissionsActivity extends SettingsActivity private PackageManager mPackageManager; - private ActivityResultLauncher<Intent> mShowWarningDialog = + private final ActivityResultLauncher<Intent> mShowWarningDialog = registerForActivityResult( new ActivityResultContracts.StartActivityForResult(), result -> { @@ -284,7 +283,7 @@ public class GrantPermissionsActivity extends SettingsActivity return; } try { - PackageInfo packageInfo = mPackageManager.getPackageInfo(mTargetPackage, 0); + mPackageManager.getPackageInfo(mTargetPackage, 0); } catch (PackageManager.NameNotFoundException e) { Log.e(LOG_TAG, "Unable to get package info for the calling package.", e); finishAfterTransition(); @@ -314,20 +313,23 @@ public class GrantPermissionsActivity extends SettingsActivity .getPackageManager(); } - // When the dialog is streamed to a remote device, verify requested permissions are all - // device aware and target device is the same as the remote device. Otherwise show a - // warning dialog. + // When the permission grant dialog is streamed to a virtual device, and when requested + // permissions include both device-aware permissions and non-device aware permissions, + // device-aware permissions will use virtual device id and non-device aware permissions + // will use default device id for granting. If flag is not enabled, we would show a + // warning dialog for this use case. if (getDeviceId() != ContextCompat.DEVICE_ID_DEFAULT) { boolean showWarningDialog = mTargetDeviceId != getDeviceId(); for (String permission : mRequestedPermissions) { - if (!MultiDeviceUtils.isPermissionDeviceAware( - getApplicationContext(), mTargetDeviceId, permission)) { + if (!MultiDeviceUtils.isPermissionDeviceAware(getApplicationContext(), + mTargetDeviceId, permission)) { showWarningDialog = true; + break; } } - if (showWarningDialog) { + if (showWarningDialog && !Flags.allowHostPermissionDialogsOnVirtualDevices()) { mShowWarningDialog.launch( new Intent(this, PermissionDialogStreamingBlockedActivity.class)); return; @@ -1115,9 +1117,17 @@ public class GrantPermissionsActivity extends SettingsActivity if ((mDelegated || (mViewModel != null && mViewModel.shouldReturnPermissionState())) && mTargetPackage != null) { + PackageManager defaultDevicePackageManager = SdkLevel.isAtLeastV() + && mTargetDeviceId != ContextCompat.DEVICE_ID_DEFAULT + ? createDeviceContext(ContextCompat.DEVICE_ID_DEFAULT).getPackageManager() + : mPackageManager; + PackageManager targetDevicePackageManager = mPackageManager; for (int i = 0; i < resultPermissions.length; i++) { - grantResults[i] = - mPackageManager.checkPermission(resultPermissions[i], mTargetPackage); + String permission = resultPermissions[i]; + PackageManager pm = MultiDeviceUtils.isPermissionDeviceAware( + getApplicationContext(), mTargetDeviceId, permission) + ? targetDevicePackageManager : defaultDevicePackageManager; + grantResults[i] = pm.checkPermission(resultPermissions[i], mTargetPackage); } } else { grantResults = new int[0]; diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/auto/dashboard/AutoPermissionHistoryPreference.kt b/PermissionController/src/com/android/permissioncontroller/permission/ui/auto/dashboard/AutoPermissionHistoryPreference.kt index 2d14260a2..36597a3a3 100644 --- a/PermissionController/src/com/android/permissioncontroller/permission/ui/auto/dashboard/AutoPermissionHistoryPreference.kt +++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/auto/dashboard/AutoPermissionHistoryPreference.kt @@ -23,30 +23,30 @@ import androidx.annotation.RequiresApi import androidx.preference.Preference.OnPreferenceClickListener import com.android.car.ui.preference.CarUiPreference import com.android.permissioncontroller.R -import com.android.permissioncontroller.permission.ui.legacy.PermissionUsageDetailsViewModelLegacy import com.android.permissioncontroller.permission.ui.model.v31.PermissionUsageDetailsViewModel +import com.android.permissioncontroller.permission.ui.model.v31.PermissionUsageDetailsViewModel.AppPermissionAccessUiInfo /** Preference that displays a permission usage for an app. */ @RequiresApi(Build.VERSION_CODES.S) class AutoPermissionHistoryPreference( context: Context, - historyPreferenceData: PermissionUsageDetailsViewModelLegacy.HistoryPreferenceData + historyPreferenceData: AppPermissionAccessUiInfo, ) : CarUiPreference(context) { init { - title = historyPreferenceData.preferenceTitle + title = historyPreferenceData.packageLabel summary = if (historyPreferenceData.summaryText != null) { context.getString( R.string.auto_permission_usage_timeline_summary, DateFormat.getTimeFormat(context).format(historyPreferenceData.accessEndTime), - historyPreferenceData.summaryText + historyPreferenceData.summaryText, ) } else { DateFormat.getTimeFormat(context).format(historyPreferenceData.accessEndTime) } - if (historyPreferenceData.appIcon != null) { - icon = historyPreferenceData.appIcon + if (historyPreferenceData.badgedPackageIcon != null) { + icon = historyPreferenceData.badgedPackageIcon } onPreferenceClickListener = OnPreferenceClickListener { @@ -56,12 +56,12 @@ class AutoPermissionHistoryPreference( PermissionUsageDetailsViewModel.createHistoryPreferenceClickIntent( context = context, userHandle = historyPreferenceData.userHandle, - packageName = historyPreferenceData.pkgName, + packageName = historyPreferenceData.packageName, permissionGroup = historyPreferenceData.permissionGroup, accessEndTime = historyPreferenceData.accessEndTime, accessStartTime = historyPreferenceData.accessStartTime, showingAttribution = historyPreferenceData.showingAttribution, - attributionTags = historyPreferenceData.attributionTags.toSet() + attributionTags = historyPreferenceData.attributionTags.toSet(), ) ) true diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/auto/dashboard/AutoPermissionUsageDetailsFragment.kt b/PermissionController/src/com/android/permissioncontroller/permission/ui/auto/dashboard/AutoPermissionUsageDetailsFragment.kt index 481543eb6..8edd39913 100644 --- a/PermissionController/src/com/android/permissioncontroller/permission/ui/auto/dashboard/AutoPermissionUsageDetailsFragment.kt +++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/auto/dashboard/AutoPermissionUsageDetailsFragment.kt @@ -13,11 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -@file:Suppress("DEPRECATION") - package com.android.permissioncontroller.permission.ui.auto.dashboard -import android.app.role.RoleManager import android.content.Intent import android.os.Build import android.os.Bundle @@ -36,16 +33,12 @@ import com.android.permissioncontroller.PermissionControllerStatsLog.PERMISSION_ import com.android.permissioncontroller.PermissionControllerStatsLog.PERMISSION_USAGE_FRAGMENT_INTERACTION__ACTION__SHOW_SYSTEM_CLICKED import com.android.permissioncontroller.R import com.android.permissioncontroller.auto.AutoSettingsFrameFragment -import com.android.permissioncontroller.permission.model.legacy.PermissionApps.AppDataLoader -import com.android.permissioncontroller.permission.model.v31.AppPermissionUsage -import com.android.permissioncontroller.permission.model.v31.PermissionUsages -import com.android.permissioncontroller.permission.model.v31.PermissionUsages.PermissionsUsagesChangeCallback import com.android.permissioncontroller.permission.ui.ManagePermissionsActivity import com.android.permissioncontroller.permission.ui.auto.AutoDividerPreference -import com.android.permissioncontroller.permission.ui.legacy.PermissionUsageDetailsViewModelFactoryLegacy -import com.android.permissioncontroller.permission.ui.legacy.PermissionUsageDetailsViewModelLegacy +import com.android.permissioncontroller.permission.ui.model.v31.BasePermissionUsageDetailsViewModel +import com.android.permissioncontroller.permission.ui.model.v31.PermissionUsageDetailsViewModel +import com.android.permissioncontroller.permission.ui.model.v31.PermissionUsageDetailsViewModel.AppPermissionAccessUiInfo import com.android.permissioncontroller.permission.utils.KotlinUtils.getPermGroupLabel -import com.android.permissioncontroller.permission.utils.Utils import java.time.Clock import java.time.Instant import java.time.ZoneId @@ -54,9 +47,7 @@ import java.time.temporal.ChronoUnit import java.util.concurrent.atomic.AtomicReference @RequiresApi(Build.VERSION_CODES.S) -class AutoPermissionUsageDetailsFragment : - AutoSettingsFrameFragment(), PermissionsUsagesChangeCallback { - +class AutoPermissionUsageDetailsFragment : AutoSettingsFrameFragment() { companion object { private const val LOG_TAG = "AutoPermissionUsageDetailsFragment" private const val KEY_SESSION_ID = "_session_id" @@ -70,14 +61,11 @@ class AutoPermissionUsageDetailsFragment : .truncatedTo(ChronoUnit.DAYS) .toEpochSecond() * 1000L - // Only show the last 24 hours on Auto right now - private const val SHOW_7_DAYS = false - /** Creates a new instance of [AutoPermissionUsageDetailsFragment]. */ fun newInstance( groupName: String?, showSystem: Boolean, - sessionId: Long + sessionId: Long, ): AutoPermissionUsageDetailsFragment { return AutoPermissionUsageDetailsFragment().apply { arguments = @@ -92,14 +80,10 @@ class AutoPermissionUsageDetailsFragment : private val SESSION_ID_KEY = (AutoPermissionUsageFragment::class.java.name + KEY_SESSION_ID) - private lateinit var permissionUsages: PermissionUsages - private lateinit var usageViewModel: PermissionUsageDetailsViewModelLegacy + private lateinit var usageViewModel: BasePermissionUsageDetailsViewModel private lateinit var filterGroup: String - private lateinit var roleManager: RoleManager - private var appPermissionUsages: List<AppPermissionUsage> = listOf() private var showSystem = false - private var finishedInitialLoad = false private var hasSystemApps = false /** Unique Id of a request */ @@ -116,7 +100,7 @@ class AutoPermissionUsageDetailsFragment : !requireArguments().containsKey(Intent.EXTRA_PERMISSION_GROUP_NAME) or (requireArguments().getString(Intent.EXTRA_PERMISSION_GROUP_NAME) == null) ) { - DumpableLog.e(LOG_TAG, "Missing argument ${Intent.EXTRA_USER}") + DumpableLog.e(LOG_TAG, "Missing argument ${Intent.EXTRA_PERMISSION_GROUP_NAME}") activity?.finish() return } @@ -130,28 +114,21 @@ class AutoPermissionUsageDetailsFragment : headerLabel = resources.getString( R.string.permission_group_usage_title, - getPermGroupLabel(requireContext(), filterGroup) + getPermGroupLabel(requireContext(), filterGroup), ) - - val context = preferenceManager.getContext() - permissionUsages = PermissionUsages(context) - roleManager = Utils.getSystemServiceSafe(context, RoleManager::class.java) - val usageViewModelFactory = - PermissionUsageDetailsViewModelFactoryLegacy( + val factory = + PermissionUsageDetailsViewModel.PermissionUsageDetailsViewModelFactory( PermissionControllerApplication.get(), - roleManager, + this, filterGroup, - sessionId ) usageViewModel = - ViewModelProvider(this, usageViewModelFactory)[ - PermissionUsageDetailsViewModelLegacy::class.java] - - reloadData() + ViewModelProvider(this, factory)[BasePermissionUsageDetailsViewModel::class.java] + usageViewModel.getPermissionUsagesDetailsInfoUiLiveData().observe(this, this::updateUI) } override fun onCreatePreferences(bundlle: Bundle?, s: String?) { - preferenceScreen = preferenceManager.createPreferenceScreen(context!!) + preferenceScreen = preferenceManager.createPreferenceScreen(requireContext()) } private fun setupHeaderPreferences() { @@ -161,38 +138,16 @@ class AutoPermissionUsageDetailsFragment : preferenceScreen.addPreference(AutoDividerPreference(context)) } - /** Reloads the data to show. */ - private fun reloadData() { - usageViewModel.loadPermissionUsages( - requireActivity().getLoaderManager(), - permissionUsages, - this, - FILTER_24_HOURS - ) - if (finishedInitialLoad) { - setLoading(true) - } - } - - override fun onPermissionUsagesChanged() { - if (permissionUsages.usages.isEmpty()) { - return - } - appPermissionUsages = ArrayList(permissionUsages.usages) - updateUI() - } - private fun updateSystemToggle() { if (!showSystem) { PermissionControllerStatsLog.write( PERMISSION_USAGE_FRAGMENT_INTERACTION, sessionId, - PERMISSION_USAGE_FRAGMENT_INTERACTION__ACTION__SHOW_SYSTEM_CLICKED + PERMISSION_USAGE_FRAGMENT_INTERACTION__ACTION__SHOW_SYSTEM_CLICKED, ) } showSystem = !showSystem updateAction() - updateUI() } private fun updateAction() { @@ -206,47 +161,36 @@ class AutoPermissionUsageDetailsFragment : } else { getString(R.string.menu_show_system) } - setAction(label) { updateSystemToggle() } + setAction(label) { + usageViewModel.updateShowSystemAppsToggle(!showSystem) + updateSystemToggle() + } } - private fun updateUI() { - if (appPermissionUsages.isEmpty()) { + private fun updateUI(uiInfo: PermissionUsageDetailsViewModel.PermissionUsageDetailsUiState) { + if ( + activity == null || + uiInfo is PermissionUsageDetailsViewModel.PermissionUsageDetailsUiState.Loading + ) { return } preferenceScreen.removeAll() setupHeaderPreferences() - - val uiData = - usageViewModel.buildPermissionUsageDetailsUiData( - appPermissionUsages, - showSystem, - SHOW_7_DAYS - ) - - if (hasSystemApps != uiData.shouldDisplayShowSystemToggle) { - hasSystemApps = uiData.shouldDisplayShowSystemToggle + val uiData = uiInfo as PermissionUsageDetailsViewModel.PermissionUsageDetailsUiState.Success + if (hasSystemApps != uiData.containsSystemAppUsage) { + hasSystemApps = uiData.containsSystemAppUsage updateAction() } - val category = AtomicReference(PreferenceCategory(requireContext())) preferenceScreen.addPreference(category.get()) - AppDataLoader(context) { - renderHistoryPreferences( - uiData.getHistoryPreferenceDataList(), - category, - preferenceScreen - ) + renderHistoryPreferences(uiData.appPermissionAccessUiInfoList, category, preferenceScreen) - setLoading(false) - finishedInitialLoad = true - permissionUsages.stopLoader(requireActivity().getLoaderManager()) - } - .execute(*uiData.permissionApps.toTypedArray()) + setLoading(false) } fun createPermissionHistoryPreference( - historyPreferenceData: PermissionUsageDetailsViewModelLegacy.HistoryPreferenceData + historyPreferenceData: AppPermissionAccessUiInfo ): Preference { return AutoPermissionHistoryPreference(requireContext(), historyPreferenceData) } @@ -257,7 +201,7 @@ class AutoPermissionUsageDetailsFragment : summary = getString( R.string.permission_group_usage_subtitle_24h, - getPermGroupLabel(requireContext(), filterGroup) + getPermGroupLabel(requireContext(), filterGroup), ) isSelectable = false } @@ -271,7 +215,7 @@ class AutoPermissionUsageDetailsFragment : summary = getString( R.string.manage_permission_summary, - getPermGroupLabel(requireContext(), filterGroup) + getPermGroupLabel(requireContext(), filterGroup), ) onPreferenceClickListener = Preference.OnPreferenceClickListener { @@ -287,9 +231,8 @@ class AutoPermissionUsageDetailsFragment : } /** Render the provided [historyPreferenceDataList] into the [preferenceScreen] UI. */ - fun renderHistoryPreferences( - historyPreferenceDataList: - List<PermissionUsageDetailsViewModelLegacy.HistoryPreferenceData>, + private fun renderHistoryPreferences( + historyPreferenceDataList: List<AppPermissionAccessUiInfo>, category: AtomicReference<PreferenceCategory>, preferenceScreen: PreferenceScreen, ) { @@ -299,7 +242,7 @@ class AutoPermissionUsageDetailsFragment : val currentDateMs = ZonedDateTime.ofInstant( Instant.ofEpochMilli(usageTimestamp), - Clock.system(ZoneId.systemDefault()).zone + Clock.system(ZoneId.systemDefault()).zone, ) .truncatedTo(ChronoUnit.DAYS) .toEpochSecond() * 1000L diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/auto/dashboard/AutoPermissionUsageFragment.kt b/PermissionController/src/com/android/permissioncontroller/permission/ui/auto/dashboard/AutoPermissionUsageFragment.kt index f2e453447..f52eaadcd 100644 --- a/PermissionController/src/com/android/permissioncontroller/permission/ui/auto/dashboard/AutoPermissionUsageFragment.kt +++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/auto/dashboard/AutoPermissionUsageFragment.kt @@ -46,7 +46,7 @@ class AutoPermissionUsageFragment : AutoSettingsFrameFragment() { Manifest.permission_group.CAMERA, 1, Manifest.permission_group.MICROPHONE, - 2 + 2, ) private const val DEFAULT_ORDER: Int = 3 } @@ -54,7 +54,6 @@ class AutoPermissionUsageFragment : AutoSettingsFrameFragment() { private val SESSION_ID_KEY = (AutoPermissionUsageFragment::class.java.name + KEY_SESSION_ID) private var showSystem = false - private var finishedInitialLoad = false private var hasSystemApps = false /** Unique Id of a request */ @@ -89,7 +88,7 @@ class AutoPermissionUsageFragment : AutoSettingsFrameFragment() { PermissionControllerStatsLog.write( PERMISSION_USAGE_FRAGMENT_INTERACTION, sessionId, - PERMISSION_USAGE_FRAGMENT_INTERACTION__ACTION__SHOW_SYSTEM_CLICKED + PERMISSION_USAGE_FRAGMENT_INTERACTION__ACTION__SHOW_SYSTEM_CLICKED, ) } showSystem = !showSystem @@ -133,13 +132,13 @@ class AutoPermissionUsageFragment : AutoSettingsFrameFragment() { Comparator.comparing { permissionGroupWithUsageCount: Map.Entry<String, Int> -> PERMISSION_GROUP_ORDER.getOrDefault( permissionGroupWithUsageCount.key, - DEFAULT_ORDER + DEFAULT_ORDER, ) } .thenComparing { permissionGroupWithUsageCount: Map.Entry<String, Int> -> mViewModel.getPermissionGroupLabel( requireContext(), - permissionGroupWithUsageCount.key + permissionGroupWithUsageCount.key, ) } ) @@ -153,11 +152,10 @@ class AutoPermissionUsageFragment : AutoSettingsFrameFragment() { permissionGroupWithUsageCountsEntries[i].value, showSystem, sessionId, - false + false, ) getPreferenceScreen().addPreference(permissionUsagePreference) } - finishedInitialLoad = true setLoading(false) } } diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/AppPermissionWrapperFragment.java b/PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/AppPermissionWrapperFragment.java index 8650d99fc..080c7cfdc 100644 --- a/PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/AppPermissionWrapperFragment.java +++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/AppPermissionWrapperFragment.java @@ -29,7 +29,7 @@ public class AppPermissionWrapperFragment extends PermissionsCollapsingToolbarBa @NonNull @Override public PreferenceFragmentCompat createPreferenceFragment() { - return AppPermissionFragmentCompat.createFragment(); + return AppPermissionFragmentCompat.createFragment(getContext()); } @Override diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/ManageCustomPermissionsFragment.java b/PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/ManageCustomPermissionsFragment.java index 35236b8de..dd460aa2f 100644 --- a/PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/ManageCustomPermissionsFragment.java +++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/ManageCustomPermissionsFragment.java @@ -24,6 +24,8 @@ import android.view.MenuItem; import androidx.lifecycle.ViewModelProvider; +import com.android.permission.flags.Flags; +import com.android.permissioncontroller.permission.data.PermGroupsPackagesUiInfoLiveData; import com.android.permissioncontroller.permission.ui.model.ManageCustomPermissionsViewModel; import com.android.permissioncontroller.permission.ui.model.ManageCustomPermissionsViewModelFactory; @@ -48,6 +50,14 @@ public class ManageCustomPermissionsFragment extends ManagePermissionsFragment { return arguments; } + private PermGroupsPackagesUiInfoLiveData getPermGroupsLiveData() { + if (Flags.declutteredPermissionManagerEnabled()) { + return mViewModel.getAdditionaPermGroupsUiInfo(); + } else { + return mViewModel.getUiDataLiveData(); + } + } + @Override public void onCreate(Bundle icicle) { super.onCreate(icicle); @@ -56,9 +66,9 @@ public class ManageCustomPermissionsFragment extends ManagePermissionsFragment { new ManageCustomPermissionsViewModelFactory(getActivity().getApplication()); mViewModel = new ViewModelProvider(this, factory) .get(ManageCustomPermissionsViewModel.class); - mPermissionGroups = mViewModel.getUiDataLiveData().getValue(); + mPermissionGroups = getPermGroupsLiveData().getValue(); - mViewModel.getUiDataLiveData().observe(this, permissionGroups -> { + getPermGroupsLiveData().observe(this, permissionGroups -> { if (permissionGroups == null) { mPermissionGroups = new HashMap<>(); } else { diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/ManageStandardPermissionsFragment.java b/PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/ManageStandardPermissionsFragment.java index bf99b7134..51c0906a2 100644 --- a/PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/ManageStandardPermissionsFragment.java +++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/ManageStandardPermissionsFragment.java @@ -31,7 +31,9 @@ import androidx.preference.Preference; import androidx.preference.PreferenceScreen; import com.android.modules.utils.build.SdkLevel; +import com.android.permission.flags.Flags; import com.android.permissioncontroller.R; +import com.android.permissioncontroller.permission.data.PermGroupsPackagesUiInfoLiveData; import com.android.permissioncontroller.permission.ui.UnusedAppsFragment; import com.android.permissioncontroller.permission.ui.model.ManageStandardPermissionsViewModel; import com.android.permissioncontroller.permission.utils.StringUtils; @@ -58,6 +60,14 @@ public final class ManageStandardPermissionsFragment extends ManagePermissionsFr return arguments; } + private PermGroupsPackagesUiInfoLiveData getPermGroupsLiveData() { + if (Flags.declutteredPermissionManagerEnabled()) { + return mViewModel.getUsedStandardPermGroupsUiInfo(); + } else { + return mViewModel.getUiDataLiveData(); + } + } + @Override public void onCreate(Bundle icicle) { super.onCreate(icicle); @@ -65,12 +75,12 @@ public final class ManageStandardPermissionsFragment extends ManagePermissionsFr final Application application = getActivity().getApplication(); mViewModel = new ViewModelProvider(this, AndroidViewModelFactory.getInstance(application)) .get(ManageStandardPermissionsViewModel.class); - mPermissionGroups = mViewModel.getUiDataLiveData().getValue(); + mPermissionGroups = getPermGroupsLiveData().getValue(); - mViewModel.getUiDataLiveData().observe(this, permissionGroups -> { + getPermGroupsLiveData().observe(this, permissionGroups -> { // Once we have loaded data for the first time, further loads should be staggered, // for performance reasons. - mViewModel.getUiDataLiveData().setLoadStaggered(true); + getPermGroupsLiveData().setLoadStaggered(true); if (permissionGroups != null) { mPermissionGroups = permissionGroups; updatePermissionsUi(); @@ -80,13 +90,18 @@ public final class ManageStandardPermissionsFragment extends ManagePermissionsFr } // If we've loaded all LiveDatas, no need to prioritize loading any particular one - if (!mViewModel.getUiDataLiveData().isStale()) { - mViewModel.getUiDataLiveData().setFirstLoadGroup(null); + if (!getPermGroupsLiveData().isStale()) { + getPermGroupsLiveData().setFirstLoadGroup(null); } }); mViewModel.getNumCustomPermGroups().observe(this, permNames -> updatePermissionsUi()); mViewModel.getNumAutoRevoked().observe(this, show -> updatePermissionsUi()); + if (Flags.declutteredPermissionManagerEnabled()) { + mViewModel.getNumUnusedStandardPermGroups().observe( + this, show -> updatePermissionsUi() + ); + } } @Override @@ -118,6 +133,14 @@ public final class ManageStandardPermissionsFragment extends ManagePermissionsFr if (mViewModel.getNumCustomPermGroups().getValue() != null) { numExtraPermissions = mViewModel.getNumCustomPermGroups().getValue(); } + if (Flags.declutteredPermissionManagerEnabled()) { + if (mViewModel.getNumUnusedStandardPermGroups().getValue() != null) { + // When decluttered permission manager is enabled, unused + // permission groups will also be displayed in the additional + // permissions screen. + numExtraPermissions += mViewModel.getNumUnusedStandardPermGroups().getValue(); + } + } Preference additionalPermissionsPreference = screen.findPreference(EXTRA_PREFS_KEY); if (numExtraPermissions == 0) { @@ -198,7 +221,7 @@ public final class ManageStandardPermissionsFragment extends ManagePermissionsFr public void showPermissionApps(String permissionGroupName) { // If we return to this page within a reasonable time, prioritize loading data from the // permission group whose page we are going to, as that is group most likely to have changed - mViewModel.getUiDataLiveData().setFirstLoadGroup(permissionGroupName); + getPermGroupsLiveData().setFirstLoadGroup(permissionGroupName); mViewModel.showPermissionApps(this, PermissionAppsFragment.createArgs( permissionGroupName, getArguments().getLong(EXTRA_SESSION_ID))); } diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/PermissionFooterPreference.kt b/PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/PermissionFooterPreference.kt index 1cd4ed23a..e7749d827 100644 --- a/PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/PermissionFooterPreference.kt +++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/PermissionFooterPreference.kt @@ -17,16 +17,21 @@ package com.android.permissioncontroller.permission.ui.handheld import android.content.Context +import android.util.AttributeSet import android.view.View import com.android.modules.utils.build.SdkLevel import com.android.permissioncontroller.R import com.android.settingslib.widget.FooterPreference -class PermissionFooterPreference(c: Context) : FooterPreference(c) { +class PermissionFooterPreference : FooterPreference { + constructor(context: Context) : super(context) + + constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) + init { if (SdkLevel.isAtLeastV()) { layoutResource = R.layout.permission_footer_preference - if (c.resources.getBoolean(R.bool.config_permissionFooterPreferenceIconVisible)) { + if (context.resources.getBoolean(R.bool.config_permissionFooterPreferenceIconVisible)) { setIconVisibility(View.VISIBLE) } else { setIconVisibility(View.GONE) diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/PermissionPreference.kt b/PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/PermissionPreference.kt index 5e30183ec..010ca28a7 100644 --- a/PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/PermissionPreference.kt +++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/PermissionPreference.kt @@ -18,15 +18,30 @@ package com.android.permissioncontroller.permission.ui.handheld import android.content.Context import android.util.AttributeSet +import androidx.annotation.AttrRes +import androidx.annotation.StyleRes import androidx.preference.Preference import com.android.modules.utils.build.SdkLevel import com.android.permissioncontroller.DeviceUtils import com.android.permissioncontroller.R open class PermissionPreference : Preference { - constructor(c: Context) : super(c) + constructor(context: Context) : super(context) - constructor(c: Context, a: AttributeSet) : super(c, a) + constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) + + constructor( + context: Context, + attrs: AttributeSet?, + @AttrRes defStyleAttr: Int, + ) : super(context, attrs, defStyleAttr) + + constructor( + context: Context, + attrs: AttributeSet?, + @AttrRes defStyleAttr: Int, + @StyleRes defStyleRes: Int, + ) : super(context, attrs, defStyleAttr, defStyleRes) init { if (SdkLevel.isAtLeastV() && DeviceUtils.isHandheld(context)) { diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/PermissionPreferenceCategory.kt b/PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/PermissionPreferenceCategory.kt index ef1752530..ef95c6c5c 100644 --- a/PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/PermissionPreferenceCategory.kt +++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/PermissionPreferenceCategory.kt @@ -18,15 +18,30 @@ package com.android.permissioncontroller.permission.ui.handheld import android.content.Context import android.util.AttributeSet +import androidx.annotation.AttrRes +import androidx.annotation.StyleRes import androidx.preference.PreferenceCategory import com.android.modules.utils.build.SdkLevel import com.android.permissioncontroller.DeviceUtils import com.android.permissioncontroller.R open class PermissionPreferenceCategory : PreferenceCategory { - constructor(c: Context) : super(c) + constructor(context: Context) : super(context) - constructor(c: Context, a: AttributeSet) : super(c, a) + constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) + + constructor( + context: Context, + attrs: AttributeSet?, + @AttrRes defStyleAttr: Int, + ) : super(context, attrs, defStyleAttr) + + constructor( + context: Context, + attrs: AttributeSet?, + @AttrRes defStyleAttr: Int, + @StyleRes defStyleRes: Int, + ) : super(context, attrs, defStyleAttr, defStyleRes) init { if (SdkLevel.isAtLeastV() && DeviceUtils.isHandheld(context)) { diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/v35/SectionPreferenceGroupAdapter.kt b/PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/v35/SectionPreferenceGroupAdapter.kt index 72e066777..e5dce40b0 100644 --- a/PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/v35/SectionPreferenceGroupAdapter.kt +++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/v35/SectionPreferenceGroupAdapter.kt @@ -25,6 +25,7 @@ import androidx.preference.PreferenceGroup import androidx.preference.PreferenceGroupAdapter import androidx.preference.PreferenceViewHolder import com.android.permissioncontroller.R +import com.android.permissioncontroller.permission.ui.handheld.v36.AppPermissionFooterLinkPreference import com.android.settingslib.widget.FooterPreference /** @@ -106,7 +107,10 @@ class SectionPreferenceGroupAdapter(preferenceGroup: PreferenceGroup) : } private val Preference.isSectionDivider: Boolean - get() = this is PreferenceCategory || this is FooterPreference + get() = + this is PreferenceCategory || + this is FooterPreference || + this is AppPermissionFooterLinkPreference override fun onBindViewHolder(holder: PreferenceViewHolder, position: Int) { super.onBindViewHolder(holder, position) diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/v36/AppPermissionFooterLinkPreference.kt b/PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/v36/AppPermissionFooterLinkPreference.kt new file mode 100644 index 000000000..01554880a --- /dev/null +++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/v36/AppPermissionFooterLinkPreference.kt @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.permissioncontroller.permission.ui.handheld.v36 + +import android.content.Context +import android.os.Build +import android.util.AttributeSet +import android.widget.TextView +import androidx.annotation.AttrRes +import androidx.annotation.RequiresApi +import androidx.annotation.StyleRes +import androidx.preference.PreferenceViewHolder +import com.android.permissioncontroller.R +import com.android.permissioncontroller.permission.ui.handheld.PermissionPreference + +@RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM) +class AppPermissionFooterLinkPreference : PermissionPreference { + constructor(context: Context) : super(context) + + constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) + + constructor( + context: Context, + attrs: AttributeSet?, + @AttrRes defStyleAttr: Int, + ) : super(context, attrs, defStyleAttr) + + constructor( + context: Context, + attrs: AttributeSet?, + @AttrRes defStyleAttr: Int, + @StyleRes defStyleRes: Int, + ) : super(context, attrs, defStyleAttr, defStyleRes) + + init { + layoutResource = R.layout.app_permission_footer_link_preference + } + + override fun onBindViewHolder(holder: PreferenceViewHolder) { + super.onBindViewHolder(holder) + if ( + context.resources.getBoolean( + R.bool.config_appPermissionFooterLinkPreferenceSummaryUnderlined + ) + ) { + val summary = holder.findViewById(android.R.id.summary) as TextView + summary.paint.isUnderlineText = true + } + } +} diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/v36/AppPermissionFragment.java b/PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/v36/AppPermissionFragment.java index 481dc0dac..4fde26c9d 100644 --- a/PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/v36/AppPermissionFragment.java +++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/v36/AppPermissionFragment.java @@ -79,11 +79,10 @@ import com.android.permissioncontroller.permission.ui.GrantPermissionsViewHandle import com.android.permissioncontroller.permission.ui.handheld.AllAppPermissionsFragment; import com.android.permissioncontroller.permission.ui.handheld.AppPermissionGroupsFragment; import com.android.permissioncontroller.permission.ui.handheld.PermissionAppsFragment; +import com.android.permissioncontroller.permission.ui.handheld.PermissionFooterPreference; import com.android.permissioncontroller.permission.ui.handheld.PermissionPreference; import com.android.permissioncontroller.permission.ui.handheld.PermissionPreferenceCategory; -import com.android.permissioncontroller.permission.ui.handheld.PermissionSelectorWithWidgetPreference; import com.android.permissioncontroller.permission.ui.handheld.PermissionSwitchPreference; -import com.android.permissioncontroller.permission.ui.handheld.PermissionTwoTargetPreference; import com.android.permissioncontroller.permission.ui.handheld.SettingsWithLargeHeader; import com.android.permissioncontroller.permission.ui.model.AppPermissionViewModel; import com.android.permissioncontroller.permission.ui.model.AppPermissionViewModel.ButtonState; @@ -133,10 +132,10 @@ public class AppPermissionFragment extends SettingsWithLargeHeader private @NonNull SelectorWithWidgetPreference mDenyForegroundButton; private @NonNull PermissionSwitchPreference mLocationAccuracySwitch; private @NonNull PermissionTwoTargetPreference mDetails; - private @NonNull PermissionPreference mFooterLink1; - private @NonNull PermissionPreference mFooterLink2; - private @NonNull PermissionPreference mFooterStorageSpecialAppAccess; - private @NonNull PermissionPreference mAdditionalInfo; + private @NonNull AppPermissionFooterLinkPreference mFooterLink1; + private @NonNull AppPermissionFooterLinkPreference mFooterLink2; + private @NonNull PermissionFooterPreference mFooterStorageSpecialAppAccess; + private @NonNull PermissionFooterPreference mAdditionalInfo; private @NonNull String mPackageName; private @NonNull String mPermGroupName; @@ -268,7 +267,7 @@ public class AppPermissionFragment extends SettingsWithLargeHeader if (exemptedPackages.contains(mPackageName)) { int additional_info_label = Utils.isStatusBarIndicatorPermission(mPermGroupName) ? R.string.exempt_mic_camera_info_label : R.string.exempt_info_label; - mAdditionalInfo.setSummary(context.getString(additional_info_label, mPackageLabel)); + mAdditionalInfo.setTitle(context.getString(additional_info_label, mPackageLabel)); mAdditionalInfo.setVisible(true); } else { mAdditionalInfo.setVisible(false); diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/PermissionSelectorWithWidgetPreference.kt b/PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/v36/PermissionSelectorWithWidgetPreference.kt index 9d095c7dc..1574eaba3 100644 --- a/PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/PermissionSelectorWithWidgetPreference.kt +++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/v36/PermissionSelectorWithWidgetPreference.kt @@ -14,14 +14,16 @@ * limitations under the License. */ -package com.android.permissioncontroller.permission.ui.handheld +package com.android.permissioncontroller.permission.ui.handheld.v36 import android.content.Context +import android.os.Build import android.util.AttributeSet import android.widget.ImageView import androidx.annotation.AttrRes import androidx.annotation.DrawableRes import androidx.annotation.IdRes +import androidx.annotation.RequiresApi import androidx.preference.PreferenceViewHolder import com.android.permissioncontroller.R import com.android.permissioncontroller.permission.utils.ResourceUtils @@ -34,6 +36,7 @@ import com.android.settingslib.widget.SelectorWithWidgetPreference * - Propagates the supplied `app:checkboxId` id to the checkbox (or radio button, on the left) * - Allows defining a "disabled click listener" handler that handles clicks when disabled */ +@RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM) class PermissionSelectorWithWidgetPreference : SelectorWithWidgetPreference { constructor(context: Context) : super(context) { init(context, null) @@ -46,7 +49,7 @@ class PermissionSelectorWithWidgetPreference : SelectorWithWidgetPreference { constructor( context: Context, attrs: AttributeSet?, - @AttrRes defStyleAttr: Int + @AttrRes defStyleAttr: Int, ) : super(context, attrs, defStyleAttr) { init(context, attrs) } @@ -56,6 +59,8 @@ class PermissionSelectorWithWidgetPreference : SelectorWithWidgetPreference { } private fun init(context: Context, attrs: AttributeSet?) { + layoutResource = R.layout.permission_preference_selector_with_widget + widgetLayoutResource = R.layout.permission_preference_widget_radiobutton extraWidgetIconRes = ResourceUtils.getResourceIdByAttr(context, attrs, R.attr.extraWidgetIcon) extraWidgetIdRes = ResourceUtils.getResourceIdByAttr(context, attrs, R.attr.extraWidgetId) @@ -69,9 +74,10 @@ class PermissionSelectorWithWidgetPreference : SelectorWithWidgetPreference { override fun onBindViewHolder(holder: PreferenceViewHolder) { super.onBindViewHolder(holder) - val extraWidget = holder.findViewById( - com.android.settingslib.widget.preference.selector.R.id.selector_extra_widget - ) as? ImageView + val extraWidget = + holder.findViewById( + com.android.settingslib.widget.preference.selector.R.id.selector_extra_widget + ) as? ImageView val checkbox = holder.findViewById(android.R.id.checkbox) if (extraWidgetIconRes != 0) { extraWidget?.setImageResource(extraWidgetIconRes) diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/PermissionTwoTargetPreference.kt b/PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/v36/PermissionTwoTargetPreference.kt index 13c9ee7c4..cf6585f4d 100644 --- a/PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/PermissionTwoTargetPreference.kt +++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/v36/PermissionTwoTargetPreference.kt @@ -14,13 +14,15 @@ * limitations under the License. */ -package com.android.permissioncontroller.permission.ui.handheld +package com.android.permissioncontroller.permission.ui.handheld.v36 import android.content.Context +import android.os.Build import android.util.AttributeSet import android.widget.ImageView import androidx.annotation.AttrRes import androidx.annotation.DrawableRes +import androidx.annotation.RequiresApi import androidx.annotation.StyleRes import androidx.preference.PreferenceViewHolder import com.android.permissioncontroller.R @@ -32,6 +34,7 @@ import com.android.settingslib.widget.TwoTargetPreference * - Propagates the supplied `app:extraWidgetIcon` drawable to the second target * - Allows defining a click listener on the second target (the icon on the right) */ +@RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM) class PermissionTwoTargetPreference : TwoTargetPreference { constructor(context: Context) : super(context) { init(context, null) @@ -44,7 +47,7 @@ class PermissionTwoTargetPreference : TwoTargetPreference { constructor( context: Context, attrs: AttributeSet?, - @AttrRes defStyleAttr: Int + @AttrRes defStyleAttr: Int, ) : super(context, attrs, defStyleAttr) { init(context, attrs) } @@ -53,12 +56,13 @@ class PermissionTwoTargetPreference : TwoTargetPreference { context: Context, attrs: AttributeSet?, @AttrRes defStyleAttr: Int, - @StyleRes defStyleRes: Int + @StyleRes defStyleRes: Int, ) : super(context, attrs, defStyleAttr, defStyleRes) { init(context, attrs) } private fun init(context: Context, attrs: AttributeSet?) { + layoutResource = R.layout.permission_preference_two_target extraWidgetIconRes = ResourceUtils.getResourceIdByAttr(context, attrs, R.attr.extraWidgetIcon) } diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/legacy/PermissionUsageDetailsViewModelLegacy.kt b/PermissionController/src/com/android/permissioncontroller/permission/ui/legacy/PermissionUsageDetailsViewModelLegacy.kt deleted file mode 100644 index 1369bfdaa..000000000 --- a/PermissionController/src/com/android/permissioncontroller/permission/ui/legacy/PermissionUsageDetailsViewModelLegacy.kt +++ /dev/null @@ -1,596 +0,0 @@ -/* - * Copyright (C) 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -@file:Suppress("DEPRECATION") - -package com.android.permissioncontroller.permission.ui.legacy - -import android.Manifest -import android.app.AppOpsManager -import android.app.Application -import android.app.LoaderManager -import android.app.role.RoleManager -import android.content.Context -import android.content.pm.ApplicationInfo -import android.content.res.Resources -import android.graphics.drawable.Drawable -import android.os.Build -import android.os.UserHandle -import androidx.annotation.RequiresApi -import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider -import com.android.permissioncontroller.PermissionControllerApplication -import com.android.permissioncontroller.R -import com.android.permissioncontroller.permission.model.AppPermissionGroup -import com.android.permissioncontroller.permission.model.legacy.PermissionApps.PermissionApp -import com.android.permissioncontroller.permission.model.v31.AppPermissionUsage -import com.android.permissioncontroller.permission.model.v31.AppPermissionUsage.TimelineUsage -import com.android.permissioncontroller.permission.model.v31.PermissionUsages -import com.android.permissioncontroller.permission.ui.handheld.v31.getDurationUsedStr -import com.android.permissioncontroller.permission.ui.handheld.v31.shouldShowSubattributionInPermissionsDashboard -import com.android.permissioncontroller.permission.utils.KotlinUtils.getPackageLabel -import com.android.permissioncontroller.permission.utils.PermissionMapping -import com.android.permissioncontroller.permission.utils.StringUtils -import com.android.permissioncontroller.permission.utils.Utils -import com.android.permissioncontroller.permission.utils.v31.SubattributionUtils -import java.time.Instant -import java.util.concurrent.TimeUnit -import java.util.concurrent.TimeUnit.DAYS -import kotlin.math.max - -/** View model for the permission details fragment. */ -@RequiresApi(Build.VERSION_CODES.S) -class PermissionUsageDetailsViewModelLegacy( - val application: Application, - val roleManager: RoleManager, - private val permissionGroup: String, - val sessionId: Long -) : ViewModel() { - - companion object { - private const val ONE_HOUR_MS = 3_600_000 - private const val ONE_MINUTE_MS = 60_000 - private const val CLUSTER_SPACING_MINUTES: Long = 1L - private val TIME_7_DAYS_DURATION: Long = DAYS.toMillis(7) - private val TIME_24_HOURS_DURATION: Long = DAYS.toMillis(1) - } - - private val mTimeFilterItemMs = mutableListOf<TimeFilterItemMs>() - - init { - initializeTimeFilterItems(application) - } - - /** Loads permission usages using [PermissionUsages]. Response is returned to the [callback]. */ - fun loadPermissionUsages( - loaderManager: LoaderManager, - permissionUsages: PermissionUsages, - callback: PermissionUsages.PermissionsUsagesChangeCallback, - filterTimesIndex: Int - ) { - val timeFilterItemMs: TimeFilterItemMs = mTimeFilterItemMs[filterTimesIndex] - val filterTimeBeginMillis = max(System.currentTimeMillis() - timeFilterItemMs.timeMs, 0) - permissionUsages.load( - /* filterPackageName= */ null, - /* filterPermissionGroups= */ null, - filterTimeBeginMillis, - Long.MAX_VALUE, - PermissionUsages.USAGE_FLAG_LAST or PermissionUsages.USAGE_FLAG_HISTORICAL, - loaderManager, - /* getUiInfo= */ false, - /* getNonPlatformPermissions= */ false, - /* callback= */ callback, - /* sync= */ false - ) - } - - /** - * Create a [PermissionUsageDetailsUiData] based on the provided data. - * - * @param appPermissionUsages data about app permission usages - * @param showSystem whether system apps should be shown - * @param show7Days whether the last 7 days of history should be shown - */ - fun buildPermissionUsageDetailsUiData( - appPermissionUsages: List<AppPermissionUsage>, - showSystem: Boolean, - show7Days: Boolean - ): PermissionUsageDetailsUiData { - val showPermissionUsagesDuration = - if (show7Days) { - TIME_7_DAYS_DURATION - } else { - TIME_24_HOURS_DURATION - } - val startTime = - (System.currentTimeMillis() - showPermissionUsagesDuration).coerceAtLeast( - Instant.EPOCH.toEpochMilli() - ) - val appPermissionTimelineUsages: List<AppPermissionTimelineUsage> = - extractAppPermissionTimelineUsagesForGroup(appPermissionUsages, permissionGroup) - val shouldDisplayShowSystemToggle = - shouldDisplayShowSystemToggle(appPermissionTimelineUsages) - val permissionApps: List<PermissionApp> = - getPermissionAppsWithRecentDiscreteUsage( - appPermissionTimelineUsages, - showSystem, - startTime - ) - val appPermissionUsageEntries = - buildDiscreteAccessClusterData(appPermissionTimelineUsages, showSystem, startTime) - - return PermissionUsageDetailsUiData( - permissionApps, - shouldDisplayShowSystemToggle, - appPermissionUsageEntries - ) - } - - private fun getHistoryPreferenceData( - discreteAccessClusterData: DiscreteAccessClusterData, - ): HistoryPreferenceData { - val context = application - val accessTimeList = - discreteAccessClusterData.discreteAccessDataList.map { p -> p.accessTimeMs } - val durationSummaryLabel = - getDurationSummary(discreteAccessClusterData, accessTimeList, context) - val proxyLabel = getProxyPackageLabel(discreteAccessClusterData) - val subattributionLabel = getSubattributionLabel(discreteAccessClusterData) - val showingSubattribution = subattributionLabel != null && subattributionLabel.isNotEmpty() - val summary = - buildUsageSummary(durationSummaryLabel, proxyLabel, subattributionLabel, context) - - return HistoryPreferenceData( - UserHandle.getUserHandleForUid( - discreteAccessClusterData.appPermissionTimelineUsage.permissionApp.uid - ), - discreteAccessClusterData.appPermissionTimelineUsage.permissionApp.packageName, - discreteAccessClusterData.appPermissionTimelineUsage.permissionApp.icon, - discreteAccessClusterData.appPermissionTimelineUsage.permissionApp.label, - permissionGroup, - discreteAccessClusterData.discreteAccessDataList.last().accessTimeMs, - discreteAccessClusterData.discreteAccessDataList.first().accessTimeMs, - summary, - showingSubattribution, - discreteAccessClusterData.appPermissionTimelineUsage.attributionTags, - sessionId - ) - } - - /** - * Returns whether the provided [AppPermissionUsage] instances contains the provided platform - * permission group. - */ - fun containsPlatformAppPermissionGroup( - appPermissionUsages: List<AppPermissionUsage>, - groupName: String, - ) = appPermissionUsages.extractAllPlatformAppPermissionGroups().any { it.name == groupName } - - /** Extracts a list of [AppPermissionTimelineUsage] for a particular permission group. */ - private fun extractAppPermissionTimelineUsagesForGroup( - appPermissionUsages: List<AppPermissionUsage>, - group: String - ): List<AppPermissionTimelineUsage> { - val exemptedPackages = Utils.getExemptedPackages(roleManager) - return appPermissionUsages - .filter { !exemptedPackages.contains(it.packageName) } - .map { appPermissionUsage -> - getAppPermissionTimelineUsages( - appPermissionUsage.app, - appPermissionUsage.groupUsages.firstOrNull { it.group.name == group } - ) - } - .flatten() - } - - /** Returns whether the show/hide system toggle should be displayed in the UI. */ - private fun shouldDisplayShowSystemToggle( - appPermissionTimelineUsages: List<AppPermissionTimelineUsage>, - ): Boolean = - appPermissionTimelineUsages - .map { it.timelineUsage } - .filter { it.hasDiscreteData() } - .any { it.group.isSystem() } - - /** - * Returns a list of [PermissionApp] instances which had recent discrete permission usage - * (recent here refers to usages occurring after the provided start time). - */ - private fun getPermissionAppsWithRecentDiscreteUsage( - appPermissionTimelineUsageList: List<AppPermissionTimelineUsage>, - showSystem: Boolean, - startTime: Long, - ): List<PermissionApp> = - appPermissionTimelineUsageList - .filter { it.timelineUsage.hasDiscreteData() } - .filter { showSystem || !it.timelineUsage.group.isSystem() } - .filter { it.timelineUsage.allDiscreteAccessTime.any { it.first >= startTime } } - .map { it.permissionApp } - - /** - * Builds a list of [DiscreteAccessClusterData] from the provided list of - * [AppPermissionTimelineUsage]. - */ - private fun buildDiscreteAccessClusterData( - appPermissionTimelineUsageList: List<AppPermissionTimelineUsage>, - showSystem: Boolean, - startTime: Long, - ): List<DiscreteAccessClusterData> = - appPermissionTimelineUsageList - .map { appPermissionTimelineUsages -> - val accessDataList = - extractRecentDiscreteAccessData( - appPermissionTimelineUsages.timelineUsage, - showSystem, - startTime - ) - - if (accessDataList.size <= 1) { - return@map accessDataList.map { - DiscreteAccessClusterData(appPermissionTimelineUsages, listOf(it)) - } - } - - clusterDiscreteAccessData(appPermissionTimelineUsages, accessDataList) - } - .flatten() - .sortedWith( - compareBy( - { -it.discreteAccessDataList.first().accessTimeMs }, - { it.appPermissionTimelineUsage.permissionApp.label } - ) - ) - .toList() - - /** - * Clusters a list of [DiscreteAccessData] into a list of [DiscreteAccessClusterData] instances. - * - * [DiscreteAccessData] which have accesses sufficiently close together in time will be places - * in the same cluster. - */ - private fun clusterDiscreteAccessData( - appPermissionTimelineUsage: AppPermissionTimelineUsage, - discreteAccessDataList: List<DiscreteAccessData> - ): List<DiscreteAccessClusterData> { - val clusterDataList = mutableListOf<DiscreteAccessClusterData>() - val currentDiscreteAccessDataList: MutableList<DiscreteAccessData> = mutableListOf() - for (discreteAccessData in discreteAccessDataList) { - if (currentDiscreteAccessDataList.isEmpty()) { - currentDiscreteAccessDataList.add(discreteAccessData) - } else if ( - !canAccessBeAddedToCluster(discreteAccessData, currentDiscreteAccessDataList) - ) { - clusterDataList.add( - DiscreteAccessClusterData( - appPermissionTimelineUsage, - currentDiscreteAccessDataList.toMutableList() - ) - ) - currentDiscreteAccessDataList.clear() - currentDiscreteAccessDataList.add(discreteAccessData) - } else { - currentDiscreteAccessDataList.add(discreteAccessData) - } - } - if (currentDiscreteAccessDataList.isNotEmpty()) { - clusterDataList.add( - DiscreteAccessClusterData(appPermissionTimelineUsage, currentDiscreteAccessDataList) - ) - } - return clusterDataList - } - - /** - * Extract recent [DiscreteAccessData] from a list of [TimelineUsage] instances, and return them - * ordered descending by access time (recent here refers to accesses occurring after the - * provided start time). - */ - private fun extractRecentDiscreteAccessData( - timelineUsages: TimelineUsage, - showSystem: Boolean, - startTime: Long - ): List<DiscreteAccessData> { - return if ( - timelineUsages.hasDiscreteData() && (showSystem || !timelineUsages.group.isSystem()) - ) { - getRecentDiscreteAccessData(timelineUsages, startTime) - .sortedWith(compareBy { -it.accessTimeMs }) - .toList() - } else { - listOf() - } - } - - /** - * Extract recent [DiscreteAccessData] from a [TimelineUsage]. (recent here refers to accesses - * occurring after the provided start time). - */ - private fun getRecentDiscreteAccessData( - timelineUsage: TimelineUsage, - startTime: Long - ): List<DiscreteAccessData> { - return timelineUsage.allDiscreteAccessTime - .filter { it.first >= startTime } - .map { - DiscreteAccessData( - it.first, - it.second, - it.third, - ) - } - } - - /** - * Returns whether the provided [DiscreteAccessData] occurred close enough to those in the - * clustered list that it can be added to the cluster - */ - private fun canAccessBeAddedToCluster( - accessData: DiscreteAccessData, - clusteredAccessDataList: List<DiscreteAccessData> - ): Boolean = - accessData.accessTimeMs / ONE_HOUR_MS == - clusteredAccessDataList.first().accessTimeMs / ONE_HOUR_MS && - clusteredAccessDataList.last().accessTimeMs / ONE_MINUTE_MS - - accessData.accessTimeMs / ONE_MINUTE_MS > CLUSTER_SPACING_MINUTES - - /** - * Returns whether the provided [AppPermissionGroup] is considered a system group. - * - * For the purpose of Permissions Hub UI, non user-sensitive [AppPermissionGroup]s are - * considered "system" and should be hidden from the main page unless requested by the user - * through the "show/hide system" toggle. - */ - private fun AppPermissionGroup.isSystem() = !Utils.isGroupOrBgGroupUserSensitive(this) - - /** Returns whether app subattribution should be shown. */ - private fun shouldShowSubattributionForApp(appInfo: ApplicationInfo): Boolean { - return shouldShowSubattributionInPermissionsDashboard() && - SubattributionUtils.isSubattributionSupported(application, appInfo) - } - - /** Returns a summary of the duration the permission was accessed for. */ - private fun getDurationSummary( - usage: DiscreteAccessClusterData, - accessTimeList: List<Long>, - context: Context - ): String? { - if (accessTimeList.isEmpty()) { - return null - } - - var durationMs: Long - - // Since Location accesses are atomic, we manually calculate the access duration - // by comparing the first and last access within the cluster. - if (permissionGroup == Manifest.permission_group.LOCATION) { - durationMs = accessTimeList[0] - accessTimeList[accessTimeList.size - 1] - } else { - durationMs = - usage.discreteAccessDataList.map { it.accessDurationMs }.filter { it > 0 }.sum() - } - // Only show the duration summary if it is at least (CLUSTER_SPACING_MINUTES + 1) minutes. - // Displaying a time that is shorter than the cluster granularity - // (CLUSTER_SPACING_MINUTES) will not convey useful information. - if (durationMs >= TimeUnit.MINUTES.toMillis(CLUSTER_SPACING_MINUTES + 1)) { - return getDurationUsedStr(context, durationMs) - } - - return null - } - - /** Returns the proxied package label if the permission access was proxied. */ - private fun getProxyPackageLabel(usage: DiscreteAccessClusterData): String? = - usage.discreteAccessDataList - .firstOrNull { it.proxy?.packageName != null } - ?.let { - getPackageLabel( - PermissionControllerApplication.get(), - it.proxy!!.packageName!!, - UserHandle.getUserHandleForUid(it.proxy.uid) - ) - } - - /** Returns the attribution label for the permission access, if any. */ - private fun getSubattributionLabel(usage: DiscreteAccessClusterData): String? = - if (usage.appPermissionTimelineUsage.label == Resources.ID_NULL) null - else - usage.appPermissionTimelineUsage.permissionApp.attributionLabels?.let { - it[usage.appPermissionTimelineUsage.label] - } - - /** Builds a summary of the permission access. */ - private fun buildUsageSummary( - subattributionLabel: String?, - proxyPackageLabel: String?, - durationSummary: String?, - context: Context - ): String? { - val subTextStrings: MutableList<String?> = mutableListOf() - - subattributionLabel?.let { subTextStrings.add(subattributionLabel) } - proxyPackageLabel?.let { subTextStrings.add(it) } - durationSummary?.let { subTextStrings.add(it) } - return when (subTextStrings.size) { - 3 -> - context.getString( - R.string.history_preference_subtext_3, - subTextStrings[0], - subTextStrings[1], - subTextStrings[2] - ) - 2 -> - context.getString( - R.string.history_preference_subtext_2, - subTextStrings[0], - subTextStrings[1] - ) - 1 -> subTextStrings[0] - else -> null - } - } - - /** - * Builds a list of [AppPermissionTimelineUsage] from the provided - * [AppPermissionUsage.GroupUsage]. - */ - private fun getAppPermissionTimelineUsages( - app: PermissionApp, - groupUsage: AppPermissionUsage.GroupUsage? - ): List<AppPermissionTimelineUsage> { - if (groupUsage == null) { - return listOf() - } - - if (shouldShowSubattributionForApp(app.appInfo)) { - return groupUsage.attributionLabelledGroupUsages.map { - AppPermissionTimelineUsage(permissionGroup, app, it, it.label) - } - } - - return listOf( - AppPermissionTimelineUsage(permissionGroup, app, groupUsage, Resources.ID_NULL) - ) - } - - /** Extracts to a set all the permission groups declared by the platform. */ - private fun List<AppPermissionUsage>.extractAllPlatformAppPermissionGroups(): - Set<AppPermissionGroup> = - this.flatMap { it.groupUsages } - .map { it.group } - .filter { PermissionMapping.isPlatformPermissionGroup(it.name) } - .toSet() - - /** Initialize all relevant [TimeFilterItemMs] values. */ - private fun initializeTimeFilterItems(context: Context) { - mTimeFilterItemMs.add( - TimeFilterItemMs(Long.MAX_VALUE, context.getString(R.string.permission_usage_any_time)) - ) - mTimeFilterItemMs.add( - TimeFilterItemMs( - DAYS.toMillis(7), - StringUtils.getIcuPluralsString(context, R.string.permission_usage_last_n_days, 7) - ) - ) - mTimeFilterItemMs.add( - TimeFilterItemMs( - DAYS.toMillis(1), - StringUtils.getIcuPluralsString(context, R.string.permission_usage_last_n_days, 1) - ) - ) - - // TODO: theianchen add code for filtering by time here. - } - - /** Data used to create a preference for an app's permission usage. */ - data class HistoryPreferenceData( - val userHandle: UserHandle, - val pkgName: String, - val appIcon: Drawable?, - val preferenceTitle: String, - val permissionGroup: String, - val accessStartTime: Long, - val accessEndTime: Long, - val summaryText: CharSequence?, - val showingAttribution: Boolean, - val attributionTags: ArrayList<String>, - val sessionId: Long - ) - - /** - * A class representing a given time, e.g., "in the last hour". - * - * @param timeMs the time represented by this object in milliseconds. - * @param label the label to describe the timeframe - */ - data class TimeFilterItemMs(val timeMs: Long, val label: String) - - /** - * Class containing all the information needed by the permission usage details fragments to - * render UI. - */ - inner class PermissionUsageDetailsUiData( - /** List of [PermissionApp] instances */ - // Note that these are used only to cache app data for the permission usage details - // fragment, and have no bearing on the UI on the main permission usage page. - val permissionApps: List<PermissionApp>, - /** Whether to show the "show/hide system" toggle. */ - val shouldDisplayShowSystemToggle: Boolean, - /** [DiscreteAccessClusterData] instances ordered for display in UI */ - private val discreteAccessClusterDataList: List<DiscreteAccessClusterData>, - ) { - // Note that the HistoryPreferenceData are not initialized within the - // PermissionUsageDetailsUiData instance as the need to be constructed only after the - // calling fragment loads the necessary PermissionApp instances. We will attempt to remove - // this dependency in b/240978905. - /** Builds a list of [HistoryPreferenceData] to be displayed in the UI. */ - fun getHistoryPreferenceDataList(): List<HistoryPreferenceData> { - return discreteAccessClusterDataList.map { - this@PermissionUsageDetailsViewModelLegacy.getHistoryPreferenceData(it) - } - } - } - - /** - * Data class representing a cluster of accesses, to be represented as a single entry in the UI. - */ - data class DiscreteAccessClusterData( - val appPermissionTimelineUsage: AppPermissionTimelineUsage, - val discreteAccessDataList: List<DiscreteAccessData> - ) - - /** Data class representing a discrete permission access. */ - data class DiscreteAccessData( - val accessTimeMs: Long, - val accessDurationMs: Long, - val proxy: AppOpsManager.OpEventProxyInfo? - ) - - /** Data class representing an app's permissions usages for a particular permission group. */ - data class AppPermissionTimelineUsage( - /** Permission group whose usage is being tracked. */ - val permissionGroup: String, - // we need a PermissionApp because the loader takes the PermissionApp - // object and loads the icon and label information asynchronously - /** App whose permissions are being tracked. */ - val permissionApp: PermissionApp, - /** Timeline usage for the given app and permission. */ - val timelineUsage: TimelineUsage, - val label: Int - ) { - val attributionTags: java.util.ArrayList<String> - get() = ArrayList(timelineUsage.attributionTags) - } -} - -/** Factory for an [PermissionUsageDetailsViewModelLegacy] */ -@RequiresApi(Build.VERSION_CODES.S) -class PermissionUsageDetailsViewModelFactoryLegacy( - private val application: Application, - private val roleManager: RoleManager, - private val filterGroup: String, - private val sessionId: Long -) : ViewModelProvider.Factory { - - override fun <T : ViewModel> create(modelClass: Class<T>): T { - @Suppress("UNCHECKED_CAST") - return PermissionUsageDetailsViewModelLegacy( - application, - roleManager, - filterGroup, - sessionId - ) - as T - } -} diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/model/GrantPermissionsViewModel.kt b/PermissionController/src/com/android/permissioncontroller/permission/ui/model/GrantPermissionsViewModel.kt index 0a01929e6..1e5b96c2e 100644 --- a/PermissionController/src/com/android/permissioncontroller/permission/ui/model/GrantPermissionsViewModel.kt +++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/model/GrantPermissionsViewModel.kt @@ -29,6 +29,7 @@ import android.annotation.SuppressLint import android.app.Activity import android.app.Application import android.app.admin.DevicePolicyManager +import android.content.Context import android.content.Intent import android.content.pm.PackageManager import android.content.pm.PackageManager.FLAG_PERMISSION_POLICY_FIXED @@ -41,6 +42,8 @@ import android.os.Build import android.os.Bundle import android.os.Process import android.permission.PermissionManager +import android.permission.flags.Flags +import android.util.ArrayMap import android.util.Log import androidx.core.util.Consumer import androidx.lifecycle.ViewModel @@ -116,6 +119,7 @@ import com.android.permissioncontroller.permission.utils.SafetyNetLogger import com.android.permissioncontroller.permission.utils.Utils import com.android.permissioncontroller.permission.utils.v31.AdminRestrictedPermissionsUtils import com.android.permissioncontroller.permission.utils.v34.SafetyLabelUtils +import com.android.permissioncontroller.permission.utils.v35.MultiDeviceUtils.isPermissionDeviceAware /** * ViewModel for the GrantPermissionsActivity. Tracks all permission groups that are affected by the @@ -153,6 +157,21 @@ class GrantPermissionsViewModel( } else { null } + private val permissionGroupToDeviceIdMap: Map<String, Int> = + if (SdkLevel.isAtLeastV() && Flags.allowHostPermissionDialogsOnVirtualDevices()) { + requestedPermissions + .filter({ PermissionMapping.getGroupOfPlatformPermission(it) != null }) + .associateBy({ PermissionMapping.getGroupOfPlatformPermission(it)!! }, { + if (isPermissionDeviceAware( + app.applicationContext, + deviceId, + it + ) + ) deviceId else Context.DEVICE_ID_DEFAULT + }) + } else { + ArrayMap() + } private val dpm = app.getSystemService(DevicePolicyManager::class.java)!! private val permissionPolicy = dpm.getPermissionPolicy(null) private val groupStates = mutableMapOf<String, GroupState>() @@ -314,7 +333,8 @@ class GrantPermissionsViewModel( } val getLiveDataFun = { groupName: String -> - LightAppPermGroupLiveData[packageName, groupName, user, deviceId] + LightAppPermGroupLiveData[packageName, groupName, user, + permissionGroupToDeviceIdMap.get(groupName) ?: deviceId] } setSourcesToDifference(requestedGroups.keys, appPermGroupLiveDatas, getLiveDataFun) } @@ -398,7 +418,8 @@ class GrantPermissionsViewModel( safetyLabel, groupState.group.permGroupName ), - deviceId + permissionGroupToDeviceIdMap.get(groupState.group.permGroupName) + ?: deviceId ) ) } diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/model/ManageCustomPermissionsViewModel.kt b/PermissionController/src/com/android/permissioncontroller/permission/ui/model/ManageCustomPermissionsViewModel.kt index bd80a88cd..429799157 100644 --- a/PermissionController/src/com/android/permissioncontroller/permission/ui/model/ManageCustomPermissionsViewModel.kt +++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/model/ManageCustomPermissionsViewModel.kt @@ -16,17 +16,23 @@ package com.android.permissioncontroller.permission.ui.model +import android.Manifest import android.app.Application +import android.content.Intent +import android.health.connect.HealthPermissions.HEALTH_PERMISSION_GROUP import android.os.Bundle import androidx.fragment.app.Fragment import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.navigation.fragment.findNavController +import com.android.permission.flags.Flags import com.android.permissioncontroller.R import com.android.permissioncontroller.permission.data.PermGroupsPackagesLiveData import com.android.permissioncontroller.permission.data.PermGroupsPackagesUiInfoLiveData import com.android.permissioncontroller.permission.data.SmartUpdateMediatorLiveData +import com.android.permissioncontroller.permission.utils.Utils import com.android.permissioncontroller.permission.utils.navigateSafe /** @@ -38,6 +44,12 @@ import com.android.permissioncontroller.permission.utils.navigateSafe class ManageCustomPermissionsViewModel(private val app: Application) : AndroidViewModel(app) { val uiDataLiveData = PermGroupsPackagesUiInfoLiveData(app, UsedCustomPermGroupNamesLiveData()) + val additionaPermGroupsUiInfo = + PermGroupsPackagesUiInfoLiveData( + app, + if (Flags.declutteredPermissionManagerEnabled()) AdditionalPermGroupNamesLiveData(app) + else MutableLiveData<List<String>>(), + ) /** * Navigate to a Permission Apps fragment @@ -46,6 +58,15 @@ class ManageCustomPermissionsViewModel(private val app: Application) : AndroidVi * @param args The args to pass to the new fragment */ fun showPermissionApps(fragment: Fragment, args: Bundle) { + val groupName = args.getString(Intent.EXTRA_PERMISSION_GROUP_NAME) + if (groupName == Manifest.permission_group.NOTIFICATIONS) { + Utils.navigateToNotificationSettings(fragment.context!!) + return + } + if (Utils.isHealthPermissionUiEnabled() && groupName == HEALTH_PERMISSION_GROUP) { + Utils.navigateToHealthConnectSettings(fragment.context!!) + return + } fragment.findNavController().navigateSafe(R.id.manage_to_perm_apps, args) } } @@ -58,7 +79,8 @@ class ManageCustomPermissionsViewModel(private val app: Application) : AndroidVi class ManageCustomPermissionsViewModelFactory(private val app: Application) : ViewModelProvider.Factory { override fun <T : ViewModel> create(modelClass: Class<T>): T { - @Suppress("UNCHECKED_CAST") return ManageCustomPermissionsViewModel(app) as T + @Suppress("UNCHECKED_CAST") + return ManageCustomPermissionsViewModel(app) as T } } @@ -77,3 +99,32 @@ class UsedCustomPermGroupNamesLiveData : SmartUpdateMediatorLiveData<List<String /* No op override */ } } + +/** + * A LiveData that is the union of LiveData UsedCustomPermGroupNamesLiveData and + * UnusedStandardPermGroupNamesLiveData. + * + * @param app The current application of the fragment + */ +class AdditionalPermGroupNamesLiveData(private val app: Application) : + SmartUpdateMediatorLiveData<List<String>>() { + + val usedCustomGroupNames = UsedCustomPermGroupNamesLiveData() + val unusedStandardGroupNames = UnusedStandardPermGroupNamesLiveData(app) + + init { + addSource(usedCustomGroupNames) { update() } + addSource(unusedStandardGroupNames) { update() } + } + + private fun combineGroupNames( + groupNames1: List<String>?, + groupNames2: List<String>?, + ): List<String> { + return (groupNames1 ?: emptyList()) + (groupNames2 ?: emptyList()) + } + + override fun onUpdate() { + value = combineGroupNames(usedCustomGroupNames.value, unusedStandardGroupNames.value) + } +} diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/model/ManageStandardPermissionsViewModel.kt b/PermissionController/src/com/android/permissioncontroller/permission/ui/model/ManageStandardPermissionsViewModel.kt index aeab0aa89..6dabe8ab7 100644 --- a/PermissionController/src/com/android/permissioncontroller/permission/ui/model/ManageStandardPermissionsViewModel.kt +++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/model/ManageStandardPermissionsViewModel.kt @@ -23,8 +23,11 @@ import android.health.connect.HealthPermissions.HEALTH_PERMISSION_GROUP import android.os.Bundle import androidx.fragment.app.Fragment import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.MediatorLiveData +import androidx.lifecycle.MutableLiveData import androidx.lifecycle.map import androidx.navigation.fragment.findNavController +import com.android.permission.flags.Flags import com.android.permissioncontroller.R import com.android.permissioncontroller.permission.data.PermGroupsPackagesLiveData import com.android.permissioncontroller.permission.data.PermGroupsPackagesUiInfoLiveData @@ -45,7 +48,21 @@ import com.android.permissioncontroller.permission.utils.navigateSafe class ManageStandardPermissionsViewModel(private val app: Application) : AndroidViewModel(app) { val uiDataLiveData = PermGroupsPackagesUiInfoLiveData(app, StandardPermGroupNamesLiveData) + val usedStandardPermGroupsUiInfo = + PermGroupsPackagesUiInfoLiveData( + app, + if (Flags.declutteredPermissionManagerEnabled()) UsedStandardPermGroupNamesLiveData(app) + else MutableLiveData<List<String>>(), + ) val numCustomPermGroups = NumCustomPermGroupsWithPackagesLiveData() + val numUnusedStandardPermGroups = + MediatorLiveData<Int>().apply { + if (Flags.declutteredPermissionManagerEnabled()) { + addSource(UnusedStandardPermGroupNamesLiveData(app)) { groupNames -> + value = groupNames.size + } + } + } val numAutoRevoked = unusedAutoRevokePackagesLiveData.map { it?.size ?: 0 } /** @@ -98,3 +115,47 @@ class NumCustomPermGroupsWithPackagesLiveData() : SmartUpdateMediatorLiveData<In value = customPermGroupPackages.value?.size ?: 0 } } + +/** + * A LiveData that tracks the names of the platform-defined permission groups, such that at least + * one of the permissions in the group has been requested at runtime by at least one non-system + * application. + * + * @param app The current application of the fragment + */ +class UsedStandardPermGroupNamesLiveData(private val app: Application) : + SmartUpdateMediatorLiveData<List<String>>() { + init { + addSource(PermGroupsPackagesUiInfoLiveData(app, StandardPermGroupNamesLiveData)) { + permGroups -> + if (permGroups.values.any { it != null }) { + value = + permGroups.filterValues { it != null && it.nonSystemTotal > 0 }.keys.toList() + } + } + } + + override fun onUpdate() { + /* No op override */ + } +} + +/** + * A LiveData that tracks the names of the platform-defined permission groups, such that none of the + * the permissions in the group has been requested at runtime by any non-system application. + * + * @param app The current application of the fragment + */ +class UnusedStandardPermGroupNamesLiveData(private val app: Application) : + SmartUpdateMediatorLiveData<List<String>>() { + init { + addSource(PermGroupsPackagesUiInfoLiveData(app, StandardPermGroupNamesLiveData)) { + permGroups -> + value = permGroups.filterValues { it != null && it.nonSystemTotal == 0 }.keys.toList() + } + } + + override fun onUpdate() { + /* No op override */ + } +} diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/LocationProviderDialogScreen.kt b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/LocationProviderDialogScreen.kt index 6af62e01f..510d19706 100644 --- a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/LocationProviderDialogScreen.kt +++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/LocationProviderDialogScreen.kt @@ -27,8 +27,9 @@ import androidx.wear.compose.material.ChipDefaults import androidx.wear.compose.material.MaterialTheme import androidx.wear.compose.material.SwipeToDismissBox import com.android.permissioncontroller.permission.ui.wear.elements.Chip -import com.android.permissioncontroller.permission.ui.wear.elements.Scaffold +import com.android.permissioncontroller.permission.ui.wear.elements.material3.WearPermissionScaffold import com.android.permissioncontroller.permission.ui.wear.model.LocationProviderInterceptDialogArgs +import com.android.permissioncontroller.permission.ui.wear.theme.WearPermissionMaterialUIVersion @Composable fun LocationProviderDialogScreen(args: LocationProviderInterceptDialogArgs?) { @@ -41,7 +42,8 @@ fun LocationProviderDialogScreen(args: LocationProviderInterceptDialogArgs?) { } } SwipeToDismissBox(state = state) { isBackground -> - Scaffold( + WearPermissionScaffold( + materialUIVersion = WearPermissionMaterialUIVersion.MATERIAL2_5, showTimeText = false, image = iconId, title = stringResource(titleId), @@ -54,7 +56,7 @@ fun LocationProviderDialogScreen(args: LocationProviderInterceptDialogArgs?) { onClick = onLocationSettingsClick, modifier = Modifier.fillMaxWidth(), textColor = MaterialTheme.colors.surface, - colors = ChipDefaults.primaryChipColors() + colors = ChipDefaults.primaryChipColors(), ) } item { @@ -64,7 +66,7 @@ fun LocationProviderDialogScreen(args: LocationProviderInterceptDialogArgs?) { modifier = Modifier.fillMaxWidth(), ) } - } + }, ) } } diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/WearAppPermissionGroupsHelper.kt b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/WearAppPermissionGroupsHelper.kt index 078eefe3b..2933d6fda 100644 --- a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/WearAppPermissionGroupsHelper.kt +++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/WearAppPermissionGroupsHelper.kt @@ -19,6 +19,7 @@ package com.android.permissioncontroller.permission.ui.wear import android.content.Context import android.content.pm.PackageManager import android.content.pm.PermissionInfo +import android.health.connect.HealthPermissions.HEALTH_PERMISSION_GROUP import android.os.Build import android.os.UserHandle import android.util.ArraySet @@ -319,6 +320,10 @@ class WearAppPermissionGroupsHelper( ) { // Redirect to location controller extra package settings. LocationUtils.startLocationControllerExtraPackageSettings(context, user) + } else if (permGroupName.equals(HEALTH_PERMISSION_GROUP) + && android.permission.flags.Flags.replaceBodySensorPermissionEnabled()) { + // Redirect to Health&Fitness UI + Utils.navigateToAppHealthConnectSettings(fragment.requireContext(), packageName, user) } else { val args = WearAppPermissionFragment.createArgs( diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/WearGrantPermissionsScreen.kt b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/WearGrantPermissionsScreen.kt index 950353f52..1498b91b6 100644 --- a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/WearGrantPermissionsScreen.kt +++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/WearGrantPermissionsScreen.kt @@ -37,17 +37,20 @@ import com.android.permissioncontroller.permission.ui.GrantPermissionsActivity.N import com.android.permissioncontroller.permission.ui.GrantPermissionsActivity.NO_UPGRADE_OT_AND_DONT_ASK_AGAIN_BUTTON import com.android.permissioncontroller.permission.ui.GrantPermissionsActivity.NO_UPGRADE_OT_BUTTON import com.android.permissioncontroller.permission.ui.wear.GrantPermissionsWearViewHandler.BUTTON_RES_ID_TO_NUM -import com.android.permissioncontroller.permission.ui.wear.elements.Chip import com.android.permissioncontroller.permission.ui.wear.elements.ScrollableScreen -import com.android.permissioncontroller.permission.ui.wear.elements.ToggleChip import com.android.permissioncontroller.permission.ui.wear.elements.ToggleChipToggleControl +import com.android.permissioncontroller.permission.ui.wear.elements.material3.WearPermissionButton +import com.android.permissioncontroller.permission.ui.wear.elements.material3.WearPermissionToggleControl import com.android.permissioncontroller.permission.ui.wear.model.WearGrantPermissionsViewModel +import com.android.permissioncontroller.permission.ui.wear.theme.ResourceHelper +import com.android.permissioncontroller.permission.ui.wear.theme.WearPermissionMaterialUIVersion.MATERIAL2_5 +import com.android.permissioncontroller.permission.ui.wear.theme.WearPermissionMaterialUIVersion.MATERIAL3 @Composable fun WearGrantPermissionsScreen( viewModel: WearGrantPermissionsViewModel, onButtonClicked: (Int) -> Unit, - onLocationSwitchChanged: (Boolean) -> Unit + onLocationSwitchChanged: (Boolean) -> Unit, ) { val groupMessage = viewModel.groupMessageLiveData.observeAsState("") val icon = viewModel.iconLiveData.observeAsState(null) @@ -55,8 +58,15 @@ fun WearGrantPermissionsScreen( val locationVisibilities = viewModel.locationVisibilitiesLiveData.observeAsState(emptyList()) val preciseLocationChecked = viewModel.preciseLocationCheckedLiveData.observeAsState(false) val buttonVisibilities = viewModel.buttonVisibilitiesLiveData.observeAsState(emptyList()) + val materialUIVersion = + if (ResourceHelper.material3Enabled) { + MATERIAL3 + } else { + MATERIAL2_5 + } ScrollableScreen( + materialUIVersion = materialUIVersion, showTimeText = false, image = icon.value, title = groupMessage.value, @@ -69,13 +79,14 @@ fun WearGrantPermissionsScreen( locationVisibilities.value.getOrElse(DIALOG_WITH_BOTH_LOCATIONS) { false } ) { item { - ToggleChip( + WearPermissionToggleControl( checked = preciseLocationChecked.value, - onCheckedChanged = { onLocationSwitchChanged(it) }, + onCheckedChanged = onLocationSwitchChanged, label = stringResource(R.string.app_permission_location_accuracy), toggleControl = ToggleChipToggleControl.Switch, modifier = Modifier.fillMaxWidth(), - labelMaxLine = Integer.MAX_VALUE + labelMaxLines = Integer.MAX_VALUE, + materialUIVersion = materialUIVersion, ) } } @@ -87,16 +98,17 @@ fun WearGrantPermissionsScreen( } if (buttonVisibilities.value[pos]) { item { - Chip( + WearPermissionButton( label = getPrimaryText( - pos, - locationVisibilities.value, - labelsByButton(BUTTON_RES_ID_TO_NUM.valueAt(i)) + pos = pos, + locationVisibilities = locationVisibilities.value, + default = labelsByButton(BUTTON_RES_ID_TO_NUM.valueAt(i)), ), onClick = { onButtonClicked(BUTTON_RES_ID_TO_NUM.keyAt(i)) }, modifier = Modifier.fillMaxWidth(), - labelMaxLines = Integer.MAX_VALUE + labelMaxLines = Integer.MAX_VALUE, + materialUIVersion = materialUIVersion, ) } } @@ -108,7 +120,7 @@ fun setContent( composeView: ComposeView, viewModel: WearGrantPermissionsViewModel, onButtonClicked: (Int) -> Unit, - onLocationSwitchChanged: (Boolean) -> Unit + onLocationSwitchChanged: (Boolean) -> Unit, ) { composeView.setContent { WearGrantPermissionsScreen(viewModel, onButtonClicked, onLocationSwitchChanged) diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/WearManageStandardPermissionScreen.kt b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/WearManageStandardPermissionScreen.kt index bd1946759..9aacd65d3 100644 --- a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/WearManageStandardPermissionScreen.kt +++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/WearManageStandardPermissionScreen.kt @@ -17,6 +17,7 @@ package com.android.permissioncontroller.permission.ui.wear import android.graphics.drawable.Drawable +import android.permission.flags.Flags import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState @@ -77,7 +78,13 @@ internal fun getPermGroupChipParams( } return permissionGroups // Removing Health Connect from the list of permissions to fix b/331260850 - .filterNot { Utils.isHealthPermissionGroup(it.key) } + .let { + if (Flags.replaceBodySensorPermissionEnabled()) { + it + } else { + it.filterNot { Utils.isHealthPermissionGroup(it.key) } + } + } .mapNotNull { val uiInfo = it.value ?: return@mapNotNull null PermGroupChipParam( diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/AnnotatedText.kt b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/AnnotatedText.kt index bcdf3b661..07bb88e80 100644 --- a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/AnnotatedText.kt +++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/AnnotatedText.kt @@ -32,47 +32,70 @@ import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.style.TextDecoration import androidx.wear.compose.material.MaterialTheme +import com.android.permissioncontroller.permission.ui.wear.WearUtils.capitalize const val CLICKABLE_SPAN_TAG = "CLICKABLE_SPAN_TAG" @Composable -fun AnnotatedText(text: CharSequence, style: TextStyle, modifier: Modifier = Modifier) { +fun AnnotatedText( + text: CharSequence, + style: TextStyle, + modifier: Modifier = Modifier, + shouldCapitalize: Boolean, +) { val onClickCallbacks = mutableMapOf<String, (View) -> Unit>() val context = LocalContext.current val listener = LinkInteractionListener { if (it is LinkAnnotation.Clickable) { - onClickCallbacks.get(it.tag)?.invoke(View(context)) + onClickCallbacks[it.tag]?.invoke(View(context)) } } val annotatedString = - spannableStringToAnnotatedString(text, onClickCallbacks, listener = listener) + spannableStringToAnnotatedString( + text, + shouldCapitalize, + onClickCallbacks, + listener = listener, + ) BasicText(text = annotatedString, style = style, modifier = modifier) } @Composable private fun spannableStringToAnnotatedString( text: CharSequence, + shouldCapitalize: Boolean, onClickCallbacks: MutableMap<String, (View) -> Unit>, spanColor: Color = MaterialTheme.colors.primary, - listener: LinkInteractionListener -) = - if (text is Spanned) { - buildAnnotatedString { - append((text.toString())) - for (span in text.getSpans(0, text.length, Any::class.java)) { - val start = text.getSpanStart(span) - val end = text.getSpanEnd(span) - when (span) { - is ClickableSpan -> - addClickableSpan(span, spanColor, start, end, onClickCallbacks, listener) - else -> addStyle(SpanStyle(), start, end) + listener: LinkInteractionListener, +): AnnotatedString { + val finalString = if (shouldCapitalize) text.toString().capitalize() else text.toString() + val annotatedString = + if (text is Spanned) { + buildAnnotatedString { + append(finalString) + for (span in text.getSpans(0, text.length, Any::class.java)) { + val start = text.getSpanStart(span) + val end = text.getSpanEnd(span) + when (span) { + is ClickableSpan -> + addClickableSpan( + span, + spanColor, + start, + end, + onClickCallbacks, + listener, + ) + else -> addStyle(SpanStyle(), start, end) + } } } + } else { + AnnotatedString(finalString) } - } else { - AnnotatedString(text.toString()) - } + return annotatedString +} private fun AnnotatedString.Builder.addClickableSpan( span: ClickableSpan, @@ -80,14 +103,10 @@ private fun AnnotatedString.Builder.addClickableSpan( start: Int, end: Int, onClickCallbacks: MutableMap<String, (View) -> Unit>, - listener: LinkInteractionListener + listener: LinkInteractionListener, ) { val key = "${CLICKABLE_SPAN_TAG}:$start:$end" onClickCallbacks[key] = span::onClick addLink(LinkAnnotation.Clickable(key, linkInteractionListener = listener), start, end) - addStyle( - SpanStyle(color = spanColor, textDecoration = TextDecoration.Underline), - start, - end, - ) + addStyle(SpanStyle(color = spanColor, textDecoration = TextDecoration.Underline), start, end) } diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/Button.kt b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/Button.kt deleted file mode 100644 index 1394c56ea..000000000 --- a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/Button.kt +++ /dev/null @@ -1,133 +0,0 @@ -/* - * Copyright (C) 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.permissioncontroller.permission.ui.wear.elements - -import androidx.annotation.DrawableRes -import androidx.compose.foundation.layout.size -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.unit.Dp -import androidx.wear.compose.material.Button -import androidx.wear.compose.material.ButtonColors -import androidx.wear.compose.material.ButtonDefaults -import androidx.wear.compose.material.ButtonDefaults.DefaultButtonSize -import androidx.wear.compose.material.ButtonDefaults.DefaultIconSize -import androidx.wear.compose.material.ButtonDefaults.LargeButtonSize -import androidx.wear.compose.material.ButtonDefaults.LargeIconSize -import androidx.wear.compose.material.ButtonDefaults.SmallButtonSize -import androidx.wear.compose.material.ButtonDefaults.SmallIconSize - -/** - * This component is an alternative to [Button], providing the following: - * - a convenient way of providing an icon and choosing its size from a range of sizes recommended - * by the Wear guidelines; - */ -@Composable -public fun Button( - imageVector: ImageVector, - contentDescription: String, - onClick: () -> Unit, - modifier: Modifier = Modifier, - colors: ButtonColors = ButtonDefaults.primaryButtonColors(), - buttonSize: ButtonSize = ButtonSize.Default, - iconRtlMode: IconRtlMode = IconRtlMode.Default, - enabled: Boolean = true -) { - Button( - icon = imageVector, - contentDescription = contentDescription, - onClick = onClick, - modifier = modifier, - colors = colors, - buttonSize = buttonSize, - iconRtlMode = iconRtlMode, - enabled = enabled - ) -} - -/** - * This component is an alternative to [Button], providing the following: - * - a convenient way of providing an icon and choosing its size from a range of sizes recommended - * by the Wear guidelines; - */ -@Composable -public fun Button( - @DrawableRes id: Int, - contentDescription: String, - onClick: () -> Unit, - modifier: Modifier = Modifier, - colors: ButtonColors = ButtonDefaults.primaryButtonColors(), - buttonSize: ButtonSize = ButtonSize.Default, - iconRtlMode: IconRtlMode = IconRtlMode.Default, - enabled: Boolean = true -) { - Button( - icon = id, - contentDescription = contentDescription, - onClick = onClick, - modifier = modifier, - colors = colors, - buttonSize = buttonSize, - iconRtlMode = iconRtlMode, - enabled = enabled - ) -} - -@Composable -internal fun Button( - icon: Any, - contentDescription: String, - onClick: () -> Unit, - modifier: Modifier = Modifier, - colors: ButtonColors = ButtonDefaults.primaryButtonColors(), - buttonSize: ButtonSize = ButtonSize.Default, - iconRtlMode: IconRtlMode = IconRtlMode.Default, - enabled: Boolean = true -) { - Button( - onClick = onClick, - modifier = modifier.size(buttonSize.tapTargetSize), - enabled = enabled, - colors = colors - ) { - val iconModifier = Modifier.size(buttonSize.iconSize).align(Alignment.Center) - - Icon( - icon = icon, - contentDescription = contentDescription, - modifier = iconModifier, - rtlMode = iconRtlMode - ) - } -} - -public sealed class ButtonSize(public val iconSize: Dp, public val tapTargetSize: Dp) { - public object Default : - ButtonSize(iconSize = DefaultIconSize, tapTargetSize = DefaultButtonSize) - - public object Large : ButtonSize(iconSize = LargeIconSize, tapTargetSize = LargeButtonSize) - public object Small : ButtonSize(iconSize = SmallIconSize, tapTargetSize = SmallButtonSize) - - /** - * Custom sizes should follow the - * [accessibility principles and guidance for touch targets](https://developer.android.com/training/wearables/accessibility#set-minimum). - */ - public data class Custom(val customIconSize: Dp, val customTapTargetSize: Dp) : - ButtonSize(iconSize = customIconSize, tapTargetSize = customTapTargetSize) -} diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/ScrollableScreen.kt b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/ScrollableScreen.kt index 53013def7..d1b7e899b 100644 --- a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/ScrollableScreen.kt +++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/ScrollableScreen.kt @@ -63,7 +63,10 @@ import androidx.wear.compose.material.TimeText import androidx.wear.compose.material.Vignette import androidx.wear.compose.material.VignettePosition import androidx.wear.compose.material.scrollAway +import com.android.permissioncontroller.permission.ui.wear.elements.material3.WearPermissionScaffold import com.android.permissioncontroller.permission.ui.wear.elements.rotaryinput.rotaryWithScroll +import com.android.permissioncontroller.permission.ui.wear.theme.WearPermissionMaterialUIVersion +import com.android.permissioncontroller.permission.ui.wear.theme.WearPermissionMaterialUIVersion.MATERIAL2_5 import com.android.permissioncontroller.permission.ui.wear.theme.WearPermissionTheme /** @@ -74,6 +77,7 @@ import com.android.permissioncontroller.permission.ui.wear.theme.WearPermissionT */ @Composable fun ScrollableScreen( + materialUIVersion: WearPermissionMaterialUIVersion = MATERIAL2_5, showTimeText: Boolean = true, title: String? = null, subtitle: CharSequence? = null, @@ -103,7 +107,8 @@ fun ScrollableScreen( if (getBackStackEntryCount(activity) > 0) { SwipeToDismissBox(state = state) { isBackground -> - Scaffold( + WearPermissionScaffold( + materialUIVersion, showTimeText, title, subtitle, @@ -111,11 +116,12 @@ fun ScrollableScreen( isLoading = isLoading || isBackground || dismissed, content, titleTestTag, - subtitleTestTag + subtitleTestTag, ) } } else { - Scaffold( + WearPermissionScaffold( + materialUIVersion, showTimeText, title, subtitle, @@ -123,13 +129,13 @@ fun ScrollableScreen( isLoading, content, titleTestTag, - subtitleTestTag + subtitleTestTag, ) } } @Composable -internal fun Scaffold( +internal fun Wear2Scaffold( showTimeText: Boolean, title: String?, subtitle: CharSequence?, @@ -165,14 +171,14 @@ internal fun Scaffold( start = titleHorizontalPadding, top = 4.dp, bottom = titleBottomPadding, - end = titleHorizontalPadding + end = titleHorizontalPadding, ) val subTitlePaddingValues = PaddingValues( start = subtitleHorizontalPadding, top = 4.dp, bottom = subtitleBottomPadding, - end = subtitleHorizontalPadding + end = subtitleHorizontalPadding, ) val initialCenterIndex = 0 val centerHeightDp = Dp(LocalConfiguration.current.screenHeightDp / 2.0f) @@ -191,14 +197,14 @@ internal fun Scaffold( modifier = Modifier.rotaryWithScroll( scrollableState = listState, - focusRequester = focusRequester + focusRequester = focusRequester, ), timeText = { if (showTimeText && !isLoading) { TimeText( modifier = Modifier.scrollAway(listState, initialCenterIndex, scrollAwayOffset) - .padding(top = timeTextTopPadding), + .padding(top = timeTextTopPadding) ) } }, @@ -208,7 +214,7 @@ internal fun Scaffold( { PositionIndicator(scalingLazyListState = listState) } } else { null - } + }, ) { Box(modifier = Modifier.fillMaxSize()) { if (isLoading) { @@ -225,8 +231,8 @@ internal fun Scaffold( start = scrollContentHorizontalPadding, end = scrollContentHorizontalPadding, top = scrollContentTopPadding, - bottom = scrollContentBottomPadding - ) + bottom = scrollContentBottomPadding, + ), ) { staticItem() image?.let { @@ -238,7 +244,7 @@ internal fun Scaffold( painter = painterResource(id = image), contentDescription = null, contentScale = ContentScale.Crop, - modifier = imageModifier + modifier = imageModifier, ) } is Drawable -> @@ -247,7 +253,7 @@ internal fun Scaffold( painter = rememberDrawablePainter(image), contentDescription = null, contentScale = ContentScale.Crop, - modifier = imageModifier + modifier = imageModifier, ) } else -> {} @@ -263,7 +269,7 @@ internal fun Scaffold( Text( text = title, textAlign = TextAlign.Center, - modifier = modifier + modifier = modifier, ) } } @@ -282,6 +288,7 @@ internal fun Scaffold( color = MaterialTheme.colors.onSurfaceVariant ), modifier = modifier, + shouldCapitalize = true, ) } } diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/ToggleChip.kt b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/ToggleChip.kt index a21a9d015..4f4201748 100644 --- a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/ToggleChip.kt +++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/ToggleChip.kt @@ -29,11 +29,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.compositeOver -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.semantics.Role -import androidx.compose.ui.semantics.role -import androidx.compose.ui.semantics.semantics -import androidx.compose.ui.semantics.stateDescription import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.wear.compose.material.ChipDefaults @@ -44,7 +39,6 @@ import androidx.wear.compose.material.ToggleChip import androidx.wear.compose.material.ToggleChipColors import androidx.wear.compose.material.ToggleChipDefaults import androidx.wear.compose.material.contentColorFor -import com.android.permissioncontroller.R /** * This component is an alternative to [ToggleChip], providing the following: @@ -67,7 +61,7 @@ fun ToggleChip( secondaryLabelMaxLine: Int? = null, colors: ToggleChipColors = ToggleChipDefaults.toggleChipColors(), enabled: Boolean = true, - interactionSource: MutableInteractionSource = remember { MutableInteractionSource() } + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, ) { val hasSecondaryLabel = secondaryLabel != null @@ -78,7 +72,7 @@ fun ToggleChip( textAlign = TextAlign.Start, overflow = TextOverflow.Ellipsis, maxLines = labelMaxLine ?: if (hasSecondaryLabel) 1 else 2, - style = MaterialTheme.typography.button + style = MaterialTheme.typography.button, ) } @@ -89,7 +83,7 @@ fun ToggleChip( text = secondaryLabel, overflow = TextOverflow.Ellipsis, maxLines = secondaryLabelMaxLine ?: 1, - style = MaterialTheme.typography.caption2 + style = MaterialTheme.typography.caption2, ) } } @@ -110,7 +104,7 @@ fun ToggleChip( IconRtlMode.Mirrored } else { IconRtlMode.Default - } + }, ) } @@ -123,42 +117,23 @@ fun ToggleChip( tint = iconColor, contentDescription = null, modifier = Modifier.size(ChipDefaults.IconSize).clip(CircleShape), - rtlMode = iconRtlMode + rtlMode = iconRtlMode, ) } } } - val semanticsRole = - when (toggleControl) { - ToggleChipToggleControl.Switch -> Role.Switch - ToggleChipToggleControl.Radio -> Role.RadioButton - ToggleChipToggleControl.Checkbox -> Role.Checkbox - } - - val stateDescriptionSemantics = - stringResource( - if (checked) { - R.string.on - } else { - R.string.off - } - ) ToggleChip( checked = checked, onCheckedChange = onCheckedChanged, label = labelParam, toggleControl = toggleControlParam, - modifier = - modifier.fillMaxWidth().semantics { - role = semanticsRole - stateDescription = stateDescriptionSemantics - }, + modifier = modifier.fillMaxWidth().toggleControlSemantics(toggleControl, checked), appIcon = iconParam, secondaryLabel = secondaryLabelParam, colors = colors, enabled = enabled, - interactionSource = interactionSource + interactionSource = interactionSource, ) } @@ -198,7 +173,7 @@ fun toggleChipDisabledColors(): ToggleChipColors { uncheckedSecondaryContentColor = uncheckedSecondaryContentColor.copy(alpha = ContentAlpha.disabled), uncheckedToggleControlColor = - uncheckedToggleControlColor.copy(alpha = ContentAlpha.disabled) + uncheckedToggleControlColor.copy(alpha = ContentAlpha.disabled), ) } @@ -236,6 +211,6 @@ fun toggleChipBackgroundColors(): ToggleChipColors { uncheckedEndBackgroundColor = uncheckedEndBackgroundColor, uncheckedContentColor = uncheckedContentColor, uncheckedSecondaryContentColor = uncheckedSecondaryContentColor, - uncheckedToggleControlColor = uncheckedToggleControlColor + uncheckedToggleControlColor = uncheckedToggleControlColor, ) } diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/ToggleChipToggleControl.kt b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/ToggleChipToggleControl.kt index a4ce4e764..b6f6db4d3 100644 --- a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/ToggleChipToggleControl.kt +++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/ToggleChipToggleControl.kt @@ -16,8 +16,43 @@ package com.android.permissioncontroller.permission.ui.wear.elements -public enum class ToggleChipToggleControl { +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.semantics.role +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.semantics.stateDescription +import com.android.permissioncontroller.R + +enum class ToggleChipToggleControl { Switch, Radio, - Checkbox + Checkbox, +} + +@Composable +fun Modifier.toggleControlSemantics( + toggleControl: ToggleChipToggleControl, + checked: Boolean, +): Modifier { + val semanticsRole = + when (toggleControl) { + ToggleChipToggleControl.Switch -> Role.Switch + ToggleChipToggleControl.Radio -> Role.RadioButton + ToggleChipToggleControl.Checkbox -> Role.Checkbox + } + val stateDescriptionSemantics = + stringResource( + if (checked) { + R.string.on + } else { + R.string.off + } + ) + + return semantics { + role = semanticsRole + stateDescription = stateDescriptionSemantics + } } diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/material3/WearPermissionButton.kt b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/material3/WearPermissionButton.kt new file mode 100644 index 000000000..1d660ca35 --- /dev/null +++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/material3/WearPermissionButton.kt @@ -0,0 +1,143 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.permissioncontroller.permission.ui.wear.elements.material3 + +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.requiredSizeIn +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.Hyphens +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.wear.compose.material3.Button +import androidx.wear.compose.material3.ButtonColors +import androidx.wear.compose.material3.ButtonDefaults +import androidx.wear.compose.material3.LocalTextConfiguration +import androidx.wear.compose.material3.LocalTextStyle +import androidx.wear.compose.material3.Text +import com.android.permissioncontroller.permission.ui.wear.elements.Chip +import com.android.permissioncontroller.permission.ui.wear.theme.WearPermissionMaterialUIVersion + +/** + * This component is wrapper on material Button component + * 1. It takes icon, primary, secondary label resources and construct them applying permission app + * defaults + */ +@Composable +fun WearPermissionButton( + label: String, + modifier: Modifier = Modifier, + materialUIVersion: WearPermissionMaterialUIVersion = + WearPermissionMaterialUIVersion.MATERIAL2_5, + iconBuilder: WearPermissionIconBuilder? = null, + labelMaxLines: Int? = null, + secondaryLabel: String? = null, + secondaryLabelMaxLines: Int? = null, + onClick: () -> Unit, + enabled: Boolean = true, + style: WearPermissionButtonStyle = WearPermissionButtonStyle.Secondary, +) { + if (materialUIVersion == WearPermissionMaterialUIVersion.MATERIAL2_5) { + Chip( + label = label, + labelMaxLines = labelMaxLines, + onClick = onClick, + modifier = modifier, + secondaryLabel = secondaryLabel, + secondaryLabelMaxLines = secondaryLabelMaxLines, + icon = { iconBuilder?.build() }, + largeIcon = false, + colors = style.material2ChipColors(), + enabled = enabled, + ) + } else { + WearPermissionButtonInternal( + iconBuilder = iconBuilder, + label = label, + labelMaxLines = labelMaxLines, + secondaryLabel = secondaryLabel, + secondaryLabelMaxLines = secondaryLabelMaxLines, + onClick = onClick, + modifier = modifier, + enabled = enabled, + colors = style.material3ButtonColors(), + ) + } +} + +@Composable +internal fun WearPermissionButtonInternal( + modifier: Modifier = Modifier, + label: String? = null, + iconBuilder: WearPermissionIconBuilder? = null, + labelMaxLines: Int? = null, + secondaryLabel: String? = null, + secondaryLabelMaxLines: Int? = null, + onClick: () -> Unit, + enabled: Boolean = true, + colors: ButtonColors = ButtonDefaults.filledTonalButtonColors(), + contentPadding: PaddingValues = ButtonDefaults.ContentPadding, + requiresMinimumHeight: Boolean = true, +) { + val minHeight: Dp = + if (requiresMinimumHeight) { + 0.dp + } else { + 1.dp + } + val iconParam: (@Composable BoxScope.() -> Unit)? = iconBuilder?.let { { it.build() } } + val labelParam: (@Composable RowScope.() -> Unit)? = + label?.let { + { + Text( + text = label, + modifier = Modifier.fillMaxWidth(), + maxLines = labelMaxLines ?: LocalTextConfiguration.current.maxLines, + style = + LocalTextStyle.current.copy( + fontWeight = FontWeight.W600, + hyphens = Hyphens.Auto, + ), + ) + } + } + + val secondaryLabelParam: (@Composable RowScope.() -> Unit)? = + secondaryLabel?.let { + { + Text( + text = secondaryLabel, + modifier = Modifier.fillMaxWidth(), + maxLines = secondaryLabelMaxLines ?: LocalTextConfiguration.current.maxLines, + ) + } + } + + Button( + icon = iconParam, + label = labelParam ?: {}, + secondaryLabel = secondaryLabelParam, + enabled = enabled, + onClick = onClick, + modifier = modifier.requiredSizeIn(minHeight = minHeight).fillMaxWidth(), + contentPadding = contentPadding, + colors = colors, + ) +} diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/material3/WearPermissionButtonStyle.kt b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/material3/WearPermissionButtonStyle.kt new file mode 100644 index 000000000..504c69bb0 --- /dev/null +++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/material3/WearPermissionButtonStyle.kt @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.permissioncontroller.permission.ui.wear.elements.material3 + +import androidx.compose.runtime.Composable +import androidx.wear.compose.material.ChipColors +import androidx.wear.compose.material.ChipDefaults +import androidx.wear.compose.material3.ButtonColors +import androidx.wear.compose.material3.ButtonDefaults +import com.android.permissioncontroller.permission.ui.wear.elements.chipDefaultColors +import com.android.permissioncontroller.permission.ui.wear.elements.chipDisabledColors +import com.android.permissioncontroller.permission.ui.wear.elements.material3.WearPermissionButtonStyle.DisabledLike +import com.android.permissioncontroller.permission.ui.wear.elements.material3.WearPermissionButtonStyle.Primary +import com.android.permissioncontroller.permission.ui.wear.elements.material3.WearPermissionButtonStyle.Secondary +import com.android.permissioncontroller.permission.ui.wear.elements.material3.WearPermissionButtonStyle.Transparent + +/** + * This component is wrapper on material control colors, It applies the right colors based material + * ui version. + */ +enum class WearPermissionButtonStyle { + Primary, + Secondary, + Transparent, + DisabledLike, +} + +@Composable +internal fun WearPermissionButtonStyle.material2ChipColors(): ChipColors { + return when (this) { + Primary -> chipDefaultColors() + Secondary -> ChipDefaults.secondaryChipColors() + Transparent -> ChipDefaults.childChipColors() + DisabledLike -> chipDisabledColors() + } +} + +@Composable +internal fun WearPermissionButtonStyle.material3ButtonColors(): ButtonColors { + return when (this) { + Primary -> ButtonDefaults.buttonColors() + Secondary -> ButtonDefaults.filledTonalButtonColors() + Transparent -> ButtonDefaults.childButtonColors() + DisabledLike -> ButtonDefaults.disabledLikeColors() + } +} + +@Composable +private fun ButtonDefaults.disabledLikeColors() = + filledTonalButtonColors().run { + ButtonColors( + containerPainter = disabledContainerPainter, + contentColor = disabledContentColor, + secondaryContentColor = disabledSecondaryContentColor, + iconColor = disabledIconColor, + disabledContainerPainter = disabledContainerPainter, + disabledContentColor = disabledContentColor, + disabledSecondaryContentColor = disabledSecondaryContentColor, + disabledIconColor = disabledIconColor, + ) + } diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/material3/WearPermissionIconBuilder.kt b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/material3/WearPermissionIconBuilder.kt new file mode 100644 index 000000000..b7521d073 --- /dev/null +++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/material3/WearPermissionIconBuilder.kt @@ -0,0 +1,101 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.permissioncontroller.permission.ui.wear.elements.material3 + +import android.graphics.drawable.Drawable +import androidx.compose.foundation.layout.size +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.painterResource +import androidx.wear.compose.material3.Icon +import androidx.wear.compose.material3.IconButtonDefaults +import com.android.permissioncontroller.permission.ui.wear.elements.rememberDrawablePainter + +/** + * This class simplifies the construction of icons with various attributes like resource type, + * content description, modifier, and tint. It supports different icon resource types, including: + * - ImageVector + * - Resource ID (Int) + * - Drawable + * - ImageBitmap + * + * Usage: + * ``` + * val icon = WearPermissionIconBuilder.builder(IconResourceId) + * .contentDescription("Location Permission") + * .modifier(Modifier.size(24.dp)) + * .tint(Color.Red) + * .build() + * ``` + * + * Note: This builder uses a private constructor and is initialized through the `builder()` + * companion object method. + */ +class WearPermissionIconBuilder private constructor() { + var iconResource: Any? = null + private set + + var contentDescription: String? = null + private set + + var modifier: Modifier = Modifier.size(IconButtonDefaults.LargeIconSize) + private set + + var tint: Color = Color.Unspecified + private set + + fun contentDescription(description: String?): WearPermissionIconBuilder { + contentDescription = description + return this + } + + fun modifier(modifier: Modifier): WearPermissionIconBuilder { + this.modifier then modifier + return this + } + + fun tint(tint: Color): WearPermissionIconBuilder { + this.tint = tint + return this + } + + @Composable + fun build() { + when (iconResource) { + is ImageVector -> Icon(iconResource as ImageVector, contentDescription, modifier, tint) + is Int -> + Icon(painterResource(id = iconResource as Int), contentDescription, modifier, tint) + + is Drawable -> + Icon( + rememberDrawablePainter(iconResource as Drawable), + contentDescription, + modifier, + tint, + ) + + is ImageBitmap -> Icon(iconResource as ImageBitmap, contentDescription, modifier, tint) + else -> throw IllegalArgumentException("Type not supported.") + } + } + + companion object { + fun builder(icon: Any) = WearPermissionIconBuilder().apply { iconResource = icon } + } +} diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/material3/WearPermissionListFooter.kt b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/material3/WearPermissionListFooter.kt new file mode 100644 index 000000000..10125c873 --- /dev/null +++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/material3/WearPermissionListFooter.kt @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.permissioncontroller.permission.ui.wear.elements.material3 + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.runtime.Composable +import androidx.compose.ui.unit.dp +import androidx.wear.compose.material3.ButtonDefaults +import com.android.permissioncontroller.permission.ui.wear.elements.ListFooter +import com.android.permissioncontroller.permission.ui.wear.theme.WearPermissionMaterialUIVersion + +/** This component is creates a transparent styled button to use as a list footer. */ +@Composable +fun WearPermissionListFooter( + materialUIVersion: WearPermissionMaterialUIVersion, + label: String, + iconBuilder: WearPermissionIconBuilder? = null, + onClick: (() -> Unit) = {}, +) { + if (materialUIVersion == WearPermissionMaterialUIVersion.MATERIAL2_5) { + ListFooter( + description = label, + iconRes = iconBuilder?.let { it.iconResource as Int }, + onClick = onClick, + ) + } else { + WearPermissionButtonInternal( + iconBuilder = iconBuilder, + secondaryLabel = label, + secondaryLabelMaxLines = Int.MAX_VALUE, + onClick = onClick, + contentPadding = PaddingValues(0.dp), + colors = ButtonDefaults.childButtonColors(), + requiresMinimumHeight = false, + ) + } +} diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/material3/WearPermissionScaffold.kt b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/material3/WearPermissionScaffold.kt new file mode 100644 index 000000000..bd7636273 --- /dev/null +++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/material3/WearPermissionScaffold.kt @@ -0,0 +1,298 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.permissioncontroller.permission.ui.wear.elements.material3 + +import android.graphics.drawable.Drawable +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.requiredHeightIn +import androidx.compose.foundation.layout.size +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.wear.compose.foundation.ScrollInfoProvider +import androidx.wear.compose.foundation.lazy.ScalingLazyListScope +import androidx.wear.compose.material3.AppScaffold +import androidx.wear.compose.material3.CircularProgressIndicator +import androidx.wear.compose.material3.ListHeader +import androidx.wear.compose.material3.MaterialTheme +import androidx.wear.compose.material3.ScreenScaffold +import androidx.wear.compose.material3.ScrollIndicator +import androidx.wear.compose.material3.Text +import androidx.wear.compose.material3.TimeText +import com.android.permissioncontroller.permission.ui.wear.elements.AnnotatedText +import com.android.permissioncontroller.permission.ui.wear.elements.Wear2Scaffold +import com.android.permissioncontroller.permission.ui.wear.elements.layout.ScalingLazyColumn +import com.android.permissioncontroller.permission.ui.wear.elements.layout.ScalingLazyColumnState +import com.android.permissioncontroller.permission.ui.wear.elements.layout.rememberResponsiveColumnState +import com.android.permissioncontroller.permission.ui.wear.elements.rememberDrawablePainter +import com.android.permissioncontroller.permission.ui.wear.theme.WearPermissionMaterialUIVersion +import com.android.permissioncontroller.permission.ui.wear.theme.WearPermissionMaterialUIVersion.MATERIAL2_5 +import com.android.permissioncontroller.permission.ui.wear.theme.WearPermissionTheme + +/** + * This component is wrapper on material scaffold component. It helps with time text, scroll + * indicator and standard list elements like title, icon and subtitle. + */ +@Composable +internal fun WearPermissionScaffold( + materialUIVersion: WearPermissionMaterialUIVersion = MATERIAL2_5, + showTimeText: Boolean, + title: String?, + subtitle: CharSequence?, + image: Any?, + isLoading: Boolean, + content: ScalingLazyListScope.() -> Unit, + titleTestTag: String? = null, + subtitleTestTag: String? = null, +) { + + if (materialUIVersion == MATERIAL2_5) { + Wear2Scaffold( + showTimeText, + title, + subtitle, + image, + isLoading, + content, + titleTestTag, + subtitleTestTag, + ) + } else { + WearPermissionScaffoldInternal( + showTimeText, + title, + subtitle, + image, + isLoading, + content, + titleTestTag, + subtitleTestTag, + ) + } +} + +@Composable +private fun WearPermissionScaffoldInternal( + showTimeText: Boolean, + title: String?, + subtitle: CharSequence?, + image: Any?, + isLoading: Boolean, + content: ScalingLazyListScope.() -> Unit, + titleTestTag: String? = null, + subtitleTestTag: String? = null, +) { + val screenWidth = LocalConfiguration.current.screenWidthDp + val screenHeight = LocalConfiguration.current.screenHeightDp + val paddingDefaults = + WearPermissionScaffoldPaddingDefaults( + screenWidth = screenWidth, + screenHeight = screenHeight, + titleNeedsLargePadding = subtitle == null, + ) + val columnState = + rememberResponsiveColumnState(contentPadding = { paddingDefaults.scrollContentPadding }) + WearPermissionTheme(version = WearPermissionMaterialUIVersion.MATERIAL3) { + AppScaffold(timeText = wearPermissionTimeText(showTimeText && !isLoading)) { + ScreenScaffold( + scrollInfoProvider = ScrollInfoProvider(columnState.state), + scrollIndicator = wearPermissionScrollIndicator(!isLoading, columnState), + ) { + Box(modifier = Modifier.fillMaxSize()) { + if (isLoading) { + CircularProgressIndicator(modifier = Modifier.align(Alignment.Center)) + } else { + ScrollingView( + columnState = columnState, + icon = painterFromImage(image), + title = title, + titleTestTag = titleTestTag, + titlePaddingValues = paddingDefaults.titlePaddingValues, + subtitle = subtitle, + subtitleTestTag = subtitleTestTag, + subTitlePaddingValues = paddingDefaults.subTitlePaddingValues, + content = content, + ) + } + } + } + } + } +} + +private class WearPermissionScaffoldPaddingDefaults( + screenWidth: Int, + screenHeight: Int, + titleNeedsLargePadding: Boolean, +) { + private val firstSpacerItemHeight = 0.dp + private val scrollContentHorizontalPadding = (screenWidth * 0.052).dp + private val titleHorizontalPadding = (screenWidth * 0.0884).dp + private val subtitleHorizontalPadding = (screenWidth * 0.0416).dp + private val scrollContentTopPadding = (screenHeight * 0.1456).dp - firstSpacerItemHeight + private val scrollContentBottomPadding = (screenHeight * 0.3636).dp + private val defaultItemPadding = 4.dp + private val largeItemPadding = 8.dp + val titlePaddingValues = + PaddingValues( + start = titleHorizontalPadding, + top = defaultItemPadding, + bottom = if (titleNeedsLargePadding) largeItemPadding else defaultItemPadding, + end = titleHorizontalPadding, + ) + val subTitlePaddingValues = + PaddingValues( + start = subtitleHorizontalPadding, + top = defaultItemPadding, + bottom = largeItemPadding, + end = subtitleHorizontalPadding, + ) + val scrollContentPadding = + PaddingValues( + start = scrollContentHorizontalPadding, + end = scrollContentHorizontalPadding, + top = scrollContentTopPadding, + bottom = scrollContentBottomPadding, + ) +} + +@Composable +private fun BoxScope.ScrollingView( + columnState: ScalingLazyColumnState, + icon: Painter?, + title: String?, + titleTestTag: String?, + subtitle: CharSequence?, + subtitleTestTag: String?, + titlePaddingValues: PaddingValues, + subTitlePaddingValues: PaddingValues, + content: ScalingLazyListScope.() -> Unit, +) { + ScalingLazyColumn(columnState = columnState) { + iconItem(icon, Modifier.size(24.dp)) + titleItem(text = title, testTag = titleTestTag, contentPaddingValues = titlePaddingValues) + subtitleItem( + text = subtitle, + testTag = subtitleTestTag, + modifier = Modifier.align(Alignment.Center).padding(subTitlePaddingValues), + ) + content() + } +} + +private fun wearPermissionTimeText(showTime: Boolean): @Composable () -> Unit { + return if (showTime) { + { TimeText { time() } } + } else { + {} + } +} + +private fun wearPermissionScrollIndicator( + showIndicator: Boolean, + columnState: ScalingLazyColumnState, +): @Composable (BoxScope.() -> Unit)? { + return if (showIndicator) { + { + ScrollIndicator( + modifier = Modifier.align(Alignment.CenterEnd), + state = columnState.state, + ) + } + } else { + null + } +} + +@Composable +private fun painterFromImage(image: Any?): Painter? { + return when (image) { + is Int -> painterResource(id = image) + is Drawable -> rememberDrawablePainter(image) + else -> null + } +} + +private fun Modifier.optionalTestTag(tag: String?): Modifier { + if (tag == null) { + return this + } + return this then testTag(tag) +} + +private fun ScalingLazyListScope.iconItem(painter: Painter?, modifier: Modifier = Modifier) = + painter?.let { + item { + Image( + painter = it, + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = modifier, + ) + } + } + +private fun ScalingLazyListScope.titleItem( + text: String?, + testTag: String?, + contentPaddingValues: PaddingValues, + modifier: Modifier = Modifier, +) = + text?.let { + item { + ListHeader( + modifier = modifier.requiredHeightIn(1.dp), // We do not want default min height + contentPadding = contentPaddingValues, + ) { + Text( + text = it, + textAlign = TextAlign.Center, + modifier = Modifier.optionalTestTag(testTag), + ) + } + } + } + +private fun ScalingLazyListScope.subtitleItem( + text: CharSequence?, + testTag: String?, + modifier: Modifier = Modifier, +) = + text?.let { + item { + AnnotatedText( + text = it, + style = + MaterialTheme.typography.bodyMedium.copy( + color = MaterialTheme.colorScheme.onSurfaceVariant + ), + modifier = modifier.optionalTestTag(testTag), + shouldCapitalize = true, + ) + } + } diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/material3/WearPermissionToggleControl.kt b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/material3/WearPermissionToggleControl.kt new file mode 100644 index 000000000..4a139f91f --- /dev/null +++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/material3/WearPermissionToggleControl.kt @@ -0,0 +1,165 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.permissioncontroller.permission.ui.wear.elements.material3 + +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.wear.compose.material3.CheckboxButton +import androidx.wear.compose.material3.LocalTextConfiguration +import androidx.wear.compose.material3.RadioButton +import androidx.wear.compose.material3.SwitchButton +import androidx.wear.compose.material3.Text +import com.android.permissioncontroller.permission.ui.wear.elements.ToggleChip +import com.android.permissioncontroller.permission.ui.wear.elements.ToggleChipToggleControl +import com.android.permissioncontroller.permission.ui.wear.elements.toggleControlSemantics +import com.android.permissioncontroller.permission.ui.wear.theme.WearPermissionMaterialUIVersion + +/** + * The custom component is a wrapper on different material3 toggle controls. + * 1. It provides an unified interface for RadioButton,CheckButton and SwitchButton. + * 2. It takes icon, primary, secondary label resources and construct them applying permission app + * defaults + * 3. Applies custom semantics for based on the toggle control type + */ +@Composable +fun WearPermissionToggleControl( + toggleControl: ToggleChipToggleControl, + label: String, + checked: Boolean, + onCheckedChanged: (Boolean) -> Unit, + modifier: Modifier = Modifier, + labelMaxLines: Int? = null, + materialUIVersion: WearPermissionMaterialUIVersion = + WearPermissionMaterialUIVersion.MATERIAL2_5, + iconBuilder: WearPermissionIconBuilder? = null, + secondaryLabel: String? = null, + secondaryLabelMaxLines: Int? = null, + enabled: Boolean = true, + style: WearPermissionToggleControlStyle = WearPermissionToggleControlStyle.Default, +) { + if (materialUIVersion == WearPermissionMaterialUIVersion.MATERIAL2_5) { + ToggleChip( + toggleControl = toggleControl, + label = label, + labelMaxLine = labelMaxLines, + checked = checked, + onCheckedChanged = onCheckedChanged, + modifier = modifier, + icon = iconBuilder?.iconResource, + secondaryLabel = secondaryLabel, + secondaryLabelMaxLine = secondaryLabelMaxLines, + enabled = enabled, + colors = style.material2ToggleControlColors(), + ) + } else { + WearPermissionToggleControlInternal( + label = label, + toggleControl = toggleControl, + checked = checked, + onCheckedChanged = onCheckedChanged, + modifier = modifier, + iconBuilder = iconBuilder, + labelMaxLines = labelMaxLines, + secondaryLabel = secondaryLabel, + secondaryLabelMaxLines = secondaryLabelMaxLines, + enabled = enabled, + style = style, + ) + } +} + +@Composable +private fun WearPermissionToggleControlInternal( + label: String, + toggleControl: ToggleChipToggleControl, + checked: Boolean, + onCheckedChanged: (Boolean) -> Unit, + modifier: Modifier = Modifier, + iconBuilder: WearPermissionIconBuilder? = null, + labelMaxLines: Int? = null, + secondaryLabel: String? = null, + secondaryLabelMaxLines: Int? = null, + enabled: Boolean = true, + style: WearPermissionToggleControlStyle = WearPermissionToggleControlStyle.Default, +) { + val labelParam: (@Composable RowScope.() -> Unit) = { + Text( + text = label, + modifier = Modifier.fillMaxWidth(), + maxLines = labelMaxLines ?: LocalTextConfiguration.current.maxLines, + ) + } + + val secondaryLabelParam: (@Composable RowScope.() -> Unit)? = + secondaryLabel?.let { + { + Text( + text = it, + modifier = Modifier.fillMaxWidth(), + maxLines = secondaryLabelMaxLines ?: LocalTextConfiguration.current.maxLines, + ) + } + } + + val iconParam: (@Composable BoxScope.() -> Unit)? = iconBuilder?.let { { it.build() } } + + val updatedModifier = + modifier + .fillMaxWidth() + // .heightIn(min = 58.dp) // TODO(b/370783358): This should be a overlaid value + .toggleControlSemantics(toggleControl, checked) + + when (toggleControl) { + ToggleChipToggleControl.Radio -> + RadioButton( + selected = checked, + onSelect = { onCheckedChanged(true) }, + modifier = updatedModifier, + enabled = enabled, + icon = iconParam, + secondaryLabel = secondaryLabelParam, + label = labelParam, + colors = style.radioButtonColorScheme(), + ) + + ToggleChipToggleControl.Checkbox -> + CheckboxButton( + checked = checked, + onCheckedChange = onCheckedChanged, + modifier = updatedModifier, + enabled = enabled, + icon = iconParam, + secondaryLabel = secondaryLabelParam, + label = labelParam, + colors = style.checkboxColorScheme(), + ) + + ToggleChipToggleControl.Switch -> + SwitchButton( + checked = checked, + onCheckedChange = onCheckedChanged, + modifier = updatedModifier, + enabled = enabled, + icon = iconParam, + secondaryLabel = secondaryLabelParam, + label = labelParam, + colors = style.switchButtonColorScheme(), + ) + } +} diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/material3/WearPermissionToggleControlStyle.kt b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/material3/WearPermissionToggleControlStyle.kt new file mode 100644 index 000000000..b5746f019 --- /dev/null +++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/material3/WearPermissionToggleControlStyle.kt @@ -0,0 +1,158 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.permissioncontroller.permission.ui.wear.elements.material3 + +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color +import androidx.wear.compose.material.ToggleChipColors +import androidx.wear.compose.material.ToggleChipDefaults.toggleChipColors +import androidx.wear.compose.material3.CheckboxButtonColors +import androidx.wear.compose.material3.CheckboxButtonDefaults.checkboxButtonColors +import androidx.wear.compose.material3.RadioButtonColors +import androidx.wear.compose.material3.RadioButtonDefaults.radioButtonColors +import androidx.wear.compose.material3.SwitchButtonColors +import androidx.wear.compose.material3.SwitchButtonDefaults.switchButtonColors +import com.android.permissioncontroller.permission.ui.wear.elements.toggleChipBackgroundColors +import com.android.permissioncontroller.permission.ui.wear.elements.toggleChipDisabledColors + +/** + * Defines toggle control styles, It helps in setting the right colors scheme to a toggle control. + */ +enum class WearPermissionToggleControlStyle { + Default, + Transparent, + DisabledLike, +} + +@Composable +internal fun WearPermissionToggleControlStyle.radioButtonColorScheme(): RadioButtonColors { + return when (this) { + WearPermissionToggleControlStyle.Default -> radioButtonColors() + WearPermissionToggleControlStyle.Transparent -> radioButtonTransparentColors() + WearPermissionToggleControlStyle.DisabledLike -> radioButtonDisabledLikeColors() + } +} + +@Composable +internal fun WearPermissionToggleControlStyle.checkboxColorScheme(): CheckboxButtonColors { + return when (this) { + WearPermissionToggleControlStyle.Default -> checkboxButtonColors() + WearPermissionToggleControlStyle.Transparent -> checkButtonTransparentColors() + WearPermissionToggleControlStyle.DisabledLike -> checkboxDisabledLikeColors() + } +} + +@Composable +internal fun WearPermissionToggleControlStyle.switchButtonColorScheme(): SwitchButtonColors { + return when (this) { + WearPermissionToggleControlStyle.Default -> switchButtonColors() + WearPermissionToggleControlStyle.Transparent -> switchButtonTransparentColors() + WearPermissionToggleControlStyle.DisabledLike -> switchButtonDisabledLikeColors() + } +} + +@Composable +internal fun WearPermissionToggleControlStyle.material2ToggleControlColors(): ToggleChipColors { + return when (this) { + WearPermissionToggleControlStyle.Default -> toggleChipColors() + WearPermissionToggleControlStyle.Transparent -> toggleChipBackgroundColors() + WearPermissionToggleControlStyle.DisabledLike -> toggleChipDisabledColors() + } +} + +@Composable +private fun checkButtonTransparentColors() = + checkboxButtonColors( + checkedContainerColor = Color.Transparent, + uncheckedContainerColor = Color.Transparent, + disabledCheckedContainerColor = Color.Transparent, + disabledUncheckedContainerColor = Color.Transparent, + ) + +@Composable +private fun radioButtonTransparentColors() = + radioButtonColors( + selectedContainerColor = Color.Transparent, + unselectedContainerColor = Color.Transparent, + disabledSelectedContainerColor = Color.Transparent, + disabledUnselectedContainerColor = Color.Transparent, + ) + +@Composable +private fun switchButtonTransparentColors() = + switchButtonColors( + checkedContainerColor = Color.Transparent, + uncheckedContainerColor = Color.Transparent, + disabledCheckedContainerColor = Color.Transparent, + disabledUncheckedContainerColor = Color.Transparent, + ) + +@Composable +private fun checkboxDisabledLikeColors(): CheckboxButtonColors { + val defaultColors = checkboxButtonColors() + return checkboxButtonColors( + checkedContainerColor = defaultColors.disabledCheckedContainerColor, + checkedContentColor = defaultColors.disabledCheckedContentColor, + checkedSecondaryContentColor = defaultColors.disabledCheckedSecondaryContentColor, + checkedIconColor = defaultColors.disabledCheckedIconColor, + checkedBoxColor = defaultColors.disabledCheckedBoxColor, + checkedCheckmarkColor = defaultColors.disabledCheckedCheckmarkColor, + uncheckedContainerColor = defaultColors.disabledUncheckedContainerColor, + uncheckedContentColor = defaultColors.disabledUncheckedContentColor, + uncheckedSecondaryContentColor = defaultColors.disabledUncheckedSecondaryContentColor, + uncheckedIconColor = defaultColors.disabledUncheckedIconColor, + uncheckedBoxColor = defaultColors.disabledUncheckedBoxColor, + ) +} + +@Composable +private fun radioButtonDisabledLikeColors(): RadioButtonColors { + val defaultColors = radioButtonColors() + return radioButtonColors( + selectedContainerColor = defaultColors.disabledSelectedContainerColor, + selectedContentColor = defaultColors.disabledSelectedContentColor, + selectedSecondaryContentColor = defaultColors.disabledSelectedSecondaryContentColor, + selectedIconColor = defaultColors.disabledSelectedIconColor, + selectedControlColor = defaultColors.disabledSelectedControlColor, + unselectedContentColor = defaultColors.disabledUnselectedContentColor, + unselectedContainerColor = defaultColors.disabledUnselectedContainerColor, + unselectedSecondaryContentColor = defaultColors.disabledUnselectedSecondaryContentColor, + unselectedIconColor = defaultColors.disabledUnselectedIconColor, + unselectedControlColor = defaultColors.disabledUnselectedControlColor, + ) +} + +@Composable +private fun switchButtonDisabledLikeColors(): SwitchButtonColors { + val defaultColors = switchButtonColors() + return switchButtonColors( + checkedContainerColor = defaultColors.disabledCheckedContainerColor, + checkedContentColor = defaultColors.disabledCheckedContentColor, + checkedSecondaryContentColor = defaultColors.disabledCheckedSecondaryContentColor, + checkedIconColor = defaultColors.disabledCheckedIconColor, + checkedThumbColor = defaultColors.disabledCheckedThumbColor, + checkedThumbIconColor = defaultColors.disabledCheckedThumbIconColor, + checkedTrackColor = defaultColors.disabledCheckedTrackColor, + checkedTrackBorderColor = defaultColors.disabledCheckedTrackBorderColor, + uncheckedContainerColor = defaultColors.disabledUncheckedContainerColor, + uncheckedContentColor = defaultColors.disabledUncheckedContentColor, + uncheckedSecondaryContentColor = defaultColors.disabledUncheckedSecondaryContentColor, + uncheckedIconColor = defaultColors.disabledUncheckedIconColor, + uncheckedThumbColor = defaultColors.disabledUncheckedThumbColor, + uncheckedTrackColor = defaultColors.checkedTrackColor.run { copy(alpha = alpha * 0.12f) }, + uncheckedTrackBorderColor = defaultColors.disabledUncheckedTrackBorderColor, + ) +} diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/theme/ResourceHelper.kt b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/theme/ResourceHelper.kt new file mode 100644 index 000000000..c7ed0958c --- /dev/null +++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/theme/ResourceHelper.kt @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.permissioncontroller.permission.ui.wear.theme + +import android.content.Context +import android.os.SystemProperties +import androidx.annotation.ColorRes +import androidx.annotation.DimenRes +import androidx.annotation.DoNotInline +import androidx.annotation.StringRes +import androidx.compose.ui.graphics.Color + +internal object ResourceHelper { + + private const val MATERIAL3_ENABLED_SYSPROP = "persist.cw_build.bluechip.enabled" + + val material3Enabled: Boolean + get() { + return SystemProperties.getBoolean(MATERIAL3_ENABLED_SYSPROP, false) + } + + @DoNotInline + fun getColor(context: Context, @ColorRes id: Int): Color? { + return try { + val colorInt = context.resources.getColor(id, context.theme) + Color(colorInt) + } catch (e: Exception) { + null + } + } + + @DoNotInline + fun getString(context: Context, @StringRes id: Int): String? { + return try { + context.resources.getString(id) + } catch (e: Exception) { + null + } + } + + @DoNotInline + fun getDimen(context: Context, @DimenRes id: Int): Float? { + return try { + context.resources.getDimension(id) / context.resources.displayMetrics.density + } catch (e: Exception) { + null + } + } +} diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/theme/WearComposeMaterial3ColorScheme.kt b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/theme/WearComposeMaterial3ColorScheme.kt new file mode 100644 index 000000000..7ac6c8114 --- /dev/null +++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/theme/WearComposeMaterial3ColorScheme.kt @@ -0,0 +1,209 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.permissioncontroller.permission.ui.wear.theme + +import android.content.Context +import android.os.Build +import androidx.annotation.ColorRes +import androidx.annotation.RequiresApi +import androidx.compose.ui.graphics.Color +import androidx.wear.compose.material3.ColorScheme + +/** + * Creates a dynamic color maps that can be overlaid. In wear we only support dark theme for the + * time being. If the device supports dynamic color generation these resources are updated with the + * generated colors + */ +internal object WearComposeMaterial3ColorScheme { + + @RequiresApi(Build.VERSION_CODES.S) + fun tonalColorScheme(context: Context): ColorScheme { + val tonalPalette = dynamicTonalPalette(context) + return ColorScheme( + background = tonalPalette.neutral0, + onBackground = tonalPalette.neutral100, + onPrimary = tonalPalette.primary10, + onPrimaryContainer = tonalPalette.primary90, + onSecondary = tonalPalette.secondary10, + onSecondaryContainer = tonalPalette.secondary90, + onSurface = tonalPalette.neutral95, + onSurfaceVariant = tonalPalette.neutralVariant80, + onTertiary = tonalPalette.tertiary10, + onTertiaryContainer = tonalPalette.tertiary90, + outline = tonalPalette.neutralVariant60, + outlineVariant = tonalPalette.neutralVariant40, + primary = tonalPalette.primary90, + primaryContainer = tonalPalette.primary30, + primaryDim = tonalPalette.primary80, + secondary = tonalPalette.secondary90, + secondaryContainer = tonalPalette.secondary30, + secondaryDim = tonalPalette.secondary80, + surfaceContainer = tonalPalette.neutral20, + surfaceContainerHigh = tonalPalette.neutral30, + tertiary = tonalPalette.tertiary90, + tertiaryContainer = tonalPalette.tertiary30, + tertiaryDim = tonalPalette.tertiary80, + ) + } + + private fun Color.updatedColor(context: Context, @ColorRes colorRes: Int): Color { + return ResourceHelper.getColor(context, colorRes) ?: this + } + + @RequiresApi(36) + fun dynamicColorScheme(context: Context): ColorScheme { + val defaultColorScheme = ColorScheme() + return ColorScheme( + primary = + defaultColorScheme.primary.updatedColor( + context, + android.R.color.system_primary_fixed, + ), + primaryDim = + defaultColorScheme.primaryDim.updatedColor( + context, + android.R.color.system_primary_fixed_dim, + ), + primaryContainer = + defaultColorScheme.primaryContainer.updatedColor( + context, + android.R.color.system_primary_container_dark, + ), + onPrimary = + defaultColorScheme.onPrimary.updatedColor( + context, + android.R.color.system_on_primary_fixed, + ), + onPrimaryContainer = + defaultColorScheme.onPrimaryContainer.updatedColor( + context, + android.R.color.system_on_primary_container_dark, + ), + secondary = + defaultColorScheme.secondary.updatedColor( + context, + android.R.color.system_secondary_fixed, + ), + secondaryDim = + defaultColorScheme.secondaryDim.updatedColor( + context, + android.R.color.system_secondary_fixed_dim, + ), + secondaryContainer = + defaultColorScheme.secondaryContainer.updatedColor( + context, + android.R.color.system_secondary_container_dark, + ), + onSecondary = + defaultColorScheme.onSecondary.updatedColor( + context, + android.R.color.system_on_secondary_fixed, + ), + onSecondaryContainer = + defaultColorScheme.onSecondaryContainer.updatedColor( + context, + android.R.color.system_on_secondary_container_dark, + ), + tertiary = + defaultColorScheme.tertiary.updatedColor( + context, + android.R.color.system_tertiary_fixed, + ), + tertiaryDim = + defaultColorScheme.tertiaryDim.updatedColor( + context, + android.R.color.system_tertiary_fixed_dim, + ), + tertiaryContainer = + defaultColorScheme.tertiaryContainer.updatedColor( + context, + android.R.color.system_tertiary_container_dark, + ), + onTertiary = + defaultColorScheme.onTertiary.updatedColor( + context, + android.R.color.system_on_tertiary_fixed, + ), + onTertiaryContainer = + defaultColorScheme.onTertiaryContainer.updatedColor( + context, + android.R.color.system_on_tertiary_container_dark, + ), + surfaceContainerLow = + defaultColorScheme.surfaceContainerLow.updatedColor( + context, + android.R.color.system_surface_container_low_dark, + ), + surfaceContainer = + defaultColorScheme.surfaceContainer.updatedColor( + context, + android.R.color.system_surface_container_dark, + ), + surfaceContainerHigh = + defaultColorScheme.surfaceContainerHigh.updatedColor( + context, + android.R.color.system_surface_container_high_dark, + ), + onSurface = + defaultColorScheme.onSurface.updatedColor( + context, + android.R.color.system_on_surface_dark, + ), + onSurfaceVariant = + defaultColorScheme.onSurfaceVariant.updatedColor( + context, + android.R.color.system_on_surface_variant_dark, + ), + outline = + defaultColorScheme.outline.updatedColor( + context, + android.R.color.system_outline_dark, + ), + outlineVariant = + defaultColorScheme.outlineVariant.updatedColor( + context, + android.R.color.system_outline_variant_dark, + ), + background = + defaultColorScheme.background.updatedColor( + context, + android.R.color.system_background_dark, + ), + onBackground = + defaultColorScheme.onBackground.updatedColor( + context, + android.R.color.system_on_background_dark, + ), + error = + defaultColorScheme.error.updatedColor(context, android.R.color.system_error_dark), + onError = + defaultColorScheme.onError.updatedColor( + context, + android.R.color.system_on_error_dark, + ), + errorContainer = + defaultColorScheme.errorContainer.updatedColor( + context, + android.R.color.system_error_container_dark, + ), + onErrorContainer = + defaultColorScheme.onErrorContainer.updatedColor( + context, + android.R.color.system_on_error_container_dark, + ), + ) + } +} diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/theme/WearComposeMaterial3Shapes.kt b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/theme/WearComposeMaterial3Shapes.kt new file mode 100644 index 000000000..f81022842 --- /dev/null +++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/theme/WearComposeMaterial3Shapes.kt @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.permissioncontroller.permission.ui.wear.theme + +import android.content.Context +import androidx.annotation.DimenRes +import androidx.compose.foundation.shape.CornerBasedShape +import androidx.compose.foundation.shape.CornerSize +import androidx.compose.ui.unit.dp +import androidx.wear.compose.material3.Shapes +import com.android.permissioncontroller.R + +// TODO(b/324928718): Use system defined symbols. +internal object WearComposeMaterial3Shapes { + private fun CornerBasedShape.updatedShape( + context: Context, + @DimenRes cornerSizeRes: Int, + ): CornerBasedShape { + val size = ResourceHelper.getDimen(context, cornerSizeRes)?.dp ?: return this + return copy(CornerSize(size)) + } + + fun dynamicShapes(context: Context): Shapes { + val defaultShapes = Shapes() + return Shapes( + extraLarge = + defaultShapes.extraLarge.updatedShape( + context, + R.dimen.wear_compose_material3_shape_corner_extra_large_size, + ), + large = + defaultShapes.large.updatedShape( + context, + R.dimen.wear_compose_material3_shape_corner_large_size, + ), + medium = + defaultShapes.medium.updatedShape( + context, + R.dimen.wear_compose_material3_shape_corner_medium_size, + ), + small = + defaultShapes.small.updatedShape( + context, + R.dimen.wear_compose_material3_shape_corner_small_size, + ), + extraSmall = + defaultShapes.extraSmall.updatedShape( + context, + R.dimen.wear_compose_material3_shape_corner_extra_small_size, + ), + ) + } +} diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/theme/WearComposeMaterial3TypeScaleTokens.kt b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/theme/WearComposeMaterial3TypeScaleTokens.kt new file mode 100644 index 000000000..a4ec9ee1d --- /dev/null +++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/theme/WearComposeMaterial3TypeScaleTokens.kt @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.permissioncontroller.permission.ui.wear.theme + +/* + * These values are retrieved from https://carbon.googleplex.com/wear-m3/pages and + * modified by UX. + * These values are internal to material3 library. We copied them to support flex font families + * Do not edit directly. Copy paste from compose material library. + */ + +internal object WearComposeMaterial3TypeScaleTokens { + val ArcLargeRoundness = 100.0f + val ArcLargeWeight = 600.0f + val ArcLargeWidth = 100.0f + + val ArcMediumRoundness = 100.0f + val ArcMediumWeight = 600.0f + val ArcMediumWidth = 90.0f + + val ArcSmallRoundness = 100.0f + val ArcSmallWeight = 550.0f + val ArcSmallWidth = 90.0f + + val BodyExtraSmallRoundness = 100.0f + val BodyExtraSmallWeight = 500.0f + val BodyExtraSmallWidth = 84.0f + + val BodyLargeRoundness = 100.0f + val BodyLargeWeight = 450.0f + val BodyLargeWidth = 90.0f + + val BodyMediumRoundness = 100.0f + val BodyMediumWeight = 450.0f + val BodyMediumWidth = 90.0f + + val BodySmallRoundness = 100.0f + val BodySmallWeight = 500.0f + val BodySmallWidth = 86.0f + + val DisplayLargeRoundness = 100.0f + val DisplayLargeWeight = 450.0f + val DisplayLargeWidth = 100.0f + + val DisplayMediumRoundness = 100.0f + val DisplayMediumWeight = 500.0f + val DisplayMediumWidth = 100.0f + + val DisplaySmallRoundness = 100.0f + val DisplaySmallWeight = 500.0f + val DisplaySmallWidth = 100.0f + + val LabelLargeRoundness = 100.0f + val LabelLargeWeight = 500.0f + val LabelLargeWidth = 100.0f + + val LabelMediumRoundness = 100.0f + val LabelMediumWeight = 500.0f + val LabelMediumWidth = 90.0f + + val LabelSmallRoundness = 100.0f + val LabelSmallWeight = 500.0f + val LabelSmallWidth = 84.0f + + val NumeralExtraLargeRoundness = 100.0f + val NumeralExtraLargeWeight = 550.0f + val NumeralExtraLargeWidth = 100.0f + + val NumeralExtraSmallRoundness = 100.0f + val NumeralExtraSmallWeight = 550.0f + val NumeralExtraSmallWidth = 100.0f + + val NumeralLargeRoundness = 100.0f + val NumeralLargeWeight = 600.0f + val NumeralLargeWidth = 100.0f + + val NumeralMediumRoundness = 100.0f + val NumeralMediumWidth = 100.0f + val NumeralMediumWeight = 600.0f + + val NumeralSmallRoundness = 100.0f + val NumeralSmallWeight = 600.0f + val NumeralSmallWidth = 100.0f + + val TitleLargeRoundness = 100.0f + val TitleLargeWeight = 500.0f + val TitleLargeWidth = 100.0f + + val TitleMediumRoundness = 100.0f + val TitleMediumWeight = 550.0f + val TitleMediumWidth = 100.0f + + val TitleSmallRoundness = 100.0f + val TitleSmallWeight = 550.0f + val TitleSmallWidth = 100.0f +} diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/theme/WearComposeMaterial3Typography.kt b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/theme/WearComposeMaterial3Typography.kt new file mode 100644 index 000000000..ceae526a7 --- /dev/null +++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/theme/WearComposeMaterial3Typography.kt @@ -0,0 +1,240 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.permissioncontroller.permission.ui.wear.theme + +import android.content.Context +import androidx.annotation.DimenRes +import androidx.annotation.StringRes +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.DeviceFontFamilyName +import androidx.compose.ui.text.font.Font +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontVariation +import androidx.compose.ui.unit.sp +import androidx.wear.compose.material3.Typography +import com.android.permissioncontroller.R + +internal object WearComposeMaterial3Typography { + + private const val DEVICE_DEFAULT_FLEX_FONT_TYPE = "font-family-flex-device-default" + + fun fontFamily( + context: Context, + @StringRes id: Int, + variationSettings: FontVariation.Settings? = null, + ): FontFamily { + val typefaceName = ResourceHelper.getString(context, id) ?: DEVICE_DEFAULT_FLEX_FONT_TYPE + + val font = + if (variationSettings != null) { + Font( + familyName = DeviceFontFamilyName(typefaceName), + variationSettings = variationSettings, + ) + } else { + Font(familyName = DeviceFontFamilyName(typefaceName)) + } + return FontFamily(font) + } + + private fun TextStyle.updatedTextStyle( + context: Context, + @StringRes fontRes: Int, + variationSettings: FontVariation.Settings? = null, + @DimenRes fontSizeRes: Int, + ): TextStyle { + + val fontFamily = + fontFamily(context = context, id = fontRes, variationSettings = variationSettings) + val fontSize = ResourceHelper.getDimen(context = context, id = fontSizeRes)?.sp ?: fontSize + + return copy(fontFamily = fontFamily, fontSize = fontSize) + } + + fun dynamicTypography(context: Context): Typography { + val defaultTypography = Typography() + return Typography( + arcLarge = + defaultTypography.arcLarge.updatedTextStyle( + context = context, + fontRes = R.string.wear_compose_material3_arc_large_font_family, + fontSizeRes = R.dimen.wear_compose_material3_arc_large_font_size, + variationSettings = + WearComposeMaterial3VariableFontTokens.ArcLargeVariationSettings, + ), + arcMedium = + defaultTypography.arcMedium.updatedTextStyle( + context = context, + fontRes = R.string.wear_compose_material3_arc_medium_font_family, + fontSizeRes = R.dimen.wear_compose_material3_arc_medium_font_size, + variationSettings = + WearComposeMaterial3VariableFontTokens.ArcMediumVariationSettings, + ), + arcSmall = + defaultTypography.arcSmall.updatedTextStyle( + context = context, + fontRes = R.string.wear_compose_material3_arc_small_font_family, + fontSizeRes = R.dimen.wear_compose_material3_arc_small_font_size, + variationSettings = + WearComposeMaterial3VariableFontTokens.ArcSmallVariationSettings, + ), + bodyLarge = + defaultTypography.bodyLarge.updatedTextStyle( + context = context, + fontRes = R.string.wear_compose_material3_body_large_font_family, + fontSizeRes = R.dimen.wear_compose_material3_body_large_font_size, + variationSettings = + WearComposeMaterial3VariableFontTokens.BodyLargeVariationSettings, + ), + bodyMedium = + defaultTypography.bodyMedium.updatedTextStyle( + context = context, + fontRes = R.string.wear_compose_material3_body_medium_font_family, + fontSizeRes = R.dimen.wear_compose_material3_body_medium_font_size, + variationSettings = + WearComposeMaterial3VariableFontTokens.BodyMediumVariationSettings, + ), + bodySmall = + defaultTypography.bodySmall.updatedTextStyle( + context = context, + fontRes = R.string.wear_compose_material3_body_small_font_family, + fontSizeRes = R.dimen.wear_compose_material3_body_small_font_size, + variationSettings = + WearComposeMaterial3VariableFontTokens.BodySmallVariationSettings, + ), + bodyExtraSmall = + defaultTypography.bodyExtraSmall.updatedTextStyle( + context = context, + fontRes = R.string.wear_compose_material3_body_extra_small_font_family, + fontSizeRes = R.dimen.wear_compose_material3_body_extra_small_font_size, + variationSettings = + WearComposeMaterial3VariableFontTokens.BodyExtraSmallVariationSettings, + ), + displayLarge = + defaultTypography.displayLarge.updatedTextStyle( + context = context, + fontRes = R.string.wear_compose_material3_display_large_font_family, + fontSizeRes = R.dimen.wear_compose_material3_display_large_font_size, + variationSettings = + WearComposeMaterial3VariableFontTokens.DisplayLargeVariationSettings, + ), + displayMedium = + defaultTypography.displayMedium.updatedTextStyle( + context = context, + fontRes = R.string.wear_compose_material3_display_medium_font_family, + fontSizeRes = R.dimen.wear_compose_material3_display_medium_font_size, + variationSettings = + WearComposeMaterial3VariableFontTokens.DisplayMediumVariationSettings, + ), + displaySmall = + defaultTypography.displaySmall.updatedTextStyle( + context = context, + fontRes = R.string.wear_compose_material3_display_small_font_family, + fontSizeRes = R.dimen.wear_compose_material3_display_small_font_size, + variationSettings = + WearComposeMaterial3VariableFontTokens.DisplaySmallVariationSettings, + ), + labelLarge = + defaultTypography.labelLarge.updatedTextStyle( + context = context, + fontRes = R.string.wear_compose_material3_label_large_font_family, + fontSizeRes = R.dimen.wear_compose_material3_label_large_font_size, + variationSettings = + WearComposeMaterial3VariableFontTokens.LabelLargeVariationSettings, + ), + labelMedium = + defaultTypography.labelMedium.updatedTextStyle( + context = context, + fontRes = R.string.wear_compose_material3_label_medium_font_family, + fontSizeRes = R.dimen.wear_compose_material3_label_medium_font_size, + variationSettings = + WearComposeMaterial3VariableFontTokens.LabelMediumVariationSettings, + ), + labelSmall = + defaultTypography.labelSmall.updatedTextStyle( + context = context, + fontRes = R.string.wear_compose_material3_label_small_font_family, + fontSizeRes = R.dimen.wear_compose_material3_label_small_font_size, + variationSettings = + WearComposeMaterial3VariableFontTokens.LabelSmallVariationSettings, + ), + numeralExtraLarge = + defaultTypography.numeralExtraLarge.updatedTextStyle( + context = context, + fontRes = R.string.wear_compose_material3_numeral_extra_large_font_family, + fontSizeRes = R.dimen.wear_compose_material3_numeral_extra_large_font_size, + variationSettings = + WearComposeMaterial3VariableFontTokens.NumeralExtraLargeVariationSettings, + ), + numeralLarge = + defaultTypography.numeralLarge.updatedTextStyle( + context = context, + fontRes = R.string.wear_compose_material3_numeral_large_font_family, + fontSizeRes = R.dimen.wear_compose_material3_numeral_large_font_size, + variationSettings = + WearComposeMaterial3VariableFontTokens.NumeralLargeVariationSettings, + ), + numeralMedium = + defaultTypography.numeralMedium.updatedTextStyle( + context = context, + fontRes = R.string.wear_compose_material3_numeral_medium_font_family, + fontSizeRes = R.dimen.wear_compose_material3_numeral_medium_font_size, + variationSettings = + WearComposeMaterial3VariableFontTokens.NumeralMediumVariationSettings, + ), + numeralSmall = + defaultTypography.numeralSmall.updatedTextStyle( + context = context, + fontRes = R.string.wear_compose_material3_numeral_small_font_family, + fontSizeRes = R.dimen.wear_compose_material3_numeral_small_font_size, + variationSettings = + WearComposeMaterial3VariableFontTokens.NumeralSmallVariationSettings, + ), + numeralExtraSmall = + defaultTypography.numeralExtraSmall.updatedTextStyle( + context = context, + fontRes = R.string.wear_compose_material3_numeral_extra_small_font_family, + fontSizeRes = R.dimen.wear_compose_material3_numeral_extra_small_font_size, + variationSettings = + WearComposeMaterial3VariableFontTokens.NumeralExtraSmallVariationSettings, + ), + titleLarge = + defaultTypography.titleLarge.updatedTextStyle( + context = context, + fontRes = R.string.wear_compose_material3_title_large_font_family, + fontSizeRes = R.dimen.wear_compose_material3_title_large_font_size, + variationSettings = + WearComposeMaterial3VariableFontTokens.TitleLargeVariationSettings, + ), + titleMedium = + defaultTypography.titleMedium.updatedTextStyle( + context = context, + fontRes = R.string.wear_compose_material3_title_medium_font_family, + fontSizeRes = R.dimen.wear_compose_material3_title_medium_font_size, + variationSettings = + WearComposeMaterial3VariableFontTokens.TitleMediumVariationSettings, + ), + titleSmall = + defaultTypography.titleSmall.updatedTextStyle( + context = context, + fontRes = R.string.wear_compose_material3_title_small_font_family, + fontSizeRes = R.dimen.wear_compose_material3_title_small_font_size, + variationSettings = + WearComposeMaterial3VariableFontTokens.TitleSmallVariationSettings, + ), + ) + } +} diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/theme/WearComposeMaterial3VariableFontTokens.kt b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/theme/WearComposeMaterial3VariableFontTokens.kt new file mode 100644 index 000000000..1b42a3b05 --- /dev/null +++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/theme/WearComposeMaterial3VariableFontTokens.kt @@ -0,0 +1,186 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.permissioncontroller.permission.ui.wear.theme + +import androidx.compose.ui.text.font.FontVariation + +internal object WearComposeMaterial3VariableFontTokens { + val ArcLargeVariationSettings = + FontVariation.Settings( + FontVariation.Setting("ROND", WearComposeMaterial3TypeScaleTokens.ArcLargeRoundness), + FontVariation.Setting("wdth", WearComposeMaterial3TypeScaleTokens.ArcLargeWidth), + FontVariation.Setting("wght", WearComposeMaterial3TypeScaleTokens.ArcLargeWeight), + ) + val ArcMediumVariationSettings = + FontVariation.Settings( + FontVariation.Setting("ROND", WearComposeMaterial3TypeScaleTokens.ArcMediumRoundness), + FontVariation.Setting("wdth", WearComposeMaterial3TypeScaleTokens.ArcMediumWidth), + FontVariation.Setting("wght", WearComposeMaterial3TypeScaleTokens.ArcMediumWeight), + ) + val ArcSmallVariationSettings = + FontVariation.Settings( + FontVariation.Setting("ROND", WearComposeMaterial3TypeScaleTokens.ArcSmallRoundness), + FontVariation.Setting("wdth", WearComposeMaterial3TypeScaleTokens.ArcSmallWidth), + FontVariation.Setting("wght", WearComposeMaterial3TypeScaleTokens.ArcSmallWeight), + ) + val BodyExtraSmallVariationSettings = + FontVariation.Settings( + FontVariation.Setting( + "ROND", + WearComposeMaterial3TypeScaleTokens.BodyExtraSmallRoundness, + ), + FontVariation.Setting("wdth", WearComposeMaterial3TypeScaleTokens.BodyExtraSmallWidth), + FontVariation.Setting("wght", WearComposeMaterial3TypeScaleTokens.BodyExtraSmallWeight), + ) + val BodyLargeVariationSettings = + FontVariation.Settings( + FontVariation.Setting("ROND", WearComposeMaterial3TypeScaleTokens.BodyLargeRoundness), + FontVariation.Setting("wdth", WearComposeMaterial3TypeScaleTokens.BodyLargeWidth), + FontVariation.Setting("wght", WearComposeMaterial3TypeScaleTokens.BodyLargeWeight), + ) + val BodyMediumVariationSettings = + FontVariation.Settings( + FontVariation.Setting("ROND", WearComposeMaterial3TypeScaleTokens.BodyMediumRoundness), + FontVariation.Setting("wdth", WearComposeMaterial3TypeScaleTokens.BodyMediumWidth), + FontVariation.Setting("wght", WearComposeMaterial3TypeScaleTokens.BodyMediumWeight), + ) + val BodySmallVariationSettings = + FontVariation.Settings( + FontVariation.Setting("ROND", WearComposeMaterial3TypeScaleTokens.BodySmallRoundness), + FontVariation.Setting("wdth", WearComposeMaterial3TypeScaleTokens.BodySmallWidth), + FontVariation.Setting("wght", WearComposeMaterial3TypeScaleTokens.BodySmallWeight), + ) + val DisplayLargeVariationSettings = + FontVariation.Settings( + FontVariation.Setting( + "ROND", + WearComposeMaterial3TypeScaleTokens.DisplayLargeRoundness, + ), + FontVariation.Setting("wdth", WearComposeMaterial3TypeScaleTokens.DisplayLargeWidth), + FontVariation.Setting("wght", WearComposeMaterial3TypeScaleTokens.DisplayLargeWeight), + ) + val DisplayMediumVariationSettings = + FontVariation.Settings( + FontVariation.Setting( + "ROND", + WearComposeMaterial3TypeScaleTokens.DisplayMediumRoundness, + ), + FontVariation.Setting("wdth", WearComposeMaterial3TypeScaleTokens.DisplayMediumWidth), + FontVariation.Setting("wght", WearComposeMaterial3TypeScaleTokens.DisplayMediumWeight), + ) + val DisplaySmallVariationSettings = + FontVariation.Settings( + FontVariation.Setting( + "ROND", + WearComposeMaterial3TypeScaleTokens.DisplaySmallRoundness, + ), + FontVariation.Setting("wdth", WearComposeMaterial3TypeScaleTokens.DisplaySmallWidth), + FontVariation.Setting("wght", WearComposeMaterial3TypeScaleTokens.DisplaySmallWeight), + ) + val LabelLargeVariationSettings = + FontVariation.Settings( + FontVariation.Setting("ROND", WearComposeMaterial3TypeScaleTokens.LabelLargeRoundness), + FontVariation.Setting("wdth", WearComposeMaterial3TypeScaleTokens.LabelLargeWidth), + FontVariation.Setting("wght", WearComposeMaterial3TypeScaleTokens.LabelLargeWeight), + ) + val LabelMediumVariationSettings = + FontVariation.Settings( + FontVariation.Setting("ROND", WearComposeMaterial3TypeScaleTokens.LabelMediumRoundness), + FontVariation.Setting("wdth", WearComposeMaterial3TypeScaleTokens.LabelMediumWidth), + FontVariation.Setting("wght", WearComposeMaterial3TypeScaleTokens.LabelMediumWeight), + ) + val LabelSmallVariationSettings = + FontVariation.Settings( + FontVariation.Setting("ROND", WearComposeMaterial3TypeScaleTokens.LabelSmallRoundness), + FontVariation.Setting("wdth", WearComposeMaterial3TypeScaleTokens.LabelSmallWidth), + FontVariation.Setting("wght", WearComposeMaterial3TypeScaleTokens.LabelSmallWeight), + ) + val NumeralExtraLargeVariationSettings = + FontVariation.Settings( + FontVariation.Setting( + "ROND", + WearComposeMaterial3TypeScaleTokens.NumeralExtraLargeRoundness, + ), + FontVariation.Setting( + "wdth", + WearComposeMaterial3TypeScaleTokens.NumeralExtraLargeWidth, + ), + FontVariation.Setting( + "wght", + WearComposeMaterial3TypeScaleTokens.NumeralExtraLargeWeight, + ), + ) + val NumeralExtraSmallVariationSettings = + FontVariation.Settings( + FontVariation.Setting( + "ROND", + WearComposeMaterial3TypeScaleTokens.NumeralExtraSmallRoundness, + ), + FontVariation.Setting( + "wdth", + WearComposeMaterial3TypeScaleTokens.NumeralExtraSmallWidth, + ), + FontVariation.Setting( + "wght", + WearComposeMaterial3TypeScaleTokens.NumeralExtraSmallWeight, + ), + ) + val NumeralLargeVariationSettings = + FontVariation.Settings( + FontVariation.Setting( + "ROND", + WearComposeMaterial3TypeScaleTokens.NumeralLargeRoundness, + ), + FontVariation.Setting("wdth", WearComposeMaterial3TypeScaleTokens.NumeralLargeWidth), + FontVariation.Setting("wght", WearComposeMaterial3TypeScaleTokens.NumeralLargeWeight), + ) + val NumeralMediumVariationSettings = + FontVariation.Settings( + FontVariation.Setting( + "ROND", + WearComposeMaterial3TypeScaleTokens.NumeralMediumRoundness, + ), + FontVariation.Setting("wdth", WearComposeMaterial3TypeScaleTokens.NumeralMediumWidth), + FontVariation.Setting("wght", WearComposeMaterial3TypeScaleTokens.NumeralMediumWeight), + ) + val NumeralSmallVariationSettings = + FontVariation.Settings( + FontVariation.Setting( + "ROND", + WearComposeMaterial3TypeScaleTokens.NumeralSmallRoundness, + ), + FontVariation.Setting("wdth", WearComposeMaterial3TypeScaleTokens.NumeralSmallWidth), + FontVariation.Setting("wght", WearComposeMaterial3TypeScaleTokens.NumeralSmallWeight), + ) + val TitleLargeVariationSettings = + FontVariation.Settings( + FontVariation.Setting("ROND", WearComposeMaterial3TypeScaleTokens.TitleLargeRoundness), + FontVariation.Setting("wdth", WearComposeMaterial3TypeScaleTokens.TitleLargeWidth), + FontVariation.Setting("wght", WearComposeMaterial3TypeScaleTokens.TitleLargeWeight), + ) + val TitleMediumVariationSettings = + FontVariation.Settings( + FontVariation.Setting("ROND", WearComposeMaterial3TypeScaleTokens.TitleMediumRoundness), + FontVariation.Setting("wdth", WearComposeMaterial3TypeScaleTokens.TitleMediumWidth), + FontVariation.Setting("wght", WearComposeMaterial3TypeScaleTokens.TitleMediumWeight), + ) + val TitleSmallVariationSettings = + FontVariation.Settings( + FontVariation.Setting("ROND", WearComposeMaterial3TypeScaleTokens.TitleSmallRoundness), + FontVariation.Setting("wdth", WearComposeMaterial3TypeScaleTokens.TitleSmallWidth), + FontVariation.Setting("wght", WearComposeMaterial3TypeScaleTokens.TitleSmallWeight), + ) +} diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/theme/WearMaterialBridgedLegacyTheme.kt b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/theme/WearMaterialBridgedLegacyTheme.kt new file mode 100644 index 000000000..160dc2e93 --- /dev/null +++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/theme/WearMaterialBridgedLegacyTheme.kt @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.permissioncontroller.permission.ui.wear.theme + +import android.os.Build +import androidx.annotation.RequiresApi +import androidx.compose.ui.unit.sp +import androidx.wear.compose.material.Colors +import androidx.wear.compose.material.Shapes +import androidx.wear.compose.material.Typography + +/** + * This exists to support Permission Controller screens that may still use Material 2.5 components + * to maintain consistency with the settings screens. + * + * However to avoid maintaining two sets of resources for overlays, this class construct 2.5 theme + * from 3.0 + */ +@RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM) +internal class WearMaterialBridgedLegacyTheme +private constructor(newTheme: WearOverlayableMaterial3Theme) { + + val colors = + newTheme.colorScheme.run { + Colors( + background = background, + onBackground = onBackground, + primary = onPrimaryContainer, // primary90 + primaryVariant = primaryDim, // primary80 + onPrimary = onPrimary, // primary10 + secondary = tertiary, // Tertiary90 + secondaryVariant = tertiaryDim, // Tertiary60 - Tertiary80 BestFit. + onSecondary = onTertiary, // Tertiary10 + surface = surfaceContainer, // neutral20 + onSurface = onSurface, // neutral95 + onSurfaceVariant = onSurfaceVariant, // neutralVariant80 + ) + } + + // Based on: + // Material 2: + // wear/compose/compose-material/src/main/java/androidx/wear/compose/material/Typography.kt + // Material 3: + // wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/tokens/TypeScaleTokens.kt + val typography = + newTheme.typography.run { + Typography( + display1 = displayLarge, // 40.sp + display2 = displayMedium.copy(fontSize = 34.sp, lineHeight = 40.sp), + display3 = displayMedium, // 30.sp + title1 = displaySmall, // 24.sp + title2 = titleLarge, // 20.sp + title3 = titleMedium, // 16.sp + body1 = bodyLarge, // 16.sp + body2 = bodyMedium, // 14.sp + caption1 = bodyMedium, // 14.sp + caption2 = bodySmall, // 12.sp + caption3 = bodyExtraSmall, // 10.sp + button = labelMedium, // 15.sp + ) + } + + val shapes = newTheme.shapes.run { Shapes(large = large, medium = medium, small = small) } + + companion object { + fun createFrom(newTheme: WearOverlayableMaterial3Theme) = + WearMaterialBridgedLegacyTheme(newTheme) + } +} diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/theme/WearOverlayableMaterial3Theme.kt b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/theme/WearOverlayableMaterial3Theme.kt new file mode 100644 index 000000000..8aeb5f74d --- /dev/null +++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/theme/WearOverlayableMaterial3Theme.kt @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.permissioncontroller.permission.ui.wear.theme + +import android.content.Context +import android.os.Build +import androidx.annotation.RequiresApi + +/** + * Theme wrapper providing Material 3 styling while maintaining compatibility with Runtime Resource + * Overlay (RRO). + * + * Uses the tonal palette from the previous Material Design version until dynamic color tokens are + * available in SDK 36. + */ +@RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM) +internal class WearOverlayableMaterial3Theme(context: Context) { + val colorScheme = + if (Build.VERSION.SDK_INT >= 36) { + WearComposeMaterial3ColorScheme.dynamicColorScheme(context) + } else { + WearComposeMaterial3ColorScheme.tonalColorScheme(context) + } + + val typography = WearComposeMaterial3Typography.dynamicTypography(context) + + val shapes = WearComposeMaterial3Shapes.dynamicShapes(context) +} diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/theme/WearPermissionTheme.kt b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/theme/WearPermissionTheme.kt index 933cf19f9..8823bee07 100644 --- a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/theme/WearPermissionTheme.kt +++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/theme/WearPermissionTheme.kt @@ -1,3 +1,18 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package com.android.permissioncontroller.permission.ui.wear.theme import android.content.Context @@ -14,11 +29,71 @@ import androidx.compose.ui.text.font.FontFamily import androidx.wear.compose.material.Colors import androidx.wear.compose.material.MaterialTheme import androidx.wear.compose.material.Typography +import androidx.wear.compose.material3.MaterialTheme as Material3Theme import com.android.permissioncontroller.R +import com.android.permissioncontroller.permission.ui.wear.theme.WearPermissionMaterialUIVersion.MATERIAL2_5 +import com.android.permissioncontroller.permission.ui.wear.theme.WearPermissionMaterialUIVersion.MATERIAL3 + +/** This enum is used to specify the material version used for a specific screen */ +enum class WearPermissionMaterialUIVersion { + MATERIAL2_5, + MATERIAL3, +} + +/** + * Supports both Material 3 and Material 2_5 theme. default version for permission theme will be 2_5 + * until we migrate enough screens to 3. 2_5 version will use material 3 overlay resources if we + * enable material3 for even one screen (Permission screens will be migrated in phases). + */ +@Composable +fun WearPermissionTheme( + version: WearPermissionMaterialUIVersion = MATERIAL2_5, + content: @Composable () -> Unit, +) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.VANILLA_ICE_CREAM) { + WearPermissionLegacyTheme(content) + } else { + // Whether we are ready to use material3 for any screen. + val useBridgedTheme = ResourceHelper.material3Enabled -/** The Material 3 Theme Wrapper for Supporting RRO. */ + // Material3 UI controls are still being used in the screen that the theme is applied + if (version == MATERIAL3) { + val material3Theme = WearOverlayableMaterial3Theme(LocalContext.current) + Material3Theme( + colorScheme = material3Theme.colorScheme, + typography = material3Theme.typography, + shapes = material3Theme.shapes, + content = content, + ) + } + // Material2_5 UI controls are still being used in the screen that the theme is applied, + // But some in-app screens(like permission grant screen) are migrated to material3. + // To avoid having two set of overlay resources, we will use material3 overlay resources to + // support material2_5 UI controls as well. + else if (version == MATERIAL2_5 && useBridgedTheme) { + val material3Theme = WearOverlayableMaterial3Theme(LocalContext.current) + val bridgedLegacyTheme = WearMaterialBridgedLegacyTheme.createFrom(material3Theme) + MaterialTheme( + colors = bridgedLegacyTheme.colors, + typography = bridgedLegacyTheme.typography, + shapes = bridgedLegacyTheme.shapes, + content = content, + ) + } + // We are not ready for material3 yet in any screens. + else { + WearPermissionLegacyTheme(content) + } + } +} + +/** + * The Material 2.5 Theme Wrapper for Supporting RRO with legacy resources. This theme is kept here + * for backward compatibility. When grant screen is updated to material3 will clean up legacy + * resources. + */ @Composable -fun WearPermissionTheme(content: @Composable () -> Unit) { +fun WearPermissionLegacyTheme(content: @Composable () -> Unit) { val context = LocalContext.current val colors = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { diff --git a/PermissionController/src/com/android/permissioncontroller/permission/utils/PermissionMapping.kt b/PermissionController/src/com/android/permissioncontroller/permission/utils/PermissionMapping.kt index 0d1d960ab..a3446f802 100644 --- a/PermissionController/src/com/android/permissioncontroller/permission/utils/PermissionMapping.kt +++ b/PermissionController/src/com/android/permissioncontroller/permission/utils/PermissionMapping.kt @@ -139,7 +139,6 @@ object PermissionMapping { PLATFORM_PERMISSIONS[Manifest.permission.NEARBY_WIFI_DEVICES] = Manifest.permission_group.NEARBY_DEVICES } - // Ranging permission will be supported from Android B+, update this when isAtLeastB() // is available. if (SdkLevel.isAtLeastV() && Flags.rangingPermissionEnabled()) { diff --git a/PermissionController/src/com/android/permissioncontroller/permission/utils/Utils.java b/PermissionController/src/com/android/permissioncontroller/permission/utils/Utils.java index e5de63f32..3d3b47272 100644 --- a/PermissionController/src/com/android/permissioncontroller/permission/utils/Utils.java +++ b/PermissionController/src/com/android/permissioncontroller/permission/utils/Utils.java @@ -126,6 +126,7 @@ import java.util.List; import java.util.Locale; import java.util.Random; import java.util.Set; +import java.util.function.Supplier; public final class Utils { @@ -1566,18 +1567,40 @@ public final class Utils { public static String getEnterpriseString(@NonNull Context context, @NonNull String updatableStringId, int defaultStringId, @NonNull Object... formatArgs) { return SdkLevel.isAtLeastT() - ? getUpdatableEnterpriseString( - context, updatableStringId, defaultStringId, formatArgs) + ? getUpdatableEnterpriseString(context, updatableStringId, + () -> context.getString(defaultStringId, formatArgs), formatArgs) : context.getString(defaultStringId, formatArgs); } + /** + * Selects the appropriate enterprise string for the provided resource ID and a fallback string + */ + @NonNull + public static String getEnterpriseString(@NonNull Context context, + @NonNull String updatableStringId, @NonNull String defaultString) { + return SdkLevel.isAtLeastT() + ? getUpdatableEnterpriseString(context, updatableStringId, () -> defaultString) + : defaultString; + } + @RequiresApi(Build.VERSION_CODES.TIRAMISU) @NonNull private static String getUpdatableEnterpriseString(@NonNull Context context, - @NonNull String updatableStringId, int defaultStringId, @NonNull Object... formatArgs) { + @NonNull String updatableStringId, @NonNull Supplier<String> defaultStringLoader, + @NonNull Object... formatArgs) { DevicePolicyManager dpm = getSystemServiceSafe(context, DevicePolicyManager.class); - return dpm.getResources().getString(updatableStringId, () -> context.getString( - defaultStringId, formatArgs), formatArgs); + return dpm.getResources().getString(updatableStringId, defaultStringLoader, formatArgs); + } + + /** + * Returns the profile label from the {@link UserManager} for the provided profile + */ + @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM) + @NonNull + public static String getProfileLabel(@NonNull UserHandle profile, @NonNull Context context) { + Context profileContext = context.createContextAsUser(profile, 0); + UserManager profileUserManager = profileContext.getSystemService(UserManager.class); + return profileUserManager.getProfileLabel(); } /** diff --git a/PermissionController/src/com/android/permissioncontroller/role/Role.md b/PermissionController/src/com/android/permissioncontroller/role/Role.md index d4a514784..255214495 100644 --- a/PermissionController/src/com/android/permissioncontroller/role/Role.md +++ b/PermissionController/src/com/android/permissioncontroller/role/Role.md @@ -62,6 +62,13 @@ is optional and defaults to `false`. the Java method on the `Flags` class which will be invoked via reflection. Note that any new aconfig library dependency will need corresponding jarjar rules for PermissionController and the system service JAR. +- `ignoreDisabledSystemPackageWhenGranting`: Whether the role should ignore the requested +permissions of the disabled system package (if any) when granting permissions. If `false`, the +permission will need to be requested by the disabled system package as well, if there is one. This +attribute is optional and defaults to the opposite of `systemOnly` on Android S+, or `true` below +Android S. **Note:** Extra care should be taken when adding a runtime permission to a role with +this attribute explicitly set to `true`, because that may allow apps to update and silently obtain +a new runtime permission. - `label`: The string resource for the label of the role, e.g. `@string/role_sms_label`, which says "Default SMS app". For default apps, this string will appear in the default app detail page as the title. This attribute is required if the role is `visible`. diff --git a/PermissionController/src/com/android/permissioncontroller/role/ui/DefaultAppListChildFragment.java b/PermissionController/src/com/android/permissioncontroller/role/ui/DefaultAppListChildFragment.java index 0b96eb8ba..48472bc5e 100644 --- a/PermissionController/src/com/android/permissioncontroller/role/ui/DefaultAppListChildFragment.java +++ b/PermissionController/src/com/android/permissioncontroller/role/ui/DefaultAppListChildFragment.java @@ -22,6 +22,7 @@ import android.content.Intent; import android.content.pm.ApplicationInfo; import android.os.Bundle; import android.os.UserHandle; +import android.permission.flags.Flags; import android.provider.Settings; import android.util.ArrayMap; @@ -36,6 +37,7 @@ import androidx.preference.PreferenceGroup; import androidx.preference.PreferenceManager; import androidx.preference.PreferenceScreen; +import com.android.modules.utils.build.SdkLevel; import com.android.permissioncontroller.R; import com.android.permissioncontroller.permission.utils.Utils; import com.android.permissioncontroller.role.utils.PackageUtils; @@ -145,15 +147,25 @@ public class DefaultAppListChildFragment<PF extends PreferenceFragmentCompat addMoreDefaultAppsPreference(preferenceScreen, oldPreferences, context); addManageDomainUrlsPreference(preferenceScreen, oldPreferences, context); if (hasWorkProfile && !workRoleItems.isEmpty()) { + String defaultWorkTitle; + if (SdkLevel.isAtLeastV() && Flags.useProfileLabelsForDefaultAppSectionTitles()) { + defaultWorkTitle = Utils.getProfileLabel(mViewModel.getWorkProfile(), context); + } else { + defaultWorkTitle = context.getString(R.string.default_apps_for_work); + } String workTitle = Utils.getEnterpriseString(context, - DefaultAppSettings.WORK_PROFILE_DEFAULT_APPS_TITLE, - R.string.default_apps_for_work); + DefaultAppSettings.WORK_PROFILE_DEFAULT_APPS_TITLE, defaultWorkTitle); addPreferenceCategory(oldWorkPreferenceCategory, PREFERENCE_KEY_WORK_CATEGORY, workTitle, preferenceScreen, workRoleItems, oldWorkPreferences, this, mViewModel.getWorkProfile(), context); } if (hasPrivateProfile && !privateRoleItems.isEmpty()) { - String privateTitle = context.getString(R.string.default_apps_for_private_profile); + String privateTitle; + if (SdkLevel.isAtLeastV() && Flags.useProfileLabelsForDefaultAppSectionTitles()) { + privateTitle = Utils.getProfileLabel(mViewModel.getPrivateProfile(), context); + } else { + privateTitle = context.getString(R.string.default_apps_for_private_profile); + } addPreferenceCategory(oldPrivatePreferenceCategory, PREFERENCE_KEY_PRIVATE_CATEGORY, privateTitle, preferenceScreen, privateRoleItems, oldPrivatePreferences, this, mViewModel.getPrivateProfile(), context); diff --git a/PermissionController/src/com/android/permissioncontroller/role/ui/wear/WearRequestRoleScreen.kt b/PermissionController/src/com/android/permissioncontroller/role/ui/wear/WearRequestRoleScreen.kt index 13a9cb6d6..aa9b31e0d 100644 --- a/PermissionController/src/com/android/permissioncontroller/role/ui/wear/WearRequestRoleScreen.kt +++ b/PermissionController/src/com/android/permissioncontroller/role/ui/wear/WearRequestRoleScreen.kt @@ -30,22 +30,26 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import androidx.wear.compose.material.ChipDefaults -import androidx.wear.compose.material.MaterialTheme import com.android.permissioncontroller.R -import com.android.permissioncontroller.permission.ui.wear.elements.Chip -import com.android.permissioncontroller.permission.ui.wear.elements.ListFooter import com.android.permissioncontroller.permission.ui.wear.elements.ScrollableScreen -import com.android.permissioncontroller.permission.ui.wear.elements.ToggleChip import com.android.permissioncontroller.permission.ui.wear.elements.ToggleChipToggleControl -import com.android.permissioncontroller.permission.ui.wear.elements.toggleChipBackgroundColors +import com.android.permissioncontroller.permission.ui.wear.elements.material3.WearPermissionButton +import com.android.permissioncontroller.permission.ui.wear.elements.material3.WearPermissionButtonStyle +import com.android.permissioncontroller.permission.ui.wear.elements.material3.WearPermissionIconBuilder +import com.android.permissioncontroller.permission.ui.wear.elements.material3.WearPermissionListFooter +import com.android.permissioncontroller.permission.ui.wear.elements.material3.WearPermissionToggleControl +import com.android.permissioncontroller.permission.ui.wear.elements.material3.WearPermissionToggleControlStyle +import com.android.permissioncontroller.permission.ui.wear.theme.ResourceHelper +import com.android.permissioncontroller.permission.ui.wear.theme.WearPermissionMaterialUIVersion +import com.android.permissioncontroller.permission.ui.wear.theme.WearPermissionMaterialUIVersion.MATERIAL2_5 +import com.android.permissioncontroller.permission.ui.wear.theme.WearPermissionMaterialUIVersion.MATERIAL3 import com.android.permissioncontroller.role.ui.ManageRoleHolderStateLiveData @Composable fun WearRequestRoleScreen( helper: WearRequestRoleHelper, onSetAsDefault: (Boolean, String?) -> Unit, - onCanceled: () -> Unit + onCanceled: () -> Unit, ) { val roleLiveData = helper.viewModel.roleLiveData.observeAsState(emptyList()) val manageRoleHolderState = @@ -74,8 +78,14 @@ fun WearRequestRoleScreen( helper.initializeSelectedPackageName() } } - + val materialUIVersion = + if (ResourceHelper.material3Enabled) { + MATERIAL3 + } else { + MATERIAL2_5 + } WearRequestRoleContent( + materialUIVersion, isLoading, helper, roleLiveData.value, @@ -85,7 +95,7 @@ fun WearRequestRoleScreen( onCheckedChanged, onDontAskAgainCheckedChanged, onSetAsDefault, - onCanceled + onCanceled, ) if (isLoading && roleLiveData.value.isNotEmpty()) { @@ -95,6 +105,7 @@ fun WearRequestRoleScreen( @Composable internal fun WearRequestRoleContent( + materialUIVersion: WearPermissionMaterialUIVersion, isLoading: Boolean, helper: WearRequestRoleHelper, qualifyingApplications: List<Pair<ApplicationInfo, Boolean>>, @@ -104,56 +115,74 @@ internal fun WearRequestRoleContent( onCheckedChanged: (Boolean, String?, Boolean) -> Unit, onDontAskAgainCheckedChanged: (Boolean) -> Unit, onSetAsDefault: (Boolean, String?) -> Unit, - onCanceled: () -> Unit + onCanceled: () -> Unit, ) { ScrollableScreen( + materialUIVersion = materialUIVersion, image = helper.getIcon(), title = helper.getTitle(), showTimeText = false, - isLoading = isLoading + isLoading = isLoading, ) { - helper.getNonePreference(qualifyingApplications, selectedPackageName)?.let { + helper.getNonePreference(qualifyingApplications, selectedPackageName)?.let { pref -> item { - ToggleChip( - label = it.label, - icon = it.icon, - enabled = enabled && it.enabled, - checked = it.checked, + WearPermissionToggleControl( + materialUIVersion = materialUIVersion, + label = pref.label, + iconBuilder = pref.icon?.let { WearPermissionIconBuilder.builder(it) }, + enabled = enabled && pref.enabled, + checked = pref.checked, onCheckedChanged = { checked -> - run { onCheckedChanged(checked, it.packageName, it.isHolder) } + onCheckedChanged(checked, pref.packageName, pref.isHolder) }, toggleControl = ToggleChipToggleControl.Radio, - labelMaxLine = Integer.MAX_VALUE + labelMaxLines = Integer.MAX_VALUE, ) } - it.subTitle?.let { subTitle -> item { ListFooter(description = subTitle) } } + pref.subTitle?.let { subTitle -> + item { + WearPermissionListFooter( + materialUIVersion = materialUIVersion, + label = subTitle, + ) + } + } } for (pref in helper.getPreferences(qualifyingApplications, selectedPackageName)) { item { - ToggleChip( + WearPermissionToggleControl( + materialUIVersion = materialUIVersion, label = pref.label, - icon = pref.icon, + iconBuilder = pref.icon?.let { WearPermissionIconBuilder.builder(it) }, enabled = enabled && pref.enabled, checked = pref.checked, onCheckedChanged = { checked -> - run { onCheckedChanged(checked, pref.packageName, pref.isHolder) } + onCheckedChanged(checked, pref.packageName, pref.isHolder) }, toggleControl = ToggleChipToggleControl.Radio, ) } - pref.subTitle?.let { subTitle -> item { ListFooter(description = subTitle) } } + pref.subTitle?.let { subTitle -> + item { + WearPermissionListFooter( + materialUIVersion = materialUIVersion, + label = subTitle, + ) + } + } } if (helper.showDontAskButton()) { item { - ToggleChip( + WearPermissionToggleControl( + materialUIVersion = materialUIVersion, checked = dontAskAgain, enabled = enabled, onCheckedChanged = { checked -> run { onDontAskAgainCheckedChanged(checked) } }, label = stringResource(R.string.request_role_dont_ask_again), toggleControl = ToggleChipToggleControl.Checkbox, - colors = toggleChipBackgroundColors(), + style = WearPermissionToggleControlStyle.Transparent, modifier = Modifier.testTag("com.android.permissioncontroller:id/dont_ask_again"), ) @@ -163,17 +192,18 @@ internal fun WearRequestRoleContent( item { Spacer(modifier = Modifier.height(14.dp)) } item { - Chip( + WearPermissionButton( + materialUIVersion = materialUIVersion, label = stringResource(R.string.request_role_set_as_default), - textColor = MaterialTheme.colors.background, - colors = ChipDefaults.primaryChipColors(), + style = WearPermissionButtonStyle.Primary, enabled = helper.shouldSetAsDefaultEnabled(enabled), onClick = { onSetAsDefault(dontAskAgain, selectedPackageName) }, modifier = Modifier.testTag("android:id/button1"), ) } item { - Chip( + WearPermissionButton( + materialUIVersion = materialUIVersion, label = stringResource(R.string.cancel), enabled = enabled, onClick = { onCanceled() }, diff --git a/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/ClickableDisabledSwitchPreference.java b/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/ClickableDisabledSwitchPreference.java index 3ab4faa66..54cb86ff3 100644 --- a/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/ClickableDisabledSwitchPreference.java +++ b/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/ClickableDisabledSwitchPreference.java @@ -26,7 +26,7 @@ import android.view.ViewGroup; import androidx.annotation.RequiresApi; import androidx.fragment.app.Fragment; import androidx.preference.PreferenceViewHolder; -import androidx.preference.SwitchPreference; +import androidx.preference.SwitchPreferenceCompat; import com.android.permissioncontroller.R; import com.android.permissioncontroller.safetycenter.ui.model.PrivacyControlsViewModel; @@ -38,7 +38,7 @@ import com.android.permissioncontroller.safetycenter.ui.model.PrivacyControlsVie * method will not register any changes while it appears disabled. */ @RequiresApi(TIRAMISU) -public class ClickableDisabledSwitchPreference extends SwitchPreference { +public class ClickableDisabledSwitchPreference extends SwitchPreferenceCompat { private boolean mAppearDisabled; diff --git a/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/IssueCardPreference.java b/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/IssueCardPreference.java index 7622270b9..88759797e 100644 --- a/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/IssueCardPreference.java +++ b/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/IssueCardPreference.java @@ -52,6 +52,7 @@ import androidx.preference.PreferenceViewHolder; import com.android.modules.utils.build.SdkLevel; import com.android.permissioncontroller.R; import com.android.permissioncontroller.safetycenter.ui.model.SafetyCenterViewModel; +import com.android.settingslib.widget.GroupSectionDividerMixin; import com.google.android.material.button.MaterialButton; import com.google.android.material.shape.AbsoluteCornerSize; @@ -62,7 +63,8 @@ import java.util.Objects; /** A preference that displays a card representing a {@link SafetyCenterIssue}. */ @RequiresApi(TIRAMISU) -public class IssueCardPreference extends Preference implements ComparablePreference { +public class IssueCardPreference extends Preference + implements ComparablePreference, GroupSectionDividerMixin { public static final String TAG = IssueCardPreference.class.getSimpleName(); @@ -101,12 +103,13 @@ public class IssueCardPreference extends Preference implements ComparablePrefere public void onBindViewHolder(PreferenceViewHolder holder) { super.onBindViewHolder(holder); - holder.itemView.setBackgroundResource(mPositionInCardList.getBackgroundDrawableResId()); + View issueCardView = holder.itemView.requireViewById(R.id.issue_card); + issueCardView.setBackgroundResource(mPositionInCardList.getBackgroundDrawableResId()); int topMargin = getTopMargin(mPositionInCardList, getContext()); - MarginLayoutParams layoutParams = (MarginLayoutParams) holder.itemView.getLayoutParams(); + MarginLayoutParams layoutParams = (MarginLayoutParams) issueCardView.getLayoutParams(); if (layoutParams.topMargin != topMargin) { layoutParams.topMargin = topMargin; - holder.itemView.setLayoutParams(layoutParams); + issueCardView.setLayoutParams(layoutParams); } // Set default group visibility in case view is being reused @@ -202,19 +205,20 @@ public class IssueCardPreference extends Preference implements ComparablePrefere private void configureSafetyProtectionView(PreferenceViewHolder holder) { View safetyProtectionSectionView = holder.findViewById(R.id.issue_card_protected_by_android); + View issueCard = holder.findViewById(R.id.issue_card); if (safetyProtectionSectionView.getVisibility() == View.GONE) { - holder.itemView.setPaddingRelative( - holder.itemView.getPaddingStart(), - holder.itemView.getPaddingTop(), - holder.itemView.getPaddingEnd(), + issueCard.setPaddingRelative( + issueCard.getPaddingStart(), + issueCard.getPaddingTop(), + issueCard.getPaddingEnd(), /* bottom= */ getContext() .getResources() .getDimensionPixelSize(R.dimen.sc_card_margin_bottom)); } else { - holder.itemView.setPaddingRelative( - holder.itemView.getPaddingStart(), - holder.itemView.getPaddingTop(), - holder.itemView.getPaddingEnd(), + issueCard.setPaddingRelative( + issueCard.getPaddingStart(), + issueCard.getPaddingTop(), + issueCard.getPaddingEnd(), /* bottom= */ 0); } } @@ -427,9 +431,7 @@ public class IssueCardPreference extends Preference implements ComparablePrefere TypedValue buttonThemeValue = new TypedValue(); mContext.getTheme() .resolveAttribute( - R.attr.scActionButtonTheme, - buttonThemeValue, - /* resolveRefs= */ false); + R.attr.scActionButtonTheme, buttonThemeValue, /* resolveRefs= */ false); mContextThemeWrapper = new ContextThemeWrapper(context, buttonThemeValue.data); } diff --git a/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/MoreIssuesCardPreference.kt b/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/MoreIssuesCardPreference.kt index a63e19984..5c86b4515 100644 --- a/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/MoreIssuesCardPreference.kt +++ b/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/MoreIssuesCardPreference.kt @@ -24,6 +24,7 @@ import androidx.preference.Preference import androidx.preference.PreferenceViewHolder import com.android.permissioncontroller.R import com.android.permissioncontroller.safetycenter.ui.view.MoreIssuesHeaderView +import com.android.settingslib.widget.GroupSectionDividerMixin /** A preference that displays a card linking to a list of more {@link SafetyCenterIssue}. */ @RequiresApi(Build.VERSION_CODES.TIRAMISU) @@ -34,8 +35,8 @@ internal class MoreIssuesCardPreference( private var newMoreIssuesCardData: MoreIssuesCardData, private val dismissedOnly: Boolean, val isStaticHeader: Boolean, - private val onClickListener: () -> Unit -) : Preference(context), ComparablePreference { + private val onClickListener: () -> Unit, +) : Preference(context), ComparablePreference, GroupSectionDividerMixin { init { layoutResource = R.layout.preference_more_issues_card @@ -44,11 +45,12 @@ internal class MoreIssuesCardPreference( override fun onBindViewHolder(holder: PreferenceViewHolder) { super.onBindViewHolder(holder) - val issueHeaderView = holder.itemView as MoreIssuesHeaderView + val issueHeaderView = + holder.itemView.requireViewById<MoreIssuesHeaderView>(R.id.more_issues_card) if (isStaticHeader) { issueHeaderView.showStaticHeader( context.getString(R.string.safety_center_dismissed_issues_card_title), - newMoreIssuesCardData.severityLevel + newMoreIssuesCardData.severityLevel, ) } else { issueHeaderView.showExpandableHeader( @@ -62,7 +64,7 @@ internal class MoreIssuesCardPreference( } ), overrideChevronIconResId, - onClickListener + onClickListener, ) } } @@ -94,5 +96,5 @@ internal class MoreIssuesCardPreference( internal data class MoreIssuesCardData( val severityLevel: Int, val hiddenIssueCount: Int, - val isExpanded: Boolean + val isExpanded: Boolean, ) diff --git a/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/SafetyBrandChipPreference.kt b/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/SafetyBrandChipPreference.kt index 57e4175ca..c5287af53 100644 --- a/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/SafetyBrandChipPreference.kt +++ b/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/SafetyBrandChipPreference.kt @@ -28,11 +28,12 @@ import androidx.preference.PreferenceViewHolder import com.android.permissioncontroller.Constants.EXTRA_SESSION_ID import com.android.permissioncontroller.R import com.android.permissioncontroller.safetycenter.SafetyCenterConstants +import com.android.settingslib.widget.GroupSectionDividerMixin /** A preference that displays the Security and Privacy brand name on a Safety Center subpage. */ @RequiresApi(UPSIDE_DOWN_CAKE) internal class SafetyBrandChipPreference(context: Context, attrs: AttributeSet) : - Preference(context, attrs) { + Preference(context, attrs), GroupSectionDividerMixin { init { setLayoutResource(R.layout.preference_brand_chip) @@ -67,7 +68,7 @@ internal class SafetyBrandChipPreference(context: Context, attrs: AttributeSet) fun closeSubpage( fragmentActivity: FragmentActivity, fragmentContext: Context, - sessionId: Long + sessionId: Long, ) { val openedFromHomepage = fragmentActivity diff --git a/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/SafetyCenterActivity.java b/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/SafetyCenterActivity.java index c6f2d146f..04206479f 100644 --- a/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/SafetyCenterActivity.java +++ b/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/SafetyCenterActivity.java @@ -29,6 +29,8 @@ import static com.android.permissioncontroller.safetycenter.SafetyCenterConstant import static com.android.permissioncontroller.safetycenter.SafetyCenterConstants.PRIVATE_PROFILE_SUFFIX; import static com.android.permissioncontroller.safetycenter.SafetyCenterConstants.WORK_PROFILE_SUFFIX; +import static java.util.Objects.requireNonNull; + import android.app.ActionBar; import android.content.Intent; import android.content.res.Configuration; @@ -60,6 +62,7 @@ import com.android.permissioncontroller.permission.utils.Utils; import com.android.permissioncontroller.safetycenter.ui.model.PrivacyControlsViewModel.Pref; import com.android.settingslib.activityembedding.ActivityEmbeddingUtils; import com.android.settingslib.collapsingtoolbar.CollapsingToolbarBaseActivity; +import com.android.settingslib.widget.SettingsThemeHelper; import java.util.List; import java.util.Objects; @@ -78,10 +81,10 @@ public final class SafetyCenterActivity extends CollapsingToolbarBaseActivity { private static final String EXTRA_PREVENT_TRAMPOLINE_TO_SETTINGS = "com.android.permissioncontroller.safetycenter.extra.PREVENT_TRAMPOLINE_TO_SETTINGS"; - private SafetyCenterManager mSafetyCenterManager; + @Nullable private SafetyCenterManager mSafetyCenterManager; @Override - public void onCreate(Bundle savedInstanceState) { + public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); mSafetyCenterManager = getSystemService(SafetyCenterManager.class); @@ -93,18 +96,25 @@ public final class SafetyCenterActivity extends CollapsingToolbarBaseActivity { return; } + if (SettingsThemeHelper.isExpressiveTheme(this)) { + // Setting a theme programmatically causes standard preferences to display weirdly. + // See b/377519324. + setTheme(R.style.Theme_SafetyCenterExpressive); + } + Fragment frag; + Intent intent = getIntent(); final boolean maybeOpenSubpage = SafetyCenterUiFlags.getShowSubpages() - && getIntent().getAction().equals(ACTION_SAFETY_CENTER); - if (maybeOpenSubpage && getIntent().hasExtra(EXTRA_SAFETY_SOURCES_GROUP_ID)) { - String groupId = getIntent().getStringExtra(EXTRA_SAFETY_SOURCES_GROUP_ID); + && Objects.equals(intent.getAction(), ACTION_SAFETY_CENTER); + if (maybeOpenSubpage && intent.hasExtra(EXTRA_SAFETY_SOURCES_GROUP_ID)) { + String groupId = intent.getStringExtra(EXTRA_SAFETY_SOURCES_GROUP_ID); frag = openRelevantSubpage(groupId); - } else if (maybeOpenSubpage && getIntent().hasExtra(EXTRA_SETTINGS_FRAGMENT_ARGS_KEY)) { - String preferenceKey = getIntent().getStringExtra(EXTRA_SETTINGS_FRAGMENT_ARGS_KEY); + } else if (maybeOpenSubpage && intent.hasExtra(EXTRA_SETTINGS_FRAGMENT_ARGS_KEY)) { + String preferenceKey = intent.getStringExtra(EXTRA_SETTINGS_FRAGMENT_ARGS_KEY); String groupId = getParentGroupId(preferenceKey); frag = openRelevantSubpage(groupId); - } else if (getIntent().getAction().equals(PRIVACY_CONTROLS_ACTION)) { + } else if (Objects.equals(intent.getAction(), PRIVACY_CONTROLS_ACTION)) { setTitle(R.string.privacy_controls_title); frag = PrivacyControlsFragment.newInstance(); } else { @@ -306,7 +316,8 @@ public final class SafetyCenterActivity extends CollapsingToolbarBaseActivity { return PRIVACY_SOURCES_GROUP_ID; } - SafetyCenterConfig safetyCenterConfig = mSafetyCenterManager.getSafetyCenterConfig(); + SafetyCenterConfig safetyCenterConfig = + requireNonNull(mSafetyCenterManager).getSafetyCenterConfig(); String[] splitKey; if (preferenceKey.endsWith(PERSONAL_PROFILE_SUFFIX)) { splitKey = preferenceKey.split("_" + PERSONAL_PROFILE_SUFFIX); diff --git a/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/SafetyCenterDashboardFragment.java b/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/SafetyCenterDashboardFragment.java index efbd57080..ed6bc382c 100644 --- a/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/SafetyCenterDashboardFragment.java +++ b/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/SafetyCenterDashboardFragment.java @@ -56,6 +56,7 @@ import com.android.permissioncontroller.safetycenter.ui.model.SafetyCenterUiData import com.android.permissioncontroller.safetycenter.ui.model.StatusUiData; import com.android.safetycenter.internaldata.SafetyCenterBundles; import com.android.safetycenter.resources.SafetyCenterResourcesApk; +import com.android.settingslib.widget.SettingsThemeHelper; import kotlin.Unit; @@ -121,12 +122,16 @@ public final class SafetyCenterDashboardFragment extends SafetyCenterFragment { mEntriesGroup = getPreferenceScreen().findPreference(ENTRIES_GROUP_KEY); mStaticEntriesGroup = getPreferenceScreen().findPreference(STATIC_ENTRIES_GROUP_KEY); + Preference spacerPreference = getPreferenceScreen().findPreference(SPACER_KEY); + if (SettingsThemeHelper.isExpressiveTheme(requireContext())) { + getPreferenceScreen().removePreference(spacerPreference); + } + if (mIsQuickSettingsFragment) { getPreferenceScreen().removePreference(mEntriesGroup); mEntriesGroup = null; getPreferenceScreen().removePreference(mStaticEntriesGroup); mStaticEntriesGroup = null; - Preference spacerPreference = getPreferenceScreen().findPreference(SPACER_KEY); getPreferenceScreen().removePreference(spacerPreference); } getSafetyCenterViewModel().getStatusUiLiveData().observe(this, this::updateStatus); diff --git a/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/SafetyCenterFragment.kt b/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/SafetyCenterFragment.kt index 9feecf5d4..04503de5e 100644 --- a/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/SafetyCenterFragment.kt +++ b/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/SafetyCenterFragment.kt @@ -22,7 +22,6 @@ import android.safetycenter.SafetyCenterErrorDetails import android.widget.Toast import androidx.annotation.RequiresApi import androidx.lifecycle.ViewModelProvider -import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceScreen import androidx.recyclerview.widget.RecyclerView import com.android.permissioncontroller.Constants.EXTRA_SESSION_ID @@ -33,10 +32,12 @@ import com.android.permissioncontroller.safetycenter.ui.model.LiveSafetyCenterVi import com.android.permissioncontroller.safetycenter.ui.model.SafetyCenterUiData import com.android.permissioncontroller.safetycenter.ui.model.SafetyCenterViewModel import com.android.safetycenter.resources.SafetyCenterResourcesApk +import com.android.settingslib.widget.SettingsBasePreferenceFragment +import com.android.settingslib.widget.SettingsThemeHelper /** A base fragment that represents a page in Safety Center. */ @RequiresApi(TIRAMISU) -abstract class SafetyCenterFragment : PreferenceFragmentCompat() { +abstract class SafetyCenterFragment : SettingsBasePreferenceFragment() { lateinit var safetyCenterViewModel: SafetyCenterViewModel lateinit var sameTaskSourceIds: List<String> @@ -51,12 +52,17 @@ abstract class SafetyCenterFragment : PreferenceFragmentCompat() { override fun onCreateAdapter( preferenceScreen: PreferenceScreen - ): RecyclerView.Adapter<RecyclerView.ViewHolder> { + ): RecyclerView.Adapter<out RecyclerView.ViewHolder> { /* The scroll-to-result functionality for settings search is currently implemented only for * subpages i.e. non expand-and-collapse type entries. Hence, we check that the flag is * enabled before using an adapter that does the highlighting and scrolling. */ - val adapter: RecyclerView.Adapter<RecyclerView.ViewHolder> = - if (SafetyCenterUiFlags.getShowSubpages()) { + val adapter: RecyclerView.Adapter<out RecyclerView.ViewHolder> = + if ( + SafetyCenterUiFlags.getShowSubpages() && + !SettingsThemeHelper.isExpressiveTheme(requireContext()) + ) { + // TODO: b/378433878 - Create highlight adapter for settings expressive theme, which + // has a different base class. highlightManager.createAdapter(preferenceScreen) } else { super.onCreateAdapter(preferenceScreen) @@ -80,7 +86,7 @@ abstract class SafetyCenterFragment : PreferenceFragmentCompat() { safetyCenterViewModel = ViewModelProvider( requireActivity(), - LiveSafetyCenterViewModelFactory(requireActivity().getApplication()) + LiveSafetyCenterViewModelFactory(requireActivity().getApplication()), ) .get(SafetyCenterViewModel::class.java) safetyCenterViewModel.safetyCenterUiLiveData.observe(this) { uiData: SafetyCenterUiData? -> @@ -177,7 +183,7 @@ abstract class SafetyCenterFragment : PreferenceFragmentCompat() { safetyCenterViewModel.interactionLogger.recordForIssue( Action.SAFETY_CENTER_VIEWED, maybeIssue, - isDismissed = false + isDismissed = false, ) } } diff --git a/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/SafetyCenterQsActivity.java b/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/SafetyCenterQsActivity.java index 2ad282449..d9f45cc08 100644 --- a/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/SafetyCenterQsActivity.java +++ b/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/SafetyCenterQsActivity.java @@ -24,7 +24,9 @@ import android.permission.PermissionManager; import androidx.fragment.app.FragmentActivity; import com.android.modules.utils.build.SdkLevel; +import com.android.permissioncontroller.R; import com.android.permissioncontroller.permission.utils.Utils; +import com.android.settingslib.widget.SettingsThemeHelper; /** Activity for the Safety Center Quick Settings Activity */ public class SafetyCenterQsActivity extends FragmentActivity { @@ -39,6 +41,12 @@ public class SafetyCenterQsActivity extends FragmentActivity { return; } + if (SettingsThemeHelper.isExpressiveTheme(this)) { + // Safe to set expressive theme here since QS doesn't display vanilla preferences. + // See b/377519324. + setTheme(R.style.Theme_SafetyCenterQsExpressive); + } + configureFragment(); } diff --git a/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/SafetyCenterSubpageFragment.kt b/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/SafetyCenterSubpageFragment.kt index fdade2189..2e5ba49ae 100644 --- a/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/SafetyCenterSubpageFragment.kt +++ b/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/SafetyCenterSubpageFragment.kt @@ -28,6 +28,7 @@ import com.android.permissioncontroller.safetycenter.ui.SafetyBrandChipPreferenc import com.android.permissioncontroller.safetycenter.ui.model.SafetyCenterUiData import com.android.safetycenter.resources.SafetyCenterResourcesApk import com.android.settingslib.widget.FooterPreference +import com.android.settingslib.widget.SettingsThemeHelper /** A fragment that represents a generic subpage in Safety Center. */ @RequiresApi(UPSIDE_DOWN_CAKE) @@ -45,15 +46,16 @@ class SafetyCenterSubpageFragment : SafetyCenterFragment() { setPreferencesFromResource(R.xml.safety_center_subpage, rootKey) sourceGroupId = requireArguments().getString(SOURCE_GROUP_ID_KEY)!! - subpageBrandChip = getPreferenceScreen().findPreference(BRAND_CHIP_KEY)!! - subpageIllustration = getPreferenceScreen().findPreference(ILLUSTRATION_KEY)!! - subpageIssueGroup = getPreferenceScreen().findPreference(ISSUE_GROUP_KEY)!! - subpageEntryGroup = getPreferenceScreen().findPreference(ENTRY_GROUP_KEY)!! - subpageFooter = getPreferenceScreen().findPreference(FOOTER_KEY)!! + subpageBrandChip = preferenceScreen.findPreference(BRAND_CHIP_KEY)!! + subpageIllustration = preferenceScreen.findPreference(ILLUSTRATION_KEY)!! + subpageIssueGroup = preferenceScreen.findPreference(ISSUE_GROUP_KEY)!! + subpageEntryGroup = preferenceScreen.findPreference(ENTRY_GROUP_KEY)!! + subpageFooter = preferenceScreen.findPreference(FOOTER_KEY)!! subpageBrandChip.setupListener(requireActivity(), safetyCenterSessionId) setupIllustration() setupFooter() + maybeRemoveSpacer() prerenderCurrentSafetyCenterData() } @@ -80,7 +82,7 @@ class SafetyCenterSubpageFragment : SafetyCenterFragment() { return } - requireActivity().setTitle(entryGroup.title) + requireActivity().title = entryGroup.title updateSafetyCenterIssues(uiData) updateSafetyCenterEntries(entryGroup) } @@ -91,7 +93,7 @@ class SafetyCenterSubpageFragment : SafetyCenterFragment() { val drawable = SafetyCenterResourcesApk(context).getDrawableByName(resName, context.theme) if (drawable == null) { Log.w(TAG, "$sourceGroupId doesn't have any matching illustration") - subpageIllustration.setVisible(false) + subpageIllustration.isVisible = false } subpageIllustration.illustrationDrawable = drawable @@ -102,12 +104,19 @@ class SafetyCenterSubpageFragment : SafetyCenterFragment() { val footerText = SafetyCenterResourcesApk(requireContext()).getStringByName(resName) if (footerText.isEmpty()) { Log.w(TAG, "$sourceGroupId doesn't have any matching footer") - subpageFooter.setVisible(false) + subpageFooter.isVisible = false } // footer is ordered last by default // in order to keep a spacer after the footer, footer needs to be the second from last - subpageFooter.setOrder(Int.MAX_VALUE - 2) - subpageFooter.setSummary(footerText) + subpageFooter.order = Int.MAX_VALUE - 2 + subpageFooter.summary = footerText + } + + private fun maybeRemoveSpacer() { + if (SettingsThemeHelper.isExpressiveTheme(requireContext())) { + val spacerPreference = preferenceScreen.findPreference<SpacerPreference>(SPACER_KEY)!! + preferenceScreen.removePreference(spacerPreference) + } } private fun updateSafetyCenterIssues(uiData: SafetyCenterUiData?) { @@ -131,7 +140,7 @@ class SafetyCenterSubpageFragment : SafetyCenterFragment() { subpageIssues, subpageDismissedIssues, uiData.resolvedIssues, - requireActivity().getTaskId() + requireActivity().taskId, ) } @@ -145,23 +154,24 @@ class SafetyCenterSubpageFragment : SafetyCenterFragment() { PendingIntentSender.getTaskIdForEntry( entry.id, sameTaskSourceIds, - requireActivity() + requireActivity(), ), entry, - safetyCenterViewModel + safetyCenterViewModel, ) ) } } companion object { - private val TAG: String = SafetyCenterSubpageFragment::class.java.simpleName - private const val BRAND_CHIP_KEY: String = "subpage_brand_chip" - private const val ILLUSTRATION_KEY: String = "subpage_illustration" - private const val ISSUE_GROUP_KEY: String = "subpage_issue_group" - private const val ENTRY_GROUP_KEY: String = "subpage_entry_group" - private const val FOOTER_KEY: String = "subpage_footer" - private const val SOURCE_GROUP_ID_KEY: String = "source_group_id" + private val TAG = SafetyCenterSubpageFragment::class.java.simpleName + private const val BRAND_CHIP_KEY = "subpage_brand_chip" + private const val ILLUSTRATION_KEY = "subpage_illustration" + private const val ISSUE_GROUP_KEY = "subpage_issue_group" + private const val ENTRY_GROUP_KEY = "subpage_entry_group" + private const val FOOTER_KEY = "subpage_footer" + private const val SPACER_KEY = "subpage_spacer" + private const val SOURCE_GROUP_ID_KEY = "source_group_id" /** Creates an instance of SafetyCenterSubpageFragment with the arguments set */ @JvmStatic diff --git a/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/SafetyIllustrationPreference.kt b/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/SafetyIllustrationPreference.kt index 5acb27131..3976fa7e3 100644 --- a/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/SafetyIllustrationPreference.kt +++ b/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/SafetyIllustrationPreference.kt @@ -25,11 +25,12 @@ import androidx.annotation.RequiresApi import androidx.preference.Preference import androidx.preference.PreferenceViewHolder import com.android.permissioncontroller.R +import com.android.settingslib.widget.GroupSectionDividerMixin /** A preference that displays the illustration on a Safety Center subpage. */ @RequiresApi(UPSIDE_DOWN_CAKE) internal class SafetyIllustrationPreference(context: Context, attrs: AttributeSet) : - Preference(context, attrs) { + Preference(context, attrs), GroupSectionDividerMixin { init { layoutResource = R.layout.preference_illustration diff --git a/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/SafetyStatusPreference.java b/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/SafetyStatusPreference.java index 811841845..abf159955 100644 --- a/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/SafetyStatusPreference.java +++ b/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/SafetyStatusPreference.java @@ -40,6 +40,7 @@ import com.android.permissioncontroller.R; import com.android.permissioncontroller.safetycenter.ui.model.SafetyCenterViewModel; import com.android.permissioncontroller.safetycenter.ui.model.StatusUiData; import com.android.permissioncontroller.safetycenter.ui.view.StatusCardView; +import com.android.settingslib.widget.GroupSectionDividerMixin; import kotlin.Pair; @@ -48,7 +49,8 @@ import java.util.Objects; /** Preference which displays a visual representation of {@link SafetyCenterStatus}. */ @RequiresApi(TIRAMISU) -public class SafetyStatusPreference extends Preference implements ComparablePreference { +public class SafetyStatusPreference extends Preference + implements ComparablePreference, GroupSectionDividerMixin { private static final String TAG = "SafetyStatusPreference"; @@ -82,7 +84,8 @@ public class SafetyStatusPreference extends Preference implements ComparablePref } Context context = getContext(); - StatusCardView statusCardView = (StatusCardView) holder.itemView; + StatusCardView statusCardView = holder.itemView.requireViewById(R.id.status_card); + configureButtons(context, statusCardView); statusCardView .getTitleAndSummaryContainerView() diff --git a/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/SpacerPreference.kt b/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/SpacerPreference.kt index 030b67be9..7f619d1ca 100644 --- a/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/SpacerPreference.kt +++ b/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/SpacerPreference.kt @@ -55,6 +55,7 @@ internal class SpacerPreference(context: Context, attrs: AttributeSet) : } private var maxKnownToolbarHeight = 0 + override fun onBindViewHolder(holder: PreferenceViewHolder) { super.onBindViewHolder(holder) val spacer = holder.itemView @@ -74,7 +75,7 @@ internal class SpacerPreference(context: Context, attrs: AttributeSet) : oldLeft: Int, oldTop: Int, oldRight: Int, - oldBottom: Int + oldBottom: Int, ) { adjustHeight(spacer) } 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 |