summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--PermissionController/TEST_MAPPING9
-rw-r--r--PermissionController/res/anim/text_switcher_fade_in.xml22
-rw-r--r--PermissionController/res/anim/text_switcher_fade_out.xml21
-rw-r--r--PermissionController/res/layout-v33/view_status_card.xml31
-rw-r--r--PermissionController/res/layout/permission_history_widget.xml2
-rw-r--r--PermissionController/res/values-ca/strings.xml2
-rw-r--r--PermissionController/res/values-de/strings.xml2
-rw-r--r--PermissionController/res/values-es/strings.xml2
-rw-r--r--PermissionController/res/values-et/strings.xml2
-rw-r--r--PermissionController/res/values-eu/strings.xml2
-rw-r--r--PermissionController/res/values-hr/strings.xml2
-rw-r--r--PermissionController/res/values-pl/strings.xml4
-rw-r--r--PermissionController/res/values-sl-watch/strings.xml2
-rw-r--r--PermissionController/res/values-sl/strings.xml2
-rw-r--r--PermissionController/res/values-sq/strings.xml2
-rw-r--r--PermissionController/res/values-zh-rCN/strings.xml6
-rw-r--r--PermissionController/res/xml/roles.xml10
-rw-r--r--PermissionController/role-controller/java/com/android/role/controller/model/AppOp.java5
-rw-r--r--PermissionController/role-controller/java/com/android/role/controller/model/Permission.java5
-rw-r--r--PermissionController/role-controller/java/com/android/role/controller/model/Role.java4
-rw-r--r--PermissionController/src/com/android/permissioncontroller/ecm/EnhancedConfirmationDialogActivity.kt16
-rw-r--r--PermissionController/src/com/android/permissioncontroller/incident/wear/WearConfirmationScreen.kt43
-rw-r--r--PermissionController/src/com/android/permissioncontroller/permission/service/v33/SafetyCenterQsTileService.kt1
-rw-r--r--PermissionController/src/com/android/permissioncontroller/permission/ui/GrantPermissionsActivity.java3
-rw-r--r--PermissionController/src/com/android/permissioncontroller/permission/ui/wear/GrantPermissionsWearViewHandler.java9
-rw-r--r--PermissionController/src/com/android/permissioncontroller/permission/ui/wear/LocationProviderDialogScreen.kt2
-rw-r--r--PermissionController/src/com/android/permissioncontroller/permission/ui/wear/TEST_MAPPING22
-rw-r--r--PermissionController/src/com/android/permissioncontroller/permission/ui/wear/WearAppPermissionGroupsScreen.kt47
-rw-r--r--PermissionController/src/com/android/permissioncontroller/permission/ui/wear/WearAppPermissionScreen.kt103
-rw-r--r--PermissionController/src/com/android/permissioncontroller/permission/ui/wear/WearEnhancedConfirmationScreen.kt32
-rw-r--r--PermissionController/src/com/android/permissioncontroller/permission/ui/wear/WearGrantPermissionsScreen.kt35
-rw-r--r--PermissionController/src/com/android/permissioncontroller/permission/ui/wear/WearManageCustomPermissionScreen.kt12
-rw-r--r--PermissionController/src/com/android/permissioncontroller/permission/ui/wear/WearManageStandardPermissionScreen.kt20
-rw-r--r--PermissionController/src/com/android/permissioncontroller/permission/ui/wear/WearPermissionAppsScreen.kt14
-rw-r--r--PermissionController/src/com/android/permissioncontroller/permission/ui/wear/WearPermissionUsageDetailsScreen.kt14
-rw-r--r--PermissionController/src/com/android/permissioncontroller/permission/ui/wear/WearPermissionUsageScreen.kt15
-rw-r--r--PermissionController/src/com/android/permissioncontroller/permission/ui/wear/WearUnusedAppsScreen.kt10
-rw-r--r--PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/ScrollableScreen.kt243
-rw-r--r--PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/material2/AlertDialog.kt (renamed from PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/AlertDialog.kt)116
-rw-r--r--PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/material2/Chip.kt (renamed from PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/Chip.kt)31
-rw-r--r--PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/material2/Icon.kt (renamed from PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/Icon.kt)21
-rw-r--r--PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/material2/ListFooter.kt (renamed from PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/ListFooter.kt)6
-rw-r--r--PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/material2/ListHeader.kt (renamed from PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/ListHeader.kt)12
-rw-r--r--PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/material2/ResponsiveDialog.kt (renamed from PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/ResponsiveDialog.kt)106
-rw-r--r--PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/material2/ToggleChip.kt (renamed from PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/ToggleChip.kt)2
-rw-r--r--PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/material2/ToggleChipToggleControl.kt (renamed from PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/ToggleChipToggleControl.kt)2
-rw-r--r--PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/material2/Wear2Scaffold.kt264
-rw-r--r--PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/material2/layout/ScalingLazyColumnDefaults.kt (renamed from PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/layout/ScalingLazyColumnDefaults.kt)16
-rw-r--r--PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/material2/layout/ScalingLazyColumnState.kt (renamed from PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/layout/ScalingLazyColumnState.kt)64
-rw-r--r--PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/material3/WearPermissionButton.kt4
-rw-r--r--PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/material3/WearPermissionButtonStyle.kt4
-rw-r--r--PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/material3/WearPermissionConfirmationDialog.kt159
-rw-r--r--PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/material3/WearPermissionIconBuilder.kt14
-rw-r--r--PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/material3/WearPermissionListFooter.kt2
-rw-r--r--PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/material3/WearPermissionScaffold.kt88
-rw-r--r--PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/material3/WearPermissionToggleControl.kt23
-rw-r--r--PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/material3/WearPermissionToggleControlStyle.kt4
-rw-r--r--PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/rotaryinput/Haptics.kt292
-rw-r--r--PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/rotaryinput/Rotary.kt1232
-rw-r--r--PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/rotaryinput/RotaryVelocityTracker.kt47
-rw-r--r--PermissionController/src/com/android/permissioncontroller/permission/ui/wear/theme/ResourceHelper.kt26
-rw-r--r--PermissionController/src/com/android/permissioncontroller/permission/ui/wear/theme/WearPermissionTheme.kt5
-rw-r--r--PermissionController/src/com/android/permissioncontroller/permission/utils/AndroidUtils.kt10
-rw-r--r--PermissionController/src/com/android/permissioncontroller/permission/utils/PermissionMapping.kt13
-rw-r--r--PermissionController/src/com/android/permissioncontroller/permission/utils/Utils.java8
-rw-r--r--PermissionController/src/com/android/permissioncontroller/privacysources/AccessibilitySourceService.kt3
-rw-r--r--PermissionController/src/com/android/permissioncontroller/privacysources/NotificationListenerCheck.kt3
-rw-r--r--PermissionController/src/com/android/permissioncontroller/role/TEST_MAPPING9
-rw-r--r--PermissionController/src/com/android/permissioncontroller/role/ui/wear/WearDefaultAppListScreen.kt8
-rw-r--r--PermissionController/src/com/android/permissioncontroller/role/ui/wear/WearDefaultAppScreen.kt44
-rw-r--r--PermissionController/src/com/android/permissioncontroller/role/ui/wear/WearRequestRoleScreen.kt12
-rw-r--r--PermissionController/src/com/android/permissioncontroller/safetycenter/ui/SafetyStatusPreference.java56
-rw-r--r--PermissionController/src/com/android/permissioncontroller/safetycenter/ui/view/StatusCardView.kt25
-rw-r--r--PermissionController/tests/inprocess/Android.bp1
-rw-r--r--PermissionController/tests/inprocess/src/com/android/permissioncontroller/permission/GetPermissionGroupInfoTest.kt4
-rw-r--r--PermissionController/tests/inprocess/src/com/android/permissioncontroller/permission/data/AttributionLabelLiveDataTest.kt4
-rw-r--r--PermissionController/tests/inprocess/src/com/android/permissioncontroller/permission/util/ArrayUtilsTest.kt4
-rw-r--r--PermissionController/tests/inprocess/src/com/android/permissioncontroller/permission/util/CollectionUtilsTest.kt4
-rw-r--r--PermissionController/tests/inprocess/src/com/android/permissioncontroller/permission/util/KotlinUtilsTest.kt8
-rw-r--r--PermissionController/tests/inprocess/src/com/android/permissioncontroller/permission/util/PermissionMappingTest.kt58
-rw-r--r--PermissionController/tests/inprocess/src/com/android/permissioncontroller/permission/util/UtilsTest.kt72
-rw-r--r--PermissionController/tests/mocking/src/com/android/permissioncontroller/tests/mocking/privacysources/NotificationListenerCheckInternalTest.kt5
-rw-r--r--PermissionController/tests/permissionui/Android.bp1
-rw-r--r--PermissionController/tests/permissionui/src/com/android/permissioncontroller/permissionui/ui/handheld/ManageCustomPermissionsFragmentTest.kt38
-rw-r--r--SafetyCenter/Resources/res/raw-v36/safety_center_config.xml30
-rw-r--r--SafetyCenter/Resources/res/values-v36/config.xml21
-rw-r--r--SafetyCenter/Resources/res/values-v36/strings.xml33
-rw-r--r--TEST_MAPPING3
-rw-r--r--framework-s/java/android/app/ecm/EnhancedConfirmationManager.java24
-rw-r--r--framework-s/java/android/app/ecm/IEnhancedConfirmationManager.aidl2
-rw-r--r--framework-s/java/android/app/role/TEST_MAPPING9
-rw-r--r--service/java/com/android/ecm/EnhancedConfirmationService.java33
-rw-r--r--service/java/com/android/role/TEST_MAPPING9
-rw-r--r--tests/cts/permission/src/android/permission/cts/LocationAccessCheckTest.java11
-rw-r--r--tests/cts/permissionmultidevice/src/android/permissionmultidevice/cts/DeviceAwarePermissionGrantTest.kt33
-rw-r--r--tests/cts/permissionui/src/android/permissionui/cts/BasePermissionTest.kt2
-rw-r--r--tests/cts/permissionui/src/android/permissionui/cts/EnhancedConfirmationManagerTest.kt35
-rw-r--r--tests/cts/permissionui/src/android/permissionui/cts/PermissionSplitTest.kt62
-rw-r--r--tests/cts/permissionui/src/android/permissionui/cts/PermissionTapjackingTest.kt66
-rw-r--r--tests/cts/role/Android.bp10
-rw-r--r--tests/cts/role/AndroidManifest.xml1
-rw-r--r--tests/cts/role/AndroidTest.xml3
-rw-r--r--tests/cts/role/src/android/app/role/cts/ChooseNoteRoleAppTest.kt70
-rw-r--r--tests/cts/role/src/android/app/role/cts/RoleManagerTest.java42
-rw-r--r--tests/cts/rolemultiuser/Android.bp1
-rw-r--r--tests/cts/rolemultiuser/TEST_MAPPING9
-rw-r--r--tests/cts/rolemultiuser/src/android/app/rolemultiuser/cts/RoleManagerMultiUserTest.kt3
-rw-r--r--tests/utils/safetycenter/AndroidManifest.xml1
-rw-r--r--tests/utils/safetycenter/res/layout/test_activity.xml1
-rw-r--r--tests/utils/safetycenter/res/values/styles.xml24
110 files changed, 1694 insertions, 2552 deletions
diff --git a/PermissionController/TEST_MAPPING b/PermissionController/TEST_MAPPING
index a34a16034..13f243d81 100644
--- a/PermissionController/TEST_MAPPING
+++ b/PermissionController/TEST_MAPPING
@@ -13,6 +13,9 @@
"file_patterns": ["res/xml/roles\\.xml"]
},
{
+ "name": "CtsRoleMultiUserTestCases"
+ },
+ {
"name": "PermissionUiTestCases",
"options": [
{
@@ -49,6 +52,9 @@
"file_patterns": ["res/xml/roles\\.xml"]
},
{
+ "name": "CtsRoleMultiUserTestCases[com.google.android.permission.apex]"
+ },
+ {
"name": "PermissionControllerMockingTests[com.google.android.permission.apex]",
"options": [
{
@@ -111,6 +117,9 @@
"file_patterns": ["res/xml/roles\\.xml"]
},
{
+ "name": "CtsRoleMultiUserTestCases"
+ },
+ {
"name": "PermissionControllerMockingTests",
"options": [
{
diff --git a/PermissionController/res/anim/text_switcher_fade_in.xml b/PermissionController/res/anim/text_switcher_fade_in.xml
new file mode 100644
index 000000000..b9e2812aa
--- /dev/null
+++ b/PermissionController/res/anim/text_switcher_fade_in.xml
@@ -0,0 +1,22 @@
+<?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.
+ -->
+
+<alpha xmlns:android="http://schemas.android.com/apk/res/android"
+ android:interpolator="@android:anim/linear_interpolator"
+ android:fromAlpha="0.0" android:toAlpha="1.0"
+ android:startOffset="@android:integer/config_shortAnimTime"
+ android:duration="@android:integer/config_shortAnimTime" /> \ No newline at end of file
diff --git a/PermissionController/res/anim/text_switcher_fade_out.xml b/PermissionController/res/anim/text_switcher_fade_out.xml
new file mode 100644
index 000000000..4b7274707
--- /dev/null
+++ b/PermissionController/res/anim/text_switcher_fade_out.xml
@@ -0,0 +1,21 @@
+<?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.
+ -->
+
+<alpha xmlns:android="http://schemas.android.com/apk/res/android"
+ android:interpolator="@android:anim/linear_interpolator"
+ android:fromAlpha="1.0" android:toAlpha="0.0"
+ android:duration="@android:integer/config_shortAnimTime" /> \ No newline at end of file
diff --git a/PermissionController/res/layout-v33/view_status_card.xml b/PermissionController/res/layout-v33/view_status_card.xml
index 4915347be..d8ca8b7ea 100644
--- a/PermissionController/res/layout-v33/view_status_card.xml
+++ b/PermissionController/res/layout-v33/view_status_card.xml
@@ -30,15 +30,34 @@
android:id="@+id/status_title_and_summary"
style="?attr/scStatusTitleAndSummaryContainerStyle">
- <TextView
+ <TextSwitcher
android:id="@+id/status_title"
- android:text="@string/summary_placeholder"
- style="@style/SafetyCenterStatusTitle" />
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:inAnimation="@anim/text_switcher_fade_in"
+ android:outAnimation="@anim/text_switcher_fade_out">
+ <TextView
+ android:text="@string/summary_placeholder"
+ style="@style/SafetyCenterStatusTitle" />
+ <TextView
+ android:text="@string/summary_placeholder"
+ style="@style/SafetyCenterStatusTitle" />
+ </TextSwitcher>
- <TextView
+
+ <TextSwitcher
android:id="@+id/status_summary"
- android:text="@string/summary_placeholder"
- style="@style/SafetyCenterStatusSummary" />
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:inAnimation="@anim/text_switcher_fade_in"
+ android:outAnimation="@anim/text_switcher_fade_out">
+ <TextView
+ android:text="@string/summary_placeholder"
+ style="@style/SafetyCenterStatusSummary" />
+ <TextView
+ android:text="@string/summary_placeholder"
+ style="@style/SafetyCenterStatusSummary" />
+ </TextSwitcher>
</LinearLayout>
<androidx.constraintlayout.widget.Barrier
diff --git a/PermissionController/res/layout/permission_history_widget.xml b/PermissionController/res/layout/permission_history_widget.xml
index 9bdef7200..f98a1c14b 100644
--- a/PermissionController/res/layout/permission_history_widget.xml
+++ b/PermissionController/res/layout/permission_history_widget.xml
@@ -28,6 +28,8 @@
android:layout_height="wrap_content"
android:minWidth="60dp"
android:layout_marginTop="19dp"
+ android:textAppearance="?android:attr/textAppearanceListItemSecondary"
+ android:textColor="?android:attr/textColorSecondary"
/>
<LinearLayout
diff --git a/PermissionController/res/values-ca/strings.xml b/PermissionController/res/values-ca/strings.xml
index 720c0161d..fd1e23c51 100644
--- a/PermissionController/res/values-ca/strings.xml
+++ b/PermissionController/res/values-ca/strings.xml
@@ -346,7 +346,7 @@
<string name="no_apps_allowed" msgid="7718822655254468631">"Cap aplicació amb permís"</string>
<string name="no_apps_allowed_full" msgid="8011716991498934104">"Cap aplicació té permís per accedir a tots els fitxers"</string>
<string name="no_apps_allowed_scoped" msgid="4908850477787659501">"Cap aplicació té permís per accedir només a fitxers multimèdia"</string>
- <string name="no_apps_denied" msgid="7663435886986784743">"Cap aplicació amb permís denegat"</string>
+ <string name="no_apps_denied" msgid="7663435886986784743">"Cap aplicació denegada"</string>
<string name="car_permission_selected" msgid="180837028920791596">"Seleccionat"</string>
<string name="settings" msgid="5409109923158713323">"Configuració"</string>
<string name="accessibility_service_dialog_title_single" msgid="7956432823014102366">"<xliff:g id="SERVICE_NAME">%s</xliff:g> té accés complet al dispositiu"</string>
diff --git a/PermissionController/res/values-de/strings.xml b/PermissionController/res/values-de/strings.xml
index 12a45473c..bf6047e5c 100644
--- a/PermissionController/res/values-de/strings.xml
+++ b/PermissionController/res/values-de/strings.xml
@@ -370,7 +370,7 @@
<string name="role_sms_label" msgid="8456999857547686640">"Standard-SMS-App"</string>
<string name="role_sms_short_label" msgid="4371444488034692243">"SMS-App"</string>
<string name="role_sms_description" msgid="3424020199148153513">"Apps, mit denen du über deine Telefonnummer unter anderem SMS, Fotos oder Videos senden und empfangen kannst"</string>
- <string name="role_sms_request_title" msgid="7953552109601185602">"<xliff:g id="APP_NAME">%1$s</xliff:g>als Standard-SMS-App festlegen?"</string>
+ <string name="role_sms_request_title" msgid="7953552109601185602">"<xliff:g id="APP_NAME">%1$s</xliff:g> als Standard-SMS-App festlegen?"</string>
<string name="role_sms_request_description" msgid="2691004766132144886">"Diese App erhält Zugriff auf Folgendes: Kamera, Kontakte, Mikrofon, Dateien und Medien, Telefon und SMS"</string>
<string name="role_sms_search_keywords" msgid="8022048144395047352">"textnachricht, sms, sms schicken, sms senden, nachrichten, mms"</string>
<string name="role_emergency_label" msgid="7028825857206842366">"Standardmäßige Notfall-App"</string>
diff --git a/PermissionController/res/values-es/strings.xml b/PermissionController/res/values-es/strings.xml
index d73329ef8..84900f171 100644
--- a/PermissionController/res/values-es/strings.xml
+++ b/PermissionController/res/values-es/strings.xml
@@ -250,7 +250,7 @@
<string name="app_permission_most_recent_denied_summary" msgid="7659497197737708112">"Actualmente denegado / Último acceso: <xliff:g id="TIME_DATE">%1$s</xliff:g>"</string>
<string name="app_permission_never_accessed_summary" msgid="401346181461975090">"No ha accedido nunca"</string>
<string name="app_permission_never_accessed_denied_summary" msgid="6596000497490905146">"Denegado / Último acceso: Nunca"</string>
- <string name="allowed_header" msgid="7769277978004790414">"Permitido"</string>
+ <string name="allowed_header" msgid="7769277978004790414">"Permitidas"</string>
<string name="allowed_always_header" msgid="6455903312589013545">"Permitidas siempre"</string>
<string name="allowed_foreground_header" msgid="6845655788447833353">"Permitidas solo mientras se usan"</string>
<string name="allowed_storage_scoped" msgid="5383645873719086975">"Pueden acceder solo al contenido multimedia"</string>
diff --git a/PermissionController/res/values-et/strings.xml b/PermissionController/res/values-et/strings.xml
index 7622870c7..a289032c0 100644
--- a/PermissionController/res/values-et/strings.xml
+++ b/PermissionController/res/values-et/strings.xml
@@ -207,7 +207,7 @@
<string name="auto_revoke_label" msgid="5068393642936571656">"Eemalda load, kui rakendust ei kasutata"</string>
<string name="unused_apps_label" msgid="2595428768404901064">"Eemalda load ja vabasta ruumi"</string>
<string name="unused_apps_label_v2" msgid="7058776770056517980">"Kasutamata rakenduse tegevuste peatamine"</string>
- <string name="unused_apps_label_v3" msgid="693340578642156657">"Halda kasutamata rakendusi"</string>
+ <string name="unused_apps_label_v3" msgid="693340578642156657">"Kasutamata rakenduste haldamine"</string>
<string name="unused_apps_summary" msgid="8839466950318403115">"Eemaldatakse load, kustutatakse ajutised failid ja peatatakse märguanded"</string>
<string name="unused_apps_summary_v2" msgid="5011313200815115802">"Eemalda load, kustuta ajutised failid, peata märguanded ja arhiivi rakendus"</string>
<string name="auto_revoke_summary" msgid="5867548789805911683">"Teie andmete kaitsmiseks eemaldatakse selle rakenduse load, kui seda mõne kuu jooksul ei kasutata."</string>
diff --git a/PermissionController/res/values-eu/strings.xml b/PermissionController/res/values-eu/strings.xml
index 4e81fb5f1..c7280999f 100644
--- a/PermissionController/res/values-eu/strings.xml
+++ b/PermissionController/res/values-eu/strings.xml
@@ -60,7 +60,7 @@
<string name="grant_dialog_button_allow_all_files" msgid="4955436994954829894">"Eman fitxategi guztiak kudeatzeko baimena"</string>
<string name="grant_dialog_button_allow_media_only" msgid="4832877658422573832">"Eman multimedia-fitxategiak erabiltzeko baimena"</string>
<string name="app_permissions_breadcrumb" msgid="5136969550489411650">"Aplikazioak"</string>
- <string name="app_permissions" msgid="3369917736607944781">"Aplikazio-baimenak"</string>
+ <string name="app_permissions" msgid="3369917736607944781">"Aplikazio-baimenaU+2060k"</string>
<string name="unused_apps" msgid="2058057455175955094">"Erabiltzen ez diren aplikazioak"</string>
<string name="edit_photos_description" msgid="5540108003480078892">"Editatu aplikazio honetarako hautatutako argazkiak"</string>
<string name="no_unused_apps" msgid="12809387670415295">"Ez dago erabiltzen ez duzun aplikaziorik"</string>
diff --git a/PermissionController/res/values-hr/strings.xml b/PermissionController/res/values-hr/strings.xml
index f23311cd6..7275d1681 100644
--- a/PermissionController/res/values-hr/strings.xml
+++ b/PermissionController/res/values-hr/strings.xml
@@ -675,7 +675,7 @@
<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_phone_state_dialog_title" msgid="5230100829862738467">"Radnja nije dostupna tijekom telefonskog poziva"</string>
- <string name="enhanced_confirmation_phone_state_dialog_desc" msgid="8782160971908273849">"Dopuštanje aplikacijama da instaliraju druge aplikacije nije dopušteno tijekom telefonskog poziva.\n\n Prevaranti često zahtijevaju tu vrstu radnje tijekom telefonskih razgovora, pa je blokirana radi vaše zaštite. Ako vas netko koga ne poznajete upućuje na tu radnju, možda je riječ o prijevari."</string>
+ <string name="enhanced_confirmation_phone_state_dialog_desc" msgid="8782160971908273849">"Aplikacijama nije dopušteno instalirati druge aplikacije tijekom telefonskog poziva.\n\n Prevaranti često zahtijevaju tu vrstu radnje tijekom telefonskih razgovora, pa je ona blokirana radi vaše zaštite. Ako vas netko koga ne poznajete navodi na tu radnju, možda je riječ o prijevari."</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">&lt;br&gt;&lt;br&gt;</xliff:g>Moguće je da aplikacija neće pravilno funkcionirati bez tog uskraćenog dopuštenja. &lt;a href=<xliff:g id="LEARN_MORE_LINK">%1$s</xliff:g>&gt;Saznajte kako omogućiti pristup&lt;/a&gt;"</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>
diff --git a/PermissionController/res/values-pl/strings.xml b/PermissionController/res/values-pl/strings.xml
index ae98edba8..491216726 100644
--- a/PermissionController/res/values-pl/strings.xml
+++ b/PermissionController/res/values-pl/strings.xml
@@ -250,13 +250,13 @@
<string name="app_permission_most_recent_denied_summary" msgid="7659497197737708112">"Aktualnie odmowa / ostatni dostęp: <xliff:g id="TIME_DATE">%1$s</xliff:g>"</string>
<string name="app_permission_never_accessed_summary" msgid="401346181461975090">"Nigdy nie użyto"</string>
<string name="app_permission_never_accessed_denied_summary" msgid="6596000497490905146">"Odmowa / nigdy nie użyto"</string>
- <string name="allowed_header" msgid="7769277978004790414">"Mają dostęp"</string>
+ <string name="allowed_header" msgid="7769277978004790414">"Ma dostęp"</string>
<string name="allowed_always_header" msgid="6455903312589013545">"Mają ciągły dostęp"</string>
<string name="allowed_foreground_header" msgid="6845655788447833353">"Mają dostęp tylko podczas używania"</string>
<string name="allowed_storage_scoped" msgid="5383645873719086975">"Zezwolono na dostęp tylko do multimediów"</string>
<string name="allowed_storage_full" msgid="5356699280625693530">"Zezwolono na zarządzanie wszystkimi plikami"</string>
<string name="ask_header" msgid="2633816846459944376">"Zawsze pytaj"</string>
- <string name="denied_header" msgid="903209608358177654">"Nie mają dostępu"</string>
+ <string name="denied_header" msgid="903209608358177654">"Nie ma dostępu"</string>
<string name="permission_group_name_with_device_name" msgid="8798741850536024820">"<xliff:g id="PERM_GROUP_NAME">%1$s</xliff:g> na tym urządzeniu: <xliff:g id="DEVICE_NAME">%2$s</xliff:g>"</string>
<string name="storage_footer_hyperlink_text" msgid="8873343987957834810">"Zobacz więcej aplikacji z dostępem do wszystkich plików"</string>
<string name="days" msgid="609563020985571393">"{count,plural, =1{1 dzień}few{# dni}many{# dni}other{# dnia}}"</string>
diff --git a/PermissionController/res/values-sl-watch/strings.xml b/PermissionController/res/values-sl-watch/strings.xml
index f93ba26b5..3f78007ac 100644
--- a/PermissionController/res/values-sl-watch/strings.xml
+++ b/PermissionController/res/values-sl-watch/strings.xml
@@ -21,7 +21,7 @@
<string name="preference_show_system_apps" msgid="1055740303992024300">"Prikaz sistemskih aplikacij"</string>
<string name="permission_summary_enforced_by_policy" msgid="2352478756952948019">"Ni mogoče sprem."</string>
<string name="generic_yes" msgid="2489207724988649846">"Da"</string>
- <string name="generic_cancel" msgid="2631708607129269698">"Prekliči"</string>
+ <string name="generic_cancel" msgid="2631708607129269698">"Preklic"</string>
<string name="permission_access_always" msgid="2107115233573823032">"Ves čas"</string>
<string name="permission_access_only_foreground" msgid="4412115020089923986">"Med uporabo aplikacije"</string>
<string name="app_permission_button_allow_always" msgid="4920899432212307102">"Ves čas"</string>
diff --git a/PermissionController/res/values-sl/strings.xml b/PermissionController/res/values-sl/strings.xml
index d0317471e..168646ac1 100644
--- a/PermissionController/res/values-sl/strings.xml
+++ b/PermissionController/res/values-sl/strings.xml
@@ -206,7 +206,7 @@
<string name="unused_apps_category_title" msgid="2988455616845243901">"Nastavitve neuporabljenih aplikacij"</string>
<string name="auto_revoke_label" msgid="5068393642936571656">"Odstrani dovoljenja, če aplikacija ni v uporabi"</string>
<string name="unused_apps_label" msgid="2595428768404901064">"Odstrani dovoljenja in sprosti prostor"</string>
- <string name="unused_apps_label_v2" msgid="7058776770056517980">"Zaustavi dejavnost aplikacije ob neuporabi"</string>
+ <string name="unused_apps_label_v2" msgid="7058776770056517980">"Zaustavi aplikacijo ob neuporabi"</string>
<string name="unused_apps_label_v3" msgid="693340578642156657">"Upravljanje aplikacije ob neuporabi"</string>
<string name="unused_apps_summary" msgid="8839466950318403115">"Dovoljenja se odstranijo, začasne datoteke se izbrišejo in prikazovanje obvestil se ustavi."</string>
<string name="unused_apps_summary_v2" msgid="5011313200815115802">"Odstranitev dovoljenj, izbris začasnih datotek, ustavitev prikazovanja obvestil in arhiviranje aplikacije"</string>
diff --git a/PermissionController/res/values-sq/strings.xml b/PermissionController/res/values-sq/strings.xml
index 9979f58ad..793b1beb1 100644
--- a/PermissionController/res/values-sq/strings.xml
+++ b/PermissionController/res/values-sq/strings.xml
@@ -445,7 +445,7 @@
<string name="car_default_app_selected" msgid="5416420830430644174">"Zgjedhur"</string>
<string name="car_default_app_selected_with_info" msgid="1932204186080593500">"Zgjedhur - <xliff:g id="ADDITIONAL_INFO">%1$s</xliff:g>"</string>
<string name="special_app_access_search_keyword" msgid="8032347212290774210">"qasje e veçantë e aplikacionit"</string>
- <string name="special_app_access" msgid="5019319067120213797">"Qasje e veçantë aplikacioni"</string>
+ <string name="special_app_access" msgid="5019319067120213797">"Qasja e veçantë e apl."</string>
<string name="no_special_app_access" msgid="6950277571805106247">"Jo qasje e veçantë aplikacioni"</string>
<string name="special_app_access_no_apps" msgid="4102911722787886970">"Nuk ka aplikacione"</string>
<string name="home_missing_work_profile_support" msgid="1756855847669387977">"Profili i punës nuk mbështetet"</string>
diff --git a/PermissionController/res/values-zh-rCN/strings.xml b/PermissionController/res/values-zh-rCN/strings.xml
index ee6f9e3bf..1b76de832 100644
--- a/PermissionController/res/values-zh-rCN/strings.xml
+++ b/PermissionController/res/values-zh-rCN/strings.xml
@@ -203,13 +203,13 @@
<string name="app_permission_footer_app_permissions_link" msgid="4926890342636587393">"查看“<xliff:g id="APP">%1$s</xliff:g>”的所有权限"</string>
<string name="app_permission_footer_permission_apps_link" msgid="3941988129992794327">"查看具有此权限的所有应用"</string>
<string name="assistant_mic_label" msgid="1011432357152323896">"显示 Google 助理麦克风使用情况"</string>
- <string name="unused_apps_category_title" msgid="2988455616845243901">"针对闲置应用的设置"</string>
+ <string name="unused_apps_category_title" msgid="2988455616845243901">"闲置应用设置"</string>
<string name="auto_revoke_label" msgid="5068393642936571656">"如果未使用此应用,则移除相关权限"</string>
<string name="unused_apps_label" msgid="2595428768404901064">"撤消权限并释放空间"</string>
<string name="unused_apps_label_v2" msgid="7058776770056517980">"暂停闲置应用的活动"</string>
<string name="unused_apps_label_v3" msgid="693340578642156657">"管理闲置应用"</string>
- <string name="unused_apps_summary" msgid="8839466950318403115">"移除权限、删除临时文件并停止发送通知"</string>
- <string name="unused_apps_summary_v2" msgid="5011313200815115802">"移除权限、删除临时文件、停止发送通知并归档应用"</string>
+ <string name="unused_apps_summary" msgid="8839466950318403115">"撤消权限、删除临时文件并停收通知"</string>
+ <string name="unused_apps_summary_v2" msgid="5011313200815115802">"撤消权限、删除临时文件、停收通知并归档应用"</string>
<string name="auto_revoke_summary" msgid="5867548789805911683">"为了保护您的数据,如果您连续几个月未使用此应用,系统会移除其权限。"</string>
<string name="auto_revoke_summary_with_permissions" msgid="389712086597285013">"为了保护您的数据,如果您连续几个月未使用此应用,系统会移除其以下权限:<xliff:g id="PERMS">%1$s</xliff:g>"</string>
<string name="auto_revoked_apps_page_summary" msgid="6594753657893756536">"为了保护您的数据,对于您连续几个月未使用过的应用,系统已将其权限移除。"</string>
diff --git a/PermissionController/res/xml/roles.xml b/PermissionController/res/xml/roles.xml
index fb12ed0d0..69ea7b1b7 100644
--- a/PermissionController/res/xml/roles.xml
+++ b/PermissionController/res/xml/roles.xml
@@ -724,10 +724,6 @@
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>
@@ -1495,10 +1491,8 @@
<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" />
+ <permission name="android.permission.MANAGE_DEFAULT_APPLICATIONS" minSdkVersion="36"
+ featureFlag="com.android.permission.flags.Flags.crossUserRoleEnabled" />
</permissions>
</role>
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 56c4944a0..99145c747 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
@@ -26,6 +26,7 @@ import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import com.android.modules.utils.build.SdkLevel;
+import com.android.role.controller.util.RoleFlags;
import com.android.role.controller.util.PackageUtils;
import java.util.Objects;
@@ -137,8 +138,8 @@ public class AppOp {
return false;
}
return Build.VERSION.SDK_INT >= mMinSdkVersion
- // Workaround to match the value 35 for V in roles.xml before SDK finalization.
- || (mMinSdkVersion == 35 && SdkLevel.isAtLeastV());
+ // Workaround to match the value 36 for B in roles.xml before SDK finalization.
+ || (mMinSdkVersion == 36 && RoleFlags.isAtLeastB());
}
private boolean isAvailableAsUser(@NonNull String packageName,
diff --git a/PermissionController/role-controller/java/com/android/role/controller/model/Permission.java b/PermissionController/role-controller/java/com/android/role/controller/model/Permission.java
index 05b19ff94..889f5263d 100644
--- a/PermissionController/role-controller/java/com/android/role/controller/model/Permission.java
+++ b/PermissionController/role-controller/java/com/android/role/controller/model/Permission.java
@@ -26,6 +26,7 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.android.modules.utils.build.SdkLevel;
+import com.android.role.controller.util.RoleFlags;
import com.android.role.controller.util.UserUtils;
import java.util.Objects;
@@ -97,8 +98,8 @@ public class Permission {
return false;
}
if (Build.VERSION.SDK_INT >= mMinSdkVersion
- // Workaround to match the value 35 for V in roles.xml before SDK finalization.
- || (mMinSdkVersion == 35 && SdkLevel.isAtLeastV())) {
+ // Workaround to match the value 36 for B in roles.xml before SDK finalization.
+ || (mMinSdkVersion == 36 && RoleFlags.isAtLeastB())) {
return true;
}
if (Build.VERSION.SDK_INT >= mOptionalMinSdkVersion) {
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 1d49b3c1a..9773b93a9 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
@@ -502,8 +502,8 @@ public class Role {
return false;
}
return (Build.VERSION.SDK_INT >= mMinSdkVersion
- // Workaround to match the value 35 for V in roles.xml before SDK finalization.
- || (mMinSdkVersion == 35 && SdkLevel.isAtLeastV()))
+ // Workaround to match the value 36 for B in roles.xml before SDK finalization.
+ || (mMinSdkVersion == 36 && RoleFlags.isAtLeastB()))
&& Build.VERSION.SDK_INT <= mMaxSdkVersion;
}
diff --git a/PermissionController/src/com/android/permissioncontroller/ecm/EnhancedConfirmationDialogActivity.kt b/PermissionController/src/com/android/permissioncontroller/ecm/EnhancedConfirmationDialogActivity.kt
index e6cf094e3..e2d46e519 100644
--- a/PermissionController/src/com/android/permissioncontroller/ecm/EnhancedConfirmationDialogActivity.kt
+++ b/PermissionController/src/com/android/permissioncontroller/ecm/EnhancedConfirmationDialogActivity.kt
@@ -18,7 +18,6 @@ package com.android.permissioncontroller.ecm
import android.annotation.SuppressLint
import android.app.AlertDialog
-import android.app.AppOpsManager
import android.app.Dialog
import android.app.ecm.EnhancedConfirmationManager
import android.content.Context
@@ -55,6 +54,8 @@ import com.android.role.controller.model.Roles
class EnhancedConfirmationDialogActivity : FragmentActivity() {
companion object {
private const val KEY_WAS_CLEAR_RESTRICTION_ALLOWED = "KEY_WAS_CLEAR_RESTRICTION_ALLOWED"
+ private const val REASON_PHONE_STATE = "phone_state"
+ private const val REASON_APP_OP_RESTRICTED = "app_op_restricted"
}
private var wasClearRestrictionAllowed: Boolean = false
@@ -77,6 +78,7 @@ class EnhancedConfirmationDialogActivity : FragmentActivity() {
val packageName = intent.getStringExtra(Intent.EXTRA_PACKAGE_NAME)
val settingIdentifier = intent.getStringExtra(Intent.EXTRA_SUBJECT)
val isEcmInApp = intent.getBooleanExtra(EXTRA_IS_ECM_IN_APP, false)
+ val reason = intent.getStringExtra(Intent.EXTRA_REASON)
require(uid != Process.INVALID_UID) { "EXTRA_UID cannot be null or invalid" }
require(!packageName.isNullOrEmpty()) { "EXTRA_PACKAGE_NAME cannot be null or empty" }
@@ -84,9 +86,9 @@ class EnhancedConfirmationDialogActivity : FragmentActivity() {
wasClearRestrictionAllowed =
setClearRestrictionAllowed(packageName, UserHandle.getUserHandleForUid(uid))
- val setting = Setting.fromIdentifier(this, settingIdentifier, isEcmInApp)
+ val setting = Setting.fromIdentifier(this, settingIdentifier, isEcmInApp, reason)
if (
- SettingType.fromIdentifier(this, settingIdentifier, isEcmInApp) ==
+ SettingType.fromIdentifier(this, settingIdentifier, isEcmInApp, reason) ==
SettingType.BLOCKED_DUE_TO_PHONE_STATE &&
!Flags.unknownCallPackageInstallBlockingEnabled()
) {
@@ -127,8 +129,10 @@ class EnhancedConfirmationDialogActivity : FragmentActivity() {
context: Context,
settingIdentifier: String,
isEcmInApp: Boolean,
+ reason: String?,
): Setting {
- val settingType = SettingType.fromIdentifier(context, settingIdentifier, isEcmInApp)
+ val settingType =
+ SettingType.fromIdentifier(context, settingIdentifier, isEcmInApp, reason)
val label =
when (settingType) {
SettingType.PLATFORM_PERMISSION ->
@@ -189,10 +193,10 @@ class EnhancedConfirmationDialogActivity : FragmentActivity() {
context: Context,
settingIdentifier: String,
isEcmInApp: Boolean,
+ restrictionReason: String?,
): SettingType {
return when {
- settingIdentifier == AppOpsManager.OPSTR_REQUEST_INSTALL_PACKAGES ->
- BLOCKED_DUE_TO_PHONE_STATE
+ restrictionReason == REASON_PHONE_STATE -> BLOCKED_DUE_TO_PHONE_STATE
!isEcmInApp -> OTHER
PermissionMapping.isRuntimePlatformPermission(settingIdentifier) &&
PermissionMapping.getGroupOfPlatformPermission(settingIdentifier) != null ->
diff --git a/PermissionController/src/com/android/permissioncontroller/incident/wear/WearConfirmationScreen.kt b/PermissionController/src/com/android/permissioncontroller/incident/wear/WearConfirmationScreen.kt
index 8e58d48d9..116b52cfb 100644
--- a/PermissionController/src/com/android/permissioncontroller/incident/wear/WearConfirmationScreen.kt
+++ b/PermissionController/src/com/android/permissioncontroller/incident/wear/WearConfirmationScreen.kt
@@ -29,14 +29,15 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.ComposeView
-import androidx.wear.compose.foundation.lazy.ScalingLazyListState
import androidx.wear.compose.material.CircularProgressIndicator
-import com.android.permissioncontroller.permission.ui.wear.elements.AlertDialog
-import com.android.permissioncontroller.permission.ui.wear.elements.SingleButtonAlertDialog
+import com.android.permissioncontroller.permission.ui.wear.elements.material2.DialogButtonContent
+import com.android.permissioncontroller.permission.ui.wear.elements.material3.WearPermissionConfirmationDialog
+import com.android.permissioncontroller.permission.ui.wear.theme.ResourceHelper
import com.android.permissioncontroller.permission.ui.wear.theme.WearPermissionTheme
@Composable
fun WearConfirmationScreen(viewModel: WearConfirmationActivityViewModel) {
+ val materialUIVersion = ResourceHelper.materialUIVersionInSettings
// Wear screen doesn't show incident/bug report's optional reasons and images.
val showDialog = viewModel.showDialogLiveData.observeAsState(false)
val showDenyReport = viewModel.showDenyReportLiveData.observeAsState(false)
@@ -47,27 +48,25 @@ fun WearConfirmationScreen(viewModel: WearConfirmationActivityViewModel) {
if (isLoading) {
CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
} else {
- if (showDenyReport.value) {
- contentArgs.value?.let {
- SingleButtonAlertDialog(
- showDialog = showDialog.value,
- title = it.title,
- message = it.message,
- onButtonClick = it.onDenyClick,
- scalingLazyListState = ScalingLazyListState(0)
+ contentArgs.value?.apply {
+ if (showDenyReport.value) {
+ WearPermissionConfirmationDialog(
+ materialUIVersion = materialUIVersion,
+ show = showDialog.value,
+ title = title,
+ message = message,
+ positiveButtonContent = DialogButtonContent(onClick = onDenyClick),
+ )
+ } else {
+ WearPermissionConfirmationDialog(
+ materialUIVersion = materialUIVersion,
+ show = showDialog.value,
+ title = title,
+ message = message,
+ positiveButtonContent = DialogButtonContent(onClick = onOkClick),
+ negativeButtonContent = DialogButtonContent(onClick = onCancelClick),
)
}
- return
- }
- contentArgs.value?.let {
- AlertDialog(
- showDialog = showDialog.value,
- title = it.title,
- message = it.message,
- onOKButtonClick = it.onOkClick,
- onCancelButtonClick = it.onCancelClick,
- scalingLazyListState = ScalingLazyListState(0)
- )
}
}
}
diff --git a/PermissionController/src/com/android/permissioncontroller/permission/service/v33/SafetyCenterQsTileService.kt b/PermissionController/src/com/android/permissioncontroller/permission/service/v33/SafetyCenterQsTileService.kt
index a69b78a06..5ba19f4c0 100644
--- a/PermissionController/src/com/android/permissioncontroller/permission/service/v33/SafetyCenterQsTileService.kt
+++ b/PermissionController/src/com/android/permissioncontroller/permission/service/v33/SafetyCenterQsTileService.kt
@@ -70,7 +70,6 @@ class SafetyCenterQsTileService : TileService() {
qsTile.label = getString(R.string.safety_privacy_qs_tile_title)
qsTile.subtitle = getString(R.string.safety_privacy_qs_tile_subtitle)
qsTile.contentDescription = TextUtils.concat(qsTile.label, ", ", qsTile.subtitle)
- qsTile.state = Tile.STATE_ACTIVE
qsTile.updateTile()
}
diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/GrantPermissionsActivity.java b/PermissionController/src/com/android/permissioncontroller/permission/ui/GrantPermissionsActivity.java
index c1479caf2..a7114f30b 100644
--- a/PermissionController/src/com/android/permissioncontroller/permission/ui/GrantPermissionsActivity.java
+++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/GrantPermissionsActivity.java
@@ -262,6 +262,9 @@ public class GrantPermissionsActivity extends SettingsActivity
if (DeviceUtils.isWear(this)) {
// Do not grab input focus and hide keyboard.
getWindow().addFlags(FLAG_ALT_FOCUSABLE_IM);
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
+ overrideActivityTransition(OVERRIDE_TRANSITION_OPEN, 0, 0);
+ }
}
if (PackageManager.ACTION_REQUEST_PERMISSIONS_FOR_OTHER.equals(getIntent().getAction())) {
diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/GrantPermissionsWearViewHandler.java b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/GrantPermissionsWearViewHandler.java
index c9e9a2eb1..5100b08fd 100644
--- a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/GrantPermissionsWearViewHandler.java
+++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/GrantPermissionsWearViewHandler.java
@@ -271,6 +271,12 @@ public class GrantPermissionsWearViewHandler implements GrantPermissionsViewHand
WearGrantPermissionsScreenKt.setContent(root,
mViewModel,
+ () -> {
+ if (mResultListener != null) {
+ mResultListener.onPermissionGrantResult(null, null, CANCELED);
+ }
+ return Unit.INSTANCE;
+ },
id -> {
onButtonClicked(id);
return Unit.INSTANCE;
@@ -278,7 +284,8 @@ public class GrantPermissionsWearViewHandler implements GrantPermissionsViewHand
checked -> {
onLocationSwitchChanged(checked);
return Unit.INSTANCE;
- });
+ }
+ );
if (mGroupName != null) {
updateScreen();
}
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 510d19706..691ceae25 100644
--- a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/LocationProviderDialogScreen.kt
+++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/LocationProviderDialogScreen.kt
@@ -26,7 +26,7 @@ import androidx.wear.compose.foundation.rememberSwipeToDismissBoxState
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.material2.Chip
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
diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/TEST_MAPPING b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/TEST_MAPPING
new file mode 100644
index 000000000..3cc91855d
--- /dev/null
+++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/TEST_MAPPING
@@ -0,0 +1,22 @@
+{
+ "wear-presubmit": [
+ {
+ // See b/387705174 for context
+ "name": "CtsPermissionUiTestCases",
+ "options": [
+ {
+ // Flaky
+ "exclude-filter": "android.permissionui.cts.PermissionTest22#testNoRuntimePrompt"
+ },
+ {
+ // Flaky
+ "exclude-filter": "android.permissionui.cts.NotificationPermissionTest"
+ },
+ {
+ // Flaky
+ "exclude-filter": "android.permissionui.cts.EnhancedConfirmationManagerTest"
+ }
+ ]
+ }
+ ]
+} \ No newline at end of file
diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/WearAppPermissionGroupsScreen.kt b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/WearAppPermissionGroupsScreen.kt
index ba37205a6..686dd1b62 100644
--- a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/WearAppPermissionGroupsScreen.kt
+++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/WearAppPermissionGroupsScreen.kt
@@ -24,17 +24,20 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.res.stringResource
-import androidx.wear.compose.foundation.lazy.rememberScalingLazyListState
import com.android.permissioncontroller.R
-import com.android.permissioncontroller.permission.ui.wear.elements.AlertDialog
-import com.android.permissioncontroller.permission.ui.wear.elements.Chip
import com.android.permissioncontroller.permission.ui.wear.elements.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.material2.Chip
+import com.android.permissioncontroller.permission.ui.wear.elements.material2.DialogButtonContent
+import com.android.permissioncontroller.permission.ui.wear.elements.material2.ToggleChip
+import com.android.permissioncontroller.permission.ui.wear.elements.material2.ToggleChipToggleControl
+import com.android.permissioncontroller.permission.ui.wear.elements.material3.WearPermissionConfirmationDialog
import com.android.permissioncontroller.permission.ui.wear.model.RevokeDialogArgs
+import com.android.permissioncontroller.permission.ui.wear.theme.ResourceHelper
+import com.android.permissioncontroller.permission.ui.wear.theme.WearPermissionMaterialUIVersion
@Composable
fun WearAppPermissionGroupsScreen(helper: WearAppPermissionGroupsHelper) {
+ val materialUIVersion = ResourceHelper.materialUIVersionInSettings
val packagePermGroups = helper.viewModel.packagePermGroupsLiveData.observeAsState(null)
val autoRevoke = helper.viewModel.autoRevokeLiveData.observeAsState(null)
val appPermissionUsages = helper.wearViewModel.appPermissionUsages.observeAsState(emptyList())
@@ -50,11 +53,12 @@ fun WearAppPermissionGroupsScreen(helper: WearAppPermissionGroupsHelper) {
WearAppPermissionGroupsContent(
isLoading,
helper.getPermissionGroupChipParams(appPermissionUsages.value),
- helper.getAutoRevokeChipParam(autoRevoke.value)
+ helper.getAutoRevokeChipParam(autoRevoke.value),
)
RevokeDialog(
+ materialUIVersion = materialUIVersion,
showDialog = showRevokeDialog.value,
- args = helper.revokeDialogViewModel.revokeDialogArgs
+ args = helper.revokeDialogViewModel.revokeDialogArgs,
)
if (showLocationProviderDialog.value) {
LocationProviderDialogScreen(
@@ -72,7 +76,7 @@ fun WearAppPermissionGroupsScreen(helper: WearAppPermissionGroupsHelper) {
internal fun WearAppPermissionGroupsContent(
isLoading: Boolean,
permissionGroupChipParams: List<PermissionGroupChipParam>,
- autoRevokeChipParam: AutoRevokeChipParam?
+ autoRevokeChipParam: AutoRevokeChipParam?,
) {
ScrollableScreen(title = stringResource(R.string.app_permissions), isLoading = isLoading) {
if (permissionGroupChipParams.isEmpty()) {
@@ -86,7 +90,7 @@ internal fun WearAppPermissionGroupsContent(
label = info.label,
enabled = info.enabled,
toggleControl = ToggleChipToggleControl.Switch,
- onCheckedChanged = info.onCheckedChanged
+ onCheckedChanged = info.onCheckedChanged,
)
} else {
Chip(
@@ -95,7 +99,7 @@ internal fun WearAppPermissionGroupsContent(
secondaryLabel = info.summary?.let { info.summary },
secondaryLabelMaxLines = Integer.MAX_VALUE,
enabled = info.enabled,
- onClick = info.onClick
+ onClick = info.onClick,
)
}
}
@@ -108,7 +112,7 @@ internal fun WearAppPermissionGroupsContent(
label = stringResource(it.labelRes),
labelMaxLine = 3,
toggleControl = ToggleChipToggleControl.Switch,
- onCheckedChanged = it.onCheckedChanged
+ onCheckedChanged = it.onCheckedChanged,
)
}
}
@@ -118,14 +122,19 @@ internal fun WearAppPermissionGroupsContent(
}
@Composable
-internal fun RevokeDialog(showDialog: Boolean, args: RevokeDialogArgs?) {
- args?.let {
- AlertDialog(
- showDialog = showDialog,
- message = stringResource(it.messageId),
- onOKButtonClick = it.onOkButtonClick,
- onCancelButtonClick = it.onCancelButtonClick,
- scalingLazyListState = rememberScalingLazyListState()
+internal fun RevokeDialog(
+ materialUIVersion: WearPermissionMaterialUIVersion,
+ showDialog: Boolean,
+ args: RevokeDialogArgs?,
+) {
+
+ args?.run {
+ WearPermissionConfirmationDialog(
+ materialUIVersion = materialUIVersion,
+ show = showDialog,
+ message = stringResource(messageId),
+ positiveButtonContent = DialogButtonContent(onClick = onOkButtonClick),
+ negativeButtonContent = DialogButtonContent(onClick = onCancelButtonClick),
)
}
}
diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/WearAppPermissionScreen.kt b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/WearAppPermissionScreen.kt
index 202ad49bb..55db66d41 100644
--- a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/WearAppPermissionScreen.kt
+++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/WearAppPermissionScreen.kt
@@ -24,21 +24,26 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.res.stringResource
-import androidx.wear.compose.foundation.lazy.rememberScalingLazyListState
import androidx.wear.compose.material.ToggleChipDefaults
import com.android.permissioncontroller.R
import com.android.permissioncontroller.permission.ui.model.AppPermissionViewModel
import com.android.permissioncontroller.permission.ui.model.AppPermissionViewModel.ButtonState
import com.android.permissioncontroller.permission.ui.model.AppPermissionViewModel.ButtonType
import com.android.permissioncontroller.permission.ui.v33.AdvancedConfirmDialogArgs
-import com.android.permissioncontroller.permission.ui.wear.elements.AlertDialog
-import com.android.permissioncontroller.permission.ui.wear.elements.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.toggleChipDisabledColors
+import com.android.permissioncontroller.permission.ui.wear.elements.material2.DialogButtonContent
+import com.android.permissioncontroller.permission.ui.wear.elements.material2.ListFooter
+import com.android.permissioncontroller.permission.ui.wear.elements.material2.ToggleChip
+import com.android.permissioncontroller.permission.ui.wear.elements.material2.ToggleChipToggleControl
+import com.android.permissioncontroller.permission.ui.wear.elements.material2.toggleChipDisabledColors
+import com.android.permissioncontroller.permission.ui.wear.elements.material3.WearPermissionConfirmationDialog
+import com.android.permissioncontroller.permission.ui.wear.elements.material3.WearPermissionIconBuilder
+import com.android.permissioncontroller.permission.ui.wear.elements.material3.defaultAlertConfirmIcon
+import com.android.permissioncontroller.permission.ui.wear.elements.material3.defaultAlertDismissIcon
import com.android.permissioncontroller.permission.ui.wear.model.AppPermissionConfirmDialogViewModel
import com.android.permissioncontroller.permission.ui.wear.model.ConfirmDialogArgs
+import com.android.permissioncontroller.permission.ui.wear.theme.ResourceHelper
+import com.android.permissioncontroller.permission.ui.wear.theme.WearPermissionMaterialUIVersion
import com.android.settingslib.RestrictedLockUtils
@Composable
@@ -53,8 +58,9 @@ fun WearAppPermissionScreen(
onConfirmDialogCancelButtonClick: () -> Unit,
onAdvancedConfirmDialogOkButtonClick: (AdvancedConfirmDialogArgs) -> Unit,
onAdvancedConfirmDialogCancelButtonClick: () -> Unit,
- onDisabledAllowButtonClick: () -> Unit
+ onDisabledAllowButtonClick: () -> Unit,
) {
+ val materialUIVersion = ResourceHelper.materialUIVersionInSettings
val buttonState = viewModel.buttonStateLiveData.observeAsState(null)
val detailResIds = viewModel.detailResIdLiveData.observeAsState(null)
val admin = viewModel.showAdminSupportLiveData.observeAsState(null)
@@ -73,19 +79,21 @@ fun WearAppPermissionScreen(
onLocationSwitchChanged,
onGrantedStateChanged,
onFooterClicked,
- onDisabledAllowButtonClick
+ onDisabledAllowButtonClick,
)
ConfirmDialog(
+ materialUIVersion = materialUIVersion,
showDialog = showConfirmDialog.value,
args = confirmDialogViewModel.confirmDialogArgs,
onOkButtonClick = onConfirmDialogOkButtonClick,
- onCancelButtonClick = onConfirmDialogCancelButtonClick
+ onCancelButtonClick = onConfirmDialogCancelButtonClick,
)
AdvancedConfirmDialog(
+ materialUIVersion = materialUIVersion,
showDialog = showAdvancedConfirmDialog.value,
args = confirmDialogViewModel.advancedConfirmDialogArgs,
onOkButtonClick = onAdvancedConfirmDialogOkButtonClick,
- onCancelButtonClick = onAdvancedConfirmDialogCancelButtonClick
+ onCancelButtonClick = onAdvancedConfirmDialogCancelButtonClick,
)
}
if (isLoading && !buttonState.value.isNullOrEmpty()) {
@@ -103,7 +111,7 @@ internal fun WearAppPermissionContent(
onLocationSwitchChanged: (Boolean) -> Unit,
onGrantedStateChanged: (ButtonType, Boolean) -> Unit,
onFooterClicked: (RestrictedLockUtils.EnforcedAdmin) -> Unit,
- onDisabledAllowButtonClick: () -> Unit
+ onDisabledAllowButtonClick: () -> Unit,
) {
ScrollableScreen(title = title, isLoading = isLoading) {
buttonState?.get(ButtonType.LOCATION_ACCURACY)?.let {
@@ -115,7 +123,7 @@ internal fun WearAppPermissionContent(
label = stringResource(R.string.app_permission_location_accuracy),
toggleControl = ToggleChipToggleControl.Switch,
onCheckedChanged = onLocationSwitchChanged,
- labelMaxLine = Integer.MAX_VALUE
+ labelMaxLine = Integer.MAX_VALUE,
)
}
}
@@ -141,7 +149,7 @@ internal fun WearAppPermissionContent(
onDisabledAllowButtonClick()
}
},
- labelMaxLine = Integer.MAX_VALUE
+ labelMaxLine = Integer.MAX_VALUE,
)
}
}
@@ -157,7 +165,7 @@ internal fun WearAppPermissionContent(
{ onFooterClicked(admin) }
} else {
null
- }
+ },
)
}
}
@@ -172,7 +180,7 @@ internal val buttonTypeOrder =
ButtonType.ASK_ONCE,
ButtonType.ASK,
ButtonType.DENY,
- ButtonType.DENY_FOREGROUND
+ ButtonType.DENY_FOREGROUND,
)
@Composable
@@ -191,45 +199,60 @@ internal fun labelsByButton(buttonType: ButtonType) =
@Composable
internal fun ConfirmDialog(
+ materialUIVersion: WearPermissionMaterialUIVersion,
showDialog: Boolean,
args: ConfirmDialogArgs?,
onOkButtonClick: (ConfirmDialogArgs) -> Unit,
- onCancelButtonClick: () -> Unit
+ onCancelButtonClick: () -> Unit,
) {
- args?.let {
- AlertDialog(
- showDialog = showDialog,
- message = stringResource(it.messageId),
- onOKButtonClick = { onOkButtonClick(it) },
- onCancelButtonClick = onCancelButtonClick,
- scalingLazyListState = rememberScalingLazyListState()
+ args?.run {
+ WearPermissionConfirmationDialog(
+ materialUIVersion = materialUIVersion,
+ show = showDialog,
+ message = stringResource(messageId),
+ positiveButtonContent = DialogButtonContent(onClick = { onOkButtonClick(this) }),
+ negativeButtonContent = DialogButtonContent(onClick = { onCancelButtonClick() }),
)
}
}
@Composable
internal fun AdvancedConfirmDialog(
+ materialUIVersion: WearPermissionMaterialUIVersion,
showDialog: Boolean,
args: AdvancedConfirmDialogArgs?,
onOkButtonClick: (AdvancedConfirmDialogArgs) -> Unit,
- onCancelButtonClick: () -> Unit
+ onCancelButtonClick: () -> Unit,
) {
- args?.let {
- AlertDialog(
- showDialog = showDialog,
- title =
- if (it.titleId != 0) {
- stringResource(it.titleId)
- } else {
- ""
- },
- iconRes = it.iconId,
- message = stringResource(it.messageId),
- okButtonContentDescription = stringResource(it.positiveButtonTextId),
- cancelButtonContentDescription = stringResource(it.negativeButtonTextId),
- onOKButtonClick = { onOkButtonClick(it) },
- onCancelButtonClick = onCancelButtonClick,
- scalingLazyListState = rememberScalingLazyListState()
+ args?.run {
+ val title =
+ if (titleId != 0) {
+ stringResource(titleId)
+ } else {
+ ""
+ }
+ val okButtonIconBuilder =
+ WearPermissionIconBuilder.defaultAlertConfirmIcon()
+ .contentDescription(stringResource(positiveButtonTextId))
+ val cancelButtonIconBuilder =
+ WearPermissionIconBuilder.defaultAlertDismissIcon()
+ .contentDescription(stringResource(negativeButtonTextId))
+ WearPermissionConfirmationDialog(
+ materialUIVersion = materialUIVersion,
+ show = showDialog,
+ title = title,
+ iconRes = WearPermissionIconBuilder.builder(iconId),
+ message = stringResource(messageId),
+ positiveButtonContent =
+ DialogButtonContent(
+ icon = okButtonIconBuilder,
+ onClick = { onOkButtonClick(this) },
+ ),
+ negativeButtonContent =
+ DialogButtonContent(
+ icon = cancelButtonIconBuilder,
+ onClick = { onCancelButtonClick() },
+ ),
)
}
}
diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/WearEnhancedConfirmationScreen.kt b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/WearEnhancedConfirmationScreen.kt
index 1c31ec96f..a0e41b579 100644
--- a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/WearEnhancedConfirmationScreen.kt
+++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/WearEnhancedConfirmationScreen.kt
@@ -33,24 +33,26 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.fragment.app.FragmentActivity
import androidx.wear.compose.foundation.SwipeToDismissValue
-import androidx.wear.compose.foundation.lazy.ScalingLazyListState
import androidx.wear.compose.foundation.rememberSwipeToDismissBoxState
import androidx.wear.compose.material.ChipDefaults
import androidx.wear.compose.material.CircularProgressIndicator
import androidx.wear.compose.material.MaterialTheme
import androidx.wear.compose.material.SwipeToDismissBox
import com.android.permissioncontroller.R
-import com.android.permissioncontroller.permission.ui.wear.elements.AlertDialog
import com.android.permissioncontroller.permission.ui.wear.elements.CheckYourPhoneScreen
import com.android.permissioncontroller.permission.ui.wear.elements.CheckYourPhoneState
import com.android.permissioncontroller.permission.ui.wear.elements.CheckYourPhoneState.InProgress
import com.android.permissioncontroller.permission.ui.wear.elements.CheckYourPhoneState.Success
-import com.android.permissioncontroller.permission.ui.wear.elements.Chip
import com.android.permissioncontroller.permission.ui.wear.elements.ScrollableScreen
import com.android.permissioncontroller.permission.ui.wear.elements.dismiss
import com.android.permissioncontroller.permission.ui.wear.elements.findActivity
+import com.android.permissioncontroller.permission.ui.wear.elements.material2.Chip
+import com.android.permissioncontroller.permission.ui.wear.elements.material2.DialogButtonContent
+import com.android.permissioncontroller.permission.ui.wear.elements.material3.WearPermissionConfirmationDialog
+import com.android.permissioncontroller.permission.ui.wear.elements.material3.WearPermissionIconBuilder
import com.android.permissioncontroller.permission.ui.wear.model.WearEnhancedConfirmationViewModel
import com.android.permissioncontroller.permission.ui.wear.model.WearEnhancedConfirmationViewModel.ScreenState
+import com.android.permissioncontroller.permission.ui.wear.theme.ResourceHelper
@Composable
fun WearEnhancedConfirmationScreen(
@@ -58,6 +60,7 @@ fun WearEnhancedConfirmationScreen(
title: String?,
message: CharSequence?,
) {
+ val materialUIVersion = ResourceHelper.materialUIVersionInSettings
var dismissed by remember { mutableStateOf(false) }
val context = LocalContext.current
val ecmScreenState = remember { viewModel.screenState }
@@ -97,7 +100,7 @@ fun WearEnhancedConfirmationScreen(
onClick = { dismiss(activity) },
modifier = Modifier.fillMaxWidth(),
textColor = MaterialTheme.colors.surface,
- colors = ChipDefaults.primaryChipColors()
+ colors = ChipDefaults.primaryChipColors(),
)
}
item {
@@ -107,27 +110,30 @@ fun WearEnhancedConfirmationScreen(
modifier = Modifier.fillMaxWidth(),
)
}
- }
+ },
)
@Composable
fun ShowCheckYourPhoneDialog(state: CheckYourPhoneState) =
CheckYourPhoneScreen(
title = stringResource(id = R.string.wear_check_your_phone_title),
- state = state
+ state = state,
)
@Composable
fun ShowRemoteConnectionErrorDialog() =
- AlertDialog(
+ WearPermissionConfirmationDialog(
+ materialUIVersion = materialUIVersion,
+ show = true,
title = stringResource(R.string.wear_phone_connection_error),
message = stringResource(R.string.wear_phone_connection_should_retry),
- iconRes = R.drawable.ic_error,
- showDialog = true,
- okButtonIcon = R.drawable.ic_refresh,
- onOKButtonClick = { viewModel.openUriOnPhone(context) },
- onCancelButtonClick = { dismiss(activity) },
- scalingLazyListState = ScalingLazyListState(1)
+ iconRes = WearPermissionIconBuilder.builder(R.drawable.ic_error),
+ positiveButtonContent =
+ DialogButtonContent(
+ icon = WearPermissionIconBuilder.builder(R.drawable.ic_refresh),
+ onClick = { viewModel.openUriOnPhone(context) },
+ ),
+ negativeButtonContent = DialogButtonContent(onClick = { dismiss(activity) }),
)
SwipeToDismissBox(state = state) { isBackground ->
diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/WearGrantPermissionsScreen.kt b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/WearGrantPermissionsScreen.kt
index 1498b91b6..35c2ab046 100644
--- a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/WearGrantPermissionsScreen.kt
+++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/WearGrantPermissionsScreen.kt
@@ -18,10 +18,14 @@ package com.android.permissioncontroller.permission.ui.wear
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.livedata.observeAsState
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.res.stringResource
+import androidx.wear.compose.material3.Dialog
import com.android.permissioncontroller.R
import com.android.permissioncontroller.permission.ui.GrantPermissionsActivity.ALLOW_ALWAYS_BUTTON
import com.android.permissioncontroller.permission.ui.GrantPermissionsActivity.ALLOW_BUTTON
@@ -38,13 +42,13 @@ import com.android.permissioncontroller.permission.ui.GrantPermissionsActivity.N
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.ScrollableScreen
-import com.android.permissioncontroller.permission.ui.wear.elements.ToggleChipToggleControl
+import com.android.permissioncontroller.permission.ui.wear.elements.material2.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
+import kotlinx.coroutines.delay
@Composable
fun WearGrantPermissionsScreen(
@@ -58,13 +62,7 @@ fun WearGrantPermissionsScreen(
val locationVisibilities = viewModel.locationVisibilitiesLiveData.observeAsState(emptyList())
val preciseLocationChecked = viewModel.preciseLocationCheckedLiveData.observeAsState(false)
val buttonVisibilities = viewModel.buttonVisibilitiesLiveData.observeAsState(emptyList())
- val materialUIVersion =
- if (ResourceHelper.material3Enabled) {
- MATERIAL3
- } else {
- MATERIAL2_5
- }
-
+ val materialUIVersion = ResourceHelper.materialUIVersionInApp
ScrollableScreen(
materialUIVersion = materialUIVersion,
showTimeText = false,
@@ -119,12 +117,29 @@ fun WearGrantPermissionsScreen(
fun setContent(
composeView: ComposeView,
viewModel: WearGrantPermissionsViewModel,
+ onCancelled: () -> Unit,
onButtonClicked: (Int) -> Unit,
onLocationSwitchChanged: (Boolean) -> Unit,
) {
composeView.setContent {
- WearGrantPermissionsScreen(viewModel, onButtonClicked, onLocationSwitchChanged)
+ if (ResourceHelper.materialUIVersionInApp == MATERIAL3) {
+ AsDialog(onCancelled) {
+ WearGrantPermissionsScreen(viewModel, onButtonClicked, onLocationSwitchChanged)
+ }
+ } else {
+ WearGrantPermissionsScreen(viewModel, onButtonClicked, onLocationSwitchChanged)
+ }
+ }
+}
+
+@Composable
+private fun AsDialog(onDismissRequest: () -> Unit, content: @Composable () -> Unit) {
+ val showDialog = remember { mutableStateOf(false) }
+ LaunchedEffect(Unit) {
+ delay(300)
+ showDialog.value = true
}
+ Dialog(show = showDialog.value, onDismissRequest = onDismissRequest, content = content)
}
@Composable
diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/WearManageCustomPermissionScreen.kt b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/WearManageCustomPermissionScreen.kt
index 1563f6a57..15d4cd370 100644
--- a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/WearManageCustomPermissionScreen.kt
+++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/WearManageCustomPermissionScreen.kt
@@ -25,13 +25,13 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.res.stringResource
import com.android.permissioncontroller.R
import com.android.permissioncontroller.permission.ui.model.ManageCustomPermissionsViewModel
-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.material2.Chip
@Composable
fun WearManageCustomPermissionScreen(
viewModel: ManageCustomPermissionsViewModel,
- onPermGroupClick: (String) -> Unit
+ onPermGroupClick: (String) -> Unit,
) {
val permissionGroups = viewModel.uiDataLiveData.observeAsState(emptyMap())
var isLoading by remember { mutableStateOf(true) }
@@ -39,7 +39,7 @@ fun WearManageCustomPermissionScreen(
WearManageCustomPermissionContent(
isLoading,
getPermGroupChipParams(permissionGroups.value),
- onPermGroupClick
+ onPermGroupClick,
)
if (isLoading && permissionGroups.value.isNotEmpty()) {
@@ -51,11 +51,11 @@ fun WearManageCustomPermissionScreen(
internal fun WearManageCustomPermissionContent(
isLoading: Boolean,
permGroupChipParams: List<PermGroupChipParam>,
- onPermGroupClick: (String) -> Unit
+ onPermGroupClick: (String) -> Unit,
) {
ScrollableScreen(
title = stringResource(R.string.additional_permissions),
- isLoading = isLoading
+ isLoading = isLoading,
) {
for (params in permGroupChipParams) {
item {
@@ -65,7 +65,7 @@ internal fun WearManageCustomPermissionContent(
icon = params.icon,
secondaryLabel = params.secondaryLabel,
secondaryLabelMaxLines = 3,
- onClick = { onPermGroupClick(params.permGroupName) }
+ onClick = { onPermGroupClick(params.permGroupName) },
)
}
}
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 9aacd65d3..20f87f6ba 100644
--- a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/WearManageStandardPermissionScreen.kt
+++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/WearManageStandardPermissionScreen.kt
@@ -29,8 +29,8 @@ import androidx.compose.ui.res.stringResource
import com.android.permissioncontroller.R
import com.android.permissioncontroller.permission.model.livedatatypes.PermGroupPackagesUiInfo
import com.android.permissioncontroller.permission.ui.model.ManageStandardPermissionsViewModel
-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.material2.Chip
import com.android.permissioncontroller.permission.utils.KotlinUtils.getPermGroupIcon
import com.android.permissioncontroller.permission.utils.KotlinUtils.getPermGroupLabel
import com.android.permissioncontroller.permission.utils.StringUtils
@@ -42,7 +42,7 @@ fun WearManageStandardPermissionScreen(
viewModel: ManageStandardPermissionsViewModel,
onPermGroupClick: (String) -> Unit,
onCustomPermissionsClick: () -> Unit,
- onAutoRevokedClick: () -> Unit
+ onAutoRevokedClick: () -> Unit,
) {
val permissionGroups = viewModel.uiDataLiveData.observeAsState(emptyMap())
val numCustomPermGroups = viewModel.numCustomPermGroups.observeAsState(0)
@@ -56,7 +56,7 @@ fun WearManageStandardPermissionScreen(
numAutoRevoked.value,
onPermGroupClick,
onCustomPermissionsClick,
- onAutoRevokedClick
+ onAutoRevokedClick,
)
if (isLoading && permissionGroups.value.isNotEmpty()) {
@@ -92,7 +92,7 @@ internal fun getPermGroupChipParams(
label = getPermGroupLabel(context, it.key).toString(),
icon = getPermGroupIcon(context, it.key),
secondaryLabel =
- stringResource(summary, uiInfo.nonSystemGranted, uiInfo.nonSystemTotal)
+ stringResource(summary, uiInfo.nonSystemGranted, uiInfo.nonSystemTotal),
)
}
.sortedWith { lhs, rhs -> collator.compare(lhs.label, rhs.label) }
@@ -107,11 +107,11 @@ internal fun WearManageStandardPermissionContent(
numAutoRevoked: Int,
onPermGroupClick: (String) -> Unit,
onCustomPermissionsClick: () -> Unit,
- onAutoRevokedClick: () -> Unit
+ onAutoRevokedClick: () -> Unit,
) {
ScrollableScreen(
title = stringResource(R.string.app_permission_manager),
- isLoading = isLoading
+ isLoading = isLoading,
) {
for (params in permGroupChipParams) {
item {
@@ -121,7 +121,7 @@ internal fun WearManageStandardPermissionContent(
icon = params.icon,
secondaryLabel = params.secondaryLabel,
secondaryLabelMaxLines = 3,
- onClick = { onPermGroupClick(params.permGroupName) }
+ onClick = { onPermGroupClick(params.permGroupName) },
)
}
}
@@ -136,10 +136,10 @@ internal fun WearManageStandardPermissionContent(
StringUtils.getIcuPluralsString(
LocalContext.current,
R.string.additional_permissions_more,
- numCustomPermGroups
+ numCustomPermGroups,
),
secondaryLabelMaxLines = 3,
- onClick = onCustomPermissionsClick
+ onClick = onCustomPermissionsClick,
)
}
}
@@ -152,7 +152,7 @@ internal fun WearManageStandardPermissionContent(
icon = R.drawable.ic_info,
secondaryLabel = stringResource(R.string.auto_revoke_setting_subtitle),
secondaryLabelMaxLines = 3,
- onClick = onAutoRevokedClick
+ onClick = onAutoRevokedClick,
)
}
}
diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/WearPermissionAppsScreen.kt b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/WearPermissionAppsScreen.kt
index 8e779cb8c..00ebf2f34 100644
--- a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/WearPermissionAppsScreen.kt
+++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/WearPermissionAppsScreen.kt
@@ -32,9 +32,9 @@ import androidx.compose.ui.unit.dp
import androidx.wear.compose.material.Text
import com.android.permissioncontroller.R
import com.android.permissioncontroller.permission.ui.Category
-import com.android.permissioncontroller.permission.ui.wear.elements.Chip
-import com.android.permissioncontroller.permission.ui.wear.elements.ListSubheader
import com.android.permissioncontroller.permission.ui.wear.elements.ScrollableScreen
+import com.android.permissioncontroller.permission.ui.wear.elements.material2.Chip
+import com.android.permissioncontroller.permission.ui.wear.elements.material2.ListSubheader
/** Compose the screen associated to a [WearPermissionAppsFragment]. */
@Composable
@@ -65,7 +65,7 @@ fun WearPermissionAppsScreen(helper: WearPermissionAppsHelper) {
subtitle = subTitle,
showAlways = showAlways,
isLoading = isLoading,
- onShowSystemClick = helper.onShowSystemClick
+ onShowSystemClick = helper.onShowSystemClick,
)
}
}
@@ -84,7 +84,7 @@ internal fun WearPermissionAppsContent(
subtitle: String,
showAlways: Boolean,
isLoading: Boolean,
- onShowSystemClick: (showSystem: Boolean) -> Unit
+ onShowSystemClick: (showSystem: Boolean) -> Unit,
) {
ScrollableScreen(title = title, subtitle = subtitle, isLoading = isLoading) {
val firstItemIndex = categoryOrder.indexOfFirst { !chipsByCategory[it].isNullOrEmpty() }
@@ -100,7 +100,7 @@ internal fun WearPermissionAppsContent(
top = if (index == firstItemIndex) 0.dp else 12.dp,
bottom = 4.dp,
start = 14.dp,
- end = 14.dp
+ end = 14.dp,
)
) {
Text(text = stringResource(getCategoryString(category, showAlways)))
@@ -116,7 +116,7 @@ internal fun WearPermissionAppsContent(
icon = it.icon,
enabled = it.enabled,
onClick = { it.onClick() },
- modifier = Modifier.fillMaxWidth()
+ modifier = Modifier.fillMaxWidth(),
)
}
}
@@ -163,5 +163,5 @@ internal val categoryOrder =
Category.ALLOWED.categoryName,
Category.ALLOWED_FOREGROUND.categoryName,
Category.ASK.categoryName,
- Category.DENIED.categoryName
+ Category.DENIED.categoryName,
)
diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/WearPermissionUsageDetailsScreen.kt b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/WearPermissionUsageDetailsScreen.kt
index 1259c1ab5..63a6cd5a5 100644
--- a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/WearPermissionUsageDetailsScreen.kt
+++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/WearPermissionUsageDetailsScreen.kt
@@ -37,15 +37,15 @@ import com.android.permissioncontroller.permission.ui.model.v31.BasePermissionUs
import com.android.permissioncontroller.permission.ui.model.v31.PermissionUsageDetailsViewModel
import com.android.permissioncontroller.permission.ui.model.v31.PermissionUsageDetailsViewModel.AppPermissionAccessUiInfo
import com.android.permissioncontroller.permission.ui.model.v31.PermissionUsageDetailsViewModel.PermissionUsageDetailsUiState
-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.material2.Chip
import com.android.permissioncontroller.permission.utils.KotlinUtils
@RequiresApi(Build.VERSION_CODES.S)
@Composable
fun WearPermissionUsageDetailsScreen(
permissionGroup: String,
- viewModel: BasePermissionUsageDetailsViewModel
+ viewModel: BasePermissionUsageDetailsViewModel,
) {
val context = LocalContext.current
val uiData = viewModel.getPermissionUsagesDetailsInfoUiLiveData().observeAsState(null)
@@ -56,7 +56,7 @@ fun WearPermissionUsageDetailsScreen(
val subtitle =
stringResource(
R.string.permission_group_usage_title,
- KotlinUtils.getPermGroupLabel(context, permissionGroup)
+ KotlinUtils.getPermGroupLabel(context, permissionGroup),
)
val hasSystemApps: Boolean =
@@ -80,7 +80,7 @@ fun WearPermissionUsageDetailsScreen(
uiInfo.accessStartTime,
uiInfo.accessEndTime,
uiInfo.showingAttribution,
- uiInfo.attributionTags
+ uiInfo.attributionTags,
)
context.startActivityAsUser(intent, uiInfo.userHandle)
}
@@ -108,7 +108,7 @@ fun WearPermissionUsageDetailsScreen(
onShowSystemClick,
appPermissionAccessUiInfoList,
onChipClick,
- onManagePermissionClick
+ onManagePermissionClick,
)
if (isLoading && uiData.value != null) {
@@ -126,7 +126,7 @@ internal fun WearPermissionUsageDetailsContent(
onShowSystemClick: (Boolean) -> Unit,
appPermissionAccessUiInfoList: List<AppPermissionAccessUiInfo>,
onChipClick: (AppPermissionAccessUiInfo) -> Unit,
- onManagePermissionClick: () -> Unit
+ onManagePermissionClick: () -> Unit,
) {
ScrollableScreen(title = title, subtitle = subtitle, isLoading = isLoading) {
if (appPermissionAccessUiInfoList.isEmpty()) {
@@ -142,7 +142,7 @@ internal fun WearPermissionUsageDetailsContent(
.format(uiInfo.accessEndTime),
secondaryLabelMaxLines = Int.MAX_VALUE,
icon = uiInfo.badgedPackageIcon,
- onClick = { onChipClick(uiInfo) }
+ onClick = { onChipClick(uiInfo) },
)
}
}
diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/WearPermissionUsageScreen.kt b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/WearPermissionUsageScreen.kt
index f83d3338d..20e0dd69b 100644
--- a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/WearPermissionUsageScreen.kt
+++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/WearPermissionUsageScreen.kt
@@ -32,17 +32,14 @@ import com.android.permissioncontroller.R
import com.android.permissioncontroller.permission.ui.handheld.v31.PermissionUsageControlPreference
import com.android.permissioncontroller.permission.ui.viewmodel.v31.PermissionUsageViewModel
import com.android.permissioncontroller.permission.ui.viewmodel.v31.PermissionUsagesUiState
-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.material2.Chip
import com.android.permissioncontroller.permission.utils.Utils
import java.text.Collator
@RequiresApi(Build.VERSION_CODES.S)
@Composable
-fun WearPermissionUsageScreen(
- sessionId: Long,
- viewModel: PermissionUsageViewModel,
-) {
+fun WearPermissionUsageScreen(sessionId: Long, viewModel: PermissionUsageViewModel) {
val context = LocalContext.current
val permissionUsagesUiData = viewModel.permissionUsagesUiLiveData.observeAsState(null)
val showSystem = viewModel.showSystemAppsLiveData.observeAsState(false)
@@ -97,7 +94,7 @@ fun WearPermissionUsageScreen(
hasSystemApps,
showSystem.value,
onShowSystemClick,
- permissionGroupPreferences
+ permissionGroupPreferences,
)
if (isLoading && isDataLoaded) {
@@ -111,11 +108,11 @@ internal fun WearPermissionUsageContent(
hasSystemApps: Boolean,
showSystem: Boolean,
onShowSystemClick: (Boolean) -> Unit,
- permissionGroupPreferences: List<PermissionUsageControlPreference>
+ permissionGroupPreferences: List<PermissionUsageControlPreference>,
) {
ScrollableScreen(
title = stringResource(R.string.permission_usage_title),
- isLoading = isLoading
+ isLoading = isLoading,
) {
if (permissionGroupPreferences.isEmpty()) {
item { Chip(label = stringResource(R.string.no_permissions), onClick = {}) }
@@ -129,7 +126,7 @@ internal fun WearPermissionUsageContent(
secondaryLabelMaxLines = Int.MAX_VALUE,
icon = preference.icon,
enabled = preference.isEnabled,
- onClick = { preference.performClick() }
+ onClick = { preference.performClick() },
)
}
}
diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/WearUnusedAppsScreen.kt b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/WearUnusedAppsScreen.kt
index 423fa7759..9170b7d20 100644
--- a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/WearUnusedAppsScreen.kt
+++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/WearUnusedAppsScreen.kt
@@ -25,9 +25,9 @@ import com.android.permissioncontroller.R
import com.android.permissioncontroller.hibernation.isHibernationEnabled
import com.android.permissioncontroller.permission.ui.model.UnusedAppsViewModel.UnusedPeriod
import com.android.permissioncontroller.permission.ui.model.UnusedAppsViewModel.UnusedPeriod.Companion.allPeriods
-import com.android.permissioncontroller.permission.ui.wear.elements.Chip
-import com.android.permissioncontroller.permission.ui.wear.elements.Icon
import com.android.permissioncontroller.permission.ui.wear.elements.ScrollableScreen
+import com.android.permissioncontroller.permission.ui.wear.elements.material2.Chip
+import com.android.permissioncontroller.permission.ui.wear.elements.material2.Icon
import com.android.permissioncontroller.permission.ui.wear.model.WearUnusedAppsViewModel
@Composable
@@ -43,7 +43,7 @@ fun WearUnusedAppsScreen(viewModel: WearUnusedAppsViewModel) {
showTimeText = true,
title = getScreenTitle(),
isLoading = loading.value,
- subtitle = getSubTitle(!infoMsgCategoryVisibility.value)
+ subtitle = getSubTitle(!infoMsgCategoryVisibility.value),
) {
for (period in allPeriods) {
if (!unusedAppChips.value.containsKey(period)) {
@@ -62,7 +62,7 @@ fun WearUnusedAppsScreen(viewModel: WearUnusedAppsViewModel) {
secondaryLabel = unusedAppChip.summary,
icon = unusedAppChip.icon,
iconContentDescription = unusedAppChip.contentDescription,
- onClick = unusedAppChip.onClick
+ onClick = unusedAppChip.onClick,
)
}
}
@@ -108,5 +108,5 @@ private fun posByPeriod(period: UnusedPeriod) =
private fun categoryTitleByPeriod(period: UnusedPeriod) =
MessageFormat.format(
stringResource(R.string.last_opened_category_title),
- mapOf("count" to period.months)
+ mapOf("count" to period.months),
)
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 6ce7df125..d01692159 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
@@ -19,56 +19,20 @@ package com.android.permissioncontroller.permission.ui.wear.elements
import android.app.Activity
import android.content.Context
import android.content.ContextWrapper
-import android.graphics.drawable.Drawable
-import androidx.compose.foundation.Image
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.PaddingValues
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.focus.FocusRequester
-import androidx.compose.ui.graphics.ColorFilter
-import androidx.compose.ui.layout.ContentScale
-import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext
-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.compose.ui.unit.dp
import androidx.fragment.app.FragmentActivity
-import androidx.lifecycle.Lifecycle
-import androidx.lifecycle.compose.LocalLifecycleOwner
-import androidx.lifecycle.repeatOnLifecycle
import androidx.wear.compose.foundation.SwipeToDismissValue
-import androidx.wear.compose.foundation.lazy.ScalingLazyColumn
-import androidx.wear.compose.foundation.lazy.ScalingLazyListScope
-import androidx.wear.compose.foundation.lazy.ScalingLazyListState
import androidx.wear.compose.foundation.rememberSwipeToDismissBoxState
-import androidx.wear.compose.material.CircularProgressIndicator
-import androidx.wear.compose.material.MaterialTheme
-import androidx.wear.compose.material.PositionIndicator
-import androidx.wear.compose.material.Scaffold
import androidx.wear.compose.material.SwipeToDismissBox
-import androidx.wear.compose.material.Text
-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
/**
* Screen that contains a list of items defined using the [content] parameter, adds the time text
@@ -86,7 +50,7 @@ fun ScrollableScreen(
isLoading: Boolean = false,
titleTestTag: String? = null,
subtitleTestTag: String? = null,
- content: ScalingLazyListScope.() -> Unit,
+ content: ListScopeWrapper.() -> Unit,
) {
var dismissed by remember { mutableStateOf(false) }
val activity = LocalContext.current.findActivity()
@@ -135,207 +99,6 @@ fun ScrollableScreen(
}
}
-@Composable
-internal fun Wear2Scaffold(
- showTimeText: Boolean,
- title: String?,
- subtitle: CharSequence?,
- image: Any?,
- isLoading: Boolean,
- content: ScalingLazyListScope.() -> Unit,
- titleTestTag: String? = null,
- subtitleTestTag: String? = null,
-) {
- val itemsSpacedBy = 4.dp
- val screenWidth = LocalConfiguration.current.screenWidthDp
- val screenHeight = LocalConfiguration.current.screenHeightDp
- val scrollContentHorizontalPadding = (screenWidth * 0.052).dp
- val titleHorizontalPadding = (screenWidth * 0.0884).dp
- val subtitleHorizontalPadding = (screenWidth * 0.0416).dp
- val scrollContentTopPadding = (screenHeight * 0.1456).dp - itemsSpacedBy
- val scrollContentBottomPadding = (screenHeight * 0.3636).dp
- val titleBottomPadding =
- if (subtitle == null) {
- 8.dp
- } else {
- 4.dp
- }
- val subtitleBottomPadding = 8.dp
- val timeTextTopPadding =
- if (showTimeText) {
- 1.dp
- } else {
- 0.dp
- }
- val titlePaddingValues =
- PaddingValues(
- start = titleHorizontalPadding,
- top = 4.dp,
- bottom = titleBottomPadding,
- end = titleHorizontalPadding,
- )
- val subTitlePaddingValues =
- PaddingValues(
- start = subtitleHorizontalPadding,
- top = 4.dp,
- bottom = subtitleBottomPadding,
- end = subtitleHorizontalPadding,
- )
- val initialCenterIndex = 0
- val centerHeightDp = Dp(LocalConfiguration.current.screenHeightDp / 2.0f)
- // We are adding TimeText's padding to create a smooth scrolling
- val initialCenterItemScrollOffset = scrollContentTopPadding + timeTextTopPadding
- val scrollAwayOffset = centerHeightDp - initialCenterItemScrollOffset
- val focusRequester = remember { FocusRequester() }
- val listState = remember { ScalingLazyListState(initialCenterItemIndex = initialCenterIndex) }
- LaunchedEffect(title) {
- listState.animateScrollToItem(index = 0) // Scroll to the top when triggerValue changes
- }
- WearPermissionTheme {
- Scaffold(
- // TODO: Use a rotary modifier from Wear Compose once Wear Compose 1.4 is landed.
- // (b/325560444)
- modifier =
- Modifier.rotaryWithScroll(
- scrollableState = listState,
- focusRequester = focusRequester,
- ),
- timeText = {
- if (showTimeText && !isLoading) {
- TimeText(
- modifier =
- Modifier.scrollAway(listState, initialCenterIndex, scrollAwayOffset)
- .padding(top = timeTextTopPadding)
- )
- }
- },
- vignette = { Vignette(vignettePosition = VignettePosition.TopAndBottom) },
- positionIndicator =
- if (!isLoading) {
- { PositionIndicator(scalingLazyListState = listState) }
- } else {
- null
- },
- ) {
- Box(modifier = Modifier.fillMaxSize()) {
- if (isLoading) {
- CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
- } else {
- val iconColor = chipDefaultColors().iconColor(true).value
- ScalingLazyColumn(
- modifier = Modifier.fillMaxWidth(),
- state = listState,
- // Set autoCentering to null to avoid adding extra padding based on the
- // content.
- autoCentering = null,
- contentPadding =
- PaddingValues(
- start = scrollContentHorizontalPadding,
- end = scrollContentHorizontalPadding,
- top = scrollContentTopPadding,
- bottom = scrollContentBottomPadding,
- ),
- ) {
- staticItem()
- image?.let {
- val imageModifier = Modifier.size(24.dp)
- when (image) {
- is Int ->
- item {
- Image(
- painter = painterResource(id = image),
- contentDescription = null,
- contentScale = ContentScale.Crop,
- modifier = imageModifier,
- colorFilter = ColorFilter.tint(iconColor),
- )
- }
- is Drawable ->
- item {
- Image(
- painter = rememberDrawablePainter(image),
- contentDescription = null,
- contentScale = ContentScale.Crop,
- modifier = imageModifier,
- colorFilter = ColorFilter.tint(iconColor),
- )
- }
- else -> {}
- }
- }
- if (title != null) {
- item {
- var modifier: Modifier = Modifier
- if (titleTestTag != null) {
- modifier = modifier.testTag(titleTestTag)
- }
- ListHeader(modifier = Modifier.padding(titlePaddingValues)) {
- Text(
- text = title,
- textAlign = TextAlign.Center,
- modifier = modifier,
- )
- }
- }
- }
- if (subtitle != null) {
- item {
- var modifier: Modifier =
- Modifier.align(Alignment.Center).padding(subTitlePaddingValues)
- if (subtitleTestTag != null) {
- modifier = modifier.testTag(subtitleTestTag)
- }
- AnnotatedText(
- text = subtitle,
- style =
- MaterialTheme.typography.body2.copy(
- color = MaterialTheme.colors.onSurfaceVariant
- ),
- modifier = modifier,
- shouldCapitalize = true,
- )
- }
- }
-
- content()
- }
- RequestFocusOnResume(focusRequester = focusRequester)
- }
- }
- }
- }
-}
-
-private fun ScalingLazyListScope.staticItem() {
- /*
- This empty item helps to ensure accurate scroll offset calculation. If auto centering is enabled
- initial item's(first item for us) center matches the center of the screen. Scroll offset is 0 at
- that point.
-
- if auto centering is not enabled, initial item will start at the top of the screen with the
- scroll offset equal to ScreenHeight/2 - scrollContentTopPadding - firstItemHeight/2.
-
- We need to this offset value to properly move time text.That is the scroll-away offset of the
- Time Text is equal to the scroll offset of the list at initial position.
-
- It is easier to calculate if we know the values of ScreenHeight, ScrollContentTopPadding and
- FirstItem's height. ScreenHeight and ScrollContentPadding are constants but height of the
- FirstItem depends on the content. Instead of measuring the height, we can simplify the
- calculation with an empty item with 0dp height.
- */
- item {}
-}
-
-@Composable
-private fun RequestFocusOnResume(focusRequester: FocusRequester) {
- val lifecycleOwner = LocalLifecycleOwner.current
- LaunchedEffect(Unit) {
- lifecycleOwner.repeatOnLifecycle(state = Lifecycle.State.RESUMED) {
- focusRequester.requestFocus()
- }
- }
-}
-
internal fun dismiss(activity: Activity) {
if (activity is FragmentActivity) {
if (!activity.supportFragmentManager.popBackStackImmediate()) {
@@ -364,3 +127,7 @@ internal fun Context.findActivity(): Activity {
}
throw IllegalStateException("The screen should be called in the context of an Activity")
}
+
+interface ListScopeWrapper {
+ fun item(key: Any? = null, contentType: Any? = null, content: @Composable () -> Unit)
+}
diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/AlertDialog.kt b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/material2/AlertDialog.kt
index c07d2ba9e..2bd72624f 100644
--- a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/AlertDialog.kt
+++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/material2/AlertDialog.kt
@@ -14,20 +14,15 @@
* limitations under the License.
*/
-package com.android.permissioncontroller.permission.ui.wear.elements
+package com.android.permissioncontroller.permission.ui.wear.elements.material2
import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.Check
-import androidx.compose.material.icons.filled.Close
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalFocusManager
-import androidx.compose.ui.res.painterResource
-import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.rememberTextMeasurer
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
@@ -35,15 +30,19 @@ import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.dp
import androidx.wear.compose.foundation.lazy.ScalingLazyListScope
import androidx.wear.compose.foundation.lazy.ScalingLazyListState
-import androidx.wear.compose.material.Icon
import androidx.wear.compose.material.LocalTextStyle
import androidx.wear.compose.material.MaterialTheme
import androidx.wear.compose.material.Text
-import androidx.wear.compose.material.dialog.Alert
import androidx.wear.compose.material.dialog.Dialog
-import com.android.permissioncontroller.permission.ui.wear.elements.layout.ScalingLazyColumnDefaults
-import com.android.permissioncontroller.permission.ui.wear.elements.layout.ScalingLazyColumnState
-import com.android.permissioncontroller.permission.ui.wear.elements.layout.rememberColumnState
+import com.android.permissioncontroller.permission.ui.wear.elements.material2.layout.ScalingLazyColumnDefaults
+import com.android.permissioncontroller.permission.ui.wear.elements.material2.layout.ScalingLazyColumnState
+import com.android.permissioncontroller.permission.ui.wear.elements.material2.layout.rememberColumnState
+import com.android.permissioncontroller.permission.ui.wear.elements.material3.WearPermissionIconBuilder
+
+data class DialogButtonContent(
+ val icon: WearPermissionIconBuilder? = null,
+ val onClick: (() -> Unit),
+)
/**
* This component is an alternative to [AlertContent], providing the following:
@@ -54,95 +53,44 @@ import com.android.permissioncontroller.permission.ui.wear.elements.layout.remem
*/
@Composable
fun AlertDialog(
+ title: String? = null,
message: String,
- iconRes: Int? = null,
- okButtonIcon: Any = Icons.Default.Check,
- cancelButtonIcon: Any = Icons.Default.Close,
- onCancelButtonClick: () -> Unit,
- onOKButtonClick: () -> Unit,
+ positiveButtonContent: DialogButtonContent?,
+ negativeButtonContent: DialogButtonContent?,
showDialog: Boolean,
- scalingLazyListState: ScalingLazyListState,
modifier: Modifier = Modifier,
- title: String? = null,
- okButtonContentDescription: String = stringResource(android.R.string.ok),
- cancelButtonContentDescription: String = stringResource(android.R.string.cancel)
+ iconRes: WearPermissionIconBuilder? = null,
+ scalingLazyListState: ScalingLazyListState,
) {
val focusManager = LocalFocusManager.current
Dialog(
showDialog = showDialog,
onDismissRequest = {
focusManager.clearFocus()
- onCancelButtonClick()
+ negativeButtonContent?.onClick?.invoke()
},
scrollState = scalingLazyListState,
- modifier = modifier
- ) {
- AlertContent(
- title = title,
- icon = { AlertIcon(iconRes) },
- message = message,
- okButtonIcon = okButtonIcon,
- cancelButtonIcon = cancelButtonIcon,
- onCancel = onCancelButtonClick,
- onOk = onOKButtonClick,
- okButtonContentDescription = okButtonContentDescription,
- cancelButtonContentDescription = cancelButtonContentDescription
- )
- }
-}
-
-/**
- * This component is an alternative to [Alert], providing the following:
- * - a convenient way of passing a title and a message;
- * - default one button;
- * - wrapped in a [Dialog];
- */
-@Composable
-fun SingleButtonAlertDialog(
- message: String,
- iconRes: Int? = null,
- okButtonIcon: Any = Icons.Default.Check,
- onButtonClick: () -> Unit,
- showDialog: Boolean,
- scalingLazyListState: ScalingLazyListState,
- modifier: Modifier = Modifier,
- title: String? = null,
- buttonContentDescription: String = stringResource(android.R.string.ok)
-) {
- Dialog(
- showDialog = showDialog,
- onDismissRequest = {},
- scrollState = scalingLazyListState,
- modifier = modifier
+ modifier = modifier,
) {
AlertContent(
title = title,
- icon = { AlertIcon(iconRes) },
+ icon = { iconRes?.build() },
message = message,
- okButtonIcon = okButtonIcon,
- onOk = onButtonClick,
- okButtonContentDescription = buttonContentDescription
+ positiveButtonContent = positiveButtonContent,
+ negativeButtonContent = negativeButtonContent,
)
}
}
@Composable
fun AlertContent(
- onCancel: (() -> Unit)? = null,
- onOk: (() -> Unit)? = null,
icon: @Composable (() -> Unit)? = null,
title: String? = null,
message: String? = null,
- okButtonIcon: Any = Icons.Default.Check,
- cancelButtonIcon: Any = Icons.Default.Close,
- okButtonContentDescription: String = stringResource(android.R.string.ok),
- cancelButtonContentDescription: String = stringResource(android.R.string.cancel),
+ positiveButtonContent: DialogButtonContent?,
+ negativeButtonContent: DialogButtonContent?,
state: ScalingLazyColumnState =
- rememberColumnState(
- ScalingLazyColumnDefaults.responsive(
- additionalPaddingAtBottom = 0.dp,
- ),
- ),
+ rememberColumnState(ScalingLazyColumnDefaults.responsive(additionalPaddingAtBottom = 0.dp)),
showPositionIndicator: Boolean = true,
content: (ScalingLazyListScope.() -> Unit)? = null,
) {
@@ -185,7 +133,7 @@ fun AlertContent(
maxWidth =
(maxScreenWidthPx *
(1f - totalPaddingPercentage * 2f / 100f))
- .toInt(),
+ .toInt()
),
)
.lineCount
@@ -200,21 +148,9 @@ fun AlertContent(
}
},
content = content,
- onOk = onOk,
- onCancel = onCancel,
- okButtonIcon = okButtonIcon,
- cancelButtonIcon = cancelButtonIcon,
- okButtonContentDescription = okButtonContentDescription,
- cancelButtonContentDescription = cancelButtonContentDescription,
+ positiveButtonContent = positiveButtonContent,
+ negativeButtonContent = negativeButtonContent,
state = state,
showPositionIndicator = showPositionIndicator,
)
}
-
-@Composable
-private fun AlertIcon(iconRes: Int?) =
- if (iconRes != null && iconRes != 0) {
- Icon(painter = painterResource(iconRes), contentDescription = null)
- } else {
- null
- }
diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/Chip.kt b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/material2/Chip.kt
index 40f097c67..15542ec20 100644
--- a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/Chip.kt
+++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/material2/Chip.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.permissioncontroller.permission.ui.wear.elements
+package com.android.permissioncontroller.permission.ui.wear.elements.material2
import android.graphics.drawable.Drawable
import androidx.annotation.StringRes
@@ -46,6 +46,7 @@ import androidx.wear.compose.material.Icon
import androidx.wear.compose.material.MaterialTheme
import androidx.wear.compose.material.Text
import androidx.wear.compose.material.contentColorFor
+import com.android.permissioncontroller.permission.ui.wear.elements.rememberDrawablePainter
/**
* This component is an alternative to [Chip], providing the following:
@@ -67,7 +68,7 @@ fun Chip(
textColor: Color = MaterialTheme.colors.onSurface,
iconColor: Color = Color.Unspecified,
colors: ChipColors = chipDefaultColors(),
- enabled: Boolean = true
+ enabled: Boolean = true,
) {
val iconParam: (@Composable BoxScope.() -> Unit)? =
icon?.let {
@@ -87,21 +88,21 @@ fun Chip(
imageVector = icon,
tint = iconColor,
contentDescription = iconContentDescription,
- modifier = iconModifier
+ modifier = iconModifier,
)
is Int ->
Icon(
painter = painterResource(id = icon),
tint = iconColor,
contentDescription = iconContentDescription,
- modifier = iconModifier
+ modifier = iconModifier,
)
is Drawable ->
Icon(
painter = rememberDrawablePainter(icon),
tint = iconColor,
contentDescription = iconContentDescription,
- modifier = iconModifier
+ modifier = iconModifier,
)
else -> {}
}
@@ -120,7 +121,7 @@ fun Chip(
largeIcon = largeIcon,
textColor = textColor,
colors = colors,
- enabled = enabled
+ enabled = enabled,
)
}
@@ -143,7 +144,7 @@ fun Chip(
textColor: Color = MaterialTheme.colors.onSurface,
iconColor: Color = Color.Unspecified,
colors: ChipColors = chipDefaultColors(),
- enabled: Boolean = true
+ enabled: Boolean = true,
) {
Chip(
label = stringResource(id = labelId),
@@ -157,7 +158,7 @@ fun Chip(
textColor = textColor,
iconColor = iconColor,
colors = colors,
- enabled = enabled
+ enabled = enabled,
)
}
@@ -180,7 +181,7 @@ fun Chip(
textColor: Color = MaterialTheme.colors.onSurface,
secondaryTextColor: Color = MaterialTheme.colors.primary,
colors: ChipColors = chipDefaultColors(),
- enabled: Boolean = true
+ enabled: Boolean = true,
) {
val hasSecondaryLabel = secondaryLabel != null
val hasIcon = icon != null
@@ -196,8 +197,8 @@ fun Chip(
style =
MaterialTheme.typography.button.copy(
fontWeight = FontWeight.W600,
- hyphens = Hyphens.Auto
- )
+ hyphens = Hyphens.Auto,
+ ),
)
}
@@ -209,7 +210,7 @@ fun Chip(
color = secondaryTextColor,
overflow = TextOverflow.Ellipsis,
maxLines = secondaryLabelMaxLines ?: 1,
- style = MaterialTheme.typography.caption2
+ style = MaterialTheme.typography.caption2,
)
}
}
@@ -221,7 +222,7 @@ fun Chip(
start = 10.dp,
top = verticalPadding,
end = ChipDefaults.ChipHorizontalPadding,
- bottom = verticalPadding
+ bottom = verticalPadding,
)
} else {
ChipDefaults.ContentPadding
@@ -236,7 +237,7 @@ fun Chip(
colors = colors,
enabled = enabled,
contentPadding = contentPadding,
- shape = RoundedCornerShape(26.dp)
+ shape = RoundedCornerShape(26.dp),
)
}
@@ -258,6 +259,6 @@ fun chipDisabledColors(): ChipColors {
backgroundColor = backgroundColor.copy(alpha = ContentAlpha.disabled),
contentColor = contentColor.copy(alpha = ContentAlpha.disabled),
secondaryContentColor = secondaryContentColor.copy(alpha = ContentAlpha.disabled),
- iconColor = iconColor.copy(alpha = ContentAlpha.disabled)
+ iconColor = iconColor.copy(alpha = ContentAlpha.disabled),
)
}
diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/Icon.kt b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/material2/Icon.kt
index 1a304b37e..3cfac7eef 100644
--- a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/Icon.kt
+++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/material2/Icon.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.permissioncontroller.permission.ui.wear.elements
+package com.android.permissioncontroller.permission.ui.wear.elements.material2
import android.graphics.drawable.Drawable
import androidx.annotation.DrawableRes
@@ -29,6 +29,7 @@ import androidx.compose.ui.unit.LayoutDirection
import androidx.wear.compose.material.Icon
import androidx.wear.compose.material.LocalContentAlpha
import androidx.wear.compose.material.LocalContentColor
+import com.android.permissioncontroller.permission.ui.wear.elements.rememberDrawablePainter
/**
* This component is an alternative to [Icon], providing the following:
@@ -40,7 +41,7 @@ public fun Icon(
contentDescription: String?,
modifier: Modifier = Modifier,
tint: Color = LocalContentColor.current.copy(alpha = LocalContentAlpha.current),
- rtlMode: IconRtlMode = IconRtlMode.Default
+ rtlMode: IconRtlMode = IconRtlMode.Default,
) {
val shouldMirror =
rtlMode == IconRtlMode.Mirrored && LocalLayoutDirection.current == LayoutDirection.Rtl
@@ -48,7 +49,7 @@ public fun Icon(
modifier = modifier.scale(scaleX = if (shouldMirror) -1f else 1f, scaleY = 1f),
imageVector = imageVector,
contentDescription = contentDescription,
- tint = tint
+ tint = tint,
)
}
@@ -62,7 +63,7 @@ public fun Icon(
contentDescription: String?,
modifier: Modifier = Modifier,
tint: Color = LocalContentColor.current.copy(alpha = LocalContentAlpha.current),
- rtlMode: IconRtlMode = IconRtlMode.Default
+ rtlMode: IconRtlMode = IconRtlMode.Default,
) {
val shouldMirror =
rtlMode == IconRtlMode.Mirrored && LocalLayoutDirection.current == LayoutDirection.Rtl
@@ -71,7 +72,7 @@ public fun Icon(
painter = painterResource(id = id),
contentDescription = contentDescription,
modifier = modifier.scale(scaleX = if (shouldMirror) -1f else 1f, scaleY = 1f),
- tint = tint
+ tint = tint,
)
}
@@ -86,7 +87,7 @@ fun Icon(
contentDescription: String?,
modifier: Modifier = Modifier,
tint: Color = LocalContentColor.current.copy(alpha = LocalContentAlpha.current),
- rtlMode: IconRtlMode = IconRtlMode.Default
+ rtlMode: IconRtlMode = IconRtlMode.Default,
) {
val shouldMirror =
rtlMode == IconRtlMode.Mirrored && LocalLayoutDirection.current == LayoutDirection.Rtl
@@ -98,7 +99,7 @@ fun Icon(
imageVector = icon,
modifier = iconModifier,
contentDescription = contentDescription,
- tint = tint
+ tint = tint,
)
}
is Int -> {
@@ -106,7 +107,7 @@ fun Icon(
painter = painterResource(id = icon),
contentDescription = contentDescription,
modifier = iconModifier,
- tint = tint
+ tint = tint,
)
}
is Drawable -> {
@@ -114,7 +115,7 @@ fun Icon(
painter = rememberDrawablePainter(icon),
contentDescription = contentDescription,
modifier = iconModifier,
- tint = tint
+ tint = tint,
)
}
else -> throw IllegalArgumentException("Type not supported.")
@@ -123,5 +124,5 @@ fun Icon(
public enum class IconRtlMode {
Default,
- Mirrored
+ Mirrored,
}
diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/ListFooter.kt b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/material2/ListFooter.kt
index 5ed912ec6..4f6d47faf 100644
--- a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/ListFooter.kt
+++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/material2/ListFooter.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.permissioncontroller.permission.ui.wear.elements
+package com.android.permissioncontroller.permission.ui.wear.elements.material2
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Row
@@ -52,7 +52,7 @@ fun ListFooter(description: String, iconRes: Int? = null, onClick: (() -> Unit)?
contentDescription = null,
modifier =
Modifier.size(LeadingIconSize, LeadingIconSize)
- .align(Alignment.CenterVertically)
+ .align(Alignment.CenterVertically),
)
Spacer(modifier = Modifier.width(LeadingIconEndSpacing))
}
@@ -62,7 +62,7 @@ fun ListFooter(description: String, iconRes: Int? = null, onClick: (() -> Unit)?
textAlign = TextAlign.Start,
overflow = TextOverflow.Ellipsis,
color = MaterialTheme.colors.onSurfaceVariant,
- style = MaterialTheme.typography.caption2
+ style = MaterialTheme.typography.caption2,
)
}
}
diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/ListHeader.kt b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/material2/ListHeader.kt
index 0a2a3937c..2d3eb0d52 100644
--- a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/ListHeader.kt
+++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/material2/ListHeader.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.permissioncontroller.permission.ui.wear.elements
+package com.android.permissioncontroller.permission.ui.wear.elements.material2
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
@@ -60,7 +60,7 @@ fun ListHeader(
modifier: Modifier = Modifier,
backgroundColor: Color = Color.Transparent,
contentColor: Color = MaterialTheme.colors.onBackground,
- content: @Composable RowScope.() -> Unit
+ content: @Composable RowScope.() -> Unit,
) {
Row(
horizontalArrangement = Arrangement.Center,
@@ -69,14 +69,14 @@ fun ListHeader(
mergeDescendants = true
) {
heading()
- }
+ },
) {
CompositionLocalProvider(
LocalContentColor provides contentColor,
LocalTextStyle provides
MaterialTheme.typography.title3.copy(
fontWeight = FontWeight.W600,
- hyphens = Hyphens.Auto
+ hyphens = Hyphens.Auto,
),
) {
content()
@@ -111,7 +111,7 @@ fun ListSubheader(
.fillMaxWidth()
.wrapContentSize(align = Alignment.CenterStart)
.background(backgroundColor)
- .semantics(mergeDescendants = true) { heading() }
+ .semantics(mergeDescendants = true) { heading() },
) {
CompositionLocalProvider(
LocalContentColor provides contentColor,
@@ -120,7 +120,7 @@ fun ListSubheader(
if (icon != null) {
Box(
modifier = Modifier.wrapContentSize(align = Alignment.CenterStart),
- content = icon
+ content = icon,
)
Spacer(modifier = Modifier.width(6.dp))
}
diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/ResponsiveDialog.kt b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/material2/ResponsiveDialog.kt
index e1e869f71..c43c45358 100644
--- a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/ResponsiveDialog.kt
+++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/material2/ResponsiveDialog.kt
@@ -13,8 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-
-package com.android.permissioncontroller.permission.ui.wear.elements
+package com.android.permissioncontroller.permission.ui.wear.elements.material2
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Arrangement.spacedBy
@@ -29,15 +28,11 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.Check
-import androidx.compose.material.icons.filled.Close
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalConfiguration
-import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
@@ -49,13 +44,13 @@ import androidx.wear.compose.material.LocalTextStyle
import androidx.wear.compose.material.MaterialTheme
import androidx.wear.compose.material.PositionIndicator
import androidx.wear.compose.material.Scaffold
-import com.android.permissioncontroller.permission.ui.wear.elements.layout.ScalingLazyColumn
-import com.android.permissioncontroller.permission.ui.wear.elements.layout.ScalingLazyColumnDefaults.responsive
-import com.android.permissioncontroller.permission.ui.wear.elements.layout.ScalingLazyColumnState
-import com.android.permissioncontroller.permission.ui.wear.elements.layout.rememberColumnState
-
-// This file is a copy of ResponsiveDialogContent.kt from Horologist (go/horologist),
-// remove it once after wear compose supports large screen dialogs.
+import com.android.permissioncontroller.permission.ui.wear.elements.material2.layout.ScalingLazyColumn
+import com.android.permissioncontroller.permission.ui.wear.elements.material2.layout.ScalingLazyColumnDefaults.responsive
+import com.android.permissioncontroller.permission.ui.wear.elements.material2.layout.ScalingLazyColumnState
+import com.android.permissioncontroller.permission.ui.wear.elements.material2.layout.rememberColumnState
+import com.android.permissioncontroller.permission.ui.wear.elements.material3.WearPermissionIconBuilder
+import com.android.permissioncontroller.permission.ui.wear.elements.material3.defaultAlertConfirmIcon
+import com.android.permissioncontroller.permission.ui.wear.elements.material3.defaultAlertDismissIcon
@Composable
fun ResponsiveDialogContent(
@@ -63,18 +58,11 @@ fun ResponsiveDialogContent(
icon: @Composable (() -> Unit)? = null,
title: @Composable (() -> Unit)? = null,
message: @Composable (() -> Unit)? = null,
- okButtonIcon: Any = Icons.Default.Check,
- cancelButtonIcon: Any = Icons.Default.Close,
- onOk: (() -> Unit)? = null,
- onCancel: (() -> Unit)? = null,
- okButtonContentDescription: String = stringResource(android.R.string.ok),
- cancelButtonContentDescription: String = stringResource(android.R.string.cancel),
+ positiveButtonContent: DialogButtonContent? = null,
+ negativeButtonContent: DialogButtonContent? = null,
state: ScalingLazyColumnState =
rememberColumnState(
- responsive(
- firstItemIsFullWidth = icon == null,
- additionalPaddingAtBottom = 0.dp,
- ),
+ responsive(firstItemIsFullWidth = icon == null, additionalPaddingAtBottom = 0.dp)
),
showPositionIndicator: Boolean = true,
content: (ScalingLazyListScope.() -> Unit)? = null,
@@ -89,9 +77,7 @@ fun ResponsiveDialogContent(
timeText = {},
) {
// This will be applied only to the content.
- CompositionLocalProvider(
- LocalTextStyle provides MaterialTheme.typography.body2,
- ) {
+ CompositionLocalProvider(LocalTextStyle provides MaterialTheme.typography.body2) {
ScalingLazyColumn(columnState = state) {
icon?.let {
item {
@@ -107,11 +93,11 @@ fun ResponsiveDialogContent(
item {
CompositionLocalProvider(
LocalTextStyle provides
- MaterialTheme.typography.title3.copy(fontWeight = FontWeight.W600),
+ MaterialTheme.typography.title3.copy(fontWeight = FontWeight.W600)
) {
Box(
Modifier.fillMaxWidth(titleMaxWidthFraction)
- .padding(bottom = 8.dp), // 12.dp below icon
+ .padding(bottom = 8.dp) // 12.dp below icon
) {
it()
}
@@ -123,22 +109,20 @@ fun ResponsiveDialogContent(
item { Spacer(Modifier.height(20.dp)) }
}
message?.let {
- item {
- Box(
- Modifier.fillMaxWidth(messageMaxWidthFraction),
- ) {
- it()
- }
- }
+ item { Box(Modifier.fillMaxWidth(messageMaxWidthFraction)) { it() } }
}
content?.let { it() }
- if (onOk != null || onCancel != null) {
+ if (positiveButtonContent != null || negativeButtonContent != null) {
item {
val width = LocalConfiguration.current.screenWidthDp
// Single buttons, or buttons on smaller screens are not meant to be
// responsive.
val buttonWidth =
- if (width < 225 || onOk == null || onCancel == null) {
+ if (
+ width < 225 ||
+ positiveButtonContent == null ||
+ negativeButtonContent == null
+ ) {
ButtonDefaults.DefaultButtonSize
} else {
// 14.56% on top of 5.2% margin on the sides, 12.dp between.
@@ -147,25 +131,30 @@ fun ResponsiveDialogContent(
Row(
Modifier.fillMaxWidth()
.padding(
- top = if (content != null || message != null) 12.dp else 0.dp,
+ top = if (content != null || message != null) 12.dp else 0.dp
),
horizontalArrangement = spacedBy(12.dp, Alignment.CenterHorizontally),
verticalAlignment = Alignment.CenterVertically,
) {
- onCancel?.let {
+ negativeButtonContent?.run {
ResponsiveButton(
- icon = cancelButtonIcon,
- cancelButtonContentDescription,
- onClick = it,
+ this.icon
+ ?: WearPermissionIconBuilder.defaultAlertDismissIcon()
+ .tint(
+ ChipDefaults.secondaryChipColors()
+ .contentColor(true)
+ .value
+ ),
+ onClick,
buttonWidth,
ChipDefaults.secondaryChipColors(),
)
}
- onOk?.let {
+ positiveButtonContent?.run {
ResponsiveButton(
- icon = okButtonIcon,
- okButtonContentDescription,
- onClick = it,
+ this.icon
+ ?: WearPermissionIconBuilder.defaultAlertConfirmIcon(),
+ onClick,
buttonWidth,
)
}
@@ -179,8 +168,7 @@ fun ResponsiveDialogContent(
@Composable
private fun ResponsiveButton(
- icon: Any,
- contentDescription: String,
+ icon: WearPermissionIconBuilder,
onClick: () -> Unit,
buttonWidth: Dp,
colors: ChipColors = ChipDefaults.primaryChipColors(),
@@ -188,12 +176,9 @@ private fun ResponsiveButton(
androidx.wear.compose.material.Chip(
label = {
Box(Modifier.fillMaxWidth()) {
- Icon(
- icon = icon,
- contentDescription = contentDescription,
- modifier =
- Modifier.size(ButtonDefaults.DefaultIconSize).align(Alignment.Center),
- )
+ icon
+ .modifier(Modifier.size(ButtonDefaults.DefaultIconSize).align(Alignment.Center))
+ .build()
}
},
contentPadding = PaddingValues(0.dp),
@@ -210,19 +195,10 @@ internal const val titleExtraHorizontalPadding = 8.84f
// Fraction of the max available width that message should take (after global and message padding)
internal val messageMaxWidthFraction =
- 1f -
- 2f *
- calculatePaddingFraction(
- messageExtraHorizontalPadding,
- )
+ 1f - 2f * calculatePaddingFraction(messageExtraHorizontalPadding)
// Fraction of the max available width that title should take (after global and message padding)
-internal val titleMaxWidthFraction =
- 1f -
- 2f *
- calculatePaddingFraction(
- titleExtraHorizontalPadding,
- )
+internal val titleMaxWidthFraction = 1f - 2f * calculatePaddingFraction(titleExtraHorizontalPadding)
// Calculate total padding given global padding and additional padding required inside that.
internal fun calculatePaddingFraction(extraPadding: Float) =
diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/ToggleChip.kt b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/material2/ToggleChip.kt
index 2e89586c9..421d5ca4f 100644
--- a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/ToggleChip.kt
+++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/material2/ToggleChip.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.permissioncontroller.permission.ui.wear.elements
+package com.android.permissioncontroller.permission.ui.wear.elements.material2
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.BoxScope
diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/ToggleChipToggleControl.kt b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/material2/ToggleChipToggleControl.kt
index b6f6db4d3..56fbf3d61 100644
--- a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/ToggleChipToggleControl.kt
+++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/material2/ToggleChipToggleControl.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.permissioncontroller.permission.ui.wear.elements
+package com.android.permissioncontroller.permission.ui.wear.elements.material2
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/material2/Wear2Scaffold.kt b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/material2/Wear2Scaffold.kt
new file mode 100644
index 000000000..3575b3cff
--- /dev/null
+++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/material2/Wear2Scaffold.kt
@@ -0,0 +1,264 @@
+/*
+ * Copyright 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * 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.material2
+
+import android.graphics.drawable.Drawable
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.focus.focusRequester
+import androidx.compose.ui.graphics.ColorFilter
+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.compose.ui.unit.dp
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.compose.LocalLifecycleOwner
+import androidx.lifecycle.repeatOnLifecycle
+import androidx.wear.compose.foundation.lazy.ScalingLazyColumn
+import androidx.wear.compose.foundation.lazy.ScalingLazyListScope
+import androidx.wear.compose.foundation.lazy.ScalingLazyListState
+import androidx.wear.compose.material.CircularProgressIndicator
+import androidx.wear.compose.material.MaterialTheme
+import androidx.wear.compose.material.PositionIndicator
+import androidx.wear.compose.material.Scaffold
+import androidx.wear.compose.material.Text
+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.AnnotatedText
+import com.android.permissioncontroller.permission.ui.wear.elements.rememberDrawablePainter
+import com.android.permissioncontroller.permission.ui.wear.theme.WearPermissionTheme
+
+/**
+ * This component is wrapper on material 2 scaffold component. It helps with time text, scroll
+ * indicator and standard list elements like title, icon and subtitle.
+ */
+@Composable
+fun Wear2Scaffold(
+ showTimeText: Boolean,
+ title: String?,
+ subtitle: CharSequence?,
+ image: Any?,
+ isLoading: Boolean,
+ content: ScalingLazyListScope.() -> Unit,
+ titleTestTag: String? = null,
+ subtitleTestTag: String? = null,
+) {
+ val itemsSpacedBy = 4.dp
+ val screenWidth = LocalConfiguration.current.screenWidthDp
+ val screenHeight = LocalConfiguration.current.screenHeightDp
+ val scrollContentHorizontalPadding = (screenWidth * 0.052).dp
+ val titleHorizontalPadding = (screenWidth * 0.0884).dp
+ val subtitleHorizontalPadding = (screenWidth * 0.0416).dp
+ val scrollContentTopPadding = (screenHeight * 0.1456).dp - itemsSpacedBy
+ val scrollContentBottomPadding = (screenHeight * 0.3636).dp
+ val titleBottomPadding =
+ if (subtitle == null) {
+ 8.dp
+ } else {
+ 4.dp
+ }
+ val subtitleBottomPadding = 8.dp
+ val timeTextTopPadding =
+ if (showTimeText) {
+ 1.dp
+ } else {
+ 0.dp
+ }
+ val titlePaddingValues =
+ PaddingValues(
+ start = titleHorizontalPadding,
+ top = 4.dp,
+ bottom = titleBottomPadding,
+ end = titleHorizontalPadding,
+ )
+ val subTitlePaddingValues =
+ PaddingValues(
+ start = subtitleHorizontalPadding,
+ top = 4.dp,
+ bottom = subtitleBottomPadding,
+ end = subtitleHorizontalPadding,
+ )
+ val initialCenterIndex = 0
+ val centerHeightDp = Dp(LocalConfiguration.current.screenHeightDp / 2.0f)
+ // We are adding TimeText's padding to create a smooth scrolling
+ val initialCenterItemScrollOffset = scrollContentTopPadding + timeTextTopPadding
+ val scrollAwayOffset = centerHeightDp - initialCenterItemScrollOffset
+ val focusRequester = remember { FocusRequester() }
+ val listState = remember { ScalingLazyListState(initialCenterItemIndex = initialCenterIndex) }
+ LaunchedEffect(title) {
+ listState.animateScrollToItem(index = 0) // Scroll to the top when triggerValue changes
+ }
+ WearPermissionTheme {
+ Scaffold(
+ modifier = Modifier.focusRequester(focusRequester),
+ timeText = {
+ if (showTimeText && !isLoading) {
+ TimeText(
+ modifier =
+ Modifier.scrollAway(listState, initialCenterIndex, scrollAwayOffset)
+ .padding(top = timeTextTopPadding)
+ )
+ }
+ },
+ vignette = { Vignette(vignettePosition = VignettePosition.TopAndBottom) },
+ positionIndicator =
+ if (!isLoading) {
+ { PositionIndicator(scalingLazyListState = listState) }
+ } else {
+ null
+ },
+ ) {
+ Box(modifier = Modifier.fillMaxSize()) {
+ if (isLoading) {
+ CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
+ } else {
+ val iconColor =
+ com.android.permissioncontroller.permission.ui.wear.elements.material2
+ .chipDefaultColors()
+ .iconColor(true)
+ .value
+ ScalingLazyColumn(
+ modifier = Modifier.fillMaxWidth(),
+ state = listState,
+ // Set autoCentering to null to avoid adding extra padding based on the
+ // content.
+ autoCentering = null,
+ contentPadding =
+ PaddingValues(
+ start = scrollContentHorizontalPadding,
+ end = scrollContentHorizontalPadding,
+ top = scrollContentTopPadding,
+ bottom = scrollContentBottomPadding,
+ ),
+ ) {
+ staticItem()
+ image?.let {
+ val imageModifier = Modifier.size(24.dp)
+ when (image) {
+ is Int ->
+ item {
+ Image(
+ painter = painterResource(id = image),
+ contentDescription = null,
+ contentScale = ContentScale.Crop,
+ modifier = imageModifier,
+ colorFilter = ColorFilter.tint(iconColor),
+ )
+ }
+ is Drawable ->
+ item {
+ Image(
+ painter = rememberDrawablePainter(image),
+ contentDescription = null,
+ contentScale = ContentScale.Crop,
+ modifier = imageModifier,
+ colorFilter = ColorFilter.tint(iconColor),
+ )
+ }
+ else -> {}
+ }
+ }
+ if (title != null) {
+ item {
+ var modifier: Modifier = Modifier
+ if (titleTestTag != null) {
+ modifier = modifier.testTag(titleTestTag)
+ }
+ com.android.permissioncontroller.permission.ui.wear.elements
+ .material2
+ .ListHeader(modifier = Modifier.padding(titlePaddingValues)) {
+ Text(
+ text = title,
+ textAlign = TextAlign.Center,
+ modifier = modifier,
+ )
+ }
+ }
+ }
+ if (subtitle != null) {
+ item {
+ var modifier: Modifier =
+ Modifier.align(Alignment.Center).padding(subTitlePaddingValues)
+ if (subtitleTestTag != null) {
+ modifier = modifier.testTag(subtitleTestTag)
+ }
+ AnnotatedText(
+ text = subtitle,
+ style =
+ MaterialTheme.typography.body2.copy(
+ color = MaterialTheme.colors.onSurfaceVariant
+ ),
+ modifier = modifier,
+ shouldCapitalize = true,
+ )
+ }
+ }
+
+ content()
+ }
+ RequestFocusOnResume(focusRequester = focusRequester)
+ }
+ }
+ }
+ }
+}
+
+private fun ScalingLazyListScope.staticItem() {
+ /*
+ This empty item helps to ensure accurate scroll offset calculation. If auto centering is enabled
+ initial item's(first item for us) center matches the center of the screen. Scroll offset is 0 at
+ that point.
+
+ if auto centering is not enabled, initial item will start at the top of the screen with the
+ scroll offset equal to ScreenHeight/2 - scrollContentTopPadding - firstItemHeight/2.
+
+ We need to this offset value to properly move time text.That is the scroll-away offset of the
+ Time Text is equal to the scroll offset of the list at initial position.
+
+ It is easier to calculate if we know the values of ScreenHeight, ScrollContentTopPadding and
+ FirstItem's height. ScreenHeight and ScrollContentPadding are constants but height of the
+ FirstItem depends on the content. Instead of measuring the height, we can simplify the
+ calculation with an empty item with 0dp height.
+ */
+ item {}
+}
+
+@Composable
+private fun RequestFocusOnResume(focusRequester: FocusRequester) {
+ val lifecycleOwner = LocalLifecycleOwner.current
+ LaunchedEffect(Unit) {
+ lifecycleOwner.repeatOnLifecycle(state = Lifecycle.State.RESUMED) {
+ focusRequester.requestFocus()
+ }
+ }
+}
diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/layout/ScalingLazyColumnDefaults.kt b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/material2/layout/ScalingLazyColumnDefaults.kt
index 550f1dc24..c06fdaf14 100644
--- a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/layout/ScalingLazyColumnDefaults.kt
+++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/material2/layout/ScalingLazyColumnDefaults.kt
@@ -16,7 +16,7 @@
@file:Suppress("ObjectLiteralToLambda")
-package com.android.permissioncontroller.permission.ui.wear.elements.layout
+package com.android.permissioncontroller.permission.ui.wear.elements.material2.layout
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.PaddingValues
@@ -33,7 +33,7 @@ import androidx.wear.compose.foundation.lazy.ScalingLazyColumnDefaults
import androidx.wear.compose.foundation.lazy.ScalingLazyListAnchorType
import androidx.wear.compose.foundation.lazy.ScalingParams
import androidx.wear.compose.material.ChipDefaults
-import com.android.permissioncontroller.permission.ui.wear.elements.layout.ScalingLazyColumnState.RotaryMode
+import com.android.permissioncontroller.permission.ui.wear.elements.material2.layout.ScalingLazyColumnState.RotaryMode
import kotlin.math.sqrt
// This file's content is copied from ScalingLazyColumnDefaults.kt from Horologist (go/horologist),
@@ -63,10 +63,7 @@ object ScalingLazyColumnDefaults {
firstItemIsFullWidth: Boolean = true,
additionalPaddingAtBottom: Dp = 10.dp,
verticalArrangement: Arrangement.Vertical =
- Arrangement.spacedBy(
- space = 4.dp,
- alignment = Alignment.Top,
- ),
+ Arrangement.spacedBy(space = 4.dp, alignment = Alignment.Top),
horizontalPaddingPercent: Float = 0.052f,
rotaryMode: RotaryMode? = RotaryMode.Scroll,
hapticsEnabled: Boolean = true,
@@ -145,7 +142,7 @@ object ScalingLazyColumnDefaults {
return (radius -
sqrt(
(radius - childViewHeight + childViewWidth * 0.5f) *
- (radius - childViewWidth * 0.5f),
+ (radius - childViewWidth * 0.5f)
) -
childViewHeight * 0.5f)
.dp
@@ -225,10 +222,7 @@ object ScalingLazyColumnDefaults {
last.bottomPaddingDp * height + first.paddingCorrection
} else {
if (configuration.isScreenRound) {
- calculateVerticalOffsetForChip(
- screenWidthDp,
- horizontalPercent,
- ) + 10.dp
+ calculateVerticalOffsetForChip(screenWidthDp, horizontalPercent) + 10.dp
} else {
0.dp
}
diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/layout/ScalingLazyColumnState.kt b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/material2/layout/ScalingLazyColumnState.kt
index 0603647b1..0e669f6ff 100644
--- a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/layout/ScalingLazyColumnState.kt
+++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/material2/layout/ScalingLazyColumnState.kt
@@ -17,7 +17,7 @@
@file:Suppress("ObjectLiteralToLambda")
@file:OptIn(ExperimentalWearFoundationApi::class)
-package com.android.permissioncontroller.permission.ui.wear.elements.layout
+package com.android.permissioncontroller.permission.ui.wear.elements.material2.layout
import androidx.compose.foundation.MutatePriority
import androidx.compose.foundation.gestures.FlingBehavior
@@ -42,14 +42,8 @@ import androidx.wear.compose.foundation.lazy.ScalingLazyListAnchorType
import androidx.wear.compose.foundation.lazy.ScalingLazyListScope
import androidx.wear.compose.foundation.lazy.ScalingLazyListState
import androidx.wear.compose.foundation.lazy.ScalingParams
-import androidx.wear.compose.foundation.rememberActiveFocusRequester
-import com.android.permissioncontroller.permission.ui.wear.elements.layout.ScalingLazyColumnDefaults.responsiveScalingParams
-import com.android.permissioncontroller.permission.ui.wear.elements.layout.ScalingLazyColumnState.RotaryMode
-import com.android.permissioncontroller.permission.ui.wear.elements.rotaryinput.rememberDisabledHaptic
-import com.android.permissioncontroller.permission.ui.wear.elements.rotaryinput.rememberRotaryHapticHandler
-import com.android.permissioncontroller.permission.ui.wear.elements.rotaryinput.rotaryWithScroll
-import com.android.permissioncontroller.permission.ui.wear.elements.rotaryinput.rotaryWithSnap
-import com.android.permissioncontroller.permission.ui.wear.elements.rotaryinput.toRotaryScrollAdapter
+import com.android.permissioncontroller.permission.ui.wear.elements.material2.layout.ScalingLazyColumnDefaults.responsiveScalingParams
+import com.android.permissioncontroller.permission.ui.wear.elements.material2.layout.ScalingLazyColumnState.RotaryMode
// This file is a copy of ScalingLazyColumnState.kt from Horologist (go/horologist),
// remove it once after wear compose supports large screen dialogs.
@@ -61,10 +55,7 @@ import com.android.permissioncontroller.permission.ui.wear.elements.rotaryinput.
class ScalingLazyColumnState(
val initialScrollPosition: ScrollPosition = ScrollPosition(1, 0),
val autoCentering: AutoCenteringParams? =
- AutoCenteringParams(
- initialScrollPosition.index,
- initialScrollPosition.offsetPx,
- ),
+ AutoCenteringParams(initialScrollPosition.index, initialScrollPosition.offsetPx),
val anchorType: ScalingLazyListAnchorType = ScalingLazyListAnchorType.ItemCenter,
val contentPadding: PaddingValues = PaddingValues(horizontal = 10.dp),
val rotaryMode: RotaryMode? = RotaryMode.Scroll,
@@ -120,10 +111,7 @@ class ScalingLazyColumnState(
data object Scroll : RotaryMode
}
- data class ScrollPosition(
- val index: Int,
- val offsetPx: Int,
- )
+ data class ScrollPosition(val index: Int, val offsetPx: Int)
fun interface Factory {
@Composable fun create(): ScalingLazyColumnState
@@ -133,7 +121,7 @@ class ScalingLazyColumnState(
// @Deprecated("Replaced by rememberResponsiveColumnState")
@Composable
fun rememberColumnState(
- factory: ScalingLazyColumnState.Factory = ScalingLazyColumnDefaults.responsive(),
+ factory: ScalingLazyColumnState.Factory = ScalingLazyColumnDefaults.responsive()
): ScalingLazyColumnState {
val columnState = factory.create()
@@ -150,10 +138,7 @@ fun rememberResponsiveColumnState(
last = ScalingLazyColumnDefaults.ItemType.Unspecified,
),
verticalArrangement: Arrangement.Vertical =
- Arrangement.spacedBy(
- space = 4.dp,
- alignment = Alignment.Top,
- ),
+ Arrangement.spacedBy(space = 4.dp, alignment = Alignment.Top),
rotaryMode: RotaryMode? = RotaryMode.Scroll,
hapticsEnabled: Boolean = true,
reverseLayout: Boolean = false,
@@ -173,10 +158,7 @@ fun rememberResponsiveColumnState(
val topScreenOffsetPx = screenHeightPx / 2 - topPaddingPx
val initialScrollPosition =
- ScalingLazyColumnState.ScrollPosition(
- index = 0,
- offsetPx = topScreenOffsetPx,
- )
+ ScalingLazyColumnState.ScrollPosition(index = 0, offsetPx = topScreenOffsetPx)
val columnState =
ScalingLazyColumnState(
@@ -204,36 +186,8 @@ fun ScalingLazyColumn(
modifier: Modifier = Modifier,
content: ScalingLazyListScope.() -> Unit,
) {
- val focusRequester = rememberActiveFocusRequester()
-
- val rotaryHaptics =
- if (columnState.hapticsEnabled) {
- rememberRotaryHapticHandler(columnState.state)
- } else {
- rememberDisabledHaptic()
- }
-
- val modifierWithRotary =
- when (columnState.rotaryMode) {
- RotaryMode.Snap ->
- modifier.rotaryWithSnap(
- focusRequester = focusRequester,
- rotaryScrollAdapter = columnState.state.toRotaryScrollAdapter(),
- reverseDirection = columnState.reverseLayout,
- rotaryHaptics = rotaryHaptics,
- )
- RotaryMode.Scroll ->
- modifier.rotaryWithScroll(
- focusRequester = focusRequester,
- scrollableState = columnState.state,
- reverseDirection = columnState.reverseLayout,
- rotaryHaptics = rotaryHaptics,
- )
- else -> modifier
- }
-
ScalingLazyColumn(
- modifier = modifierWithRotary.fillMaxSize(),
+ modifier = modifier.fillMaxSize(),
state = columnState.state,
contentPadding = columnState.contentPadding,
reverseLayout = columnState.reverseLayout,
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
index 79a8963d8..942a420a8 100644
--- 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
@@ -33,7 +33,7 @@ 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.elements.material2.Chip
import com.android.permissioncontroller.permission.ui.wear.theme.WearPermissionMaterialUIVersion
/**
@@ -63,7 +63,7 @@ fun WearPermissionButton(
modifier = modifier,
secondaryLabel = secondaryLabel,
secondaryLabelMaxLines = secondaryLabelMaxLines,
- icon = { iconBuilder?.build() },
+ icon = iconBuilder?.let { { iconBuilder.build() } },
largeIcon = false,
colors = style.material2ChipColors(),
enabled = enabled,
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
index 504c69bb0..36d3f9f33 100644
--- 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
@@ -21,8 +21,8 @@ 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.material2.chipDefaultColors
+import com.android.permissioncontroller.permission.ui.wear.elements.material2.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
diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/material3/WearPermissionConfirmationDialog.kt b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/material3/WearPermissionConfirmationDialog.kt
new file mode 100644
index 000000000..430831248
--- /dev/null
+++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/material3/WearPermissionConfirmationDialog.kt
@@ -0,0 +1,159 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.permissioncontroller.permission.ui.wear.elements.material3
+
+import androidx.compose.foundation.layout.RowScope
+import androidx.compose.foundation.layout.size
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import androidx.wear.compose.foundation.lazy.rememberScalingLazyListState
+import androidx.wear.compose.material3.AlertDialog as Material3AlertDialog
+import androidx.wear.compose.material3.AlertDialogDefaults
+import androidx.wear.compose.material3.Text
+import com.android.permissioncontroller.permission.ui.wear.elements.material2.AlertDialog
+import com.android.permissioncontroller.permission.ui.wear.elements.material2.DialogButtonContent
+import com.android.permissioncontroller.permission.ui.wear.theme.WearPermissionMaterialUIVersion
+
+@Composable
+fun WearPermissionConfirmationDialog(
+ materialUIVersion: WearPermissionMaterialUIVersion =
+ WearPermissionMaterialUIVersion.MATERIAL2_5,
+ show: Boolean,
+ iconRes: WearPermissionIconBuilder? = null,
+ title: String? = null,
+ message: String? = null,
+ positiveButtonContent: DialogButtonContent? = null,
+ negativeButtonContent: DialogButtonContent? = null,
+) {
+
+ if (materialUIVersion == WearPermissionMaterialUIVersion.MATERIAL3) {
+ if (
+ (positiveButtonContent == null && negativeButtonContent != null) ||
+ (positiveButtonContent != null && negativeButtonContent == null)
+ ) {
+ val edgeButtonContent = (positiveButtonContent ?: negativeButtonContent)!!
+ WearPermissionConfirmationDialogInternal(
+ show = show,
+ edgeButtonContent = edgeButtonContent,
+ iconRes = iconRes,
+ title = title,
+ message = message,
+ )
+ } else {
+ WearPermissionConfirmationDialogInternal(
+ show = show,
+ positiveButtonContent = positiveButtonContent,
+ negativeButtonContent = negativeButtonContent,
+ iconRes = iconRes,
+ title = title,
+ message = message,
+ )
+ }
+ } else {
+ AlertDialog(
+ title = title,
+ iconRes = iconRes,
+ message = message ?: "",
+ positiveButtonContent = positiveButtonContent,
+ negativeButtonContent = negativeButtonContent,
+ showDialog = show,
+ scalingLazyListState = rememberScalingLazyListState(),
+ )
+ }
+}
+
+@Composable
+private fun WearPermissionConfirmationDialogInternal(
+ show: Boolean,
+ edgeButtonContent: DialogButtonContent,
+ iconRes: WearPermissionIconBuilder?,
+ title: String?,
+ message: String?,
+) {
+ val edgeIcon: @Composable RowScope.() -> Unit =
+ edgeButtonContent.icon?.let {
+ { it.modifier(Modifier.size(36.dp).align(Alignment.CenterVertically)).build() }
+ } ?: AlertDialogDefaults.ConfirmIcon
+
+ Material3AlertDialog(
+ visible = show,
+ onDismissRequest = edgeButtonContent.onClick,
+ edgeButton = {
+ AlertDialogDefaults.EdgeButton(onClick = edgeButtonContent.onClick, content = edgeIcon)
+ },
+ icon = { iconRes?.build() },
+ title = title?.let { { Text(text = title) } } ?: {},
+ text = message?.let { { Text(text = message) } },
+ )
+}
+
+@Composable
+private fun WearPermissionConfirmationDialogInternal(
+ show: Boolean,
+ positiveButtonContent: DialogButtonContent?,
+ negativeButtonContent: DialogButtonContent?,
+ iconRes: WearPermissionIconBuilder?,
+ title: String?,
+ message: String?,
+) {
+ val positiveButton: (@Composable RowScope.() -> Unit)? =
+ positiveButtonContent?.let {
+ {
+ val positiveIcon: @Composable RowScope.() -> Unit =
+ positiveButtonContent.icon?.let {
+ {
+ it.modifier(Modifier.size(36.dp).align(Alignment.CenterVertically))
+ .build()
+ }
+ } ?: AlertDialogDefaults.ConfirmIcon
+
+ AlertDialogDefaults.ConfirmButton(
+ onClick = positiveButtonContent.onClick,
+ content = positiveIcon,
+ )
+ }
+ }
+
+ val negativeButton: (@Composable RowScope.() -> Unit)? =
+ negativeButtonContent?.let {
+ {
+ val negativeIcon: @Composable RowScope.() -> Unit =
+ negativeButtonContent.icon?.let {
+ {
+ it.modifier(Modifier.size(36.dp).align(Alignment.CenterVertically))
+ .build()
+ }
+ } ?: AlertDialogDefaults.DismissIcon
+
+ AlertDialogDefaults.DismissButton(
+ onClick = negativeButtonContent.onClick,
+ content = negativeIcon,
+ )
+ }
+ }
+
+ Material3AlertDialog(
+ visible = show,
+ onDismissRequest = negativeButtonContent?.onClick ?: {},
+ confirmButton = positiveButton ?: {},
+ dismissButton = negativeButton ?: {},
+ icon = { iconRes?.build() },
+ title = title?.let { { Text(text = title) } } ?: {},
+ text = message?.let { { Text(text = message) } },
+ )
+}
diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/material3/WearPermissionIconBuilder.kt b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/material3/WearPermissionIconBuilder.kt
index b7521d073..52674b50d 100644
--- a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/material3/WearPermissionIconBuilder.kt
+++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/material3/WearPermissionIconBuilder.kt
@@ -17,12 +17,16 @@ package com.android.permissioncontroller.permission.ui.wear.elements.material3
import android.graphics.drawable.Drawable
import androidx.compose.foundation.layout.size
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Check
+import androidx.compose.material.icons.filled.Close
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
import androidx.wear.compose.material3.Icon
import androidx.wear.compose.material3.IconButtonDefaults
import com.android.permissioncontroller.permission.ui.wear.elements.rememberDrawablePainter
@@ -66,7 +70,7 @@ class WearPermissionIconBuilder private constructor() {
}
fun modifier(modifier: Modifier): WearPermissionIconBuilder {
- this.modifier then modifier
+ this.modifier = modifier then this.modifier
return this
}
@@ -99,3 +103,11 @@ class WearPermissionIconBuilder private constructor() {
fun builder(icon: Any) = WearPermissionIconBuilder().apply { iconResource = icon }
}
}
+
+@Composable
+fun WearPermissionIconBuilder.Companion.defaultAlertConfirmIcon() =
+ builder(Icons.Default.Check).contentDescription((stringResource(android.R.string.ok)))
+
+@Composable
+fun WearPermissionIconBuilder.Companion.defaultAlertDismissIcon() =
+ builder(Icons.Default.Close).contentDescription((stringResource(android.R.string.cancel)))
diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/material3/WearPermissionListFooter.kt b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/material3/WearPermissionListFooter.kt
index cd18b5b09..35efe5db1 100644
--- 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
@@ -21,7 +21,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.RectangleShape
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.elements.material2.ListFooter
import com.android.permissioncontroller.permission.ui.wear.theme.WearPermissionMaterialUIVersion
/** This component is creates a transparent styled button to use as a list footer. */
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
index 9a926f5a3..98b8facf7 100644
--- 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
@@ -17,6 +17,7 @@ package com.android.permissioncontroller.permission.ui.wear.elements.material3
import android.graphics.drawable.Drawable
import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.PaddingValues
@@ -37,24 +38,41 @@ 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.foundation.lazy.TransformingLazyColumn
+import androidx.wear.compose.foundation.lazy.TransformingLazyColumnScope
+import androidx.wear.compose.foundation.lazy.TransformingLazyColumnState
+import androidx.wear.compose.foundation.lazy.rememberTransformingLazyColumnState
import androidx.wear.compose.material3.AppScaffold
import androidx.wear.compose.material3.CircularProgressIndicator
+import androidx.wear.compose.material3.IconButtonDefaults
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 androidx.wear.compose.material3.lazy.scrollTransform
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.ListScopeWrapper
+import com.android.permissioncontroller.permission.ui.wear.elements.material2.Wear2Scaffold
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
+private class TransformingScopeConverter(private val scope: TransformingLazyColumnScope) :
+ ListScopeWrapper {
+ override fun item(key: Any?, contentType: Any?, content: @Composable () -> Unit) {
+ scope.item { Box(modifier = Modifier.scrollTransform(this)) { content() } }
+ }
+}
+
+private class ScalingScopeConverter(private val scope: ScalingLazyListScope) : ListScopeWrapper {
+ override fun item(key: Any?, contentType: Any?, content: @Composable () -> Unit) {
+ scope.item { content() }
+ }
+}
+
/**
* This component is wrapper on material scaffold component. It helps with time text, scroll
* indicator and standard list elements like title, icon and subtitle.
@@ -67,7 +85,7 @@ internal fun WearPermissionScaffold(
subtitle: CharSequence?,
image: Any?,
isLoading: Boolean,
- content: ScalingLazyListScope.() -> Unit,
+ content: ListScopeWrapper.() -> Unit,
titleTestTag: String? = null,
subtitleTestTag: String? = null,
) {
@@ -79,7 +97,7 @@ internal fun WearPermissionScaffold(
subtitle,
image,
isLoading,
- content,
+ { content.invoke(ScalingScopeConverter(this)) },
titleTestTag,
subtitleTestTag,
)
@@ -90,7 +108,7 @@ internal fun WearPermissionScaffold(
subtitle,
image,
isLoading,
- content,
+ { content.invoke(TransformingScopeConverter(this)) },
titleTestTag,
subtitleTestTag,
)
@@ -104,7 +122,7 @@ private fun WearPermissionScaffoldInternal(
subtitle: CharSequence?,
image: Any?,
isLoading: Boolean,
- content: ScalingLazyListScope.() -> Unit,
+ content: TransformingLazyColumnScope.() -> Unit,
titleTestTag: String? = null,
subtitleTestTag: String? = null,
) {
@@ -116,12 +134,11 @@ private fun WearPermissionScaffoldInternal(
screenHeight = screenHeight,
titleNeedsLargePadding = subtitle == null,
)
- val columnState =
- rememberResponsiveColumnState(contentPadding = { paddingDefaults.scrollContentPadding })
+ val columnState = rememberTransformingLazyColumnState()
WearPermissionTheme(version = WearPermissionMaterialUIVersion.MATERIAL3) {
AppScaffold(timeText = wearPermissionTimeText(showTimeText && !isLoading)) {
ScreenScaffold(
- scrollInfoProvider = ScrollInfoProvider(columnState.state),
+ scrollInfoProvider = ScrollInfoProvider(columnState),
scrollIndicator = wearPermissionScrollIndicator(!isLoading, columnState),
) {
Box(modifier = Modifier.fillMaxSize()) {
@@ -129,6 +146,7 @@ private fun WearPermissionScaffoldInternal(
CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
} else {
ScrollingView(
+ contentPadding = paddingDefaults.scrollContentPadding,
columnState = columnState,
icon = painterFromImage(image),
title = title,
@@ -184,7 +202,8 @@ private class WearPermissionScaffoldPaddingDefaults(
@Composable
private fun BoxScope.ScrollingView(
- columnState: ScalingLazyColumnState,
+ contentPadding: PaddingValues,
+ columnState: TransformingLazyColumnState,
icon: Painter?,
title: String?,
titleTestTag: String?,
@@ -192,16 +211,26 @@ private fun BoxScope.ScrollingView(
subtitleTestTag: String?,
titlePaddingValues: PaddingValues,
subTitlePaddingValues: PaddingValues,
- content: ScalingLazyListScope.() -> Unit,
+ content: TransformingLazyColumnScope.() -> 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),
- )
+ TransformingLazyColumn(
+ contentPadding = contentPadding,
+ state = columnState,
+ modifier = Modifier.background(MaterialTheme.colorScheme.background),
+ ) {
+ with(TransformingScopeConverter(this)) {
+ iconItem(icon, Modifier.size(IconButtonDefaults.LargeIconSize))
+ titleItem(
+ text = title,
+ testTag = titleTestTag,
+ contentPaddingValues = titlePaddingValues,
+ )
+ subtitleItem(
+ text = subtitle,
+ testTag = subtitleTestTag,
+ modifier = Modifier.align(Alignment.Center).padding(subTitlePaddingValues),
+ )
+ }
content()
}
}
@@ -216,15 +245,10 @@ private fun wearPermissionTimeText(showTime: Boolean): @Composable () -> Unit {
private fun wearPermissionScrollIndicator(
showIndicator: Boolean,
- columnState: ScalingLazyColumnState,
+ columnState: TransformingLazyColumnState,
): @Composable (BoxScope.() -> Unit)? {
return if (showIndicator) {
- {
- ScrollIndicator(
- modifier = Modifier.align(Alignment.CenterEnd),
- state = columnState.state,
- )
- }
+ { ScrollIndicator(modifier = Modifier.align(Alignment.CenterEnd), state = columnState) }
} else {
null
}
@@ -246,7 +270,7 @@ private fun Modifier.optionalTestTag(tag: String?): Modifier {
return this then testTag(tag)
}
-private fun ScalingLazyListScope.iconItem(painter: Painter?, modifier: Modifier = Modifier) =
+private fun ListScopeWrapper.iconItem(painter: Painter?, modifier: Modifier = Modifier) =
painter?.let {
item {
val iconColor = WearPermissionButtonStyle.Secondary.material3ButtonColors().iconColor
@@ -260,14 +284,14 @@ private fun ScalingLazyListScope.iconItem(painter: Painter?, modifier: Modifier
}
}
-private fun ScalingLazyListScope.titleItem(
+private fun ListScopeWrapper.titleItem(
text: String?,
testTag: String?,
contentPaddingValues: PaddingValues,
modifier: Modifier = Modifier,
) =
text?.let {
- item {
+ item(contentType = "header") {
ListHeader(
modifier = modifier.requiredHeightIn(1.dp), // We do not want default min height
contentPadding = contentPaddingValues,
@@ -281,7 +305,7 @@ private fun ScalingLazyListScope.titleItem(
}
}
-private fun ScalingLazyListScope.subtitleItem(
+private fun ListScopeWrapper.subtitleItem(
text: CharSequence?,
testTag: String?,
modifier: Modifier = Modifier,
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
index 9841ca521..6fea14082 100644
--- 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
@@ -20,14 +20,17 @@ import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.semantics.stateDescription
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.R
+import com.android.permissioncontroller.permission.ui.wear.elements.material2.ToggleChip
+import com.android.permissioncontroller.permission.ui.wear.elements.material2.ToggleChipToggleControl
import com.android.permissioncontroller.permission.ui.wear.theme.WearPermissionMaterialUIVersion
/**
@@ -118,12 +121,16 @@ private fun WearPermissionToggleControlInternal(
}
val iconParam: (@Composable BoxScope.() -> Unit)? = iconBuilder?.let { { it.build() } }
-
+ val toggleControlStateDescription =
+ stringResource(
+ if (checked) {
+ R.string.on
+ } else {
+ R.string.off
+ }
+ )
val updatedModifier =
- modifier
- .fillMaxWidth()
- // .heightIn(min = 58.dp) // TODO(b/370783358): This should be a overlaid value
- .toggleControlSemantics(toggleControl, checked)
+ modifier.fillMaxWidth().semantics { stateDescription = toggleControlStateDescription }
when (toggleControl) {
ToggleChipToggleControl.Radio ->
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
index b5746f019..048a06861 100644
--- 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
@@ -25,8 +25,8 @@ 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
+import com.android.permissioncontroller.permission.ui.wear.elements.material2.toggleChipBackgroundColors
+import com.android.permissioncontroller.permission.ui.wear.elements.material2.toggleChipDisabledColors
/**
* Defines toggle control styles, It helps in setting the right colors scheme to a toggle control.
diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/rotaryinput/Haptics.kt b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/rotaryinput/Haptics.kt
deleted file mode 100644
index 817bf7efe..000000000
--- a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/rotaryinput/Haptics.kt
+++ /dev/null
@@ -1,292 +0,0 @@
-/*
- * 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.rotaryinput
-
-import android.os.Build
-import android.view.View
-import androidx.compose.foundation.gestures.ScrollableState
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.remember
-import androidx.compose.ui.platform.LocalView
-import kotlin.math.abs
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.channels.BufferOverflow
-import kotlinx.coroutines.channels.Channel
-import kotlinx.coroutines.delay
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.conflate
-import kotlinx.coroutines.flow.flow
-import kotlinx.coroutines.flow.receiveAsFlow
-import kotlinx.coroutines.withContext
-
-// This file is a copy of Haptics.kt from Horologist (go/horologist),
-// remove it once Wear Compose 1.4 is landed (b/325560444).
-
-private const val DEBUG = false
-
-/** Debug logging that can be enabled. */
-private inline fun debugLog(generateMsg: () -> String) {
- if (DEBUG) {
- println("RotaryHaptics: ${generateMsg()}")
- }
-}
-
-/**
- * Throttling events within specified timeframe. Only first and last events will be received. For a
- * flow emitting elements 1 to 30, with a 100ms delay between them:
- * ```
- * val flow = flow {
- * for (i in 1..30) {
- * delay(100)
- * emit(i)
- * }
- * }
- * ```
- *
- * With timeframe=1000 only those integers will be received: 1, 10, 20, 30 .
- */
-internal fun <T> Flow<T>.throttleLatest(timeframe: Long): Flow<T> = flow {
- conflate().collect {
- emit(it)
- delay(timeframe)
- }
-}
-
-/** Handles haptics for rotary usage */
-interface RotaryHapticHandler {
-
- /** Handles haptics when scroll is used */
- fun handleScrollHaptic(scrollDelta: Float)
-
- /** Handles haptics when scroll with snap is used */
- fun handleSnapHaptic(scrollDelta: Float)
-}
-
-/**
- * Default implementation of [RotaryHapticHandler]. It handles haptic feedback based on the
- * [scrollableState], scrolled pixels and [hapticsThresholdPx]. Haptic is not fired in this class,
- * instead it's sent to [hapticsChannel] where it'll performed later.
- *
- * @param scrollableState Haptic performed based on this state
- * @param hapticsChannel Channel to which haptic events will be sent
- * @param hapticsThresholdPx A scroll threshold after which haptic is produced.
- */
-class DefaultRotaryHapticHandler(
- private val scrollableState: ScrollableState,
- private val hapticsChannel: Channel<RotaryHapticsType>,
- private val hapticsThresholdPx: Long = 50,
-) : RotaryHapticHandler {
-
- private var overscrollHapticTriggered = false
- private var currScrollPosition = 0f
- private var prevHapticsPosition = 0f
-
- override fun handleScrollHaptic(scrollDelta: Float) {
- if (
- (scrollDelta > 0 && !scrollableState.canScrollForward) ||
- (scrollDelta < 0 && !scrollableState.canScrollBackward)
- ) {
- if (!overscrollHapticTriggered) {
- trySendHaptic(RotaryHapticsType.ScrollLimit)
- overscrollHapticTriggered = true
- }
- } else {
- overscrollHapticTriggered = false
- currScrollPosition += scrollDelta
- val diff = abs(currScrollPosition - prevHapticsPosition)
-
- if (diff >= hapticsThresholdPx) {
- trySendHaptic(RotaryHapticsType.ScrollTick)
- prevHapticsPosition = currScrollPosition
- }
- }
- }
-
- override fun handleSnapHaptic(scrollDelta: Float) {
- if (
- (scrollDelta > 0 && !scrollableState.canScrollForward) ||
- (scrollDelta < 0 && !scrollableState.canScrollBackward)
- ) {
- if (!overscrollHapticTriggered) {
- trySendHaptic(RotaryHapticsType.ScrollLimit)
- overscrollHapticTriggered = true
- }
- } else {
- overscrollHapticTriggered = false
- trySendHaptic(RotaryHapticsType.ScrollItemFocus)
- }
- }
-
- private fun trySendHaptic(rotaryHapticsType: RotaryHapticsType) {
- // Ok to ignore the ChannelResult because we default to capacity = 2 and DROP_OLDEST
- @Suppress("UNUSED_VARIABLE") val unused = hapticsChannel.trySend(rotaryHapticsType)
- }
-}
-
-/** Interface for Rotary haptic feedback */
-interface RotaryHapticFeedback {
- fun performHapticFeedback(type: RotaryHapticsType)
-}
-
-/** Rotary haptic types */
-@JvmInline
-value class RotaryHapticsType(private val type: Int) {
- companion object {
- /**
- * A scroll ticking haptic. Similar to texture haptic - performed each time when a
- * scrollable content is scrolled by a certain distance
- */
- val ScrollTick: RotaryHapticsType = RotaryHapticsType(1)
-
- /**
- * An item focus (snap) haptic. Performed when a scrollable content is snapped to a specific
- * item.
- */
- val ScrollItemFocus: RotaryHapticsType = RotaryHapticsType(2)
-
- /**
- * A limit(overscroll) haptic. Performed when a list reaches the limit (start or end) and
- * can't scroll further
- */
- val ScrollLimit: RotaryHapticsType = RotaryHapticsType(3)
- }
-}
-
-/** Remember disabled haptics handler */
-@Composable
-fun rememberDisabledHaptic(): RotaryHapticHandler = remember {
- object : RotaryHapticHandler {
-
- override fun handleScrollHaptic(scrollDelta: Float) {
- // Do nothing
- }
-
- override fun handleSnapHaptic(scrollDelta: Float) {
- // Do nothing
- }
- }
-}
-
-/**
- * Remember rotary haptic handler.
- *
- * @param scrollableState A scrollableState, used to determine whether the end of the scrollable was
- * reached or not.
- * @param throttleThresholdMs Throttling events within specified timeframe. Only first and last
- * events will be received. Check [throttleLatest] for more info.
- * @param hapticsThresholdPx A scroll threshold after which haptic is produced.
- * @param hapticsChannel Channel to which haptic events will be sent
- * @param rotaryHaptics Interface for Rotary haptic feedback which performs haptics
- */
-@Composable
-fun rememberRotaryHapticHandler(
- scrollableState: ScrollableState,
- throttleThresholdMs: Long = 30,
- hapticsThresholdPx: Long = 50,
- hapticsChannel: Channel<RotaryHapticsType> = rememberHapticChannel(),
- rotaryHaptics: RotaryHapticFeedback = rememberDefaultRotaryHapticFeedback(),
-): RotaryHapticHandler {
- return remember(scrollableState, hapticsChannel, rotaryHaptics) {
- DefaultRotaryHapticHandler(scrollableState, hapticsChannel, hapticsThresholdPx)
- }
- .apply {
- LaunchedEffect(hapticsChannel) {
- hapticsChannel.receiveAsFlow().throttleLatest(throttleThresholdMs).collect {
- hapticType ->
- // 'withContext' launches performHapticFeedback in a separate thread,
- // as otherwise it produces a visible lag (b/219776664)
- val currentTime = System.currentTimeMillis()
- debugLog { "Haptics started" }
- withContext(Dispatchers.Default) {
- debugLog {
- "Performing haptics, delay: " +
- "${System.currentTimeMillis() - currentTime}"
- }
- rotaryHaptics.performHapticFeedback(hapticType)
- }
- }
- }
- }
-}
-
-@Composable
-private fun rememberHapticChannel() = remember {
- Channel<RotaryHapticsType>(
- capacity = 2,
- onBufferOverflow = BufferOverflow.DROP_OLDEST,
- )
-}
-
-@Composable
-public fun rememberDefaultRotaryHapticFeedback(): RotaryHapticFeedback =
- LocalView.current.let { view -> remember { findDeviceSpecificHapticFeedback(view) } }
-
-internal fun findDeviceSpecificHapticFeedback(view: View): RotaryHapticFeedback =
- if (isSamsungWatch()) {
- SamsungWatchHapticFeedback(view)
- } else {
- DefaultRotaryHapticFeedback(view)
- }
-
-/** Default Rotary implementation for [RotaryHapticFeedback] */
-class DefaultRotaryHapticFeedback(private val view: View) : RotaryHapticFeedback {
-
- override fun performHapticFeedback(
- type: RotaryHapticsType,
- ) {
- when (type) {
- RotaryHapticsType.ScrollItemFocus -> {
- view.performHapticFeedback(SCROLL_ITEM_FOCUS)
- }
- RotaryHapticsType.ScrollTick -> {
- view.performHapticFeedback(SCROLL_TICK)
- }
- RotaryHapticsType.ScrollLimit -> {
- view.performHapticFeedback(SCROLL_LIMIT)
- }
- }
- }
-
- private companion object {
- // Hidden constants from HapticFeedbackConstants
- const val SCROLL_TICK: Int = 18
- const val SCROLL_ITEM_FOCUS: Int = 19
- const val SCROLL_LIMIT: Int = 20
- }
-}
-
-/** Implementation of [RotaryHapticFeedback] for Samsung devices */
-private class SamsungWatchHapticFeedback(private val view: View) : RotaryHapticFeedback {
- override fun performHapticFeedback(
- type: RotaryHapticsType,
- ) {
- when (type) {
- RotaryHapticsType.ScrollItemFocus -> {
- view.performHapticFeedback(102)
- }
- RotaryHapticsType.ScrollTick -> {
- view.performHapticFeedback(102)
- }
- RotaryHapticsType.ScrollLimit -> {
- view.performHapticFeedback(50107)
- }
- }
- }
-}
-
-private fun isSamsungWatch(): Boolean = Build.MANUFACTURER.contains("Samsung", ignoreCase = true)
diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/rotaryinput/Rotary.kt b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/rotaryinput/Rotary.kt
deleted file mode 100644
index 19a6ea671..000000000
--- a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/rotaryinput/Rotary.kt
+++ /dev/null
@@ -1,1232 +0,0 @@
-/*
- * 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.rotaryinput
-
-import android.view.ViewConfiguration
-import androidx.compose.animation.core.AnimationState
-import androidx.compose.animation.core.CubicBezierEasing
-import androidx.compose.animation.core.Easing
-import androidx.compose.animation.core.FastOutSlowInEasing
-import androidx.compose.animation.core.SpringSpec
-import androidx.compose.animation.core.animateTo
-import androidx.compose.animation.core.copy
-import androidx.compose.animation.core.spring
-import androidx.compose.animation.core.tween
-import androidx.compose.foundation.MutatePriority
-import androidx.compose.foundation.focusable
-import androidx.compose.foundation.gestures.FlingBehavior
-import androidx.compose.foundation.gestures.ScrollableDefaults
-import androidx.compose.foundation.gestures.ScrollableState
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.snapshots.Snapshot
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.focus.FocusRequester
-import androidx.compose.ui.focus.focusRequester
-import androidx.compose.ui.input.rotary.RotaryInputModifierNode
-import androidx.compose.ui.input.rotary.RotaryScrollEvent
-import androidx.compose.ui.node.ModifierNodeElement
-import androidx.compose.ui.platform.InspectorInfo
-import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.platform.LocalDensity
-import androidx.compose.ui.platform.debugInspectorInfo
-import androidx.compose.ui.unit.Dp
-import androidx.compose.ui.util.fastSumBy
-import androidx.compose.ui.util.lerp
-import androidx.wear.compose.foundation.ExperimentalWearFoundationApi
-import androidx.wear.compose.foundation.lazy.ScalingLazyListState
-import androidx.wear.compose.foundation.rememberActiveFocusRequester
-import kotlin.math.abs
-import kotlin.math.absoluteValue
-import kotlin.math.sign
-import kotlinx.coroutines.CompletableDeferred
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.Job
-import kotlinx.coroutines.async
-import kotlinx.coroutines.channels.Channel
-import kotlinx.coroutines.delay
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.collectLatest
-import kotlinx.coroutines.flow.receiveAsFlow
-import kotlinx.coroutines.flow.transformLatest
-import kotlinx.coroutines.launch
-
-// This file is a copy of Rotary.kt from Horologist (go/horologist),
-// remove it once Wear Compose 1.4 is landed (b/325560444).
-
-/**
- * A modifier which connects rotary events with scrollable. This modifier supports scroll with
- * fling.
- *
- * @param scrollableState Scrollable state which will be scrolled while receiving rotary events
- * @param focusRequester Requests the focus for rotary input. By default comes from
- * [rememberActiveFocusRequester], which is used with [HierarchicalFocusCoordinator]
- * @param flingBehavior Logic describing fling behavior. If null fling will not happen.
- * @param rotaryHaptics Class which will handle haptic feedback
- * @param reverseDirection Reverse the direction of scrolling. Should be aligned with Scrollable
- * `reverseDirection` parameter
- */
-@OptIn(ExperimentalWearFoundationApi::class)
-@Suppress("ComposableModifierFactory")
-@Composable
-fun Modifier.rotaryWithScroll(
- scrollableState: ScrollableState,
- focusRequester: FocusRequester = rememberActiveFocusRequester(),
- flingBehavior: FlingBehavior? = ScrollableDefaults.flingBehavior(),
- rotaryHaptics: RotaryHapticHandler = rememberRotaryHapticHandler(scrollableState),
- reverseDirection: Boolean = false,
-): Modifier =
- rotaryHandler(
- rotaryScrollHandler =
- RotaryDefaults.rememberFlingHandler(scrollableState, flingBehavior),
- reverseDirection = reverseDirection,
- rotaryHaptics = rotaryHaptics,
- inspectorInfo =
- debugInspectorInfo {
- name = "rotaryWithFling"
- properties["scrollableState"] = scrollableState
- properties["focusRequester"] = focusRequester
- properties["flingBehavior"] = flingBehavior
- properties["rotaryHaptics"] = rotaryHaptics
- properties["reverseDirection"] = reverseDirection
- },
- )
- .focusRequester(focusRequester)
- .focusable()
-
-/**
- * A modifier which connects rotary events with scrollable. This modifier supports snap.
- *
- * @param focusRequester Requests the focus for rotary input. By default comes from
- * [rememberActiveFocusRequester], which is used with [HierarchicalFocusCoordinator]
- * @param rotaryScrollAdapter A connection between scrollable objects and rotary events
- * @param rotaryHaptics Class which will handle haptic feedback
- * @param reverseDirection Reverse the direction of scrolling. Should be aligned with Scrollable
- * `reverseDirection` parameter
- */
-@OptIn(ExperimentalWearFoundationApi::class)
-@Suppress("ComposableModifierFactory")
-@Composable
-fun Modifier.rotaryWithSnap(
- rotaryScrollAdapter: RotaryScrollAdapter,
- focusRequester: FocusRequester = rememberActiveFocusRequester(),
- snapParameters: SnapParameters = RotaryDefaults.snapParametersDefault,
- rotaryHaptics: RotaryHapticHandler =
- rememberRotaryHapticHandler(rotaryScrollAdapter.scrollableState),
- reverseDirection: Boolean = false,
-): Modifier =
- rotaryHandler(
- rotaryScrollHandler =
- RotaryDefaults.rememberSnapHandler(rotaryScrollAdapter, snapParameters),
- reverseDirection = reverseDirection,
- rotaryHaptics = rotaryHaptics,
- inspectorInfo =
- debugInspectorInfo {
- name = "rotaryWithFling"
- properties["rotaryScrollAdapter"] = rotaryScrollAdapter
- properties["focusRequester"] = focusRequester
- properties["snapParameters"] = snapParameters
- properties["rotaryHaptics"] = rotaryHaptics
- properties["reverseDirection"] = reverseDirection
- },
- )
- .focusRequester(focusRequester)
- .focusable()
-
-/** An extension function for creating [RotaryScrollAdapter] from [ScalingLazyListState] */
-@Composable
-fun ScalingLazyListState.toRotaryScrollAdapter(): RotaryScrollAdapter =
- remember(this) { ScalingLazyColumnRotaryScrollAdapter(this) }
-
-/** An implementation of rotary scroll adapter for [ScalingLazyColumn] */
-class ScalingLazyColumnRotaryScrollAdapter(
- override val scrollableState: ScalingLazyListState,
-) : RotaryScrollAdapter {
-
- /** Calculates an average height of an item by taking an average from visible items height. */
- override fun averageItemSize(): Float {
- val visibleItems = scrollableState.layoutInfo.visibleItemsInfo
- return (visibleItems.fastSumBy { it.unadjustedSize } / visibleItems.size).toFloat()
- }
-
- /** Current (centred) item index */
- override fun currentItemIndex(): Int = scrollableState.centerItemIndex
-
- /** An offset from the item centre */
- override fun currentItemOffset(): Float = scrollableState.centerItemScrollOffset.toFloat()
-
- /** The total count of items in ScalingLazyColumn */
- override fun totalItemsCount(): Int = scrollableState.layoutInfo.totalItemsCount
-}
-
-/** An adapter which connects scrollableState to Rotary */
-interface RotaryScrollAdapter {
-
- /** A scrollable state. Used for performing scroll when Rotary events received */
- val scrollableState: ScrollableState
-
- /** Average size of an item. Used for estimating the scrollable distance */
- fun averageItemSize(): Float
-
- /** A current item index. Used for scrolling */
- fun currentItemIndex(): Int
-
- /** An offset from the centre or the border of the current item. */
- fun currentItemOffset(): Float
-
- /** The total count of items in [scrollableState] */
- fun totalItemsCount(): Int
-}
-
-/** Defaults for rotary modifiers */
-object RotaryDefaults {
-
- /** Returns default [SnapParameters] */
- val snapParametersDefault: SnapParameters =
- SnapParameters(
- snapOffset = 0,
- thresholdDivider = 1.5f,
- resistanceFactor = 3f,
- )
-
- /** Returns whether the input is Low-res (a bezel) or high-res(a crown/rsb). */
- @Composable
- fun isLowResInput(): Boolean =
- LocalContext.current.packageManager.hasSystemFeature(
- "android.hardware.rotaryencoder.lowres"
- )
-
- /**
- * Handles scroll with fling.
- *
- * @param scrollableState Scrollable state which will be scrolled while receiving rotary events
- * @param flingBehavior Logic describing Fling behavior. If null - fling will not happen
- * @param isLowRes Whether the input is Low-res (a bezel) or high-res(a crown/rsb)
- */
- @Composable
- internal fun rememberFlingHandler(
- scrollableState: ScrollableState,
- flingBehavior: FlingBehavior? = null,
- isLowRes: Boolean = isLowResInput(),
- ): RotaryScrollHandler {
- val viewConfiguration = ViewConfiguration.get(LocalContext.current)
-
- return remember(scrollableState, flingBehavior, isLowRes) {
- // Remove unnecessary recompositions by disabling tracking of changes inside of
- // this block. This algorithm properly reads all updated values and
- // don't need recomposition when those values change.
- Snapshot.withoutReadObservation {
- debugLog { "isLowRes : $isLowRes" }
- fun rotaryFlingBehavior() =
- flingBehavior?.run {
- RotaryFlingBehavior(
- scrollableState,
- flingBehavior,
- viewConfiguration,
- flingTimeframe =
- if (isLowRes) lowResFlingTimeframe else highResFlingTimeframe,
- )
- }
-
- fun scrollBehavior() = RotaryScrollBehavior(scrollableState)
-
- if (isLowRes) {
- LowResRotaryScrollHandler(
- rotaryFlingBehaviorFactory = { rotaryFlingBehavior() },
- scrollBehaviorFactory = { scrollBehavior() },
- )
- } else {
- HighResRotaryScrollHandler(
- rotaryFlingBehaviorFactory = { rotaryFlingBehavior() },
- scrollBehaviorFactory = { scrollBehavior() },
- )
- }
- }
- }
- }
-
- /**
- * Handles scroll with snap
- *
- * @param rotaryScrollAdapter A connection between scrollable objects and rotary events
- * @param snapParameters Snap parameters
- */
- @Composable
- internal fun rememberSnapHandler(
- rotaryScrollAdapter: RotaryScrollAdapter,
- snapParameters: SnapParameters = snapParametersDefault,
- isLowRes: Boolean = isLowResInput(),
- ): RotaryScrollHandler {
- return remember(rotaryScrollAdapter, snapParameters) {
- // Remove unnecessary recompositions by disabling tracking of changes inside of
- // this block. This algorithm properly reads all updated values and
- // don't need recomposition when those values change.
- Snapshot.withoutReadObservation {
- debugLog { "isLowRes : $isLowRes" }
- if (isLowRes) {
- LowResSnapHandler(
- snapBehaviourFactory = {
- RotarySnapBehavior(rotaryScrollAdapter, snapParameters)
- },
- )
- } else {
- HighResSnapHandler(
- resistanceFactor = snapParameters.resistanceFactor,
- thresholdBehaviorFactory = {
- ThresholdBehavior(
- rotaryScrollAdapter,
- snapParameters.thresholdDivider,
- )
- },
- snapBehaviourFactory = {
- RotarySnapBehavior(rotaryScrollAdapter, snapParameters)
- },
- scrollBehaviourFactory = {
- RotaryScrollBehavior(rotaryScrollAdapter.scrollableState)
- },
- )
- }
- }
- }
- }
-
- private val lowResFlingTimeframe: Long = 100L
- private val highResFlingTimeframe: Long = 30L
-}
-
-/**
- * Parameters used for snapping
- *
- * @param snapOffset an optional offset to be applied when snapping the item. After the snap the
- * snapped items offset will be [snapOffset].
- */
-class SnapParameters(
- val snapOffset: Int,
- val thresholdDivider: Float,
- val resistanceFactor: Float,
-) {
- /** Returns a snapping offset in [Dp] */
- @Composable
- fun snapOffsetDp(): Dp {
- return with(LocalDensity.current) { snapOffset.toDp() }
- }
-
- override fun equals(other: Any?): Boolean {
- if (this === other) return true
- if (other == null || this::class != other::class) return false
-
- other as SnapParameters
-
- if (snapOffset != other.snapOffset) return false
- if (thresholdDivider != other.thresholdDivider) return false
- if (resistanceFactor != other.resistanceFactor) return false
-
- return true
- }
-
- override fun hashCode(): Int {
- var result = snapOffset
- result = 31 * result + thresholdDivider.hashCode()
- result = 31 * result + resistanceFactor.hashCode()
- return result
- }
-}
-
-/** An interface for handling scroll events */
-internal interface RotaryScrollHandler {
- /**
- * Handles scrolling events
- *
- * @param coroutineScope A scope for performing async actions
- * @param event A scrollable event from rotary input, containing scrollable delta and timestamp
- * @param rotaryHaptics
- */
- suspend fun handleScrollEvent(
- coroutineScope: CoroutineScope,
- event: TimestampedDelta,
- rotaryHaptics: RotaryHapticHandler,
- )
-}
-
-/**
- * Class responsible for Fling behaviour with rotary. It tracks and produces the fling when
- * necessary
- */
-internal class RotaryFlingBehavior(
- private val scrollableState: ScrollableState,
- private val flingBehavior: FlingBehavior,
- viewConfiguration: ViewConfiguration,
- private val flingTimeframe: Long,
-) {
-
- // A time range during which the fling is valid.
- // For simplicity it's twice as long as [flingTimeframe]
- private val timeRangeToFling = flingTimeframe * 2
-
- // A default fling factor for making fling slower
- private val flingScaleFactor = 0.7f
-
- private var previousVelocity = 0f
-
- private val rotaryVelocityTracker = RotaryVelocityTracker()
-
- private val minFlingSpeed = viewConfiguration.scaledMinimumFlingVelocity.toFloat()
- private val maxFlingSpeed = viewConfiguration.scaledMaximumFlingVelocity.toFloat()
- private var latestEventTimestamp: Long = 0
-
- private var flingVelocity: Float = 0f
- private var flingTimestamp: Long = 0
-
- /** Starts a new fling tracking session with specified timestamp */
- fun startFlingTracking(timestamp: Long) {
- rotaryVelocityTracker.start(timestamp)
- latestEventTimestamp = timestamp
- previousVelocity = 0f
- }
-
- /** Observing new event within a fling tracking session with new timestamp and delta */
- fun observeEvent(timestamp: Long, delta: Float) {
- rotaryVelocityTracker.move(timestamp, delta)
- latestEventTimestamp = timestamp
- }
-
- /** Performing fling if necessary and calling [beforeFling] lambda before it is triggered */
- suspend fun trackFling(beforeFling: () -> Unit) {
- val currentVelocity = rotaryVelocityTracker.velocity
- debugLog { "currentVelocity: $currentVelocity" }
-
- if (abs(currentVelocity) >= abs(previousVelocity)) {
- flingTimestamp = latestEventTimestamp
- flingVelocity = currentVelocity * flingScaleFactor
- }
- previousVelocity = currentVelocity
-
- // Waiting for a fixed amount of time before checking the fling
- delay(flingTimeframe)
-
- // For making a fling 2 criteria should be met:
- // 1) no more than
- // `rangeToFling` ms should pass between last fling detection
- // and the time of last motion event
- // 2) flingVelocity should exceed the minFlingSpeed
- debugLog {
- "Check fling: flingVelocity: $flingVelocity " +
- "minFlingSpeed: $minFlingSpeed, maxFlingSpeed: $maxFlingSpeed"
- }
- if (
- latestEventTimestamp - flingTimestamp < timeRangeToFling &&
- abs(flingVelocity) > minFlingSpeed
- ) {
- // Stops scrollAnimationCoroutine because a fling will be performed
- beforeFling()
- val velocity = flingVelocity.coerceIn(-maxFlingSpeed, maxFlingSpeed)
- scrollableState.scroll(MutatePriority.UserInput) {
- with(flingBehavior) {
- debugLog { "Flinging with velocity $velocity" }
- performFling(velocity)
- }
- }
- }
- }
-}
-
-/**
- * A rotary event object which contains a [timestamp] of the rotary event and a scrolled [delta].
- */
-internal data class TimestampedDelta(val timestamp: Long, val delta: Float)
-
-/**
- * This class does a smooth animation when the scroll by N pixels is done. This animation works well
- * on Rsb(high-res) and Bezel(low-res) devices.
- */
-internal class RotaryScrollBehavior(
- private val scrollableState: ScrollableState,
-) {
- private var sequentialAnimation = false
- private var scrollAnimation = AnimationState(0f)
- private var prevPosition = 0f
-
- /** Handles scroll event to [targetValue] */
- suspend fun handleEvent(targetValue: Float) {
- scrollableState.scroll(MutatePriority.UserInput) {
- debugLog { "ScrollAnimation value before start: ${scrollAnimation.value}" }
-
- scrollAnimation.animateTo(
- targetValue,
- animationSpec = spring(),
- sequentialAnimation = sequentialAnimation,
- ) {
- val delta = value - prevPosition
- debugLog { "Animated by $delta, value: $value" }
- scrollBy(delta)
- prevPosition = value
- sequentialAnimation = value != this.targetValue
- }
- }
- }
-}
-
-/**
- * A helper class for snapping with rotary. Uses animateScrollToItem method for snapping to the Nth
- * item.
- */
-internal class RotarySnapBehavior(
- private val rotaryScrollAdapter: RotaryScrollAdapter,
- private val snapParameters: SnapParameters,
-) {
- private var snapTarget: Int = rotaryScrollAdapter.currentItemIndex()
- private var sequentialSnap: Boolean = false
-
- private var anim = AnimationState(0f)
- private var expectedDistance = 0f
-
- private val defaultStiffness = 200f
- private var snapTargetUpdated = true
-
- /**
- * Preparing snapping. This method should be called before [snapToTargetItem] is called.
- *
- * Snapping is done for current + [moveForElements] items.
- *
- * If [sequentialSnap] is true, items are summed up together. For example, if
- * [prepareSnapForItems] is called with [moveForElements] = 2, 3, 5 -> then the snapping will
- * happen to current + 10 items
- *
- * If [sequentialSnap] is false, then [moveForElements] are not summed up together.
- */
- fun prepareSnapForItems(moveForElements: Int, sequentialSnap: Boolean) {
- this.sequentialSnap = sequentialSnap
- if (sequentialSnap) {
- snapTarget += moveForElements
- } else {
- snapTarget = rotaryScrollAdapter.currentItemIndex() + moveForElements
- }
- snapTargetUpdated = true
- snapTarget = snapTarget.coerceIn(0 until rotaryScrollAdapter.totalItemsCount())
- }
-
- /** Performs snapping to the closest item. */
- suspend fun snapToClosestItem() {
- // Snapping to the closest item by using performFling method with 0 speed
- rotaryScrollAdapter.scrollableState.scroll(MutatePriority.UserInput) {
- debugLog { "snap to closest item" }
- var prevPosition = 0f
- AnimationState(0f).animateTo(
- targetValue = -rotaryScrollAdapter.currentItemOffset(),
- animationSpec = tween(durationMillis = 100, easing = FastOutSlowInEasing),
- ) {
- val animDelta = value - prevPosition
- scrollBy(animDelta)
- prevPosition = value
- }
- snapTarget = rotaryScrollAdapter.currentItemIndex()
- }
- }
-
- /** Returns true if top edge was reached */
- fun topEdgeReached(): Boolean = snapTarget <= 0
-
- /** Returns true if bottom edge was reached */
- fun bottomEdgeReached(): Boolean = snapTarget >= rotaryScrollAdapter.totalItemsCount() - 1
-
- /** Performs snapping to the specified in [prepareSnapForItems] element */
- suspend fun snapToTargetItem() {
- if (sequentialSnap) {
- anim = anim.copy(0f)
- } else {
- anim = AnimationState(0f)
- }
- rotaryScrollAdapter.scrollableState.scroll(MutatePriority.UserInput) {
- // If snapTargetUpdated is true - then the target was updated so we
- // need to do snap again
- while (snapTargetUpdated) {
- snapTargetUpdated = false
- var latestCenterItem: Int
- var continueFirstScroll = true
- debugLog { "snapTarget $snapTarget" }
- while (continueFirstScroll) {
- latestCenterItem = rotaryScrollAdapter.currentItemIndex()
- anim = anim.copy(0f)
- expectedDistance = expectedDistanceTo(snapTarget, snapParameters.snapOffset)
- debugLog {
- "expectedDistance = $expectedDistance, " +
- "scrollableState.centerItemScrollOffset " +
- "${rotaryScrollAdapter.currentItemOffset()}"
- }
- continueFirstScroll = false
- var prevPosition = 0f
-
- anim.animateTo(
- expectedDistance,
- animationSpec =
- SpringSpec(
- stiffness = defaultStiffness,
- visibilityThreshold = 0.1f,
- ),
- sequentialAnimation = (anim.velocity != 0f),
- ) {
- val animDelta = value - prevPosition
- debugLog {
- "First animation, value:$value, velocity:$velocity, " +
- "animDelta:$animDelta"
- }
-
- // Exit animation if snap target was updated
- if (snapTargetUpdated) cancelAnimation()
-
- scrollBy(animDelta)
- prevPosition = value
-
- if (latestCenterItem != rotaryScrollAdapter.currentItemIndex()) {
- continueFirstScroll = true
- cancelAnimation()
- return@animateTo
- }
-
- debugLog { "centerItemIndex = ${rotaryScrollAdapter.currentItemIndex()}" }
- if (rotaryScrollAdapter.currentItemIndex() == snapTarget) {
- debugLog { "Target is visible. Cancelling first animation" }
- debugLog {
- "scrollableState.centerItemScrollOffset " +
- "${rotaryScrollAdapter.currentItemOffset()}"
- }
- expectedDistance = -rotaryScrollAdapter.currentItemOffset()
- continueFirstScroll = false
- cancelAnimation()
- return@animateTo
- }
- }
- }
- // Exit animation if snap target was updated
- if (snapTargetUpdated) continue
-
- anim = anim.copy(0f)
- var prevPosition = 0f
- anim.animateTo(
- expectedDistance,
- animationSpec =
- SpringSpec(
- stiffness = defaultStiffness,
- visibilityThreshold = 0.1f,
- ),
- sequentialAnimation = (anim.velocity != 0f),
- ) {
- // Exit animation if snap target was updated
- if (snapTargetUpdated) cancelAnimation()
-
- val animDelta = value - prevPosition
- debugLog { "Final animation. velocity:$velocity, animDelta:$animDelta" }
- scrollBy(animDelta)
- prevPosition = value
- }
- }
- }
- }
-
- private fun expectedDistanceTo(index: Int, targetScrollOffset: Int): Float {
- val averageSize = rotaryScrollAdapter.averageItemSize()
- val indexesDiff = index - rotaryScrollAdapter.currentItemIndex()
- debugLog { "Average size $averageSize" }
- return (averageSize * indexesDiff) + targetScrollOffset -
- rotaryScrollAdapter.currentItemOffset()
- }
-}
-
-/**
- * A modifier which handles rotary events. It accepts ScrollHandler as the input - a class where
- * main logic about how scroll should be handled is lying
- */
-internal fun Modifier.rotaryHandler(
- rotaryScrollHandler: RotaryScrollHandler,
- reverseDirection: Boolean,
- rotaryHaptics: RotaryHapticHandler,
- inspectorInfo: InspectorInfo.() -> Unit,
-): Modifier =
- this then
- RotaryHandlerElement(
- rotaryScrollHandler,
- reverseDirection,
- rotaryHaptics,
- inspectorInfo,
- )
-
-/**
- * Batching requests for scrolling events. This function combines all events together (except first)
- * within specified timeframe. Should help with performance on high-res devices.
- */
-@OptIn(ExperimentalCoroutinesApi::class)
-internal fun Flow<TimestampedDelta>.batchRequestsWithinTimeframe(
- timeframe: Long
-): Flow<TimestampedDelta> {
- var delta = 0f
- var lastTimestamp = -timeframe
- return if (timeframe == 0L) {
- this
- } else {
- this.transformLatest {
- delta += it.delta
- debugLog { "Batching requests. delta:$delta" }
- if (lastTimestamp + timeframe <= it.timestamp) {
- lastTimestamp = it.timestamp
- debugLog { "No events before, delta= $delta" }
- emit(TimestampedDelta(it.timestamp, delta))
- } else {
- delay(timeframe)
- debugLog { "After delay, delta= $delta" }
- if (delta > 0f) {
- emit(TimestampedDelta(it.timestamp, delta))
- }
- }
- delta = 0f
- }
- }
-}
-
-/**
- * A scroll handler for RSB(high-res) without snapping and with or without fling A list is scrolled
- * by the number of pixels received from the rotary device.
- *
- * This class is a little bit different from LowResScrollHandler class - it has a filtering for
- * events which are coming with wrong sign ( this happens to rsb devices, especially at the end of
- * the scroll)
- *
- * This scroll handler supports fling. It can be set with [RotaryFlingBehavior].
- */
-internal class HighResRotaryScrollHandler(
- private val rotaryFlingBehaviorFactory: () -> RotaryFlingBehavior?,
- private val scrollBehaviorFactory: () -> RotaryScrollBehavior,
- private val hapticsThreshold: Long = 50,
-) : RotaryScrollHandler {
-
- // This constant is specific for high-res devices. Because that input values
- // can sometimes come with different sign, we have to filter them in this threshold
- private val gestureThresholdTime = 200L
- private var scrollJob: Job = CompletableDeferred<Unit>()
- private var flingJob: Job = CompletableDeferred<Unit>()
-
- private var previousScrollEventTime = 0L
- private var rotaryScrollDistance = 0f
-
- private var rotaryFlingBehavior: RotaryFlingBehavior? = rotaryFlingBehaviorFactory()
- private var scrollBehavior: RotaryScrollBehavior = scrollBehaviorFactory()
-
- override suspend fun handleScrollEvent(
- coroutineScope: CoroutineScope,
- event: TimestampedDelta,
- rotaryHaptics: RotaryHapticHandler,
- ) {
- val time = event.timestamp
- val isOppositeScrollValue = isOppositeValueAfterScroll(event.delta)
-
- if (isNewScrollEvent(time)) {
- debugLog { "New scroll event" }
- resetTracking(time)
- rotaryScrollDistance = event.delta
- } else {
- // Due to the physics of Rotary side button, some events might come
- // with an opposite axis value - either at the start or at the end of the motion.
- // We don't want to use these values for fling calculations.
- if (!isOppositeScrollValue) {
- rotaryFlingBehavior?.observeEvent(event.timestamp, event.delta)
- } else {
- debugLog { "Opposite value after scroll :${event.delta}" }
- }
- rotaryScrollDistance += event.delta
- }
-
- scrollJob.cancel()
-
- rotaryHaptics.handleScrollHaptic(event.delta)
- debugLog { "Rotary scroll distance: $rotaryScrollDistance" }
-
- previousScrollEventTime = time
- scrollJob = coroutineScope.async { scrollBehavior.handleEvent(rotaryScrollDistance) }
-
- if (rotaryFlingBehavior != null) {
- flingJob.cancel()
- flingJob =
- coroutineScope.async {
- rotaryFlingBehavior?.trackFling(
- beforeFling = {
- debugLog { "Calling before fling section" }
- scrollJob.cancel()
- scrollBehavior = scrollBehaviorFactory()
- }
- )
- }
- }
- }
-
- private fun isOppositeValueAfterScroll(delta: Float): Boolean =
- rotaryScrollDistance * delta < 0f && (abs(delta) < abs(rotaryScrollDistance))
-
- private fun isNewScrollEvent(timestamp: Long): Boolean {
- val timeDelta = timestamp - previousScrollEventTime
- return previousScrollEventTime == 0L || timeDelta > gestureThresholdTime
- }
-
- private fun resetTracking(timestamp: Long) {
- scrollBehavior = scrollBehaviorFactory()
- rotaryFlingBehavior = rotaryFlingBehaviorFactory()
- rotaryFlingBehavior?.startFlingTracking(timestamp)
- }
-}
-
-/**
- * A scroll handler for Bezel(low-res) without snapping. This scroll handler supports fling. It can
- * be set with RotaryFlingBehavior.
- */
-internal class LowResRotaryScrollHandler(
- private val rotaryFlingBehaviorFactory: () -> RotaryFlingBehavior?,
- private val scrollBehaviorFactory: () -> RotaryScrollBehavior,
-) : RotaryScrollHandler {
-
- private val gestureThresholdTime = 200L
- private var previousScrollEventTime = 0L
- private var rotaryScrollDistance = 0f
-
- private var scrollJob: Job = CompletableDeferred<Unit>()
- private var flingJob: Job = CompletableDeferred<Unit>()
-
- private var rotaryFlingBehavior: RotaryFlingBehavior? = rotaryFlingBehaviorFactory()
- private var scrollBehavior: RotaryScrollBehavior = scrollBehaviorFactory()
-
- override suspend fun handleScrollEvent(
- coroutineScope: CoroutineScope,
- event: TimestampedDelta,
- rotaryHaptics: RotaryHapticHandler,
- ) {
- val time = event.timestamp
-
- if (isNewScrollEvent(time)) {
- resetTracking(time)
- rotaryScrollDistance = event.delta
- } else {
- rotaryFlingBehavior?.observeEvent(event.timestamp, event.delta)
- rotaryScrollDistance += event.delta
- }
-
- scrollJob.cancel()
- flingJob.cancel()
-
- rotaryHaptics.handleScrollHaptic(event.delta)
- debugLog { "Rotary scroll distance: $rotaryScrollDistance" }
-
- previousScrollEventTime = time
- scrollJob = coroutineScope.async { scrollBehavior.handleEvent(rotaryScrollDistance) }
-
- flingJob =
- coroutineScope.async {
- rotaryFlingBehavior?.trackFling(
- beforeFling = {
- debugLog { "Calling before fling section" }
- scrollJob.cancel()
- scrollBehavior = scrollBehaviorFactory()
- },
- )
- }
- }
-
- private fun isNewScrollEvent(timestamp: Long): Boolean {
- val timeDelta = timestamp - previousScrollEventTime
- return previousScrollEventTime == 0L || timeDelta > gestureThresholdTime
- }
-
- private fun resetTracking(timestamp: Long) {
- scrollBehavior = scrollBehaviorFactory()
- debugLog { "Velocity tracker reset" }
- rotaryFlingBehavior = rotaryFlingBehaviorFactory()
- rotaryFlingBehavior?.startFlingTracking(timestamp)
- }
-}
-
-/**
- * A scroll handler for RSB(high-res) with snapping and without fling Snapping happens after a
- * threshold is reached ( set in [RotarySnapBehavior])
- *
- * This scroll handler doesn't support fling.
- */
-internal class HighResSnapHandler(
- private val resistanceFactor: Float,
- private val thresholdBehaviorFactory: () -> ThresholdBehavior,
- private val snapBehaviourFactory: () -> RotarySnapBehavior,
- private val scrollBehaviourFactory: () -> RotaryScrollBehavior,
-) : RotaryScrollHandler {
- private val gestureThresholdTime = 200L
- private val snapDelay = 100L
- private val maxSnapsPerEvent = 2
-
- private var scrollJob: Job = CompletableDeferred<Unit>()
- private var snapJob: Job = CompletableDeferred<Unit>()
-
- private var previousScrollEventTime = 0L
- private var snapAccumulator = 0f
- private var rotaryScrollDistance = 0f
- private var scrollInProgress = false
-
- private var snapBehaviour = snapBehaviourFactory()
- private var scrollBehaviour = scrollBehaviourFactory()
- private var thresholdBehavior = thresholdBehaviorFactory()
-
- private val scrollEasing: Easing = CubicBezierEasing(0.0f, 0.0f, 0.5f, 1.0f)
-
- override suspend fun handleScrollEvent(
- coroutineScope: CoroutineScope,
- event: TimestampedDelta,
- rotaryHaptics: RotaryHapticHandler,
- ) {
- val time = event.timestamp
-
- if (isNewScrollEvent(time)) {
- debugLog { "New scroll event" }
- resetTracking()
- snapJob.cancel()
- snapBehaviour = snapBehaviourFactory()
- scrollBehaviour = scrollBehaviourFactory()
- thresholdBehavior = thresholdBehaviorFactory()
- thresholdBehavior.startThresholdTracking(time)
- snapAccumulator = 0f
- rotaryScrollDistance = 0f
- }
-
- if (!isOppositeValueAfterScroll(event.delta)) {
- thresholdBehavior.observeEvent(event.timestamp, event.delta)
- } else {
- debugLog { "Opposite value after scroll :${event.delta}" }
- }
-
- thresholdBehavior.applySmoothing()
- val snapThreshold = thresholdBehavior.snapThreshold()
-
- snapAccumulator += event.delta
- if (!snapJob.isActive) {
- val resistanceCoeff =
- 1 - scrollEasing.transform(rotaryScrollDistance.absoluteValue / snapThreshold)
- rotaryScrollDistance += event.delta * resistanceCoeff
- }
-
- debugLog { "Snap accumulator: $snapAccumulator" }
- debugLog { "Rotary scroll distance: $rotaryScrollDistance" }
-
- debugLog { "snapThreshold: $snapThreshold" }
- previousScrollEventTime = time
-
- if (abs(snapAccumulator) > snapThreshold) {
- scrollInProgress = false
- scrollBehaviour = scrollBehaviourFactory()
- scrollJob.cancel()
-
- val snapDistance =
- (snapAccumulator / snapThreshold)
- .toInt()
- .coerceIn(-maxSnapsPerEvent..maxSnapsPerEvent)
- snapAccumulator -= snapThreshold * snapDistance
- val sequentialSnap = snapJob.isActive
-
- debugLog {
- "Snap threshold reached: snapDistance:$snapDistance, " +
- "sequentialSnap: $sequentialSnap, " +
- "snap accumulator remaining: $snapAccumulator"
- }
- if (
- (!snapBehaviour.topEdgeReached() && snapDistance < 0) ||
- (!snapBehaviour.bottomEdgeReached() && snapDistance > 0)
- ) {
- rotaryHaptics.handleSnapHaptic(event.delta)
- }
-
- snapBehaviour.prepareSnapForItems(snapDistance, sequentialSnap)
- if (!snapJob.isActive) {
- snapJob.cancel()
- snapJob =
- coroutineScope.async {
- debugLog { "Snap started" }
- try {
- snapBehaviour.snapToTargetItem()
- } finally {
- debugLog { "Snap called finally" }
- }
- }
- }
- rotaryScrollDistance = 0f
- } else {
- if (!snapJob.isActive) {
- scrollJob.cancel()
- debugLog { "Scrolling for $rotaryScrollDistance/$resistanceFactor px" }
- scrollJob =
- coroutineScope.async {
- scrollBehaviour.handleEvent(rotaryScrollDistance / resistanceFactor)
- }
- delay(snapDelay)
- scrollInProgress = false
- scrollBehaviour = scrollBehaviourFactory()
- rotaryScrollDistance = 0f
- snapAccumulator = 0f
- snapBehaviour.prepareSnapForItems(0, false)
-
- snapJob.cancel()
- snapJob = coroutineScope.async { snapBehaviour.snapToClosestItem() }
- }
- }
- }
-
- private fun isOppositeValueAfterScroll(delta: Float): Boolean =
- sign(rotaryScrollDistance) * sign(delta) == -1f && (abs(delta) < abs(rotaryScrollDistance))
-
- private fun isNewScrollEvent(timestamp: Long): Boolean {
- val timeDelta = timestamp - previousScrollEventTime
- return previousScrollEventTime == 0L || timeDelta > gestureThresholdTime
- }
-
- private fun resetTracking() {
- scrollInProgress = true
- }
-}
-
-/**
- * A scroll handler for RSB(high-res) with snapping and without fling Snapping happens after a
- * threshold is reached ( set in [RotarySnapBehavior])
- *
- * This scroll handler doesn't support fling.
- */
-internal class LowResSnapHandler(
- private val snapBehaviourFactory: () -> RotarySnapBehavior,
-) : RotaryScrollHandler {
- private val gestureThresholdTime = 200L
-
- private var snapJob: Job = CompletableDeferred<Unit>()
-
- private var previousScrollEventTime = 0L
- private var snapAccumulator = 0f
- private var scrollInProgress = false
-
- private var snapBehaviour = snapBehaviourFactory()
-
- override suspend fun handleScrollEvent(
- coroutineScope: CoroutineScope,
- event: TimestampedDelta,
- rotaryHaptics: RotaryHapticHandler,
- ) {
- val time = event.timestamp
-
- if (isNewScrollEvent(time)) {
- debugLog { "New scroll event" }
- resetTracking()
- snapJob.cancel()
- snapBehaviour = snapBehaviourFactory()
- snapAccumulator = 0f
- }
-
- snapAccumulator += event.delta
-
- debugLog { "Snap accumulator: $snapAccumulator" }
-
- previousScrollEventTime = time
-
- if (abs(snapAccumulator) > 1f) {
- scrollInProgress = false
-
- val snapDistance = sign(snapAccumulator).toInt()
- rotaryHaptics.handleSnapHaptic(event.delta)
- val sequentialSnap = snapJob.isActive
- debugLog {
- "Snap threshold reached: snapDistance:$snapDistance, " +
- "sequentialSnap: $sequentialSnap, " +
- "snap accumulator: $snapAccumulator"
- }
-
- snapBehaviour.prepareSnapForItems(snapDistance, sequentialSnap)
- if (!snapJob.isActive) {
- snapJob.cancel()
- snapJob =
- coroutineScope.async {
- debugLog { "Snap started" }
- try {
- snapBehaviour.snapToTargetItem()
- } finally {
- debugLog { "Snap called finally" }
- }
- }
- }
- snapAccumulator = 0f
- }
- }
-
- private fun isNewScrollEvent(timestamp: Long): Boolean {
- val timeDelta = timestamp - previousScrollEventTime
- return previousScrollEventTime == 0L || timeDelta > gestureThresholdTime
- }
-
- private fun resetTracking() {
- scrollInProgress = true
- }
-}
-
-internal class ThresholdBehavior(
- private val rotaryScrollAdapter: RotaryScrollAdapter,
- private val thresholdDivider: Float,
- private val minVelocity: Float = 300f,
- private val maxVelocity: Float = 3000f,
- private val smoothingConstant: Float = 0.4f,
-) {
- private val thresholdDividerEasing: Easing = CubicBezierEasing(0.5f, 0.0f, 0.5f, 1.0f)
-
- private val rotaryVelocityTracker = RotaryVelocityTracker()
-
- private var smoothedVelocity = 0f
-
- fun startThresholdTracking(time: Long) {
- rotaryVelocityTracker.start(time)
- smoothedVelocity = 0f
- }
-
- fun observeEvent(timestamp: Long, delta: Float) {
- rotaryVelocityTracker.move(timestamp, delta)
- }
-
- fun applySmoothing() {
- if (rotaryVelocityTracker.velocity != 0.0f) {
- // smooth the velocity
- smoothedVelocity =
- exponentialSmoothing(
- currentVelocity = rotaryVelocityTracker.velocity.absoluteValue,
- prevVelocity = smoothedVelocity,
- smoothingConstant = smoothingConstant,
- )
- }
- debugLog { "rotaryVelocityTracker velocity: ${rotaryVelocityTracker.velocity}" }
- debugLog { "SmoothedVelocity: $smoothedVelocity" }
- }
-
- fun snapThreshold(): Float {
- val thresholdDividerFraction =
- thresholdDividerEasing.transform(
- inverseLerp(
- minVelocity,
- maxVelocity,
- smoothedVelocity,
- ),
- )
- return rotaryScrollAdapter.averageItemSize() /
- lerp(
- 1f,
- thresholdDivider,
- thresholdDividerFraction,
- )
- }
-
- private fun exponentialSmoothing(
- currentVelocity: Float,
- prevVelocity: Float,
- smoothingConstant: Float,
- ): Float = smoothingConstant * currentVelocity + (1 - smoothingConstant) * prevVelocity
-}
-
-private data class RotaryHandlerElement(
- private val rotaryScrollHandler: RotaryScrollHandler,
- private val reverseDirection: Boolean,
- private val rotaryHaptics: RotaryHapticHandler,
- private val inspectorInfo: InspectorInfo.() -> Unit,
-) : ModifierNodeElement<RotaryInputNode>() {
- override fun create(): RotaryInputNode =
- RotaryInputNode(
- rotaryScrollHandler,
- reverseDirection,
- rotaryHaptics,
- )
-
- override fun update(node: RotaryInputNode) {
- debugLog { "Update launched!" }
- node.rotaryScrollHandler = rotaryScrollHandler
- node.reverseDirection = reverseDirection
- node.rotaryHaptics = rotaryHaptics
- }
-
- override fun InspectorInfo.inspectableProperties() {
- inspectorInfo()
- }
-
- override fun equals(other: Any?): Boolean {
- if (this === other) return true
- if (other == null || this::class != other::class) return false
-
- other as RotaryHandlerElement
-
- if (rotaryScrollHandler != other.rotaryScrollHandler) return false
- if (reverseDirection != other.reverseDirection) return false
- if (rotaryHaptics != other.rotaryHaptics) return false
- if (inspectorInfo != other.inspectorInfo) return false
-
- return true
- }
-
- override fun hashCode(): Int {
- var result = rotaryScrollHandler.hashCode()
- result = 31 * result + reverseDirection.hashCode()
- result = 31 * result + rotaryHaptics.hashCode()
- result = 31 * result + inspectorInfo.hashCode()
- return result
- }
-}
-
-private class RotaryInputNode(
- var rotaryScrollHandler: RotaryScrollHandler,
- var reverseDirection: Boolean,
- var rotaryHaptics: RotaryHapticHandler,
-) : RotaryInputModifierNode, Modifier.Node() {
-
- val channel = Channel<TimestampedDelta>(capacity = Channel.CONFLATED)
- val flow = channel.receiveAsFlow()
-
- override fun onAttach() {
- coroutineScope.launch {
- flow.collectLatest {
- debugLog {
- "Scroll event received: " + "delta:${it.delta}, timestamp:${it.timestamp}"
- }
- rotaryScrollHandler.handleScrollEvent(this, it, rotaryHaptics)
- }
- }
- }
-
- override fun onRotaryScrollEvent(event: RotaryScrollEvent): Boolean = false
-
- override fun onPreRotaryScrollEvent(event: RotaryScrollEvent): Boolean {
- debugLog { "onPreRotaryScrollEvent" }
- channel.trySend(
- TimestampedDelta(
- event.uptimeMillis,
- event.verticalScrollPixels * if (reverseDirection) -1f else 1f,
- ),
- )
- return true
- }
-}
-
-private fun inverseLerp(start: Float, stop: Float, value: Float): Float {
- return ((value - start) / (stop - start)).coerceIn(0f, 1f)
-}
-
-/** Debug logging that can be enabled. */
-private const val DEBUG = false
-
-private inline fun debugLog(generateMsg: () -> String) {
- if (DEBUG) {
- println("RotaryScroll: ${generateMsg()}")
- }
-}
diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/rotaryinput/RotaryVelocityTracker.kt b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/rotaryinput/RotaryVelocityTracker.kt
deleted file mode 100644
index 1719ecef3..000000000
--- a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/rotaryinput/RotaryVelocityTracker.kt
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
- * 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.rotaryinput
-
-import androidx.compose.ui.input.pointer.util.VelocityTracker1D
-
-// This file is a copy of RotaryVelocityTracker.kt from Horologist (go/horologist),
-// remove it once Wear Compose 1.4 is landed (b/325560444).
-
-/** A wrapper around VelocityTracker1D to provide support for rotary input. */
-class RotaryVelocityTracker {
- private var velocityTracker: VelocityTracker1D = VelocityTracker1D(true)
-
- /** Retrieve the last computed velocity. */
- val velocity: Float
- get() = velocityTracker.calculateVelocity()
-
- /** Start tracking motion. */
- fun start(currentTime: Long) {
- velocityTracker.resetTracking()
- velocityTracker.addDataPoint(currentTime, 0f)
- }
-
- /** Continue tracking motion as the input rotates. */
- fun move(currentTime: Long, delta: Float) {
- velocityTracker.addDataPoint(currentTime, delta)
- }
-
- /** Stop tracking motion. */
- fun end() {
- velocityTracker.resetTracking()
- }
-}
diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/theme/ResourceHelper.kt b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/theme/ResourceHelper.kt
index c7ed0958c..2a40a625f 100644
--- a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/theme/ResourceHelper.kt
+++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/theme/ResourceHelper.kt
@@ -27,11 +27,35 @@ internal object ResourceHelper {
private const val MATERIAL3_ENABLED_SYSPROP = "persist.cw_build.bluechip.enabled"
- val material3Enabled: Boolean
+ /* This controls in app permission controller experience. */
+ private val material3Enabled: Boolean
get() {
return SystemProperties.getBoolean(MATERIAL3_ENABLED_SYSPROP, false)
}
+ val materialUIVersionInApp: WearPermissionMaterialUIVersion =
+ if (material3Enabled) {
+ WearPermissionMaterialUIVersion.MATERIAL3
+ } else {
+ WearPermissionMaterialUIVersion.MATERIAL2_5
+ }
+
+ /*
+ This is to control the permission controller screens in settings.
+ Currently it is set as false. We will either use the flag or a common property from settings
+ based on settings implementation when we are ready" */
+ private val material3EnabledInSettings: Boolean
+ get() {
+ return false
+ }
+
+ val materialUIVersionInSettings: WearPermissionMaterialUIVersion =
+ if (material3EnabledInSettings) {
+ WearPermissionMaterialUIVersion.MATERIAL3
+ } else {
+ WearPermissionMaterialUIVersion.MATERIAL2_5
+ }
+
@DoNotInline
fun getColor(context: Context, @ColorRes id: Int): Color? {
return try {
diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/theme/WearPermissionTheme.kt b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/theme/WearPermissionTheme.kt
index 8823bee07..736d543a3 100644
--- a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/theme/WearPermissionTheme.kt
+++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/theme/WearPermissionTheme.kt
@@ -53,9 +53,6 @@ fun WearPermissionTheme(
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.VANILLA_ICE_CREAM) {
WearPermissionLegacyTheme(content)
} else {
- // Whether we are ready to use material3 for any screen.
- val useBridgedTheme = ResourceHelper.material3Enabled
-
// Material3 UI controls are still being used in the screen that the theme is applied
if (version == MATERIAL3) {
val material3Theme = WearOverlayableMaterial3Theme(LocalContext.current)
@@ -70,7 +67,7 @@ fun WearPermissionTheme(
// But some in-app screens(like permission grant screen) are migrated to material3.
// To avoid having two set of overlay resources, we will use material3 overlay resources to
// support material2_5 UI controls as well.
- else if (version == MATERIAL2_5 && useBridgedTheme) {
+ else if (version == MATERIAL2_5 && ResourceHelper.materialUIVersionInApp == MATERIAL3) {
val material3Theme = WearOverlayableMaterial3Theme(LocalContext.current)
val bridgedLegacyTheme = WearMaterialBridgedLegacyTheme.createFrom(material3Theme)
MaterialTheme(
diff --git a/PermissionController/src/com/android/permissioncontroller/permission/utils/AndroidUtils.kt b/PermissionController/src/com/android/permissioncontroller/permission/utils/AndroidUtils.kt
index a5f78aa53..081a467bd 100644
--- a/PermissionController/src/com/android/permissioncontroller/permission/utils/AndroidUtils.kt
+++ b/PermissionController/src/com/android/permissioncontroller/permission/utils/AndroidUtils.kt
@@ -24,8 +24,8 @@ import android.content.ContextWrapper
import android.content.pm.ComponentInfo
import android.content.pm.PackageManager
import android.content.pm.ResolveInfo
-import android.os.Looper
import android.os.UserHandle
+import androidx.arch.core.executor.ArchTaskExecutor
import java.util.concurrent.Executors
import kotlinx.coroutines.asCoroutineDispatcher
@@ -51,8 +51,8 @@ val IPC = Executors.newFixedThreadPool(IPC_THREAD_POOL_COUNT).asCoroutineDispatc
/** Assert that an operation is running on main thread */
fun ensureMainThread() =
- check(Looper.myLooper() == Looper.getMainLooper()) {
- "Only meant to be used on the main thread"
+ check(ArchTaskExecutor.getInstance().isMainThread) {
+ ("Only meant to be used on the main thread, current thread is " + Thread.currentThread())
}
/** A more readable version of [PackageManager.updatePermissionFlags] */
@@ -72,5 +72,7 @@ fun PackageManager.updatePermissionFlags(
val ResolveInfo.componentInfo: ComponentInfo
get() {
return (activityInfo as ComponentInfo?)
- ?: serviceInfo ?: providerInfo ?: throw IllegalStateException("Missing ComponentInfo!")
+ ?: serviceInfo
+ ?: providerInfo
+ ?: throw IllegalStateException("Missing ComponentInfo!")
}
diff --git a/PermissionController/src/com/android/permissioncontroller/permission/utils/PermissionMapping.kt b/PermissionController/src/com/android/permissioncontroller/permission/utils/PermissionMapping.kt
index a3446f802..13e3a4eb7 100644
--- a/PermissionController/src/com/android/permissioncontroller/permission/utils/PermissionMapping.kt
+++ b/PermissionController/src/com/android/permissioncontroller/permission/utils/PermissionMapping.kt
@@ -181,13 +181,18 @@ object PermissionMapping {
Manifest.permission_group.CAMERA
}
- PLATFORM_PERMISSIONS[Manifest.permission.BODY_SENSORS] = Manifest.permission_group.SENSORS
-
if (SdkLevel.isAtLeastT()) {
PLATFORM_PERMISSIONS[Manifest.permission.POST_NOTIFICATIONS] =
Manifest.permission_group.NOTIFICATIONS
- PLATFORM_PERMISSIONS[Manifest.permission.BODY_SENSORS_BACKGROUND] =
+ }
+
+ if (!Flags.replaceBodySensorPermissionEnabled()) {
+ PLATFORM_PERMISSIONS[Manifest.permission.BODY_SENSORS] =
Manifest.permission_group.SENSORS
+ if (SdkLevel.isAtLeastT()) {
+ PLATFORM_PERMISSIONS[Manifest.permission.BODY_SENSORS_BACKGROUND] =
+ Manifest.permission_group.SENSORS
+ }
}
for ((permission, permissionGroup) in PLATFORM_PERMISSIONS) {
@@ -343,7 +348,7 @@ object PermissionMapping {
val appSupportsPickerPrompt =
group.permissions[Manifest.permission.READ_MEDIA_VISUAL_USER_SELECTED]?.isImplicit ==
- false
+ false
return if (appSupportsPickerPrompt) {
PARTIAL_MEDIA_PERMISSIONS
diff --git a/PermissionController/src/com/android/permissioncontroller/permission/utils/Utils.java b/PermissionController/src/com/android/permissioncontroller/permission/utils/Utils.java
index 3d3b47272..aae5cb82c 100644
--- a/PermissionController/src/com/android/permissioncontroller/permission/utils/Utils.java
+++ b/PermissionController/src/com/android/permissioncontroller/permission/utils/Utils.java
@@ -547,8 +547,14 @@ public final class Utils {
if (group.equals(Manifest.permission_group.UNDEFINED)) {
List<PermissionInfo> undefinedPerms = new ArrayList<>();
for (PermissionInfo permissionInfo : installedRuntime) {
+ if (Flags.replaceBodySensorPermissionEnabled()
+ && (permissionInfo.name.equals(Manifest.permission.BODY_SENSORS) ||
+ permissionInfo.name.equals(Manifest.permission.BODY_SENSORS_BACKGROUND))) {
+ continue;
+ }
+
String permGroup =
- PermissionMapping.getGroupOfPlatformPermission(permissionInfo.name);
+ PermissionMapping.getGroupOfPlatformPermission(permissionInfo.name);
if (permGroup == null || permGroup.equals(Manifest.permission_group.UNDEFINED)) {
undefinedPerms.add(permissionInfo);
}
diff --git a/PermissionController/src/com/android/permissioncontroller/privacysources/AccessibilitySourceService.kt b/PermissionController/src/com/android/permissioncontroller/privacysources/AccessibilitySourceService.kt
index c633c013a..1610901bc 100644
--- a/PermissionController/src/com/android/permissioncontroller/privacysources/AccessibilitySourceService.kt
+++ b/PermissionController/src/com/android/permissioncontroller/privacysources/AccessibilitySourceService.kt
@@ -48,7 +48,6 @@ import androidx.annotation.GuardedBy
import androidx.annotation.RequiresApi
import androidx.annotation.VisibleForTesting
import androidx.annotation.WorkerThread
-import androidx.core.util.Preconditions
import com.android.modules.utils.build.SdkLevel
import com.android.permissioncontroller.Constants
import com.android.permissioncontroller.PermissionControllerStatsLog
@@ -712,7 +711,7 @@ class AccessibilityPackageResetHandler : BroadcastReceiver() {
return
}
- val data = Preconditions.checkNotNull(intent.data)
+ val data = requireNotNull(intent.data)
val coroutineScope = CoroutineScope(Dispatchers.Default + SupervisorJob())
coroutineScope.launch(Dispatchers.Default) {
if (DEBUG) {
diff --git a/PermissionController/src/com/android/permissioncontroller/privacysources/NotificationListenerCheck.kt b/PermissionController/src/com/android/permissioncontroller/privacysources/NotificationListenerCheck.kt
index 43b3edc04..58a6f1bc4 100644
--- a/PermissionController/src/com/android/permissioncontroller/privacysources/NotificationListenerCheck.kt
+++ b/PermissionController/src/com/android/permissioncontroller/privacysources/NotificationListenerCheck.kt
@@ -57,7 +57,6 @@ import androidx.annotation.GuardedBy
import androidx.annotation.RequiresApi
import androidx.annotation.VisibleForTesting
import androidx.annotation.WorkerThread
-import androidx.core.util.Preconditions
import com.android.modules.utils.build.SdkLevel
import com.android.permissioncontroller.Constants
import com.android.permissioncontroller.Constants.KEY_LAST_NOTIFICATION_LISTENER_NOTIFICATION_SHOWN
@@ -1146,7 +1145,7 @@ class NotificationListenerPackageResetHandler : BroadcastReceiver() {
return
}
- val data = Preconditions.checkNotNull(intent.data)
+ val data = requireNotNull(intent.data)
val pkg: String = data.schemeSpecificPart
if (DEBUG) Log.i(TAG, "Reset $pkg")
diff --git a/PermissionController/src/com/android/permissioncontroller/role/TEST_MAPPING b/PermissionController/src/com/android/permissioncontroller/role/TEST_MAPPING
index 93ad3d31b..83b513c04 100644
--- a/PermissionController/src/com/android/permissioncontroller/role/TEST_MAPPING
+++ b/PermissionController/src/com/android/permissioncontroller/role/TEST_MAPPING
@@ -7,6 +7,9 @@
"exclude-annotation": "androidx.test.filters.FlakyTest"
}
]
+ },
+ {
+ "name": "CtsRoleMultiUserTestCases"
}
],
"mainline-presubmit": [
@@ -24,6 +27,9 @@
"exclude-annotation": "androidx.test.filters.FlakyTest"
}
]
+ },
+ {
+ "name": "CtsRoleMultiUserTestCases[com.google.android.permission.apex]"
}
],
"permission-mainline-presubmit": [
@@ -41,6 +47,9 @@
"exclude-annotation": "androidx.test.filters.FlakyTest"
}
]
+ },
+ {
+ "name": "CtsRoleMultiUserTestCases"
}
],
"postsubmit": [
diff --git a/PermissionController/src/com/android/permissioncontroller/role/ui/wear/WearDefaultAppListScreen.kt b/PermissionController/src/com/android/permissioncontroller/role/ui/wear/WearDefaultAppListScreen.kt
index afee50389..c322b2bef 100644
--- a/PermissionController/src/com/android/permissioncontroller/role/ui/wear/WearDefaultAppListScreen.kt
+++ b/PermissionController/src/com/android/permissioncontroller/role/ui/wear/WearDefaultAppListScreen.kt
@@ -29,10 +29,10 @@ import androidx.compose.ui.res.stringResource
import androidx.lifecycle.LiveData
import androidx.wear.compose.material.Text
import com.android.permissioncontroller.R
-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.chipDefaultColors
-import com.android.permissioncontroller.permission.ui.wear.elements.chipDisabledColors
+import com.android.permissioncontroller.permission.ui.wear.elements.material2.Chip
+import com.android.permissioncontroller.permission.ui.wear.elements.material2.chipDefaultColors
+import com.android.permissioncontroller.permission.ui.wear.elements.material2.chipDisabledColors
import com.android.permissioncontroller.role.ui.RoleItem
@Composable
@@ -65,7 +65,7 @@ fun WearDefaultAppListScreen(
onClick = pref.getOnClicked(),
modifier = Modifier.fillMaxWidth(),
labelMaxLines = Int.MAX_VALUE,
- secondaryLabelMaxLines = Integer.MAX_VALUE
+ secondaryLabelMaxLines = Integer.MAX_VALUE,
)
}
}
diff --git a/PermissionController/src/com/android/permissioncontroller/role/ui/wear/WearDefaultAppScreen.kt b/PermissionController/src/com/android/permissioncontroller/role/ui/wear/WearDefaultAppScreen.kt
index 5d4233c6e..50b109248 100644
--- a/PermissionController/src/com/android/permissioncontroller/role/ui/wear/WearDefaultAppScreen.kt
+++ b/PermissionController/src/com/android/permissioncontroller/role/ui/wear/WearDefaultAppScreen.kt
@@ -25,14 +25,16 @@ import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
-import androidx.wear.compose.foundation.lazy.rememberScalingLazyListState
import androidx.wear.compose.material.ToggleChipDefaults
-import com.android.permissioncontroller.permission.ui.wear.elements.AlertDialog
-import com.android.permissioncontroller.permission.ui.wear.elements.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.toggleChipDisabledColors
+import com.android.permissioncontroller.permission.ui.wear.elements.material2.DialogButtonContent
+import com.android.permissioncontroller.permission.ui.wear.elements.material2.ListFooter
+import com.android.permissioncontroller.permission.ui.wear.elements.material2.ToggleChip
+import com.android.permissioncontroller.permission.ui.wear.elements.material2.ToggleChipToggleControl
+import com.android.permissioncontroller.permission.ui.wear.elements.material2.toggleChipDisabledColors
+import com.android.permissioncontroller.permission.ui.wear.elements.material3.WearPermissionConfirmationDialog
+import com.android.permissioncontroller.permission.ui.wear.theme.ResourceHelper
+import com.android.permissioncontroller.permission.ui.wear.theme.WearPermissionMaterialUIVersion
import com.android.permissioncontroller.role.ui.wear.model.ConfirmDialogArgs
@Composable
@@ -41,11 +43,13 @@ fun WearDefaultAppScreen(helper: WearDefaultAppHelper) {
val showConfirmDialog =
helper.confirmDialogViewModel.showConfirmDialogLiveData.observeAsState(false)
var isLoading by remember { mutableStateOf(true) }
+ val materialUIVersion = ResourceHelper.materialUIVersionInSettings
Box {
WearDefaultAppContent(isLoading, roleLiveData.value, helper)
ConfirmDialog(
+ materialUIVersion = materialUIVersion,
showDialog = showConfirmDialog.value,
- args = helper.confirmDialogViewModel.confirmDialogArgs
+ args = helper.confirmDialogViewModel.confirmDialogArgs,
)
}
if (isLoading && roleLiveData.value.isNotEmpty()) {
@@ -57,7 +61,7 @@ fun WearDefaultAppScreen(helper: WearDefaultAppHelper) {
private fun WearDefaultAppContent(
isLoading: Boolean,
qualifyingApplications: List<Pair<ApplicationInfo, Boolean>>,
- helper: WearDefaultAppHelper
+ helper: WearDefaultAppHelper,
) {
ScrollableScreen(title = helper.getTitle(), isLoading = isLoading) {
helper.getNonePreference(qualifyingApplications)?.let {
@@ -68,7 +72,7 @@ private fun WearDefaultAppContent(
checked = it.checked,
onCheckedChanged = it.onDefaultCheckChanged,
toggleControl = ToggleChipToggleControl.Radio,
- labelMaxLine = Integer.MAX_VALUE
+ labelMaxLine = Integer.MAX_VALUE,
)
}
}
@@ -88,7 +92,7 @@ private fun WearDefaultAppContent(
onCheckedChanged = pref.getOnCheckChanged(),
toggleControl = ToggleChipToggleControl.Radio,
labelMaxLine = Integer.MAX_VALUE,
- secondaryLabelMaxLine = Integer.MAX_VALUE
+ secondaryLabelMaxLine = Integer.MAX_VALUE,
)
}
}
@@ -98,14 +102,18 @@ private fun WearDefaultAppContent(
}
@Composable
-private fun ConfirmDialog(showDialog: Boolean, args: ConfirmDialogArgs?) {
- args?.let {
- AlertDialog(
- showDialog = showDialog,
- message = it.message,
- onOKButtonClick = it.onOkButtonClick,
- onCancelButtonClick = it.onCancelButtonClick,
- scalingLazyListState = rememberScalingLazyListState()
+private fun ConfirmDialog(
+ materialUIVersion: WearPermissionMaterialUIVersion,
+ showDialog: Boolean,
+ args: ConfirmDialogArgs?,
+) {
+ args?.run {
+ WearPermissionConfirmationDialog(
+ materialUIVersion = materialUIVersion,
+ show = showDialog,
+ message = message,
+ positiveButtonContent = DialogButtonContent(onClick = onOkButtonClick),
+ negativeButtonContent = DialogButtonContent(onClick = onCancelButtonClick),
)
}
}
diff --git a/PermissionController/src/com/android/permissioncontroller/role/ui/wear/WearRequestRoleScreen.kt b/PermissionController/src/com/android/permissioncontroller/role/ui/wear/WearRequestRoleScreen.kt
index fcc0d56f9..f891fc25f 100644
--- a/PermissionController/src/com/android/permissioncontroller/role/ui/wear/WearRequestRoleScreen.kt
+++ b/PermissionController/src/com/android/permissioncontroller/role/ui/wear/WearRequestRoleScreen.kt
@@ -32,7 +32,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.android.permissioncontroller.R
import com.android.permissioncontroller.permission.ui.wear.elements.ScrollableScreen
-import com.android.permissioncontroller.permission.ui.wear.elements.ToggleChipToggleControl
+import com.android.permissioncontroller.permission.ui.wear.elements.material2.ToggleChipToggleControl
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
@@ -41,8 +41,6 @@ import com.android.permissioncontroller.permission.ui.wear.elements.material3.We
import com.android.permissioncontroller.permission.ui.wear.elements.material3.WearPermissionToggleControlStyle
import com.android.permissioncontroller.permission.ui.wear.theme.ResourceHelper
import com.android.permissioncontroller.permission.ui.wear.theme.WearPermissionMaterialUIVersion
-import com.android.permissioncontroller.permission.ui.wear.theme.WearPermissionMaterialUIVersion.MATERIAL2_5
-import com.android.permissioncontroller.permission.ui.wear.theme.WearPermissionMaterialUIVersion.MATERIAL3
import com.android.permissioncontroller.role.UserPackage
import com.android.permissioncontroller.role.ui.ManageRoleHolderStateLiveData
@@ -80,14 +78,8 @@ fun WearRequestRoleScreen(
helper.initializeSelectedPackage()
}
}
- val materialUIVersion =
- if (ResourceHelper.material3Enabled) {
- MATERIAL3
- } else {
- MATERIAL2_5
- }
WearRequestRoleContent(
- materialUIVersion,
+ ResourceHelper.materialUIVersionInApp,
isLoading,
helper,
roleLiveData.value,
diff --git a/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/SafetyStatusPreference.java b/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/SafetyStatusPreference.java
index abf159955..0bef71b3e 100644
--- a/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/SafetyStatusPreference.java
+++ b/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/SafetyStatusPreference.java
@@ -29,7 +29,6 @@ import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.Log;
import android.widget.ImageView;
-import android.widget.TextView;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
@@ -42,9 +41,6 @@ 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;
-
-import java.util.List;
import java.util.Objects;
/** Preference which displays a visual representation of {@link SafetyCenterStatus}. */
@@ -54,25 +50,16 @@ public class SafetyStatusPreference extends Preference
private static final String TAG = "SafetyStatusPreference";
+ private final SafetyStatusAnimationSequencer mSequencer = new SafetyStatusAnimationSequencer();
+
@Nullable private StatusUiData mStatus;
@Nullable private SafetyCenterViewModel mViewModel;
- private final TextFadeAnimator mTitleTextAnimator = new TextFadeAnimator(R.id.status_title);
-
- private final TextFadeAnimator mSummaryTextAnimator = new TextFadeAnimator(R.id.status_summary);
-
- private final TextFadeAnimator mAllTextAnimator =
- new TextFadeAnimator(List.of(R.id.status_title, R.id.status_summary));
-
- private boolean mFirstBind = true;
-
public SafetyStatusPreference(Context context, AttributeSet attrs) {
super(context, attrs);
setLayoutResource(R.layout.preference_safety_status);
}
- private boolean mIsTextChangeAnimationRunning;
- private final SafetyStatusAnimationSequencer mSequencer = new SafetyStatusAnimationSequencer();
@Override
public void onBindViewHolder(PreferenceViewHolder holder) {
@@ -93,9 +80,7 @@ public class SafetyStatusPreference extends Preference
updateStatusIcon(statusCardView);
- updateStatusText(statusCardView.getTitleView(), statusCardView.getSummaryView());
-
- mFirstBind = false;
+ statusCardView.showText(mStatus);
}
private void configureButtons(Context context, StatusCardView statusCardView) {
@@ -125,14 +110,6 @@ public class SafetyStatusPreference extends Preference
statusCardView.showButtons(mStatus);
}
- private void updateStatusText(TextView title, TextView summary) {
- if (mFirstBind) {
- title.setText(mStatus.getTitle());
- summary.setText(mStatus.getSummary(getContext()));
- }
- runTextAnimationIfNeeded(title, summary);
- }
-
private void updateStatusIcon(StatusCardView statusCardView) {
int severityLevel = mStatus.getSeverityLevel();
boolean isRefreshing = mStatus.isRefreshInProgress();
@@ -143,33 +120,6 @@ public class SafetyStatusPreference extends Preference
/* scanningAnimation= */ null);
}
- private void runTextAnimationIfNeeded(TextView titleView, TextView summaryView) {
- if (mIsTextChangeAnimationRunning) {
- return;
- }
- Log.v(TAG, "Starting status text animation");
- String titleText = mStatus.getTitle().toString();
- String summaryText = mStatus.getSummary(getContext()).toString();
- boolean titleEquals = titleView.getText().toString().equals(titleText);
- boolean summaryEquals = summaryView.getText().toString().equals(summaryText);
- Runnable onFinish =
- () -> {
- Log.v(TAG, "Finishing status text animation");
- mIsTextChangeAnimationRunning = false;
- runTextAnimationIfNeeded(titleView, summaryView);
- };
- mIsTextChangeAnimationRunning = !titleEquals || !summaryEquals;
- if (!titleEquals && !summaryEquals) {
- Pair<TextView, String> titleChange = new Pair<>(titleView, titleText);
- Pair<TextView, String> summaryChange = new Pair<>(summaryView, summaryText);
- mAllTextAnimator.animateChangeText(List.of(titleChange, summaryChange), onFinish);
- } else if (!titleEquals) {
- mTitleTextAnimator.animateChangeText(titleView, titleText, onFinish);
- } else if (!summaryEquals) {
- mSummaryTextAnimator.animateChangeText(summaryView, summaryText, onFinish);
- }
- }
-
private void startScanningAnimation(StatusCardView statusCardView) {
mSequencer.onStartScanningAnimationStart();
ImageView statusImage = statusCardView.getStatusImageView();
diff --git a/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/view/StatusCardView.kt b/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/view/StatusCardView.kt
index 6a415c563..bb417104d 100644
--- a/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/view/StatusCardView.kt
+++ b/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/view/StatusCardView.kt
@@ -21,6 +21,7 @@ import android.os.Build
import android.util.AttributeSet
import android.widget.ImageView
import android.widget.LinearLayout
+import android.widget.TextSwitcher
import android.widget.TextView
import androidx.annotation.RequiresApi
import androidx.constraintlayout.widget.ConstraintLayout
@@ -35,7 +36,7 @@ constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0,
- defStyleRes: Int = 0
+ defStyleRes: Int = 0,
) : ConstraintLayout(context, attrs, defStyleAttr, defStyleRes) {
init {
@@ -44,11 +45,29 @@ constructor(
val statusImageView: ImageView by lazyView(R.id.status_image)
val titleAndSummaryContainerView: LinearLayout by lazyView(R.id.status_title_and_summary)
- val titleView: TextView by lazyView(R.id.status_title)
- val summaryView: TextView by lazyView(R.id.status_summary)
+ private val titleView: TextSwitcher by lazyView(R.id.status_title)
+ private val summaryView: TextSwitcher by lazyView(R.id.status_summary)
val reviewSettingsButton: MaterialButton by lazyView(R.id.review_settings_button)
val rescanButton: MaterialButton by lazyView(R.id.rescan_button)
+ fun showText(statusUiData: StatusUiData) {
+ titleView.updateText(statusUiData.title)
+ summaryView.updateText(statusUiData.getSummary(context))
+ }
+
+ private fun TextSwitcher.updateText(newText: CharSequence) {
+ val currentText: CharSequence? = (currentView as TextView).text
+ if (currentText == newText) {
+ return
+ }
+
+ if (currentText.isNullOrBlank()) {
+ setCurrentText(newText)
+ } else {
+ setText(newText)
+ }
+ }
+
fun showButtons(statusUiData: StatusUiData) {
rescanButton.isEnabled = !statusUiData.isRefreshInProgress
diff --git a/PermissionController/tests/inprocess/Android.bp b/PermissionController/tests/inprocess/Android.bp
index 4cd9e0e6f..49e4e9474 100644
--- a/PermissionController/tests/inprocess/Android.bp
+++ b/PermissionController/tests/inprocess/Android.bp
@@ -53,6 +53,7 @@ android_test {
// 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",
+ "android.permission.flags-aconfig-java-export",
],
data: [
diff --git a/PermissionController/tests/inprocess/src/com/android/permissioncontroller/permission/GetPermissionGroupInfoTest.kt b/PermissionController/tests/inprocess/src/com/android/permissioncontroller/permission/GetPermissionGroupInfoTest.kt
index b20e99c38..d06de169b 100644
--- a/PermissionController/tests/inprocess/src/com/android/permissioncontroller/permission/GetPermissionGroupInfoTest.kt
+++ b/PermissionController/tests/inprocess/src/com/android/permissioncontroller/permission/GetPermissionGroupInfoTest.kt
@@ -18,12 +18,14 @@ package com.android.permissioncontroller.permission
import android.content.Context
import android.os.Build
+import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.test.filters.SdkSuppress
import androidx.test.platform.app.InstrumentationRegistry
import com.android.permissioncontroller.permission.utils.PermissionMapping
import com.google.common.truth.Truth.assertThat
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
+import org.junit.Rule
import org.junit.Test
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.S, codeName = "S")
@@ -32,6 +34,8 @@ class GetPermissionGroupInfoTest {
private val packageManager = context.packageManager
private val timeoutMs: Long = 10000
+ @JvmField @Rule val instantTaskExecutorRule = InstantTaskExecutorRule()
+
@Test
fun assertAllPlatformPermGroupPermListsMatch() {
val groups = PermissionMapping.getPlatformPermissionGroups()
diff --git a/PermissionController/tests/inprocess/src/com/android/permissioncontroller/permission/data/AttributionLabelLiveDataTest.kt b/PermissionController/tests/inprocess/src/com/android/permissioncontroller/permission/data/AttributionLabelLiveDataTest.kt
index bc9e5d6ff..7c735a451 100644
--- a/PermissionController/tests/inprocess/src/com/android/permissioncontroller/permission/data/AttributionLabelLiveDataTest.kt
+++ b/PermissionController/tests/inprocess/src/com/android/permissioncontroller/permission/data/AttributionLabelLiveDataTest.kt
@@ -22,10 +22,12 @@ import android.os.Process.myUserHandle
import android.os.UserHandle
import android.permission.cts.PermissionUtils.install
import android.permission.cts.PermissionUtils.uninstallApp
+import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.test.platform.app.InstrumentationRegistry
import com.google.common.truth.Truth.assertThat
import org.junit.After
import org.junit.Before
+import org.junit.Rule
import org.junit.Test
private const val APK = "/data/local/tmp/pc-inprocess/AppThatUsesCameraPermission.apk"
@@ -34,6 +36,8 @@ private const val PKG = "com.android.permissioncontroller.tests.appthatrequestpe
class AttributionLabelLiveDataTest {
private val context = InstrumentationRegistry.getInstrumentation().context as Context
+ @JvmField @Rule val instantTaskExecutorRule = InstantTaskExecutorRule()
+
@Before
fun installAttributingApp() {
install(APK)
diff --git a/PermissionController/tests/inprocess/src/com/android/permissioncontroller/permission/util/ArrayUtilsTest.kt b/PermissionController/tests/inprocess/src/com/android/permissioncontroller/permission/util/ArrayUtilsTest.kt
index 708d4222f..c7b9ad823 100644
--- a/PermissionController/tests/inprocess/src/com/android/permissioncontroller/permission/util/ArrayUtilsTest.kt
+++ b/PermissionController/tests/inprocess/src/com/android/permissioncontroller/permission/util/ArrayUtilsTest.kt
@@ -16,11 +16,15 @@
package com.android.permissioncontroller.permission.util
+import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import com.android.permissioncontroller.permission.utils.ArrayUtils
import com.google.common.truth.Truth.assertThat
+import org.junit.Rule
import org.junit.Test
class ArrayUtilsTest {
+ @JvmField @Rule val instantTaskExecutorRule = InstantTaskExecutorRule()
+
@Test
fun appendString_appendToNull_returnsArrayWithString() {
assertThat(ArrayUtils.appendString(null, TEST_STRING)).isEqualTo(arrayOf(TEST_STRING))
diff --git a/PermissionController/tests/inprocess/src/com/android/permissioncontroller/permission/util/CollectionUtilsTest.kt b/PermissionController/tests/inprocess/src/com/android/permissioncontroller/permission/util/CollectionUtilsTest.kt
index 3d4bd28ff..627d19474 100644
--- a/PermissionController/tests/inprocess/src/com/android/permissioncontroller/permission/util/CollectionUtilsTest.kt
+++ b/PermissionController/tests/inprocess/src/com/android/permissioncontroller/permission/util/CollectionUtilsTest.kt
@@ -16,11 +16,15 @@
package com.android.permissioncontroller.permission.util
+import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import com.android.permissioncontroller.permission.utils.CollectionUtils
import com.google.common.truth.Truth.assertThat
+import org.junit.Rule
import org.junit.Test
class CollectionUtilsTest {
+ @JvmField @Rule val instantTaskExecutorRule = InstantTaskExecutorRule()
+
@Test
fun testContains_true() {
val byteArrays = setOf(TEST_BYTE_ARRAY_1, TEST_BYTE_ARRAY_2, TEST_BYTE_ARRAY_3)
diff --git a/PermissionController/tests/inprocess/src/com/android/permissioncontroller/permission/util/KotlinUtilsTest.kt b/PermissionController/tests/inprocess/src/com/android/permissioncontroller/permission/util/KotlinUtilsTest.kt
index 37aa8d988..34c351683 100644
--- a/PermissionController/tests/inprocess/src/com/android/permissioncontroller/permission/util/KotlinUtilsTest.kt
+++ b/PermissionController/tests/inprocess/src/com/android/permissioncontroller/permission/util/KotlinUtilsTest.kt
@@ -30,11 +30,13 @@ import android.graphics.Canvas
import android.graphics.ColorFilter
import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.Drawable
+import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import com.android.permissioncontroller.permission.utils.KotlinUtils
import com.google.common.truth.Truth.assertThat
import kotlin.test.assertFailsWith
+import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers.any
@@ -48,6 +50,8 @@ import org.mockito.Mockito.`when` as whenever
class KotlinUtilsTest {
private val targetContext = InstrumentationRegistry.getInstrumentation().targetContext
+ @JvmField @Rule val instantTaskExecutorRule = InstantTaskExecutorRule()
+
@Test
fun convertToBitmap_argb888BitmapDrawable_returnsSameBitmap() {
val bitmap = Bitmap.createBitmap(/* width= */ 5, /* height= */ 10, Bitmap.Config.ARGB_8888)
@@ -64,11 +68,15 @@ class KotlinUtilsTest {
class FakeDrawable(private val intrinsicSize: Int) : Drawable() {
override fun getIntrinsicWidth() = intrinsicSize
+
override fun getIntrinsicHeight() = intrinsicSize
override fun draw(canvas: Canvas) = Unit // no-op
+
override fun getOpacity() = throw UnsupportedOperationException()
+
override fun setAlpha(alpha: Int) = throw UnsupportedOperationException()
+
override fun setColorFilter(colorFilter: ColorFilter?) =
throw UnsupportedOperationException()
}
diff --git a/PermissionController/tests/inprocess/src/com/android/permissioncontroller/permission/util/PermissionMappingTest.kt b/PermissionController/tests/inprocess/src/com/android/permissioncontroller/permission/util/PermissionMappingTest.kt
index aa7d7da60..e8e910c4e 100644
--- a/PermissionController/tests/inprocess/src/com/android/permissioncontroller/permission/util/PermissionMappingTest.kt
+++ b/PermissionController/tests/inprocess/src/com/android/permissioncontroller/permission/util/PermissionMappingTest.kt
@@ -19,14 +19,29 @@ package com.android.permissioncontroller.permission.util
import android.Manifest
import android.app.AppOpsManager
import android.health.connect.HealthPermissions
+import android.os.Build
+import android.permission.flags.Flags
+import android.platform.test.annotations.RequiresFlagsDisabled
+import android.platform.test.annotations.RequiresFlagsEnabled
+import android.platform.test.flag.junit.CheckFlagsRule
+import android.platform.test.flag.junit.DeviceFlagsValueProvider
+import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SdkSuppress
import com.android.permissioncontroller.permission.utils.PermissionMapping
import com.google.common.truth.Truth.assertThat
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertNull
+import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class PermissionMappingTest {
+
+ @JvmField @Rule val instantTaskExecutorRule = InstantTaskExecutorRule()
+ @JvmField @Rule val checkFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule()
+
@Test
fun testGetPlatformPermissionGroupForOp_healthPermissionGroup() {
assertThat(
@@ -76,4 +91,47 @@ class PermissionMappingTest {
PermissionMapping.getGroupOfPlatformPermission(Manifest.permission.READ_CONTACTS)
)
}
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.BAKLAVA)
+ @RequiresFlagsEnabled(Flags.FLAG_REPLACE_BODY_SENSOR_PERMISSION_ENABLED)
+ @Test
+ fun getGroupOfPlatformPermission_replaceBodySensorFlagEnabled_notHaveSensorsGroup() {
+ assertNull(PermissionMapping.getGroupOfPlatformPermission(Manifest.permission.BODY_SENSORS))
+ assertNull(
+ PermissionMapping.getGroupOfPlatformPermission(
+ Manifest.permission.BODY_SENSORS_BACKGROUND
+ )
+ )
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.BAKLAVA)
+ @RequiresFlagsDisabled(Flags.FLAG_REPLACE_BODY_SENSOR_PERMISSION_ENABLED)
+ @Test
+ fun getGroupOfPlatformPermission_replaceBodySensorFlagDisabled_haveSensorsGroup() {
+ assertNotNull(
+ PermissionMapping.getGroupOfPlatformPermission(Manifest.permission.BODY_SENSORS)
+ )
+ assertNotNull(
+ PermissionMapping.getGroupOfPlatformPermission(
+ Manifest.permission.BODY_SENSORS_BACKGROUND
+ )
+ )
+ }
+
+
+ @SdkSuppress(
+ minSdkVersion = Build.VERSION_CODES.TIRAMISU,
+ maxSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE,
+ )
+ @Test
+ fun getGroupOfPlatformPermission_preV_haveSensorsGroup() {
+ assertNotNull(
+ PermissionMapping.getGroupOfPlatformPermission(Manifest.permission.BODY_SENSORS)
+ )
+ assertNotNull(
+ PermissionMapping.getGroupOfPlatformPermission(
+ Manifest.permission.BODY_SENSORS_BACKGROUND
+ )
+ )
+ }
}
diff --git a/PermissionController/tests/inprocess/src/com/android/permissioncontroller/permission/util/UtilsTest.kt b/PermissionController/tests/inprocess/src/com/android/permissioncontroller/permission/util/UtilsTest.kt
index 11bcca356..1cfe6a5d3 100644
--- a/PermissionController/tests/inprocess/src/com/android/permissioncontroller/permission/util/UtilsTest.kt
+++ b/PermissionController/tests/inprocess/src/com/android/permissioncontroller/permission/util/UtilsTest.kt
@@ -16,6 +16,8 @@
package com.android.permissioncontroller.permission.util
+import android.Manifest.permission.BODY_SENSORS
+import android.Manifest.permission.BODY_SENSORS_BACKGROUND
import android.Manifest.permission.READ_CONTACTS
import android.Manifest.permission_group.ACTIVITY_RECOGNITION
import android.Manifest.permission_group.CALENDAR
@@ -38,6 +40,14 @@ import android.content.Intent
import android.content.SharedPreferences
import android.content.pm.PackageManager.NameNotFoundException
import android.content.res.Resources
+import android.os.Build
+import android.permission.flags.Flags
+import android.platform.test.annotations.RequiresFlagsDisabled
+import android.platform.test.annotations.RequiresFlagsEnabled
+import android.platform.test.flag.junit.CheckFlagsRule
+import android.platform.test.flag.junit.DeviceFlagsValueProvider
+import androidx.arch.core.executor.testing.InstantTaskExecutorRule
+import androidx.test.filters.SdkSuppress
import androidx.test.platform.app.InstrumentationRegistry
import com.android.permissioncontroller.Constants.EXTRA_SESSION_ID
import com.android.permissioncontroller.Constants.INVALID_SESSION_ID
@@ -46,12 +56,20 @@ import com.android.permissioncontroller.permission.utils.Utils
import com.android.permissioncontroller.privacysources.WorkPolicyInfo
import com.google.common.truth.Truth.assertThat
import kotlin.test.assertFailsWith
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
import org.junit.Ignore
+import org.junit.Rule
import org.junit.Test
class UtilsTest {
+
+ @JvmField @Rule val checkFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule()
+
private val context = InstrumentationRegistry.getInstrumentation().targetContext as Context
+ @JvmField @Rule val instantTaskExecutorRule = InstantTaskExecutorRule()
+
@Test
fun getAbsoluteTimeString_zero_returnsNull() {
assertThat(Utils.getAbsoluteTimeString(context, 0)).isNull()
@@ -96,6 +114,7 @@ class UtilsTest {
fun getBlockedTitle_invalidGroupName_returnsMinusOne() {
assertThat(Utils.getBlockedTitle(INVALID_GROUP_NAME)).isEqualTo(-1)
}
+
@Test
fun getBlockedTitle_validGroupName() {
assertThat(Utils.getBlockedTitle(CAMERA)).isEqualTo(R.string.blocked_camera_title)
@@ -295,6 +314,59 @@ class UtilsTest {
assertThat(permissionInfos[0].name).isEqualTo(READ_CONTACTS)
}
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.BAKLAVA)
+ @RequiresFlagsEnabled(Flags.FLAG_REPLACE_BODY_SENSOR_PERMISSION_ENABLED)
+ @Test
+ fun getInstalledRuntimePermissionInfosForGroup_bodySensorFlagEnabled_bodySensorPermissionsNotIncluded() {
+ val permissionNamesInUndefinedGroup =
+ Utils.getInstalledRuntimePermissionInfosForGroup(context.packageManager, UNDEFINED)
+ .map { it.name }
+ val permissionNamesInSensorsGroup =
+ Utils.getInstalledRuntimePermissionInfosForGroup(context.packageManager, SENSORS)
+ .map { it.name }
+
+ assertFalse(permissionNamesInUndefinedGroup.contains(BODY_SENSORS))
+ assertFalse(permissionNamesInUndefinedGroup.contains(BODY_SENSORS_BACKGROUND))
+ assertFalse(permissionNamesInSensorsGroup.contains(BODY_SENSORS))
+ assertFalse(permissionNamesInSensorsGroup.contains(BODY_SENSORS_BACKGROUND))
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.BAKLAVA)
+ @RequiresFlagsDisabled(Flags.FLAG_REPLACE_BODY_SENSOR_PERMISSION_ENABLED)
+ @Test
+ fun getInstalledRuntimePermissionInfosForGroup_bodySensorFlagDisabled_bodySensorPermissionsIncluded() {
+ val permissionNamesInUndefinedGroup =
+ Utils.getInstalledRuntimePermissionInfosForGroup(context.packageManager, UNDEFINED)
+ .map { it.name }
+ val permissionNamesInSensorsGroup =
+ Utils.getInstalledRuntimePermissionInfosForGroup(context.packageManager, SENSORS)
+ .map { it.name }
+
+ assertFalse(permissionNamesInUndefinedGroup.contains(BODY_SENSORS))
+ assertFalse(permissionNamesInUndefinedGroup.contains(BODY_SENSORS_BACKGROUND))
+ assertTrue(permissionNamesInSensorsGroup.contains(BODY_SENSORS))
+ assertTrue(permissionNamesInSensorsGroup.contains(BODY_SENSORS_BACKGROUND))
+ }
+
+ @SdkSuppress(
+ minSdkVersion = Build.VERSION_CODES.TIRAMISU,
+ maxSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE,
+ )
+ @Test
+ fun getInstalledRuntimePermissionInfosForGroup_preV_bodySensorPermissionsIncluded() {
+ val permissionNamesInUndefinedGroup =
+ Utils.getInstalledRuntimePermissionInfosForGroup(context.packageManager, UNDEFINED)
+ .map { it.name }
+ val permissionNamesInSensorsGroup =
+ Utils.getInstalledRuntimePermissionInfosForGroup(context.packageManager, SENSORS)
+ .map { it.name }
+
+ assertFalse(permissionNamesInUndefinedGroup.contains(BODY_SENSORS))
+ assertFalse(permissionNamesInUndefinedGroup.contains(BODY_SENSORS_BACKGROUND))
+ assertTrue(permissionNamesInSensorsGroup.contains(BODY_SENSORS))
+ assertTrue(permissionNamesInSensorsGroup.contains(BODY_SENSORS_BACKGROUND))
+ }
+
@Test
fun getColorResId_validId_returnsNonZero() {
assertThat(Utils.getColorResId(context, android.R.attr.colorPrimary))
diff --git a/PermissionController/tests/mocking/src/com/android/permissioncontroller/tests/mocking/privacysources/NotificationListenerCheckInternalTest.kt b/PermissionController/tests/mocking/src/com/android/permissioncontroller/tests/mocking/privacysources/NotificationListenerCheckInternalTest.kt
index bc00d3bc8..4bb021b3d 100644
--- a/PermissionController/tests/mocking/src/com/android/permissioncontroller/tests/mocking/privacysources/NotificationListenerCheckInternalTest.kt
+++ b/PermissionController/tests/mocking/src/com/android/permissioncontroller/tests/mocking/privacysources/NotificationListenerCheckInternalTest.kt
@@ -30,7 +30,6 @@ import android.safetycenter.SafetyCenterManager
import android.safetycenter.SafetyEvent
import android.safetycenter.SafetySourceData
import android.safetycenter.SafetySourceIssue
-import androidx.core.util.Preconditions
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SdkSuppress
@@ -464,9 +463,7 @@ class NotificationListenerCheckInternalTest {
}
val safetySourceIssue =
- Preconditions.checkNotNull(
- notificationListenerCheck.createSafetySourceIssue(testComponent, 0)
- )
+ checkNotNull(notificationListenerCheck.createSafetySourceIssue(testComponent, 0))
val expectedId = "notification_listener_${testComponent.flattenToString()}"
val expectedTitle =
diff --git a/PermissionController/tests/permissionui/Android.bp b/PermissionController/tests/permissionui/Android.bp
index 5f177f40c..e0e8fed10 100644
--- a/PermissionController/tests/permissionui/Android.bp
+++ b/PermissionController/tests/permissionui/Android.bp
@@ -47,6 +47,7 @@ android_test {
"androidx.test.ext.truth",
"androidx.test.rules",
"androidx.test.uiautomator_uiautomator",
+ "android.permission.flags-aconfig-java-export",
"com.android.permission.flags-aconfig-java-export",
"compatibility-device-util-axt",
"flag-junit",
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 b38f5f40a..08143f77f 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
@@ -17,11 +17,18 @@
package com.android.permissioncontroller.permissionui.ui.handheld
import android.content.Intent
+import android.os.Build
+import android.permission.flags.Flags
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.RequiresFlagsDisabled
+import android.platform.test.annotations.RequiresFlagsEnabled
+import android.platform.test.flag.junit.CheckFlagsRule
+import android.platform.test.flag.junit.DeviceFlagsValueProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SdkSuppress
import androidx.test.uiautomator.By
import com.android.compatibility.common.util.SystemUtil.eventually
import com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity
@@ -32,13 +39,18 @@ 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.Assert.assertNull
import org.junit.Before
+import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
/** Simple tests for {@link ManageCustomPermissionsFragment} */
@RunWith(AndroidJUnit4::class)
class ManageCustomPermissionsFragmentTest : BaseHandheldPermissionUiTest() {
+
+ @JvmField @Rule val checkFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule()
+
private val ONE_PERMISSION_DEFINER_APK =
"/data/local/tmp/pc-permissionui/" + "PermissionUiDefineAdditionalPermissionApp.apk"
private val PERMISSION_USER_APK =
@@ -95,14 +107,38 @@ class ManageCustomPermissionsFragmentTest : BaseHandheldPermissionUiTest() {
eventually { assertThat(getUsageCountsFromUi(PERM_LABEL)).isEqualTo(original) }
}
+
+ @SdkSuppress(
+ minSdkVersion = Build.VERSION_CODES.TIRAMISU,
+ maxSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE,
+ )
+ @Test
+ fun testFindBodySensor_preV_labelDisplayed() {
+ if (waitFindObjectOrNull(By.textContains(BODY_SENSORS_LABEL)) == null) {
+ waitFindObject(By.textContains(ADDITIONAL_PERMISSIONS_LABEL)).click()
+ assertNotNull(waitFindObjectOrNull(By.textContains(BODY_SENSORS_LABEL)))
+ }
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.BAKLAVA)
+ @RequiresFlagsDisabled(Flags.FLAG_REPLACE_BODY_SENSOR_PERMISSION_ENABLED)
@Test
- fun bodySensorsEitherDisplayedInMainPageOrInAdditional() {
+ fun testFindBodySensor_replaceBodySensorFlagDisabled_labelDisplayed() {
if (waitFindObjectOrNull(By.textContains(BODY_SENSORS_LABEL)) == null) {
waitFindObject(By.textContains(ADDITIONAL_PERMISSIONS_LABEL)).click()
assertNotNull(waitFindObjectOrNull(By.textContains(BODY_SENSORS_LABEL)))
}
}
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.BAKLAVA)
+ @RequiresFlagsEnabled(Flags.FLAG_REPLACE_BODY_SENSOR_PERMISSION_ENABLED)
+ @Test
+ fun testFindBodySensor_replaceBodySensorFlagEnabled_labelNotDisplayed() {
+ assertNull(waitFindObjectOrNull(By.textContains(BODY_SENSORS_LABEL)))
+ waitFindObject(By.textContains(ADDITIONAL_PERMISSIONS_LABEL)).click()
+ assertNull(waitFindObjectOrNull(By.textContains(BODY_SENSORS_LABEL)))
+ }
+
@After
fun tearDown() {
uninstallApp(DEFINER_PKG)
diff --git a/SafetyCenter/Resources/res/raw-v36/safety_center_config.xml b/SafetyCenter/Resources/res/raw-v36/safety_center_config.xml
index de033ac44..cb6323fff 100644
--- a/SafetyCenter/Resources/res/raw-v36/safety_center_config.xml
+++ b/SafetyCenter/Resources/res/raw-v36/safety_center_config.xml
@@ -30,6 +30,36 @@
initialDisplayState="disabled"
notificationsAllowed="true"/>
<dynamic-safety-source
+ id="AndroidFaceUnlock"
+ packageName="com.android.settings"
+ profile="all_profiles"
+ title="@com.android.safetycenter.resources:string/face_unlock_title"
+ titleForWork="@com.android.safetycenter.resources:string/face_unlock_title_for_work"
+ titleForPrivateProfile="@com.android.safetycenter.resources:string/face_unlock_title_for_private_profile"
+ searchTerms="@com.android.safetycenter.resources:string/face_unlock_search_terms"
+ refreshOnPageOpenAllowed="true"
+ initialDisplayState="hidden"/>
+ <dynamic-safety-source
+ id="AndroidFingerprintUnlock"
+ packageName="com.android.settings"
+ profile="all_profiles"
+ title="@com.android.safetycenter.resources:string/fingerprint_unlock_title"
+ titleForWork="@com.android.safetycenter.resources:string/fingerprint_unlock_title_for_work"
+ titleForPrivateProfile="@com.android.safetycenter.resources:string/fingerprint_unlock_title_for_private_profile"
+ searchTerms="@com.android.safetycenter.resources:string/fingerprint_unlock_search_terms"
+ refreshOnPageOpenAllowed="true"
+ initialDisplayState="hidden"/>
+ <dynamic-safety-source
+ id="AndroidWearUnlock"
+ packageName="com.android.settings"
+ profile="all_profiles"
+ title="@com.android.safetycenter.resources:string/wear_unlock_title"
+ titleForWork="@com.android.safetycenter.resources:string/wear_unlock_title_for_work"
+ titleForPrivateProfile="@com.android.safetycenter.resources:string/wear_unlock_title_for_private_profile"
+ searchTerms="@com.android.safetycenter.resources:string/wear_unlock_search_terms"
+ refreshOnPageOpenAllowed="true"
+ initialDisplayState="hidden"/>
+ <dynamic-safety-source
id="AndroidBiometrics"
packageName="com.android.settings"
profile="all_profiles"
diff --git a/SafetyCenter/Resources/res/values-v36/config.xml b/SafetyCenter/Resources/res/values-v36/config.xml
new file mode 100644
index 000000000..6fa28a340
--- /dev/null
+++ b/SafetyCenter/Resources/res/values-v36/config.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2025 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <!-- Comma separated list of safety source IDs to show in the same task as the safety center -->
+ <string name="config_same_task_safety_source_ids" translatable="false">AndroidAccessibility,AndroidBackgroundLocation,AndroidBiometrics,AndroidFaceUnlock,AndroidFingerprintUnlock,AndroidHealthConnect,AndroidLockScreen,AndroidPrivateSpace,AndroidMoreSettings,AndroidNotificationListener,AndroidPermissionAutoRevoke,AndroidPermissionManager,AndroidPermissionUsage,AndroidPrivacyAppDataSharingUpdates,AndroidPrivacyControls,AndroidWearUnlock,AndroidWorkPolicyInfo</string>
+</resources>
diff --git a/SafetyCenter/Resources/res/values-v36/strings.xml b/SafetyCenter/Resources/res/values-v36/strings.xml
new file mode 100644
index 000000000..f452e045a
--- /dev/null
+++ b/SafetyCenter/Resources/res/values-v36/strings.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2025 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="face_unlock_title" description="The default title of the setting for managing face unlock options on the device">Face</string>
+ <string name="face_unlock_title_for_work" description="The default title of the setting for managing face unlock options for work on the device">Face for work</string>
+ <string name="face_unlock_title_for_private_profile" description="The default title of the setting for managing face unlock options for private profile on the device"><!-- Empty placeholder--></string>
+ <string name="face_unlock_search_terms" description="Search keywords of the setting for managing face unlock options on the device">Face unlock, Face</string>
+
+ <string name="fingerprint_unlock_title" description="The default title of the setting for managing fingerprint unlock options on the device">Fingerprint</string>
+ <string name="fingerprint_unlock_title_for_work" description="The default title of the setting for managing fingerprint unlock options for work on the device">Fingerprint for work</string>
+ <string name="fingerprint_unlock_title_for_private_profile" description="The default title of the setting for managing fingerprint unlock options for private profile on the device"><!-- Empty placeholder--></string>
+ <string name="fingerprint_unlock_search_terms" description="Search keywords of the setting for managing fingerprint unlock options on the device">Fingerprint, Finger, Add fingerprint</string>
+
+ <string name="wear_unlock_title" description="The default title of the setting for managing wear unlock options on the device">Watch</string>
+ <string name="wear_unlock_title_for_work" description="The default title of the setting for managing wear unlock options for work on the device">Watch for work</string>
+ <string name="wear_unlock_title_for_private_profile" description="The default title of the setting for managing wear unlock options for private profile on the device"><!-- Empty placeholder--></string>
+ <string name="wear_unlock_search_terms" description="Search keywords of the setting for managing wear unlock options on the device">Watch, Watch unlock</string>
+</resources>
diff --git a/TEST_MAPPING b/TEST_MAPPING
index 823738181..5c6f04426 100644
--- a/TEST_MAPPING
+++ b/TEST_MAPPING
@@ -45,6 +45,9 @@
"name" : "CtsRoleTestCases"
},
{
+ "name" : "CtsRoleMultiUserTestCases"
+ },
+ {
"name" : "CtsPermissionMultiUserTestCases"
},
{
diff --git a/framework-s/java/android/app/ecm/EnhancedConfirmationManager.java b/framework-s/java/android/app/ecm/EnhancedConfirmationManager.java
index db05a0af6..290388558 100644
--- a/framework-s/java/android/app/ecm/EnhancedConfirmationManager.java
+++ b/framework-s/java/android/app/ecm/EnhancedConfirmationManager.java
@@ -33,6 +33,7 @@ import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.os.Build;
import android.os.RemoteException;
+import android.os.UserHandle;
import android.permission.flags.Flags;
import android.util.ArraySet;
@@ -202,6 +203,19 @@ public final class EnhancedConfirmationManager {
public static final String ACTION_SHOW_ECM_RESTRICTED_SETTING_DIALOG =
"android.app.ecm.action.SHOW_ECM_RESTRICTED_SETTING_DIALOG";
+ /**
+ * The setting is restricted because of the phone state of the device
+ * @hide
+ */
+ public static final String REASON_PHONE_STATE = "phone_state";
+
+ /**
+ * The setting is restricted because the restricted app op is set for the given package
+ * @hide
+ */
+ public static final String REASON_APP_OP_RESTRICTED = "app_op_restricted";
+
+
/** A map of ECM states to their corresponding app op states */
@Retention(java.lang.annotation.RetentionPolicy.SOURCE)
@IntDef(prefix = {"ECM_STATE_"}, value = {EcmState.ECM_STATE_NOT_GUARDED,
@@ -349,8 +363,16 @@ public final class EnhancedConfirmationManager {
@NonNull String settingIdentifier) throws NameNotFoundException {
Intent intent = new Intent(ACTION_SHOW_ECM_RESTRICTED_SETTING_DIALOG);
intent.putExtra(Intent.EXTRA_PACKAGE_NAME, packageName);
- intent.putExtra(Intent.EXTRA_UID, getPackageUid(packageName));
+ int uid = getPackageUid(packageName);
+ intent.putExtra(Intent.EXTRA_UID, uid);
intent.putExtra(Intent.EXTRA_SUBJECT, settingIdentifier);
+ try {
+ intent.putExtra(Intent.EXTRA_REASON, mService.getRestrictionReason(packageName,
+ settingIdentifier, UserHandle.getUserHandleForUid(uid).getIdentifier()));
+ } catch (SecurityException | RemoteException e) {
+ // The caller of this method does not have permission to read the ECM state, so we
+ // won't include it in the return
+ }
return intent;
}
diff --git a/framework-s/java/android/app/ecm/IEnhancedConfirmationManager.aidl b/framework-s/java/android/app/ecm/IEnhancedConfirmationManager.aidl
index 5149daa49..79d2322bd 100644
--- a/framework-s/java/android/app/ecm/IEnhancedConfirmationManager.aidl
+++ b/framework-s/java/android/app/ecm/IEnhancedConfirmationManager.aidl
@@ -25,6 +25,8 @@ interface IEnhancedConfirmationManager {
boolean isRestricted(in String packageName, in String settingIdentifier, int userId);
+ String getRestrictionReason(in String packageName, in String settingIdentifier, int userId);
+
void clearRestriction(in String packageName, int userId);
boolean isClearRestrictionAllowed(in String packageName, int userId);
diff --git a/framework-s/java/android/app/role/TEST_MAPPING b/framework-s/java/android/app/role/TEST_MAPPING
index 46b148e68..62c07e5d9 100644
--- a/framework-s/java/android/app/role/TEST_MAPPING
+++ b/framework-s/java/android/app/role/TEST_MAPPING
@@ -7,6 +7,9 @@
"exclude-annotation": "androidx.test.filters.FlakyTest"
}
]
+ },
+ {
+ "name": "CtsRoleMultiUserTestCases"
}
],
"mainline-presubmit": [
@@ -24,6 +27,9 @@
"exclude-annotation": "androidx.test.filters.FlakyTest"
}
]
+ },
+ {
+ "name": "CtsRoleMultiUserTestCases[com.google.android.permission.apex]"
}
],
"permission-mainline-presubmit": [
@@ -41,6 +47,9 @@
"exclude-annotation": "androidx.test.filters.FlakyTest"
}
]
+ },
+ {
+ "name": "CtsRoleMultiUserTestCases"
}
],
"postsubmit": [
diff --git a/service/java/com/android/ecm/EnhancedConfirmationService.java b/service/java/com/android/ecm/EnhancedConfirmationService.java
index 65fde6daf..dde5404a4 100644
--- a/service/java/com/android/ecm/EnhancedConfirmationService.java
+++ b/service/java/com/android/ecm/EnhancedConfirmationService.java
@@ -16,6 +16,9 @@
package com.android.ecm;
+import static android.app.ecm.EnhancedConfirmationManager.REASON_APP_OP_RESTRICTED;
+import static android.app.ecm.EnhancedConfirmationManager.REASON_PHONE_STATE;
+
import android.Manifest;
import android.annotation.FlaggedApi;
import android.annotation.IntDef;
@@ -89,7 +92,7 @@ public class EnhancedConfirmationService extends SystemService {
private static final int CALL_TYPE_UNTRUSTED = 0;
private static final int CALL_TYPE_TRUSTED = 1;
- private static final int CALL_TYPE_EMERGENCY = 2;
+ private static final int CALL_TYPE_EMERGENCY = 1 << 1;
@IntDef(flag = true, value = {
CALL_TYPE_UNTRUSTED,
CALL_TYPE_TRUSTED,
@@ -269,6 +272,8 @@ public class EnhancedConfirmationService extends SystemService {
PROTECTED_SETTINGS.add(AppOpsManager.OPSTR_REQUEST_INSTALL_PACKAGES);
UNTRUSTED_CALL_RESTRICTED_SETTINGS.add(
AppOpsManager.OPSTR_REQUEST_INSTALL_PACKAGES);
+ UNTRUSTED_CALL_RESTRICTED_SETTINGS.add(
+ AppOpsManager.OPSTR_BIND_ACCESSIBILITY_SERVICE);
}
}
@@ -287,10 +292,16 @@ public class EnhancedConfirmationService extends SystemService {
public boolean isRestricted(@NonNull String packageName, @NonNull String settingIdentifier,
@UserIdInt int userId) {
+ return getRestrictionReason(packageName, settingIdentifier, userId) != null;
+ }
+
+ public String getRestrictionReason(@NonNull String packageName,
+ @NonNull String settingIdentifier,
+ @UserIdInt int userId) {
enforcePermissions("isRestricted", userId);
if (!UserUtils.isUserExistent(userId, getContext())) {
Log.e(LOG_TAG, "user " + userId + " does not exist");
- return false;
+ return null;
}
Preconditions.checkStringNotEmpty(packageName, "packageName cannot be null or empty");
@@ -299,12 +310,13 @@ public class EnhancedConfirmationService extends SystemService {
try {
if (!isSettingEcmProtected(settingIdentifier)) {
- return false;
+ return null;
}
- if (isSettingProtectedGlobally(settingIdentifier)) {
- return true;
+ String globalProtectionReason = getGlobalProtectionReason(settingIdentifier);
+ if (globalProtectionReason != null) {
+ return globalProtectionReason;
}
- return isPackageEcmGuarded(packageName, userId);
+ return isPackageEcmGuarded(packageName, userId) ? REASON_APP_OP_RESTRICTED : null;
} catch (NameNotFoundException e) {
throw new IllegalArgumentException(e);
}
@@ -513,12 +525,13 @@ public class EnhancedConfirmationService extends SystemService {
return false;
}
- private boolean isSettingProtectedGlobally(@NonNull String settingIdentifier) {
- if (UNTRUSTED_CALL_RESTRICTED_SETTINGS.contains(settingIdentifier)) {
- return isUntrustedCallOngoing();
+ private String getGlobalProtectionReason(@NonNull String settingIdentifier) {
+ if (UNTRUSTED_CALL_RESTRICTED_SETTINGS.contains(settingIdentifier)
+ && isUntrustedCallOngoing()) {
+ return REASON_PHONE_STATE;
}
- return false;
+ return null;
}
@Nullable
diff --git a/service/java/com/android/role/TEST_MAPPING b/service/java/com/android/role/TEST_MAPPING
index e0e1160d8..720330f82 100644
--- a/service/java/com/android/role/TEST_MAPPING
+++ b/service/java/com/android/role/TEST_MAPPING
@@ -15,6 +15,9 @@
"exclude-annotation": "androidx.test.filters.FlakyTest"
}
]
+ },
+ {
+ "name": "CtsRoleMultiUserTestCases"
}
],
"mainline-presubmit": [
@@ -32,6 +35,9 @@
"exclude-annotation": "androidx.test.filters.FlakyTest"
}
]
+ },
+ {
+ "name": "CtsRoleMultiUserTestCases[com.google.android.permission.apex]"
}
],
"permission-mainline-presubmit": [
@@ -49,6 +55,9 @@
"exclude-annotation": "androidx.test.filters.FlakyTest"
}
]
+ },
+ {
+ "name": "CtsRoleMultiUserTestCases"
}
],
"postsubmit": [
diff --git a/tests/cts/permission/src/android/permission/cts/LocationAccessCheckTest.java b/tests/cts/permission/src/android/permission/cts/LocationAccessCheckTest.java
index 166a5dbd1..024a89f2e 100644
--- a/tests/cts/permission/src/android/permission/cts/LocationAccessCheckTest.java
+++ b/tests/cts/permission/src/android/permission/cts/LocationAccessCheckTest.java
@@ -77,6 +77,7 @@ import androidx.test.filters.SdkSuppress;
import androidx.test.runner.AndroidJUnit4;
import com.android.compatibility.common.util.DeviceConfigStateChangerRule;
+import com.android.compatibility.common.util.UserHelper;
import com.android.compatibility.common.util.mainline.MainlineModule;
import com.android.compatibility.common.util.mainline.ModuleDetector;
import com.android.modules.utils.build.SdkLevel;
@@ -199,6 +200,8 @@ public class LocationAccessCheckTest {
private static boolean sWasLocationEnabled = true;
+ private UserHelper mUserHelper = new UserHelper(sContext);
+
@BeforeClass
public static void beforeClassSetup() throws Exception {
reduceDelays();
@@ -465,6 +468,14 @@ public class LocationAccessCheckTest {
@Before
public void beforeEachTestSetup() throws Throwable {
assumeIsNotLowRamDevice();
+
+ // TODO(b/380297485): Remove this assumption once NotificationListeners are supported on
+ // visible background users.
+ // Skipping each test for visible background users as all test cases depend on
+ // NotificationListeners.
+ assumeFalse("NotificationListeners are not yet supported on visible background users",
+ mUserHelper.isVisibleBackgroundUser());
+
wakeUpAndDismissKeyguard();
bindService();
resetPermissionControllerBeforeEachTest();
diff --git a/tests/cts/permissionmultidevice/src/android/permissionmultidevice/cts/DeviceAwarePermissionGrantTest.kt b/tests/cts/permissionmultidevice/src/android/permissionmultidevice/cts/DeviceAwarePermissionGrantTest.kt
index 687234582..44eef2144 100644
--- a/tests/cts/permissionmultidevice/src/android/permissionmultidevice/cts/DeviceAwarePermissionGrantTest.kt
+++ b/tests/cts/permissionmultidevice/src/android/permissionmultidevice/cts/DeviceAwarePermissionGrantTest.kt
@@ -93,7 +93,7 @@ class DeviceAwarePermissionGrantTest {
val displayConfigBuilder =
VirtualDeviceRule.createDefaultVirtualDisplayConfigBuilder(
DISPLAY_WIDTH,
- DISPLAY_HEIGHT
+ DISPLAY_HEIGHT,
)
.setFlags(
DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC or
@@ -114,7 +114,7 @@ class DeviceAwarePermissionGrantTest {
@RequiresFlagsEnabled(
Flags.FLAG_DEVICE_AWARE_PERMISSION_APIS_ENABLED,
- Flags.FLAG_DEVICE_AWARE_PERMISSIONS_ENABLED
+ Flags.FLAG_DEVICE_AWARE_PERMISSIONS_ENABLED,
)
@Test
fun onHostDevice_requestPermissionForHostDevice_shouldGrantPermission() {
@@ -124,13 +124,13 @@ class DeviceAwarePermissionGrantTest {
false,
"",
expectPermissionGrantedOnDefaultDevice = true,
- expectPermissionGrantedOnRemoteDevice = false
+ expectPermissionGrantedOnRemoteDevice = false,
)
}
@RequiresFlagsEnabled(
Flags.FLAG_DEVICE_AWARE_PERMISSION_APIS_ENABLED,
- Flags.FLAG_DEVICE_AWARE_PERMISSIONS_ENABLED
+ Flags.FLAG_DEVICE_AWARE_PERMISSIONS_ENABLED,
)
@Test
fun onHostDevice_requestPermissionForRemoteDevice_shouldGrantPermission() {
@@ -140,13 +140,13 @@ class DeviceAwarePermissionGrantTest {
true,
deviceDisplayName,
expectPermissionGrantedOnDefaultDevice = false,
- expectPermissionGrantedOnRemoteDevice = true
+ expectPermissionGrantedOnRemoteDevice = true,
)
}
@RequiresFlagsEnabled(
Flags.FLAG_DEVICE_AWARE_PERMISSION_APIS_ENABLED,
- Flags.FLAG_DEVICE_AWARE_PERMISSIONS_ENABLED
+ Flags.FLAG_DEVICE_AWARE_PERMISSIONS_ENABLED,
)
@RequiresFlagsDisabled(Flags.FLAG_ALLOW_HOST_PERMISSION_DIALOGS_ON_VIRTUAL_DEVICES)
@Test
@@ -160,8 +160,9 @@ class DeviceAwarePermissionGrantTest {
@RequiresFlagsEnabled(
Flags.FLAG_DEVICE_AWARE_PERMISSION_APIS_ENABLED,
Flags.FLAG_DEVICE_AWARE_PERMISSIONS_ENABLED,
- Flags.FLAG_ALLOW_HOST_PERMISSION_DIALOGS_ON_VIRTUAL_DEVICES
+ Flags.FLAG_ALLOW_HOST_PERMISSION_DIALOGS_ON_VIRTUAL_DEVICES,
)
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.BAKLAVA, codeName = "Baklava")
@Test
fun onRemoteDevice_requestPermissionForHostDevice_shouldGrantPermission() {
// Create a virtual device with default policy, so that camera permission request will
@@ -176,16 +177,18 @@ class DeviceAwarePermissionGrantTest {
virtualDisplay.display.displayId,
virtualDevice.deviceId,
true,
- Settings.Global.getString(defaultDeviceContext.contentResolver,
- Settings.Global.DEVICE_NAME),
+ Settings.Global.getString(
+ defaultDeviceContext.contentResolver,
+ Settings.Global.DEVICE_NAME,
+ ),
expectPermissionGrantedOnDefaultDevice = true,
- expectPermissionGrantedOnRemoteDevice = false
+ expectPermissionGrantedOnRemoteDevice = false,
)
}
@RequiresFlagsEnabled(
Flags.FLAG_DEVICE_AWARE_PERMISSION_APIS_ENABLED,
- Flags.FLAG_DEVICE_AWARE_PERMISSIONS_ENABLED
+ Flags.FLAG_DEVICE_AWARE_PERMISSIONS_ENABLED,
)
@Test
fun onRemoteDevice_requestPermissionForRemoteDevice_shouldGrantPermission() {
@@ -195,7 +198,7 @@ class DeviceAwarePermissionGrantTest {
true,
deviceDisplayName,
expectPermissionGrantedOnDefaultDevice = false,
- expectPermissionGrantedOnRemoteDevice = true
+ expectPermissionGrantedOnRemoteDevice = true,
)
}
@@ -205,7 +208,7 @@ class DeviceAwarePermissionGrantTest {
showDeviceName: Boolean,
expectedDeviceNameInDialog: String,
expectPermissionGrantedOnDefaultDevice: Boolean,
- expectPermissionGrantedOnRemoteDevice: Boolean
+ expectPermissionGrantedOnRemoteDevice: Boolean,
) {
// Assert no permission granted to either default device or virtual device at the beginning
assertAppHasPermissionForDevice(DEVICE_ID_DEFAULT, false)
@@ -240,13 +243,13 @@ class DeviceAwarePermissionGrantTest {
assertAppHasPermissionForDevice(DEVICE_ID_DEFAULT, expectPermissionGrantedOnDefaultDevice)
assertAppHasPermissionForDevice(
virtualDevice.deviceId,
- expectPermissionGrantedOnRemoteDevice
+ expectPermissionGrantedOnRemoteDevice,
)
}
private fun requestPermissionOnDevice(
displayId: Int,
- targetDeviceId: Int
+ targetDeviceId: Int,
): CompletableFuture<Bundle> {
val future = CompletableFuture<Bundle>()
val callback = RemoteCallback { result: Bundle? -> future.complete(result) }
diff --git a/tests/cts/permissionui/src/android/permissionui/cts/BasePermissionTest.kt b/tests/cts/permissionui/src/android/permissionui/cts/BasePermissionTest.kt
index d8eb153bf..b2da92d22 100644
--- a/tests/cts/permissionui/src/android/permissionui/cts/BasePermissionTest.kt
+++ b/tests/cts/permissionui/src/android/permissionui/cts/BasePermissionTest.kt
@@ -121,7 +121,7 @@ abstract class BasePermissionTest {
/* PackageManager.FEATURE_CAR_SPLITSCREEN_MULTITASKING */
"android.software.car.splitscreen_multitasking")
@JvmStatic
- private val isAutomotiveVisibleBackgroundUser = isAutomotive &&
+ protected val isAutomotiveVisibleBackgroundUser = isAutomotive &&
UserHelper(context).isVisibleBackgroundUser()
// TODO(b/382327037):find a way to avoid specifying the display ID for each UiSelector.
diff --git a/tests/cts/permissionui/src/android/permissionui/cts/EnhancedConfirmationManagerTest.kt b/tests/cts/permissionui/src/android/permissionui/cts/EnhancedConfirmationManagerTest.kt
index 8e91a00ce..9ec09dab7 100644
--- a/tests/cts/permissionui/src/android/permissionui/cts/EnhancedConfirmationManagerTest.kt
+++ b/tests/cts/permissionui/src/android/permissionui/cts/EnhancedConfirmationManagerTest.kt
@@ -78,20 +78,14 @@ class EnhancedConfirmationManagerTest : BaseUsePermissionTest() {
@Test
fun installedAppStartsWithModeDefault() {
installPackageWithInstallSourceAndMetadataFromStore(APP_APK_NAME_LATEST)
- eventually {
- runWithShellPermissionIdentity {
- assertEquals(
- getAppEcmState(context, appOpsManager, APP_PACKAGE_NAME),
- AppOpsManager.MODE_DEFAULT
- )
- }
- }
+ waitForModeDefault()
}
@RequiresFlagsEnabled(Flags.FLAG_ENHANCED_CONFIRMATION_MODE_APIS_ENABLED)
@Test
fun givenStoreAppThenIsNotRestrictedFromProtectedSetting() {
installPackageWithInstallSourceAndMetadataFromStore(APP_APK_NAME_LATEST)
+ waitForModeDefault()
runWithShellPermissionIdentity {
eventually { assertFalse(ecm.isRestricted(APP_PACKAGE_NAME, PROTECTED_SETTING)) }
}
@@ -101,6 +95,7 @@ class EnhancedConfirmationManagerTest : BaseUsePermissionTest() {
@Test
fun givenLocalAppThenIsRestrictedFromProtectedSetting() {
installPackageWithInstallSourceAndMetadataFromLocalFile(APP_APK_NAME_LATEST)
+ waitForModeDefault()
runWithShellPermissionIdentity {
eventually { assertTrue(ecm.isRestricted(APP_PACKAGE_NAME, PROTECTED_SETTING)) }
}
@@ -110,6 +105,7 @@ class EnhancedConfirmationManagerTest : BaseUsePermissionTest() {
@Test
fun givenDownloadedThenAppIsRestrictedFromProtectedSetting() {
installPackageWithInstallSourceAndMetadataFromDownloadedFile(APP_APK_NAME_LATEST)
+ waitForModeDefault()
runWithShellPermissionIdentity {
eventually { assertTrue(ecm.isRestricted(APP_PACKAGE_NAME, PROTECTED_SETTING)) }
}
@@ -119,6 +115,7 @@ class EnhancedConfirmationManagerTest : BaseUsePermissionTest() {
@Test
fun givenExplicitlyRestrictedAppThenIsRestrictedFromProtectedSetting() {
installPackageWithInstallSourceAndMetadataFromStore(APP_APK_NAME_LATEST)
+ waitForModeDefault()
eventually {
runWithShellPermissionIdentity {
assertEquals(
@@ -138,6 +135,7 @@ class EnhancedConfirmationManagerTest : BaseUsePermissionTest() {
@Test
fun givenRestrictedAppThenIsNotRestrictedFromNonProtectedSetting() {
installPackageWithInstallSourceAndMetadataFromDownloadedFile(APP_APK_NAME_LATEST)
+ waitForModeDefault()
runWithShellPermissionIdentity {
eventually { assertFalse(ecm.isRestricted(APP_PACKAGE_NAME, NON_PROTECTED_SETTING)) }
}
@@ -147,6 +145,7 @@ class EnhancedConfirmationManagerTest : BaseUsePermissionTest() {
@Test
fun givenRestrictedAppThenClearRestrictionNotAllowedByDefault() {
installPackageWithInstallSourceAndMetadataFromDownloadedFile(APP_APK_NAME_LATEST)
+ waitForModeDefault()
runWithShellPermissionIdentity {
eventually { assertFalse(ecm.isClearRestrictionAllowed(APP_PACKAGE_NAME)) }
}
@@ -156,6 +155,7 @@ class EnhancedConfirmationManagerTest : BaseUsePermissionTest() {
@Test
fun givenRestrictedAppWhenClearRestrictionThenNotRestrictedFromProtectedSetting() {
installPackageWithInstallSourceAndMetadataFromDownloadedFile(APP_APK_NAME_LATEST)
+ waitForModeDefault()
runWithShellPermissionIdentity {
eventually { assertTrue(ecm.isRestricted(APP_PACKAGE_NAME, PROTECTED_SETTING)) }
ecm.setClearRestrictionAllowed(APP_PACKAGE_NAME)
@@ -169,6 +169,7 @@ class EnhancedConfirmationManagerTest : BaseUsePermissionTest() {
@Test
fun createRestrictedSettingDialogIntentReturnsIntent() {
installPackageWithInstallSourceAndMetadataFromDownloadedFile(APP_APK_NAME_LATEST)
+ waitForModeDefault()
val intent = ecm.createRestrictedSettingDialogIntent(APP_PACKAGE_NAME, PROTECTED_SETTING)
@@ -181,6 +182,7 @@ class EnhancedConfirmationManagerTest : BaseUsePermissionTest() {
installPackageWithInstallSourceFromDownloadedFileAndAllowHardRestrictedPerms(
APP_APK_NAME_LATEST
)
+ waitForModeDefault()
val permissionAndExpectedGrantResults =
arrayOf(
GROUP_2_PERMISSION_1_RESTRICTED to false,
@@ -207,6 +209,7 @@ class EnhancedConfirmationManagerTest : BaseUsePermissionTest() {
installPackageWithInstallSourceFromDownloadedFileAndAllowHardRestrictedPerms(
APP_APK_NAME_LATEST
)
+ waitForModeDefault()
requestAppPermissionsAndAssertResult(
GROUP_3_PERMISSION_1_UNRESTRICTED to false,
@@ -236,6 +239,7 @@ class EnhancedConfirmationManagerTest : BaseUsePermissionTest() {
installPackageWithInstallSourceFromDownloadedFileAndAllowHardRestrictedPerms(
APP_APK_NAME_LATEST
)
+ waitForModeDefault()
requestAppPermissionsAndAssertResult(
GROUP_3_PERMISSION_1_UNRESTRICTED to true,
@@ -254,6 +258,7 @@ class EnhancedConfirmationManagerTest : BaseUsePermissionTest() {
installPackageWithInstallSourceFromDownloadedFileAndAllowHardRestrictedPerms(
APP_APK_NAME_LATEST
)
+ waitForModeDefault()
requestAppPermissionsAndAssertResult(
GROUP_4_PERMISSION_1_UNRESTRICTED to true,
@@ -287,6 +292,18 @@ class EnhancedConfirmationManagerTest : BaseUsePermissionTest() {
)
}
+ private fun waitForModeDefault() {
+ eventually {
+ runWithShellPermissionIdentity {
+ assertEquals(
+ "Timed out waiting for package mode to change to MODE_DEFAULT",
+ getAppEcmState(context, appOpsManager, APP_PACKAGE_NAME),
+ AppOpsManager.MODE_DEFAULT
+ )
+ }
+ }
+ }
+
companion object {
private const val GROUP_2_PERMISSION_1_RESTRICTED = Manifest.permission.SEND_SMS
private const val GROUP_2_PERMISSION_2_RESTRICTED = Manifest.permission.READ_SMS
@@ -294,7 +311,7 @@ class EnhancedConfirmationManagerTest : BaseUsePermissionTest() {
Manifest.permission.ACCESS_FINE_LOCATION
private const val GROUP_3_PERMISSION_2_UNRESTRICTED =
Manifest.permission.ACCESS_COARSE_LOCATION
- private const val GROUP_4_PERMISSION_1_UNRESTRICTED = Manifest.permission.BODY_SENSORS
+ private const val GROUP_4_PERMISSION_1_UNRESTRICTED = Manifest.permission.CAMERA
private const val NON_PROTECTED_SETTING = "example_setting_which_is_not_protected"
private const val PROTECTED_SETTING = "android:bind_accessibility_service"
diff --git a/tests/cts/permissionui/src/android/permissionui/cts/PermissionSplitTest.kt b/tests/cts/permissionui/src/android/permissionui/cts/PermissionSplitTest.kt
index e71ac32a5..4af2890ab 100644
--- a/tests/cts/permissionui/src/android/permissionui/cts/PermissionSplitTest.kt
+++ b/tests/cts/permissionui/src/android/permissionui/cts/PermissionSplitTest.kt
@@ -17,6 +17,8 @@
package android.permissionui.cts
import android.os.Build
+import android.permission.flags.Flags.FLAG_REPLACE_BODY_SENSOR_PERMISSION_ENABLED
+import android.platform.test.annotations.RequiresFlagsDisabled
import android.platform.test.flag.junit.DeviceFlagsValueProvider
import androidx.test.filters.FlakyTest
import androidx.test.filters.SdkSuppress
@@ -60,32 +62,80 @@ class PermissionSplitTest : BaseUsePermissionTest() {
testLocationPermissionSplit(false)
}
+ // TODO: b/388596433 - Update maxSdkVersion to VANILLA_ICE_CREAM after SDK bumps.
+ // TODO: b/383440585 - Remove this test when flag annotation issue is fixed.
@SdkSuppress(
minSdkVersion = Build.VERSION_CODES.TIRAMISU,
- maxSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM,
+ maxSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE,
)
@Test
- fun testBodySensorSplitOnTToV() {
+ fun testBodySensorSplitOnTToU() {
installPackage(APP_APK_PATH_31)
testBodySensorPermissionSplit(true)
}
+ // Before SDK_INT bumps to 36, the in-development B images are using SDK_INT=35(V). This will
+ // cause test failures on main builds where replaceBodySensor flag is enabled to remove Sensor
+ // group UI. As a workaround, we move SDK_INT=35 tests out and requires replaceBodySensor flag
+ // disabled when running on these images.
+ // TODO: b/388596433 - Update minSdkVersion to BAKLAVA after SDK bumps.
+ // TODO: b/383440585 - Update minSdkVersion to TIRAMISU when flag annotation issue is fixed.
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ @RequiresFlagsDisabled(FLAG_REPLACE_BODY_SENSOR_PERMISSION_ENABLED)
+ @Test
+ fun testBodySensorSplitPostV_replaceBodySensorFlagDisabled() {
+ installPackage(APP_APK_PATH_31)
+ testBodySensorPermissionSplit(true)
+ }
+
+ // TODO: b/388596433 - Update maxSdkVersion to VANILLA_ICE_CREAM after SDK bumps.
+ // TODO: b/383440585 - Remove this test when flag annotation issue is fixed.
@SdkSuppress(
minSdkVersion = Build.VERSION_CODES.TIRAMISU,
- maxSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM,
+ maxSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE,
)
@Test
- fun testBodySensorSplit32OnTToV() {
+ fun testBodySensorSplit32OnTToU() {
installPackage(APP_APK_PATH_32)
testBodySensorPermissionSplit(true)
}
+ // Before SDK_INT bumps to 36, the in-development B images are using SDK_INT=35(V). This will
+ // cause test failures on main builds where replaceBodySensor flag is enabled to remove Sensor
+ // group UI. As a workaround, we move SDK_INT=35 tests out and requires replaceBodySensor flag
+ // disabled when running on these images.
+ // TODO: b/388596433 - Update minSdkVersion to BAKLAVA after SDK bumps.
+ // TODO: b/383440585 - Update minSdkVersion to TIRAMISU when flag annotation issue is fixed.
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ @RequiresFlagsDisabled(FLAG_REPLACE_BODY_SENSOR_PERMISSION_ENABLED)
+ @Test
+ fun testBodySensorSplit32PostV_replaceBodySensorFlagDisabled() {
+ installPackage(APP_APK_PATH_32)
+ testBodySensorPermissionSplit(true)
+ }
+
+ // TODO: b/388596433 - Update maxSdkVersion to VANILLA_ICE_CREAM after SDK bumps.
+ // TODO: b/383440585 - Remove this test when flag annotation issue is fixed.
@SdkSuppress(
minSdkVersion = Build.VERSION_CODES.TIRAMISU,
- maxSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM,
+ maxSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE,
)
@Test
- fun testBodySensorNonSplitonTToV() {
+ fun testBodySensorNonSplitOnTToU() {
+ installPackage(APP_APK_PATH_LATEST)
+ testBodySensorPermissionSplit(false)
+ }
+
+ // Before SDK_INT bumps to 36, the in-development B images are using SDK_INT=35(V). This will
+ // cause test failures on main builds where replaceBodySensor flag is enabled to remove Sensor
+ // group UI. As a workaround, we move SDK_INT=35 tests out and requires replaceBodySensor flag
+ // disabled when running on these images.
+ // TODO: b/388596433 - Update minSdkVersion to BAKLAVA after SDK bumps.
+ // TODO: b/383440585 - Update minSdkVersion to TIRAMISU when flag annotation issue is fixed.
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ @RequiresFlagsDisabled(FLAG_REPLACE_BODY_SENSOR_PERMISSION_ENABLED)
+ @Test
+ fun testBodySensorNonSplitPostV_replaceBodySensorFlagDisabled() {
installPackage(APP_APK_PATH_LATEST)
testBodySensorPermissionSplit(false)
}
diff --git a/tests/cts/permissionui/src/android/permissionui/cts/PermissionTapjackingTest.kt b/tests/cts/permissionui/src/android/permissionui/cts/PermissionTapjackingTest.kt
index baebfe06f..68af60dde 100644
--- a/tests/cts/permissionui/src/android/permissionui/cts/PermissionTapjackingTest.kt
+++ b/tests/cts/permissionui/src/android/permissionui/cts/PermissionTapjackingTest.kt
@@ -52,14 +52,17 @@ class PermissionTapjackingTest : BaseUsePermissionTest() {
requestAppPermissionsForNoResult(ACCESS_FINE_LOCATION) {}
val buttonCenter =
- waitFindObject(By.text(getPermissionControllerString(ALLOW_FOREGROUND_BUTTON_TEXT))
- .displayId(displayId))
+ waitFindObject(
+ By.text(getPermissionControllerString(ALLOW_FOREGROUND_BUTTON_TEXT))
+ .displayId(displayId)
+ )
.visibleCenter
// Wait for overlay to hide the dialog
context.sendBroadcast(Intent(ACTION_SHOW_OVERLAY).putExtra(EXTRA_FULL_OVERLAY, true))
waitFindObject(
- By.res("android.permissionui.cts.usepermission:id/overlay").displayId(displayId))
+ By.res("android.permissionui.cts.usepermission:id/overlay").displayId(displayId)
+ )
tryClicking(buttonCenter)
}
@@ -76,18 +79,19 @@ class PermissionTapjackingTest : BaseUsePermissionTest() {
assertAppHasPermission(ACCESS_FINE_LOCATION, false)
requestAppPermissionsForNoResult(ACCESS_FINE_LOCATION) {}
- val foregroundButtonCenter =
- waitFindObject(By.text(getPermissionControllerString(ALLOW_FOREGROUND_BUTTON_TEXT))
- .displayId(displayId))
- .visibleCenter
val oneTimeButton =
- waitFindObjectOrNull(By.text(getPermissionControllerString(ALLOW_ONE_TIME_BUTTON_TEXT))
- .displayId(displayId))
+ waitFindObjectOrNull(
+ By.text(getPermissionControllerString(ALLOW_ONE_TIME_BUTTON_TEXT))
+ .displayId(displayId)
+ )
+
// If one-time button is not available, fallback to deny button
val overlayButtonBounds =
oneTimeButton?.visibleBounds
- ?: waitFindObject(By.text(getPermissionControllerString(DENY_BUTTON_TEXT))
- .displayId(displayId))
+ ?: waitFindObject(
+ By.text(getPermissionControllerString(DENY_BUTTON_TEXT))
+ .displayId(displayId)
+ )
.visibleBounds
// Wait for overlay to hide the dialog
@@ -100,7 +104,15 @@ class PermissionTapjackingTest : BaseUsePermissionTest() {
.putExtra(OVERLAY_BOTTOM, overlayButtonBounds.bottom)
)
waitFindObject(
- By.res("android.permissionui.cts.usepermission:id/overlay").displayId(displayId))
+ By.res("android.permissionui.cts.usepermission:id/overlay").displayId(displayId)
+ )
+
+ val foregroundButtonCenter =
+ waitFindObject(
+ By.text(getPermissionControllerString(ALLOW_FOREGROUND_BUTTON_TEXT))
+ .displayId(displayId)
+ )
+ .visibleCenter
tryClicking(foregroundButtonCenter)
}
@@ -119,7 +131,7 @@ class PermissionTapjackingTest : BaseUsePermissionTest() {
}
assertAppHasPermission(ACCESS_FINE_LOCATION, true)
},
- 10000
+ 10000,
)
} catch (e: RuntimeException) {
// expected
@@ -140,22 +152,26 @@ class PermissionTapjackingTest : BaseUsePermissionTest() {
}
assertAppHasPermission(ACCESS_FINE_LOCATION, true)
},
- 10000
+ 10000,
)
}
private fun click(buttonCenter: Point) {
- val downTime = SystemClock.uptimeMillis()
- val x= buttonCenter.x.toFloat()
- val y = buttonCenter.y.toFloat()
- var event = MotionEvent.obtain(downTime, downTime, MotionEvent.ACTION_DOWN,x , y, 0)
- event.displayId = displayId
- uiAutomation.injectInputEvent(event, true)
-
- val upTime = SystemClock.uptimeMillis()
- event = MotionEvent.obtain(upTime, upTime, MotionEvent.ACTION_UP, x, y, 0)
- event.displayId = displayId
- uiAutomation.injectInputEvent(event, true)
+ if (isAutomotiveVisibleBackgroundUser) {
+ val downTime = SystemClock.uptimeMillis()
+ val x = buttonCenter.x.toFloat()
+ val y = buttonCenter.y.toFloat()
+ var event = MotionEvent.obtain(downTime, downTime, MotionEvent.ACTION_DOWN, x, y, 0)
+ event.displayId = displayId
+ uiAutomation.injectInputEvent(event, true)
+
+ val upTime = SystemClock.uptimeMillis()
+ event = MotionEvent.obtain(upTime, upTime, MotionEvent.ACTION_UP, x, y, 0)
+ event.displayId = displayId
+ uiAutomation.injectInputEvent(event, true)
+ } else {
+ uiDevice.click(buttonCenter.x, buttonCenter.y)
+ }
}
companion object {
diff --git a/tests/cts/role/Android.bp b/tests/cts/role/Android.bp
index 9f1e6cff6..ea9af5d8e 100644
--- a/tests/cts/role/Android.bp
+++ b/tests/cts/role/Android.bp
@@ -37,7 +37,9 @@ android_test {
"bedstead-multiuser",
"flag-junit",
"platform-test-annotations",
+ "platform-test-rules",
"truth",
+ "uiautomator-helpers",
],
test_suites: [
@@ -48,9 +50,17 @@ android_test {
],
data: [
+ ":CtsDefaultNotesApp",
":CtsRoleTestApp",
":CtsRoleTestApp28",
":CtsRoleTestApp33WithoutInCallService",
":CtsRoleTestAppClone",
],
}
+
+filegroup {
+ name: "CtsRoleTestUtils",
+ srcs: [
+ "src/android/app/role/cts/RoleManagerUtil.kt",
+ ],
+}
diff --git a/tests/cts/role/AndroidManifest.xml b/tests/cts/role/AndroidManifest.xml
index a8c8c8e3d..7ea4287dc 100644
--- a/tests/cts/role/AndroidManifest.xml
+++ b/tests/cts/role/AndroidManifest.xml
@@ -22,6 +22,7 @@
<uses-permission android:name="android.permission.DISABLE_KEYGUARD" />
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
+ <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" />
<application>
diff --git a/tests/cts/role/AndroidTest.xml b/tests/cts/role/AndroidTest.xml
index 73f23dd1b..9a60b09e3 100644
--- a/tests/cts/role/AndroidTest.xml
+++ b/tests/cts/role/AndroidTest.xml
@@ -32,6 +32,8 @@
<target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
<option name="cleanup-apks" value="true" />
<option name="test-file-name" value="CtsRoleTestCases.apk" />
+ <option name="install-arg" value="-t" />
+ <option name="test-file-name" value="CtsDefaultNotesApp.apk" />
</target_preparer>
<target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
@@ -40,6 +42,7 @@
</target_preparer>
<target_preparer class="com.android.compatibility.common.tradefed.targetprep.FilePusher">
<option name="cleanup" value="true" />
+ <option name="push" value="CtsDefaultNotesApp.apk->/data/local/tmp/cts-role/CtsDefaultNotesApp.apk" />
<option name="push" value="CtsRoleTestApp.apk->/data/local/tmp/cts-role/CtsRoleTestApp.apk" />
<option name="push" value="CtsRoleTestApp28.apk->/data/local/tmp/cts-role/CtsRoleTestApp28.apk" />
<option name="push" value="CtsRoleTestApp33WithoutInCallService.apk->/data/local/tmp/cts-role/CtsRoleTestApp33WithoutInCallService.apk" />
diff --git a/tests/cts/role/src/android/app/role/cts/ChooseNoteRoleAppTest.kt b/tests/cts/role/src/android/app/role/cts/ChooseNoteRoleAppTest.kt
new file mode 100644
index 000000000..18003d1d9
--- /dev/null
+++ b/tests/cts/role/src/android/app/role/cts/ChooseNoteRoleAppTest.kt
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.role.cts
+
+import android.content.Intent
+import android.platform.test.rule.NotesRoleManagerRule
+import android.platform.uiautomatorhelpers.WaitUtils.ensureThat
+import android.provider.Settings
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.uiautomator.By.text
+import com.android.compatibility.common.util.UiAutomatorUtils2.getUiDevice
+import com.android.compatibility.common.util.UiAutomatorUtils2.waitFindObject
+import org.junit.After
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+
+class ChooseNoteRoleAppTest {
+
+ @[Rule JvmField]
+ val rule = NotesRoleManagerRule(requiredNotesRoleHolderPackage = NOTES_APP_PACKAGE_NAME)
+
+ @Before
+ fun setUp() {
+ rule.utils.clearRoleHolder()
+ InstrumentationRegistry.getInstrumentation()
+ .context
+ .startActivity(
+ Intent(Settings.ACTION_MANAGE_DEFAULT_APPS_SETTINGS)
+ .addCategory(Intent.CATEGORY_DEFAULT)
+ .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
+ )
+ }
+
+ @After
+ fun after() {
+ getUiDevice().pressHome()
+ }
+
+ @Test
+ fun chooseNoteRoleHolderApp() {
+ ensureThat { rule.utils.getRoleHolderPackageName().isEmpty() }
+
+ // Scroll to "Notes app" item in Default apps screen and click on it
+ waitFindObject(text("Notes app")).click()
+ // Scroll to "CtsDefaultNotesApp" item and click on it
+ waitFindObject(text("CtsDefaultNotesApp")).click()
+
+ assertEquals(rule.utils.getRoleHolderPackageName(), NOTES_APP_PACKAGE_NAME)
+ }
+
+ private companion object {
+ const val NOTES_APP_PACKAGE_NAME = "com.android.cts.notesapp"
+ }
+}
diff --git a/tests/cts/role/src/android/app/role/cts/RoleManagerTest.java b/tests/cts/role/src/android/app/role/cts/RoleManagerTest.java
index f26bc0eb5..c55fbf779 100644
--- a/tests/cts/role/src/android/app/role/cts/RoleManagerTest.java
+++ b/tests/cts/role/src/android/app/role/cts/RoleManagerTest.java
@@ -1402,16 +1402,6 @@ public class RoleManagerTest {
});
}
- @RequiresFlagsDisabled(com.android.permission.flags.Flags.FLAG_CROSS_USER_ROLE_ENABLED)
- @SdkSuppress(minSdkVersion = Build.VERSION_CODES.BAKLAVA, codeName = "Baklava")
- @Test
- public void cannotGetDefaultHoldersForTestFlagDisabled() throws Exception {
- runWithShellPermissionIdentity(() -> {
- assertThrows(IllegalStateException.class, () ->
- sRoleManager.getDefaultHoldersForTest(PROFILE_GROUP_EXCLUSIVE_ROLE_NAME));
- });
- }
-
@RequiresFlagsEnabled(com.android.permission.flags.Flags.FLAG_CROSS_USER_ROLE_ENABLED)
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.BAKLAVA, codeName = "Baklava")
@Test
@@ -1441,18 +1431,6 @@ public class RoleManagerTest {
});
}
- @RequiresFlagsDisabled(com.android.permission.flags.Flags.FLAG_CROSS_USER_ROLE_ENABLED)
- @SdkSuppress(minSdkVersion = Build.VERSION_CODES.BAKLAVA, codeName = "Baklava")
- @Test
- public void cannotSetDefaultHoldersForTestFlagDisabled() throws Exception {
- List<String> testRoleHolders = List.of("a", "b", "c");
- runWithShellPermissionIdentity(() -> {
- assertThrows(IllegalStateException.class, () ->
- sRoleManager.setDefaultHoldersForTest(PROFILE_GROUP_EXCLUSIVE_ROLE_NAME,
- testRoleHolders));
- });
- }
-
@RequiresFlagsEnabled(com.android.permission.flags.Flags.FLAG_CROSS_USER_ROLE_ENABLED)
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.BAKLAVA, codeName = "Baklava")
@Test
@@ -1528,16 +1506,6 @@ public class RoleManagerTest {
});
}
- @RequiresFlagsDisabled(com.android.permission.flags.Flags.FLAG_CROSS_USER_ROLE_ENABLED)
- @SdkSuppress(minSdkVersion = Build.VERSION_CODES.BAKLAVA, codeName = "Baklava")
- @Test
- public void cannotGetIsRoleVisibleForTestFlagDisabled() throws Exception {
- runWithShellPermissionIdentity(() -> {
- assertThrows(IllegalStateException.class, () ->
- sRoleManager.isRoleVisibleForTest(PROFILE_GROUP_EXCLUSIVE_ROLE_NAME));
- });
- }
-
@RequiresFlagsEnabled(com.android.permission.flags.Flags.FLAG_CROSS_USER_ROLE_ENABLED)
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.BAKLAVA, codeName = "Baklava")
@Test
@@ -1566,16 +1534,6 @@ public class RoleManagerTest {
});
}
- @RequiresFlagsDisabled(com.android.permission.flags.Flags.FLAG_CROSS_USER_ROLE_ENABLED)
- @SdkSuppress(minSdkVersion = Build.VERSION_CODES.BAKLAVA, codeName = "Baklava")
- @Test
- public void cannotSetRoleVisibleForTestFlagDisabled() throws Exception {
- runWithShellPermissionIdentity(() -> {
- assertThrows(IllegalStateException.class, () ->
- sRoleManager.setRoleVisibleForTest(PROFILE_GROUP_EXCLUSIVE_ROLE_NAME, false));
- });
- }
-
@RequiresFlagsEnabled(com.android.permission.flags.Flags.FLAG_CROSS_USER_ROLE_ENABLED)
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.BAKLAVA, codeName = "Baklava")
@Test
diff --git a/tests/cts/rolemultiuser/Android.bp b/tests/cts/rolemultiuser/Android.bp
index 7a49bc4e5..51eff83b9 100644
--- a/tests/cts/rolemultiuser/Android.bp
+++ b/tests/cts/rolemultiuser/Android.bp
@@ -24,6 +24,7 @@ android_test {
srcs: [
"src/**/*.kt",
+ ":CtsRoleTestUtils",
],
static_libs: [
diff --git a/tests/cts/rolemultiuser/TEST_MAPPING b/tests/cts/rolemultiuser/TEST_MAPPING
index 323e3094c..b45469e14 100644
--- a/tests/cts/rolemultiuser/TEST_MAPPING
+++ b/tests/cts/rolemultiuser/TEST_MAPPING
@@ -1,12 +1,17 @@
{
- "postsubmit": [
+ "presubmit": [
{
"name": "CtsRoleMultiUserTestCases"
}
],
- "mainline-postsubmit": [
+ "mainline-presubmit": [
{
"name": "CtsRoleMultiUserTestCases[com.google.android.permission.apex]"
}
+ ],
+ "permission-mainline-presubmit": [
+ {
+ "name": "CtsRoleMultiUserTestCases"
+ }
]
}
diff --git a/tests/cts/rolemultiuser/src/android/app/rolemultiuser/cts/RoleManagerMultiUserTest.kt b/tests/cts/rolemultiuser/src/android/app/rolemultiuser/cts/RoleManagerMultiUserTest.kt
index ee00c2c39..e8aaddf4c 100644
--- a/tests/cts/rolemultiuser/src/android/app/rolemultiuser/cts/RoleManagerMultiUserTest.kt
+++ b/tests/cts/rolemultiuser/src/android/app/rolemultiuser/cts/RoleManagerMultiUserTest.kt
@@ -16,6 +16,7 @@
package android.app.rolemultiuser.cts
import android.app.Activity
+import android.app.role.cts.RoleManagerUtil
import android.app.role.RoleManager
import android.content.ComponentName
import android.content.Context
@@ -70,6 +71,7 @@ import java.util.function.Consumer
import org.junit.After
import org.junit.Assert.assertThrows
import org.junit.Assume.assumeFalse
+import org.junit.Assume.assumeTrue
import org.junit.Before
import org.junit.ClassRule
import org.junit.Rule
@@ -88,6 +90,7 @@ class RoleManagerMultiUserTest {
@Before
@Throws(java.lang.Exception::class)
fun setUp() {
+ assumeTrue(RoleManagerUtil.isCddCompliantScreenSize());
installAppForAllUsers()
}
diff --git a/tests/utils/safetycenter/AndroidManifest.xml b/tests/utils/safetycenter/AndroidManifest.xml
index f0a4fcbb6..ce3724318 100644
--- a/tests/utils/safetycenter/AndroidManifest.xml
+++ b/tests/utils/safetycenter/AndroidManifest.xml
@@ -39,7 +39,6 @@
android:exported="false"/>
<activity android:name=".TestActivity"
- android:theme="@style/OptOutEdgeToEdgeEnforcement"
android:exported="false">
<intent-filter android:priority="-1">
<action android:name="com.android.safetycenter.testing.action.TEST_ACTIVITY"/>
diff --git a/tests/utils/safetycenter/res/layout/test_activity.xml b/tests/utils/safetycenter/res/layout/test_activity.xml
index edbe3641a..b0b7523c8 100644
--- a/tests/utils/safetycenter/res/layout/test_activity.xml
+++ b/tests/utils/safetycenter/res/layout/test_activity.xml
@@ -19,6 +19,7 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
+ android:fitsSystemWindows="true"
android:orientation="vertical" >
<Button android:id="@+id/button"
android:layout_width="wrap_content"
diff --git a/tests/utils/safetycenter/res/values/styles.xml b/tests/utils/safetycenter/res/values/styles.xml
deleted file mode 100644
index ce54568ed..000000000
--- a/tests/utils/safetycenter/res/values/styles.xml
+++ /dev/null
@@ -1,24 +0,0 @@
-<?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.
--->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android">
- <!--
- TODO(b/309578419): Make activities handle insets properly and then remove this.
- -->
- <style name="OptOutEdgeToEdgeEnforcement">
- <item name="android:windowOptOutEdgeToEdgeEnforcement">true</item>
- </style>
-</resources>