diff options
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"><br><br></xliff:g>Moguće je da aplikacija neće pravilno funkcionirati bez tog uskraćenog dopuštenja. <a href=<xliff:g id="LEARN_MORE_LINK">%1$s</xliff:g>>Saznajte kako omogućiti pristup</a>"</string> <string name="enhanced_confirmation_dialog_title_role" msgid="1737023798483772780">"Aplikaciji je uskraćeno da bude zadana <xliff:g id="ROLE_NAME">%1$s</xliff:g>"</string> 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> |