Merge "Catch UnsupportedOperationException of startLegacyVpn" into main am: 485e8800ed am: 16fd898705
Original change: https://android-review.googlesource.com/c/platform/packages/apps/Settings/+/2822270
Change-Id: Ife7070b102904af0c5eb21bf3a899de9b01d79ef
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
diff --git a/Android.bp b/Android.bp
index 9680b8b..d8ca75e 100644
--- a/Android.bp
+++ b/Android.bp
@@ -70,6 +70,7 @@
"androidx.appcompat_appcompat",
"androidx.cardview_cardview",
"androidx.compose.runtime_runtime-livedata",
+ "androidx.activity_activity-ktx",
"androidx.preference_preference",
"androidx.recyclerview_recyclerview",
"androidx.window_window",
@@ -83,6 +84,8 @@
"net-utils-framework-common",
"app-usage-event-protos-lite",
"battery-event-protos-lite",
+ "battery-usage-slot-protos-lite",
+ "power-anomaly-event-protos-lite",
"settings-contextual-card-protos-lite",
"settings-log-bridge-protos-lite",
"settings-telephony-protos-lite",
@@ -150,14 +153,17 @@
srcs: ["proguard.flags"],
}
-// The sources for Settings need to be exposed to SettingsGoogle, etc.
-// so they can run the com.android.settingslib.search.IndexableProcessor
-// over all the sources together.
+// Deprecated. The sources for Settings need to be exposed to ArcSettings, so they can run the
+// com.android.settingslib.search.IndexableProcessor over all the sources together.
+// Use "-Acom.android.settingslib.search.processor.package=" instead to generate the search data
+// separately for different modules.
filegroup {
name: "Settings_srcs",
srcs: ["src/**/*.java", "src/**/*.kt"],
}
+// Deprecated. Do not depend on this, only depend on Settings-core, and its manifest is also
+// included.
filegroup {
name: "Settings_manifest",
srcs: ["AndroidManifest.xml"],
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index f82f970..a405032 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -33,6 +33,7 @@
<uses-permission android:name="android.permission.HARDWARE_TEST" />
<uses-permission android:name="android.permission.CALL_PHONE" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
+ <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED" />
<uses-permission android:name="android.permission.QUERY_AUDIO_STATE" />
<uses-permission android:name="android.permission.MASTER_CLEAR" />
<uses-permission android:name="com.google.android.googleapps.permission.GOOGLE_AUTH" />
@@ -658,7 +659,7 @@
<activity android:name="Settings$FaceSettingsActivity"
android:label="@string/security_settings_face_preference_title"
android:exported="true"
- android:configChanges="orientation|screenSize"
+ android:theme="@style/Theme.Settings.NoActionBar"
android:icon="@drawable/ic_face_header">
<intent-filter>
<action android:name="android.settings.FACE_SETTINGS" />
@@ -673,8 +674,8 @@
<activity android:name="Settings$FaceSettingsInternalActivity"
android:label="@string/security_settings_face_preference_title"
android:exported="false"
+ android:theme="@style/Theme.Settings.NoActionBar"
android:icon="@drawable/ic_face_header"
- android:configChanges="orientation|screenSize"
android:taskAffinity="com.android.settings.root">
<meta-data android:name="com.android.settings.FRAGMENT_CLASS"
android:value="com.android.settings.biometrics.face.FaceSettings" />
@@ -864,6 +865,7 @@
<activity
android:name="Settings$LongBackgroundTasksActivity"
+ android:knownActivityEmbeddingCerts="@array/config_known_host_certs"
android:exported="true"
android:label="@string/long_background_tasks_label">
<intent-filter android:priority="1">
@@ -1545,6 +1547,7 @@
<activity
android:name="Settings$ManageApplicationsActivity"
+ android:knownActivityEmbeddingCerts="@array/config_known_host_certs"
android:exported="true"
android:label="@string/applications_settings">
<intent-filter android:priority="1">
@@ -1577,6 +1580,33 @@
android:value="@string/menu_key_apps"/>
</activity-alias>
+ <activity android:name="Settings$UserAspectRatioAppListActivity"
+ android:exported="true"
+ android:label="@string/aspect_ratio_experimental_title">
+ <intent-filter android:priority="1">
+ <action android:name="android.settings.MANAGE_USER_ASPECT_RATIO_SETTINGS"/>
+ <category android:name="android.intent.category.DEFAULT" />
+ </intent-filter>
+ <meta-data android:name="com.android.settings.FRAGMENT_CLASS"
+ android:value="com.android.settings.applications.manageapplications.ManageApplications" />
+ <meta-data android:name="com.android.settings.HIGHLIGHT_MENU_KEY"
+ android:value="@string/menu_key_apps"/>
+ </activity>
+
+ <activity android:name="Settings$UserAspectRatioAppActivity"
+ android:exported="true"
+ android:label="@string/aspect_ratio_experimental_title">
+ <intent-filter>
+ <action android:name="android.settings.MANAGE_USER_ASPECT_RATIO_SETTINGS" />
+ <category android:name="android.intent.category.DEFAULT" />
+ <data android:scheme="package" />
+ </intent-filter>
+ <meta-data android:name="com.android.settings.FRAGMENT_CLASS"
+ android:value="com.android.settings.applications.appcompat.UserAspectRatioDetails" />
+ <meta-data android:name="com.android.settings.HIGHLIGHT_MENU_KEY"
+ android:value="@string/menu_key_apps"/>
+ </activity>
+
<activity
android:name="Settings$ManageDomainUrlsActivity"
android:exported="true"
@@ -1607,6 +1637,7 @@
<activity
android:name="Settings$HighPowerApplicationsActivity"
+ android:knownActivityEmbeddingCerts="@array/config_known_host_certs"
android:exported="true"
android:label="@string/high_power_apps">
<intent-filter android:priority="1">
@@ -1665,6 +1696,7 @@
This is for compatibility with old shortcuts. -->
<activity-alias android:name=".RunningServices"
android:label="@string/runningservices_settings_title"
+ android:knownActivityEmbeddingCerts="@array/config_known_host_certs"
android:exported="true"
android:targetActivity="Settings$ManageApplicationsActivity">
<meta-data android:name="com.android.settings.FRAGMENT_CLASS"
@@ -1677,6 +1709,7 @@
This is for compatibility with old shortcuts. -->
<activity-alias android:name=".applications.StorageUse"
android:label="@string/storageuse_settings_title"
+ android:knownActivityEmbeddingCerts="@array/config_known_host_certs"
android:exported="true"
android:targetActivity="Settings$ManageApplicationsActivity">
<meta-data android:name="com.android.settings.FRAGMENT_CLASS"
@@ -1743,6 +1776,7 @@
<!-- Provide direct entry into manage apps showing running services. -->
<activity android:name="Settings$RunningServicesActivity"
android:exported="true"
+ android:knownActivityEmbeddingCerts="@array/config_known_host_certs"
android:label="@string/runningservices_settings_title">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
@@ -1761,6 +1795,7 @@
<!-- Provide direct entry into manage apps showing storage usage of apps. -->
<activity
android:name="Settings$StorageUseActivity"
+ android:knownActivityEmbeddingCerts="@array/config_known_host_certs"
android:exported="true"
android:label="@string/storageuse_settings_title">
<intent-filter android:priority="1">
@@ -2105,6 +2140,7 @@
<activity
android:name="Settings$UsageAccessSettingsActivity"
+ android:knownActivityEmbeddingCerts="@array/config_known_host_certs"
android:exported="true"
android:label="@string/usage_access_title">
<intent-filter android:priority="1">
@@ -2389,6 +2425,8 @@
<intent-filter android:priority="1">
<action android:name="android.app.action.CONFIRM_DEVICE_CREDENTIAL" />
<action android:name="android.app.action.CONFIRM_FRP_CREDENTIAL" />
+ <action android:name="android.app.action.PREPARE_REPAIR_MODE_DEVICE_CREDENTIAL" />
+ <action android:name="android.app.action.CONFIRM_REPAIR_MODE_DEVICE_CREDENTIAL" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
@@ -3240,6 +3278,7 @@
android:name="Settings$DataUsageSummaryActivity"
android:label="@string/data_usage_summary_title"
android:exported="true"
+ android:enabled="@bool/config_show_sim_info"
android:icon="@drawable/ic_homepage_data_usage">
<intent-filter android:priority="1">
<action android:name="android.settings.DATA_USAGE_SETTINGS" />
@@ -3466,6 +3505,7 @@
<activity
android:name="Settings$TurnScreenOnSettingsActivity"
android:exported="true"
+ android:knownActivityEmbeddingCerts="@array/config_known_host_certs"
android:label="@string/turn_screen_on_title">
<intent-filter android:priority="1">
<action android:name="android.settings.TURN_SCREEN_ON_SETTINGS" />
@@ -3645,6 +3685,7 @@
<activity android:name="Settings$NotificationAppListActivity"
android:label="@string/app_notifications_title"
android:icon="@drawable/ic_notifications"
+ android:knownActivityEmbeddingCerts="@array/config_known_host_certs"
android:exported="true">
<intent-filter android:priority="1">
<action android:name="android.settings.ALL_APPS_NOTIFICATION_SETTINGS" />
@@ -3659,6 +3700,7 @@
<!-- Displays a list of apps available for cloning on the device -->
<activity android:name=".Settings$ClonedAppsListActivity"
android:label="@string/cloned_apps_dashboard_title"
+ android:knownActivityEmbeddingCerts="@array/config_known_host_certs"
android:exported="true">
<intent-filter android:priority="1">
<action android:name="android.settings.MANAGE_CLONED_APPS_SETTINGS" />
@@ -3731,6 +3773,7 @@
<!-- Show regulatory info (from settings item or dialing "*#07#") -->
<activity
android:name="RegulatoryInfoDisplayActivity"
+ android:theme="@style/Theme.AlertDialog"
android:label="@string/regulatory_labels"
android:exported="true"
android:enabled="@bool/config_show_regulatory_info">
@@ -3924,6 +3967,7 @@
<activity
android:name="Settings$OverlaySettingsActivity"
+ android:knownActivityEmbeddingCerts="@array/config_known_host_certs"
android:exported="true"
android:label="@string/draw_overlay">
<intent-filter android:priority="1">
@@ -3961,6 +4005,7 @@
<activity
android:name="Settings$WriteSettingsActivity"
+ android:knownActivityEmbeddingCerts="@array/config_known_host_certs"
android:exported="true"
android:label="@string/write_settings_title">
<intent-filter android:priority="1">
@@ -3994,6 +4039,7 @@
<activity
android:name="Settings$AlarmsAndRemindersActivity"
android:exported="true"
+ android:knownActivityEmbeddingCerts="@array/config_known_host_certs"
android:label="@string/alarms_and_reminders_label">
<intent-filter android:priority="1">
<action android:name="android.settings.REQUEST_SCHEDULE_EXACT_ALARM" />
@@ -4024,6 +4070,7 @@
<activity
android:name="Settings$ManageExternalSourcesActivity"
+ android:knownActivityEmbeddingCerts="@array/config_known_host_certs"
android:exported="true"
android:label="@string/install_other_apps">
<intent-filter android:priority="1">
@@ -4078,6 +4125,7 @@
<activity
android:name="Settings$ManageExternalStorageActivity"
+ android:knownActivityEmbeddingCerts="@array/config_known_host_certs"
android:exported="true"
android:label="@string/manage_external_storage_title">
<intent-filter android:priority="1">
@@ -4109,6 +4157,7 @@
<activity
android:name="Settings$MediaManagementAppsActivity"
+ android:knownActivityEmbeddingCerts="@array/config_known_host_certs"
android:exported="true"
android:label="@string/media_management_apps_title">
<intent-filter android:priority="1">
@@ -4804,7 +4853,7 @@
</activity>
<activity android:name="Settings$FactoryResetActivity"
- android:permission="android.permission.BACKUP"
+ android:permission="android.permission.MASTER_CLEAR"
android:label="@string/main_clear_title"
android:exported="true"
android:theme="@style/SudThemeGlif.Light">
@@ -4864,10 +4913,25 @@
<activity
android:name=".spa.SpaActivity"
android:configChanges="orientation|screenLayout|screenSize|smallestScreenSize"
+ android:knownActivityEmbeddingCerts="@array/config_known_host_certs"
android:exported="false" />
<activity android:name=".spa.SpaBridgeActivity" android:exported="false"/>
<activity android:name=".spa.SpaAppBridgeActivity" android:exported="false"/>
+ <activity android:name=".Settings$FingerprintSettingsActivityV2"
+ android:label="@string/security_settings_fingerprint_preference_title"
+ android:exported="false"
+ android:icon="@drawable/ic_fingerprint_header">
+ <intent-filter>
+ <action android:name="android.settings.FINGERPRINT_SETTINGS_V2" />
+ <category android:name="android.intent.category.DEFAULT" />
+ </intent-filter>
+ <meta-data android:name="com.android.settings.FRAGMENT_CLASS"
+ android:value="com.android.settings.biometrics.fingerprint2.ui.fragment.FingerprintSettingsV2Fragment" />
+ <meta-data android:name="com.android.settings.HIGHLIGHT_MENU_KEY"
+ android:value="@string/menu_key_security"/>
+ </activity>
+
<activity-alias android:name="UsageStatsActivity"
android:exported="true"
android:label="@string/testing_usage_stats"
diff --git a/protos/fuelgauge_log.proto b/protos/fuelgauge_log.proto
index 150c2e2..e75ca48 100644
--- a/protos/fuelgauge_log.proto
+++ b/protos/fuelgauge_log.proto
@@ -5,13 +5,12 @@
option java_package = "com.android.settings.fuelgauge";
option java_outer_classname = "FuelgaugeLogProto";
-// Stores history of setting optimize mode
+// Store history of setting optimize mode
message BatteryOptimizeHistoricalLog {
repeated BatteryOptimizeHistoricalLogEntry log_entry = 1;
}
message BatteryOptimizeHistoricalLogEntry {
-
// The action to set optimize mode
enum Action {
UNKNOWN = 0;
@@ -28,3 +27,25 @@
optional string action_description = 3;
optional int64 timestamp = 4;
}
+
+
+// Store history of battery usage periodic job
+message BatteryUsageHistoricalLog {
+ repeated BatteryUsageHistoricalLogEntry log_entry = 1;
+}
+
+message BatteryUsageHistoricalLogEntry {
+ // The action to record battery usage job event
+ enum Action {
+ UNKNOWN = 0;
+ SCHEDULE_JOB = 1;
+ EXECUTE_JOB = 2;
+ RECHECK_JOB = 3;
+ FETCH_USAGE_DATA = 4;
+ INSERT_USAGE_DATA = 5;
+ }
+
+ optional int64 timestamp = 1;
+ optional Action action = 2;
+ optional string action_description = 3;
+}
diff --git a/res-product/values-am/strings.xml b/res-product/values-am/strings.xml
index fb2eb91..21b8107 100644
--- a/res-product/values-am/strings.xml
+++ b/res-product/values-am/strings.xml
@@ -58,12 +58,9 @@
<string name="security_settings_face_enroll_introduction_consent_message_0" product="default" msgid="9086377203303858619">"ልጅዎ ስልካቸውን ለመክፈት ፊታቸውን እንዲጠቀሙ ይፍቀዱ"</string>
<string name="security_settings_face_enroll_introduction_consent_message_0" product="tablet" msgid="4560949471246282574">"ልጅዎ ጡባዊያቸውን ለመክፈት ፊታቸውን እንዲጠቀሙ ይፍቀዱ"</string>
<string name="security_settings_face_enroll_introduction_consent_message_0" product="device" msgid="1156063265854416046">"ልጅዎ መሣሪያቸውን ለመክፈት ፊታቸውን እንዲጠቀሙ ይፍቀዱ"</string>
- <!-- no translation found for security_settings_face_enroll_introduction_consent_message_0_class3 (5082581184108528408) -->
- <skip />
- <!-- no translation found for security_settings_face_enroll_introduction_consent_message_0_class3 (5932555218164668532) -->
- <skip />
- <!-- no translation found for security_settings_face_enroll_introduction_consent_message_0_class3 (8943878265098867810) -->
- <skip />
+ <string name="security_settings_face_enroll_introduction_consent_message_0_class3" product="default" msgid="5082581184108528408">"ልጅዎ ስልካቸውን ለመክፈት ወይም እነሱ መሆናቸውን ለማረጋገጥ መልካቸውን እንዲጠቀሙ ይፍቀዱ። ይህ የሚሆነው ወደ መተግበሪያዎች በመለያ ሲገቡ፣ ግዢን ሲያጸድቁ እና ሌሎችንም ሲያደርጉ ነው።"</string>
+ <string name="security_settings_face_enroll_introduction_consent_message_0_class3" product="tablet" msgid="5932555218164668532">"ልጅዎ ጡባዊያቸውን ለመክፈት ወይም እነሱ መሆናቸውን ለማረጋገጥ መልካቸውን እንዲጠቀሙ ይፍቀዱ። ይህ የሚሆነው ወደ መተግበሪያዎች በመለያ ሲገቡ፣ ግዢን ሲያጸድቁ እና ሌሎችንም ሲያደርጉ ነው።"</string>
+ <string name="security_settings_face_enroll_introduction_consent_message_0_class3" product="device" msgid="8943878265098867810">"ልጅዎ መሣሪያቸውን ለመክፈት ወይም እነሱ መሆናቸውን ለማረጋገጥ መልካቸውን እንዲጠቀሙ ይፍቀዱ። ይህ የሚሆነው ወደ መተግበሪያዎች በመለያ ሲገቡ፣ ግዢን ሲያጸድቁ እና ሌሎችንም ሲያደርጉ ነው።"</string>
<string name="security_settings_face_enroll_introduction_consent_message" product="default" msgid="3698558920963989416">"የልጅዎን ስልክ ለመክፈት መልካቸውን መጠቀሙ ከጠንካራ ስርዓተ ጥለት ወይም ፒን ያነሰ ደህንነት ሊኖረው ይችላል።"</string>
<string name="security_settings_face_enroll_introduction_consent_message" product="tablet" msgid="2689983368730833505">"የልጅዎን ጡባዊ ለመክፈት መልካቸውን መጠቀሙ ከጠንካራ ስርዓተ ጥለት ወይም ፒን ያነሰ ደህንነት ሊኖረው ይችላል።"</string>
<string name="security_settings_face_enroll_introduction_consent_message" product="device" msgid="5768077532130409820">"የልጅዎን መሣሪያ ለመክፈት መልካቸውን መጠቀሙ ከጠንካራ ስርዓተ ጥለት ወይም ፒን ያነሰ ደህንነት ሊኖረው ይችላል።"</string>
diff --git a/res-product/values-as/strings.xml b/res-product/values-as/strings.xml
index a86cc9c..e047813 100644
--- a/res-product/values-as/strings.xml
+++ b/res-product/values-as/strings.xml
@@ -125,7 +125,7 @@
<string name="security_settings_fingerprint_v2_enroll_introduction_footer_message_consent_4" product="default" msgid="5003753461032107715">"আপুনি আৰু আপোনাৰ শিশুৱে যিকোনো সময়তে ছেটিঙলৈ গৈ তেওঁৰ ফিংগাৰপ্ৰিণ্টৰ প্ৰতিচ্ছবি আৰু মডেলটো মচিব পাৰে অথবা ফিংগাৰপ্ৰিণ্টৰ দ্বাৰা আনলক কৰাৰ সুবিধাটো অফ কৰিব পাৰে। ফিংগাৰপ্ৰিণ্টৰ প্ৰতিচ্ছবি আৰু মডেলসমূহ মচি নেপেলোৱালৈকে ফ’নটোত ষ্ট’ৰ হৈ থাকে।"</string>
<string name="security_settings_fingerprint_v2_enroll_introduction_footer_message_consent_4" product="tablet" msgid="8772005555323461143">"আপুনি আৰু আপোনাৰ শিশুৱে যিকোনো সময়তে ছেটিঙলৈ গৈ তেওঁৰ ফিংগাৰপ্ৰিণ্টৰ প্ৰতিচ্ছবি আৰু মডেলটো মচিব পাৰে অথবা ফিংগাৰপ্ৰিণ্ট আনলকৰ সুবিধাটো অফ কৰিব পাৰে। ফিংগাৰপ্ৰিণ্টৰ প্ৰতিচ্ছবি আৰু মডেলসমূহ মচি নেপেলোৱালৈকে টেবলেটোত ষ্ট’ৰ হৈ থাকে।"</string>
<string name="security_settings_fingerprint_v2_enroll_introduction_footer_message_consent_4" product="device" msgid="7254955922685507093">"আপুনি আৰু আপোনাৰ শিশুৱে যিকোনো সময়তে ছেটিঙলৈ গৈ তেওঁৰ ফিংগাৰপ্ৰিণ্টৰ প্ৰতিচ্ছবি আৰু মডেলটো মচিব পাৰে অথবা ফিংগাৰপ্ৰিণ্ট আনলকৰ সুবিধাটো অফ কৰিব পাৰে। ফিংগাৰপ্ৰিণ্টৰ প্ৰতিচ্ছবি আৰু মডেলসমূহ মচি নেপেলোৱালৈকে ডিভাইচটোত ষ্ট’ৰ হৈ থাকে।"</string>
- <string name="security_settings_fingerprint_v2_enroll_introduction_footer_message_5" product="default" msgid="6272159089589340181">"আপুনি নিবিচাৰিলেও আপোনাৰ ফ’নটো আনলক হ\'ব পাৰে, যেনে কোনোবাই এইটো আপোনাৰ আঙুলিৰ আগত দাঙি ধৰিলে।"</string>
+ <string name="security_settings_fingerprint_v2_enroll_introduction_footer_message_5" product="default" msgid="6272159089589340181">"আপুনি নিবিচাৰিলেও আপোনাৰ ফ’নটো আনলক হ\'ব পাৰে, যেনে কোনোবাই এইটো আপোনাৰ আঙুলিত স্পৰ্শ কৰালে।"</string>
<string name="security_settings_fingerprint_v2_enroll_introduction_footer_message_5" product="tablet" msgid="2420109998272019149">"আপুনি নিবিচাৰিলেও আপোনাৰ টেবেলেটটো আনলক হ’ব পাৰে, যেনে কোনোবাই এইটো আপোনাৰ আঙুলিত লগাই দিলে।"</string>
<string name="security_settings_fingerprint_v2_enroll_introduction_footer_message_5" product="device" msgid="5915844445830045866">"আপুনি নিবিচাৰিলেও আপোনাৰ ডিভাইচটো আনলক হ’ব পাৰে, যেনে কোনোবাই এইটো আপোনাৰ আঙুলিত লগাই দিলে।"</string>
<string name="security_settings_fingerprint_v2_enroll_introduction_footer_message_consent_5" product="default" msgid="6556725426776167791">"তেওঁ নিবিচাৰিলেও আপোনাৰ শিশুৰ ফ’নটো আনলক কৰিব পৰা যায়, যেনে কোনোবাই এইটো তেওঁৰ আঙুলিত লগাই দিলে।"</string>
diff --git a/res-product/values-b+sr+Latn/strings.xml b/res-product/values-b+sr+Latn/strings.xml
index cf0868a..ae4231d 100644
--- a/res-product/values-b+sr+Latn/strings.xml
+++ b/res-product/values-b+sr+Latn/strings.xml
@@ -58,12 +58,9 @@
<string name="security_settings_face_enroll_introduction_consent_message_0" product="default" msgid="9086377203303858619">"Dozvolite detetu da koristi otključavanje telefona licem"</string>
<string name="security_settings_face_enroll_introduction_consent_message_0" product="tablet" msgid="4560949471246282574">"Dozvolite detetu da koristi otključavanje tableta licem"</string>
<string name="security_settings_face_enroll_introduction_consent_message_0" product="device" msgid="1156063265854416046">"Dozvolite detetu da koristi otključavanje uređaja licem"</string>
- <!-- no translation found for security_settings_face_enroll_introduction_consent_message_0_class3 (5082581184108528408) -->
- <skip />
- <!-- no translation found for security_settings_face_enroll_introduction_consent_message_0_class3 (5932555218164668532) -->
- <skip />
- <!-- no translation found for security_settings_face_enroll_introduction_consent_message_0_class3 (8943878265098867810) -->
- <skip />
+ <string name="security_settings_face_enroll_introduction_consent_message_0_class3" product="default" msgid="5082581184108528408">"Dozvolite detetu da koristi lice za otključavanje telefona ili potvrdu identiteta. Ovo se dešava kada se prijavljuje u aplikacije, odobrava kupovinu i u drugim situacijama."</string>
+ <string name="security_settings_face_enroll_introduction_consent_message_0_class3" product="tablet" msgid="5932555218164668532">"Dozvolite detetu da koristi lice za otključavanje tableta ili potvrdu identiteta. Ovo se dešava kada se prijavljuje u aplikacije, odobrava kupovinu i u drugim situacijama."</string>
+ <string name="security_settings_face_enroll_introduction_consent_message_0_class3" product="device" msgid="8943878265098867810">"Dozvolite detetu da koristi lice za otključavanje uređaja ili potvrdu identiteta. Ovo se dešava kada se prijavljuje u aplikacije, odobrava kupovinu i u drugim situacijama."</string>
<string name="security_settings_face_enroll_introduction_consent_message" product="default" msgid="3698558920963989416">"Korišćenje lica deteta za otključavanje telefona može da bude manje bezbedno od jakog šablona ili PIN-a."</string>
<string name="security_settings_face_enroll_introduction_consent_message" product="tablet" msgid="2689983368730833505">"Korišćenje lica deteta za otključavanje tableta može da bude manje bezbedno od jakog šablona ili PIN-a."</string>
<string name="security_settings_face_enroll_introduction_consent_message" product="device" msgid="5768077532130409820">"Korišćenje lica deteta za otključavanje uređaja može da bude manje bezbedno od jakog šablona ili PIN-a."</string>
@@ -208,9 +205,9 @@
<string name="biometrics_unlock_title" product="device" msgid="3342994085226864170">"Možete da otključate uređaj pomoću lica ili otiska prsta. Ova opcija zahteva rezervni metod zaključavanja ekrana iz bezbednosnih razloga."</string>
<string name="encrypt_title" product="tablet" msgid="8915795247786124547">"Šifruj tablet"</string>
<string name="encrypt_title" product="default" msgid="511146128799853404">"Šifrovanje telefona"</string>
- <string name="suggested_lock_settings_summary" product="tablet" msgid="8821254377043173267">"Podesite zaključavanje ekrana da biste zaštitili tablet"</string>
- <string name="suggested_lock_settings_summary" product="device" msgid="4863929838844014122">"Podesite zaključavanje ekrana da biste zaštitili uređaj"</string>
- <string name="suggested_lock_settings_summary" product="default" msgid="8050809409337082738">"Podesite zaključavanje ekrana da biste zaštitili telefon"</string>
+ <string name="suggested_lock_settings_summary" product="tablet" msgid="8821254377043173267">"Podesite otključavanje ekrana da biste zaštitili tablet"</string>
+ <string name="suggested_lock_settings_summary" product="device" msgid="4863929838844014122">"Podesite otključavanje ekrana da biste zaštitili uređaj"</string>
+ <string name="suggested_lock_settings_summary" product="default" msgid="8050809409337082738">"Podesite otključavanje ekrana da biste zaštitili telefon"</string>
<string name="suggested_fingerprint_lock_settings_summary" product="tablet" msgid="8565330205932332157"></string>
<string name="suggested_fingerprint_lock_settings_summary" product="device" msgid="8565330205932332157"></string>
<string name="suggested_fingerprint_lock_settings_summary" product="default" msgid="8565330205932332157"></string>
diff --git a/res-product/values-bg/strings.xml b/res-product/values-bg/strings.xml
index e454e1c..044f24b 100644
--- a/res-product/values-bg/strings.xml
+++ b/res-product/values-bg/strings.xml
@@ -58,12 +58,9 @@
<string name="security_settings_face_enroll_introduction_consent_message_0" product="default" msgid="9086377203303858619">"Разрешете на детето си да използва лицето си, за да отключва телефона си"</string>
<string name="security_settings_face_enroll_introduction_consent_message_0" product="tablet" msgid="4560949471246282574">"Разрешете на детето си да използва лицето си, за да отключва таблета си"</string>
<string name="security_settings_face_enroll_introduction_consent_message_0" product="device" msgid="1156063265854416046">"Разрешете на детето си да използва лицето си, за да отключва устройството си"</string>
- <!-- no translation found for security_settings_face_enroll_introduction_consent_message_0_class3 (5082581184108528408) -->
- <skip />
- <!-- no translation found for security_settings_face_enroll_introduction_consent_message_0_class3 (5932555218164668532) -->
- <skip />
- <!-- no translation found for security_settings_face_enroll_introduction_consent_message_0_class3 (8943878265098867810) -->
- <skip />
+ <string name="security_settings_face_enroll_introduction_consent_message_0_class3" product="default" msgid="5082581184108528408">"Разрешете на детето си да използва своето лице, за да отключва телефона или да потвърждава самоличността си, например при влизане в приложения, одобряване на покупки и др."</string>
+ <string name="security_settings_face_enroll_introduction_consent_message_0_class3" product="tablet" msgid="5932555218164668532">"Разрешете на детето си да използва своето лице, за да отключва таблета си или да потвърждава самоличността си, например при влизане в приложения, одобряване на покупки и др."</string>
+ <string name="security_settings_face_enroll_introduction_consent_message_0_class3" product="device" msgid="8943878265098867810">"Разрешете на детето си да използва лицето си, за да отключва устройството си или да потвърждава самоличността си, например при влизане в приложения, одобряване на покупки и др."</string>
<string name="security_settings_face_enroll_introduction_consent_message" product="default" msgid="3698558920963989416">"Отключването на телефона на детето ви с лице може да е по-малко сигурно в сравнение с надеждни фигура или PIN код."</string>
<string name="security_settings_face_enroll_introduction_consent_message" product="tablet" msgid="2689983368730833505">"Отключването на таблета на детето ви с лице може да е по-малко сигурно в сравнение с надеждни фигура или PIN код."</string>
<string name="security_settings_face_enroll_introduction_consent_message" product="device" msgid="5768077532130409820">"Отключването на устройството на детето ви с лице може да е по-малко сигурно в сравнение с надеждни фигура или PIN код."</string>
diff --git a/res-product/values-bs/strings.xml b/res-product/values-bs/strings.xml
index 25fc9b2..ff8382c 100644
--- a/res-product/values-bs/strings.xml
+++ b/res-product/values-bs/strings.xml
@@ -94,18 +94,18 @@
<string name="security_settings_face_enroll_introduction_control_consent_message" product="default" msgid="6983939010814873996"></string>
<string name="security_settings_face_enroll_introduction_control_consent_message" product="tablet" msgid="6983939010814873996"></string>
<string name="security_settings_face_enroll_introduction_control_consent_message" product="device" msgid="6983939010814873996"></string>
- <string name="security_settings_face_settings_footer" product="default" msgid="3036403896485044957">"Koristite lice da otključate telefon ili izvršite autentifikaciju u aplikacijama, npr. kada se prijavljujete ili odobravate kupovinu.\n\nImajte na umu:\nistovremeno možete imati postavljeno samo jedno lice. Da dodate drugo lice, izbrišite postojeće.\n\nTelefon možete otključati ako pogledate u njega čak i ako vam to nije bila namjera.\n\nTelefon može otključati i neko drugi ako ga prinese vašem licu.\n\nTelefon može otključati osoba koja mnogo liči na vas, npr. identični blizanac."</string>
- <string name="security_settings_face_settings_footer" product="tablet" msgid="3467711032275909082">"Koristite lice da otključate tablet ili izvršite autentifikaciju u aplikacijama, npr. kada se prijavljujete ili odobravate kupovinu.\n\nImajte na umu:\nistovremeno možete imati postavljeno samo jedno lice. Da dodate drugo lice, izbrišite postojeće.\n\nTablet možete otključati ako pogledate u njega čak i ako vam to nije bila namjera.\n\nTablet može otključati i neko drugi ako ga prinese vašem licu.\n\nTablet može otključati osoba koja mnogo liči na vas, npr. identični blizanac."</string>
- <string name="security_settings_face_settings_footer" product="device" msgid="6237815625247917310">"Koristite lice da otključate uređaj ili izvršite autentifikaciju u aplikacijama, npr. kada se prijavljujete ili odobravate kupovinu.\n\nImajte na umu:\nistovremeno možete imati postavljeno samo jedno lice. Da dodate drugo lice, izbrišite postojeće.\n\nUređaj možete otključati ako pogledate u njega čak i ako vam to nije bila namjera.\n\nUređaj može otključati i neko drugi ako ga prinese vašem licu.\n\nUređaj može otključati osoba koja mnogo liči na vas, npr. identični blizanac."</string>
- <string name="security_settings_face_settings_footer_attention_not_supported" product="default" msgid="8266896471278294942">"Koristite lice da otključate telefon ili izvršite autentifikaciju u aplikacijama, npr. kada se prijavljujete ili odobravate kupovinu.\n\nImajte na umu:\nistovremeno možete imati postavljeno samo jedno lice. Da dodate drugo lice, izbrišite postojeće.\n\nTelefon možete otključati ako pogledate u njega čak i ako vam to nije bila namjera.\n\nTelefon može otključati i neko drugi ako ga prinese vašem licu, čak i dok su vam oči zatvorene.\n\nTelefon može otključati osoba koja mnogo liči na vas, npr. identični blizanac."</string>
- <string name="security_settings_face_settings_footer_attention_not_supported" product="tablet" msgid="6932278790700490818">"Koristite lice da otključate tablet ili izvršite autentifikaciju u aplikacijama, npr. kada se prijavljujete ili odobravate kupovinu.\n\nImajte na umu:\nistovremeno možete imati postavljeno samo jedno lice. Da dodate drugo lice, izbrišite postojeće.\n\nTablet možete otključati ako pogledate u njega čak i ako vam to nije bila namjera.\n\nTablet može otključati i neko drugi ako ga prinese vašem licu, čak i dok su vam oči zatvorene.\n\nTablet može otključati osoba koja mnogo liči na vas, npr. identični blizanac."</string>
- <string name="security_settings_face_settings_footer_attention_not_supported" product="device" msgid="2559602951942339212">"Koristite lice da otključate uređaj ili izvršite autentifikaciju u aplikacijama, npr. kada se prijavljujete ili odobravate kupovinu.\n\nImajte na umu:\nistovremeno možete imati postavljeno samo jedno lice. Da dodate drugo lice, izbrišite postojeće.\n\nUređaj možete otključati ako pogledate u njega čak i ako vam to nije bila namjera.\n\nUređaj može otključati i neko drugi ako ga prinese vašem licu, čak i dok su vam oči zatvorene.\n\nUređaj može otključati osoba koja mnogo liči na vas, npr. identični blizanac."</string>
- <string name="security_settings_face_settings_footer_class3" product="default" msgid="7050076350282827484">"Koristite lice da otključate telefon ili potvrdite identitet, npr. kada se prijavljujete u aplikacije ili odobravate kupovinu.\n\nImajte na umu:\nistovremeno možete imati postavljeno samo jedno lice. Da dodate drugo lice, izbrišite postojeće.\n\nTelefon možete otključati ako pogledate u njega čak i ako vam to nije bila namjera.\n\nTelefon može otključati i neko drugi ako ga prinese vašem licu.\n\nTelefon može otključati osoba koja mnogo liči na vas, npr. identični blizanac."</string>
- <string name="security_settings_face_settings_footer_class3" product="tablet" msgid="8013245173915280810">"Koristite lice da otključate tablet ili potvrdite identitet, npr. kada se prijavljujete u aplikacije ili odobravate kupovinu.\n\nImajte na umu:\nistovremeno možete imati postavljeno samo jedno lice. Da dodate drugo lice, izbrišite postojeće.\n\nTablet možete otključati ako pogledate u njega čak i ako vam to nije bila namjera.\n\nTablet može otključati i neko drugi ako ga prinese vašem licu.\n\nTablet može otključati osoba koja mnogo liči na vas, npr. identični blizanac."</string>
- <string name="security_settings_face_settings_footer_class3" product="device" msgid="4411845832787210264">"Koristite lice da otključate uređaj ili potvrdite identitet, npr. kada se prijavljujete u aplikacije ili odobravate kupovinu.\n\nImajte na umu:\nistovremeno možete imati postavljeno samo jedno lice. Da dodate drugo lice, izbrišite postojeće.\n\nUređaj možete otključati ako pogledate u njega čak i ako vam to nije bila namjera.\n\nUređaj može otključati i neko drugi ako ga prinese vašem licu.\n\nUređaj može otključati osoba koja mnogo liči na vas, npr. identični blizanac."</string>
- <string name="security_settings_face_settings_footer_class3_attention_not_supported" product="default" msgid="5512898803063743303">"Koristite lice da otključate telefon ili potvrdite identitet, npr. kada se prijavljujete u aplikacije ili odobravate kupovinu.\n\nImajte na umu:\nistovremeno možete imati postavljeno samo jedno lice. Da dodate drugo lice, izbrišite postojeće.\n\nTelefon možete otključati ako pogledate u njega čak i ako vam to nije bila namjera.\n\nTelefon može otključati i neko drugi ako ga prinese vašem licu, čak i dok su vam oči zatvorene.\n\nTelefon može otključati osoba koja mnogo liči na vas, npr. identični blizanac."</string>
- <string name="security_settings_face_settings_footer_class3_attention_not_supported" product="tablet" msgid="6790505667764631343">"Koristite lice da otključate tablet ili potvrdite identitet, npr. kada se prijavljujete u aplikacije ili odobravate kupovinu.\n\nImajte na umu:\nistovremeno možete imati postavljeno samo jedno lice. Da dodate drugo lice, izbrišite postojeće.\n\nTablet možete otključati ako pogledate u njega čak i ako vam to nije bila namjera.\n\nTablet može otključati i neko drugi ako ga prinese vašem licu, čak i dok su vam oči zatvorene.\n\nTablet može otključati osoba koja mnogo liči na vas, npr. identični blizanac."</string>
- <string name="security_settings_face_settings_footer_class3_attention_not_supported" product="device" msgid="7858917821957779752">"Koristite lice da otključate uređaj ili potvrdite identitet, npr. kada se prijavljujete u aplikacije ili odobravate kupovinu.\n\nImajte na umu:\nistovremeno možete imati postavljeno samo jedno lice. Da dodate drugo lice, izbrišite postojeće.\n\nUređaj možete otključati ako pogledate u njega čak i ako vam to nije bila namjera.\n\nUređaj može otključati i neko drugi ako ga prinese vašem licu, čak i dok su vam oči zatvorene.\n\nUređaj može otključati osoba koja mnogo liči na vas, npr. identični blizanac."</string>
+ <string name="security_settings_face_settings_footer" product="default" msgid="3036403896485044957">"Koristite lice da otključate telefon ili izvršite autentifikaciju u aplikacijama, npr. kada se prijavljujete ili odobravate kupovinu.\n\nImajte na umu:\nu datom trenutku možete imati postavljeno samo jedno lice. Da dodate drugo lice, izbrišite postojeće.\n\nTelefon možete otključati ako pogledate u njega čak i ako vam to nije bila namjera.\n\nTelefon može otključati i neko drugi ako ga prinese vašem licu.\n\nTelefon može otključati osoba koja mnogo liči na vas, npr. identični blizanac."</string>
+ <string name="security_settings_face_settings_footer" product="tablet" msgid="3467711032275909082">"Koristite lice da otključate tablet ili izvršite autentifikaciju u aplikacijama, npr. kada se prijavljujete ili odobravate kupovinu.\n\nImajte na umu:\nu datom trenutku možete imati postavljeno samo jedno lice. Da dodate drugo lice, izbrišite postojeće.\n\nTablet možete otključati ako pogledate u njega čak i ako vam to nije bila namjera.\n\nTablet može otključati i neko drugi ako ga prinese vašem licu.\n\nTablet može otključati osoba koja mnogo liči na vas, npr. identični blizanac."</string>
+ <string name="security_settings_face_settings_footer" product="device" msgid="6237815625247917310">"Koristite lice da otključate uređaj ili izvršite autentifikaciju u aplikacijama, npr. kada se prijavljujete ili odobravate kupovinu.\n\nImajte na umu:\nu datom trenutku možete imati postavljeno samo jedno lice. Da dodate drugo lice, izbrišite postojeće.\n\nUređaj možete otključati ako pogledate u njega čak i ako vam to nije bila namjera.\n\nUređaj može otključati i neko drugi ako ga prinese vašem licu.\n\nUređaj može otključati osoba koja mnogo liči na vas, npr. identični blizanac."</string>
+ <string name="security_settings_face_settings_footer_attention_not_supported" product="default" msgid="8266896471278294942">"Koristite lice da otključate telefon ili izvršite autentifikaciju u aplikacijama, npr. kada se prijavljujete ili odobravate kupovinu.\n\nImajte na umu:\nu datom trenutku možete imati postavljeno samo jedno lice. Da dodate drugo lice, izbrišite postojeće.\n\nTelefon možete otključati ako pogledate u njega čak i ako vam to nije bila namjera.\n\nTelefon može otključati i neko drugi ako ga prinese vašem licu, čak i dok su vam oči zatvorene.\n\nTelefon može otključati osoba koja mnogo liči na vas, npr. identični blizanac."</string>
+ <string name="security_settings_face_settings_footer_attention_not_supported" product="tablet" msgid="6932278790700490818">"Koristite lice da otključate tablet ili izvršite autentifikaciju u aplikacijama, npr. kada se prijavljujete ili odobravate kupovinu.\n\nImajte na umu:\nu datom trenutku možete imati postavljeno samo jedno lice. Da dodate drugo lice, izbrišite postojeće.\n\nTablet možete otključati ako pogledate u njega čak i ako vam to nije bila namjera.\n\nTablet može otključati i neko drugi ako ga prinese vašem licu, čak i dok su vam oči zatvorene.\n\nTablet može otključati osoba koja mnogo liči na vas, npr. identični blizanac."</string>
+ <string name="security_settings_face_settings_footer_attention_not_supported" product="device" msgid="2559602951942339212">"Koristite lice da otključate uređaj ili izvršite autentifikaciju u aplikacijama, npr. kada se prijavljujete ili odobravate kupovinu.\n\nImajte na umu:\nu datom trenutku možete imati postavljeno samo jedno lice. Da dodate drugo lice, izbrišite postojeće.\n\nUređaj možete otključati ako pogledate u njega čak i ako vam to nije bila namjera.\n\nUređaj može otključati i neko drugi ako ga prinese vašem licu, čak i dok su vam oči zatvorene.\n\nUređaj može otključati osoba koja mnogo liči na vas, npr. identični blizanac."</string>
+ <string name="security_settings_face_settings_footer_class3" product="default" msgid="7050076350282827484">"Koristite lice da otključate telefon ili potvrdite identitet, npr. kada se prijavljujete u aplikacije ili odobravate kupovinu.\n\nImajte na umu:\nu datom trenutku možete imati postavljeno samo jedno lice. Da dodate drugo lice, izbrišite postojeće.\n\nTelefon možete otključati ako pogledate u njega čak i ako vam to nije bila namjera.\n\nTelefon može otključati i neko drugi ako ga prinese vašem licu.\n\nTelefon može otključati osoba koja mnogo liči na vas, npr. identični blizanac."</string>
+ <string name="security_settings_face_settings_footer_class3" product="tablet" msgid="8013245173915280810">"Koristite lice da otključate tablet ili potvrdite identitet, npr. kada se prijavljujete u aplikacije ili odobravate kupovinu.\n\nImajte na umu:\nu datom trenutku možete imati postavljeno samo jedno lice. Da dodate drugo lice, izbrišite postojeće.\n\nTablet možete otključati ako pogledate u njega čak i ako vam to nije bila namjera.\n\nTablet može otključati i neko drugi ako ga prinese vašem licu.\n\nTablet može otključati osoba koja mnogo liči na vas, npr. identični blizanac."</string>
+ <string name="security_settings_face_settings_footer_class3" product="device" msgid="4411845832787210264">"Koristite lice da otključate uređaj ili potvrdite identitet, npr. kada se prijavljujete u aplikacije ili odobravate kupovinu.\n\nImajte na umu:\nu datom trenutku možete imati postavljeno samo jedno lice. Da dodate drugo lice, izbrišite postojeće.\n\nUređaj možete otključati ako pogledate u njega čak i ako vam to nije bila namjera.\n\nUređaj može otključati i neko drugi ako ga prinese vašem licu.\n\nUređaj može otključati osoba koja mnogo liči na vas, npr. identični blizanac."</string>
+ <string name="security_settings_face_settings_footer_class3_attention_not_supported" product="default" msgid="5512898803063743303">"Koristite lice da otključate telefon ili potvrdite identitet, npr. kada se prijavljujete u aplikacije ili odobravate kupovinu.\n\nImajte na umu:\nu datom trenutku možete imati postavljeno samo jedno lice. Da dodate drugo lice, izbrišite postojeće.\n\nTelefon možete otključati ako pogledate u njega čak i ako vam to nije bila namjera.\n\nTelefon može otključati i neko drugi ako ga prinese vašem licu, čak i dok su vam oči zatvorene.\n\nTelefon može otključati osoba koja mnogo liči na vas, npr. identični blizanac."</string>
+ <string name="security_settings_face_settings_footer_class3_attention_not_supported" product="tablet" msgid="6790505667764631343">"Koristite lice da otključate tablet ili potvrdite identitet, npr. kada se prijavljujete u aplikacije ili odobravate kupovinu.\n\nImajte na umu:\nu datom trenutku možete imati postavljeno samo jedno lice. Da dodate drugo lice, izbrišite postojeće.\n\nTablet možete otključati ako pogledate u njega čak i ako vam to nije bila namjera.\n\nTablet može otključati i neko drugi ako ga prinese vašem licu, čak i dok su vam oči zatvorene.\n\nTablet može otključati osoba koja mnogo liči na vas, npr. identični blizanac."</string>
+ <string name="security_settings_face_settings_footer_class3_attention_not_supported" product="device" msgid="7858917821957779752">"Koristite lice da otključate uređaj ili potvrdite identitet, npr. kada se prijavljujete u aplikacije ili odobravate kupovinu.\n\nImajte na umu:\nu datom trenutku možete imati postavljeno samo jedno lice. Da dodate drugo lice, izbrišite postojeće.\n\nUređaj možete otključati ako pogledate u njega čak i ako vam to nije bila namjera.\n\nUređaj može otključati i neko drugi ako ga prinese vašem licu, čak i dok su vam oči zatvorene.\n\nUređaj može otključati osoba koja mnogo liči na vas, npr. identični blizanac."</string>
<string name="security_settings_fingerprint_enroll_introduction_v3_message" msgid="2145273491174234191">"Pomoću otiska prsta otključajte uređaj <xliff:g id="DEVICENAME">%s</xliff:g> ili potvrdite svoj identitet, naprimjer kada se prijavljujete u aplikacije ili odobravate kupovinu"</string>
<string name="security_settings_fingerprint_enroll_introduction_consent_message" product="default" msgid="5101253231118659496">"Dozvolite djetetu da koristi otisak prsta da otključa telefon ili potvrdi identitet. To će se dešavati prilikom prijava u aplikacije, odobravanja kupovina i drugih radnji."</string>
<string name="security_settings_fingerprint_enroll_introduction_consent_message" product="tablet" msgid="3063978167545799342">"Dozvolite djetetu da koristi otisak prsta da otključa tablet ili potvrdi identitet. To će se dešavati prilikom prijava u aplikacije, odobravanja kupovina i drugih radnji."</string>
diff --git a/res-product/values-cs/strings.xml b/res-product/values-cs/strings.xml
index 1b92375..d058a34 100644
--- a/res-product/values-cs/strings.xml
+++ b/res-product/values-cs/strings.xml
@@ -58,12 +58,9 @@
<string name="security_settings_face_enroll_introduction_consent_message_0" product="default" msgid="9086377203303858619">"Povolte dítěti používat obličej k odemykání telefonu"</string>
<string name="security_settings_face_enroll_introduction_consent_message_0" product="tablet" msgid="4560949471246282574">"Povolte dítěti používat obličej k odemykání tabletu"</string>
<string name="security_settings_face_enroll_introduction_consent_message_0" product="device" msgid="1156063265854416046">"Povolte dítěti používat obličej k odemykání zařízení"</string>
- <!-- no translation found for security_settings_face_enroll_introduction_consent_message_0_class3 (5082581184108528408) -->
- <skip />
- <!-- no translation found for security_settings_face_enroll_introduction_consent_message_0_class3 (5932555218164668532) -->
- <skip />
- <!-- no translation found for security_settings_face_enroll_introduction_consent_message_0_class3 (8943878265098867810) -->
- <skip />
+ <string name="security_settings_face_enroll_introduction_consent_message_0_class3" product="default" msgid="5082581184108528408">"Povolte dítěti používat obličej k odemykání telefonu a ověřování totožnosti. K ověřování dochází při přihlašování do aplikací, schvalování nákupů apod."</string>
+ <string name="security_settings_face_enroll_introduction_consent_message_0_class3" product="tablet" msgid="5932555218164668532">"Povolte dítěti používat obličej k odemykání tabletu a ověřování totožnosti. K ověřování dochází při přihlašování do aplikací, schvalování nákupů apod."</string>
+ <string name="security_settings_face_enroll_introduction_consent_message_0_class3" product="device" msgid="8943878265098867810">"Povolte dítěti používat obličej k odemykání zařízení a ověřování totožnosti. K ověřování dochází při přihlašování do aplikací, schvalování nákupů apod."</string>
<string name="security_settings_face_enroll_introduction_consent_message" product="default" msgid="3698558920963989416">"Odemykání telefonu dítěte obličejem může být méně bezpečné než silné gesto nebo PIN."</string>
<string name="security_settings_face_enroll_introduction_consent_message" product="tablet" msgid="2689983368730833505">"Odemykání tabletu dítěte obličejem může být méně bezpečné než silné gesto nebo PIN."</string>
<string name="security_settings_face_enroll_introduction_consent_message" product="device" msgid="5768077532130409820">"Odemykání zařízení dítěte obličejem může být méně bezpečné než silné gesto nebo PIN."</string>
diff --git a/res-product/values-et/strings.xml b/res-product/values-et/strings.xml
index ac18b1e..58e2859 100644
--- a/res-product/values-et/strings.xml
+++ b/res-product/values-et/strings.xml
@@ -191,9 +191,9 @@
<string name="lock_screen_pin_skip_biometrics_message" product="default" msgid="2717938545326672010">"Näoga avamise ja sõrmejäljega avamise seadistamiseks on vaja PIN-koodi.\n\nPIN-kood kaitseb telefoni juhul, kui see läheb kaotsi või varastatakse."</string>
<string name="lock_screen_pattern_skip_biometrics_message" product="default" msgid="6067309080610183546">"Näoga avamise ja sõrmejäljega avamise seadistamiseks on vaja mustrit.\n\nMuster kaitseb telefoni juhul, kui see läheb kaotsi või varastatakse."</string>
<string name="lock_screen_password_skip_biometrics_message" product="default" msgid="4739690336878613804">"Näoga avamise ja sõrmejäljega avamise seadistamiseks on vaja parooli.\n\nParool kaitseb telefoni juhul, kui see läheb kaotsi või varastatakse."</string>
- <string name="fingerprint_v2_delete_message" product="default" msgid="8723083814238510088">"See kustutab sõrmejäljega „<xliff:g id="FINGERPRINT_ID">%1$s</xliff:g>“ seotud sõrmejäljekujutised ja -mudeli, mis on teie telefoni salvestatud"</string>
- <string name="fingerprint_v2_delete_message" product="tablet" msgid="527375244730792698">"See kustutab sõrmejäljega „<xliff:g id="FINGERPRINT_ID">%1$s</xliff:g>“ seotud sõrmejäljekujutised ja -mudeli, mis on teie tahvelarvutisse salvestatud"</string>
- <string name="fingerprint_v2_delete_message" product="device" msgid="4549780655045100171">"See kustutab sõrmejäljega „<xliff:g id="FINGERPRINT_ID">%1$s</xliff:g>“ seotud sõrmejäljekujutised ja -mudeli, mis on teie seadmesse salvestatud"</string>
+ <string name="fingerprint_v2_delete_message" product="default" msgid="8723083814238510088">"See kustutab sõrmejäljega „<xliff:g id="FINGERPRINT_ID">%1$s</xliff:g>“ seotud sõrmejäljekujutised ja -mudeli, mis on teie telefoni salvestatud."</string>
+ <string name="fingerprint_v2_delete_message" product="tablet" msgid="527375244730792698">"See kustutab sõrmejäljega „<xliff:g id="FINGERPRINT_ID">%1$s</xliff:g>“ seotud sõrmejäljekujutised ja -mudeli, mis on teie tahvelarvutisse salvestatud."</string>
+ <string name="fingerprint_v2_delete_message" product="device" msgid="4549780655045100171">"See kustutab sõrmejäljega „<xliff:g id="FINGERPRINT_ID">%1$s</xliff:g>“ seotud sõrmejäljekujutised ja -mudeli, mis on teie seadmesse salvestatud."</string>
<string name="fingerprint_last_delete_message" product="default" msgid="3187410175262625294">"Te ei saa oma sõrmejälge kasutades telefoni avada ega rakendustes oma isikut kinnitada."</string>
<string name="fingerprint_last_delete_message" product="tablet" msgid="8618307419148004587">"Te ei saa oma sõrmejälge kasutades tahvelarvutit avada ega rakendustes oma isikut kinnitada."</string>
<string name="fingerprint_last_delete_message" product="device" msgid="3910012280858587242">"Te ei saa oma sõrmejälge kasutades seadet avada ega rakendustes oma isikut kinnitada."</string>
diff --git a/res-product/values-fi/strings.xml b/res-product/values-fi/strings.xml
index c6accbd..79d4642 100644
--- a/res-product/values-fi/strings.xml
+++ b/res-product/values-fi/strings.xml
@@ -58,12 +58,9 @@
<string name="security_settings_face_enroll_introduction_consent_message_0" product="default" msgid="9086377203303858619">"Anna lapselle lupa käyttää kasvojaan puhelimen lukituksen avaamiseen"</string>
<string name="security_settings_face_enroll_introduction_consent_message_0" product="tablet" msgid="4560949471246282574">"Anna lapselle lupa käyttää kasvojaan tabletin lukituksen avaamiseen"</string>
<string name="security_settings_face_enroll_introduction_consent_message_0" product="device" msgid="1156063265854416046">"Anna lapselle lupa käyttää kasvojaan laitteen lukituksen avaamiseen"</string>
- <!-- no translation found for security_settings_face_enroll_introduction_consent_message_0_class3 (5082581184108528408) -->
- <skip />
- <!-- no translation found for security_settings_face_enroll_introduction_consent_message_0_class3 (5932555218164668532) -->
- <skip />
- <!-- no translation found for security_settings_face_enroll_introduction_consent_message_0_class3 (8943878265098867810) -->
- <skip />
+ <string name="security_settings_face_enroll_introduction_consent_message_0_class3" product="default" msgid="5082581184108528408">"Anna lapselle lupa käyttää kasvojaan puhelimen lukituksen avaamiseen tai henkilöllisyyden todentamiseen esim. hänen kirjautuessaan sovelluksiin tai hyväksyessään ostoksen."</string>
+ <string name="security_settings_face_enroll_introduction_consent_message_0_class3" product="tablet" msgid="5932555218164668532">"Anna lapselle lupa käyttää kasvojaan tabletin lukituksen avaamiseen tai henkilöllisyyden todentamiseen, esim. hänen kirjautuessaan sovelluksiin tai hyväksyessään ostoksen."</string>
+ <string name="security_settings_face_enroll_introduction_consent_message_0_class3" product="device" msgid="8943878265098867810">"Anna lapselle lupa käyttää kasvojaan laitteen lukituksen avaamiseen tai henkilöllisyyden todentamiseen, esim. hänen kirjautuessaan sovelluksiin tai hyväksyessään ostoksen."</string>
<string name="security_settings_face_enroll_introduction_consent_message" product="default" msgid="3698558920963989416">"Puhelimen lukituksen avaaminen lapsen kasvojen avulla ei ehkä ole yhtä turvallista kuin vahvan kuvion tai PIN-koodin käyttö."</string>
<string name="security_settings_face_enroll_introduction_consent_message" product="tablet" msgid="2689983368730833505">"Tabletin lukituksen avaaminen lapsen kasvojen avulla ei ehkä ole yhtä turvallista kuin vahvan kuvion tai PIN-koodin käyttö."</string>
<string name="security_settings_face_enroll_introduction_consent_message" product="device" msgid="5768077532130409820">"Laitteen lukituksen avaaminen lapsen kasvojen avulla ei ehkä ole yhtä turvallista kuin vahvan kuvion tai PIN-koodin käyttö."</string>
diff --git a/res-product/values-in/strings.xml b/res-product/values-in/strings.xml
index 6cbbdb6..a42a68a 100644
--- a/res-product/values-in/strings.xml
+++ b/res-product/values-in/strings.xml
@@ -58,12 +58,9 @@
<string name="security_settings_face_enroll_introduction_consent_message_0" product="default" msgid="9086377203303858619">"Izinkan anak Anda menggunakan wajah untuk membuka kunci ponselnya"</string>
<string name="security_settings_face_enroll_introduction_consent_message_0" product="tablet" msgid="4560949471246282574">"Izinkan anak Anda menggunakan wajah untuk membuka kunci tabletnya"</string>
<string name="security_settings_face_enroll_introduction_consent_message_0" product="device" msgid="1156063265854416046">"Izinkan anak Anda menggunakan wajah untuk membuka kunci perangkatnya"</string>
- <!-- no translation found for security_settings_face_enroll_introduction_consent_message_0_class3 (5082581184108528408) -->
- <skip />
- <!-- no translation found for security_settings_face_enroll_introduction_consent_message_0_class3 (5932555218164668532) -->
- <skip />
- <!-- no translation found for security_settings_face_enroll_introduction_consent_message_0_class3 (8943878265098867810) -->
- <skip />
+ <string name="security_settings_face_enroll_introduction_consent_message_0_class3" product="default" msgid="5082581184108528408">"Izinkan anak Anda menggunakan wajahnya untuk membuka kunci ponsel atau memverifikasi dirinya. Hal ini dilakukan saat dia login ke aplikasi, menyetujui pembelian, dan lainnya."</string>
+ <string name="security_settings_face_enroll_introduction_consent_message_0_class3" product="tablet" msgid="5932555218164668532">"Izinkan anak Anda menggunakan wajahnya untuk membuka kunci tablet atau memverifikasi dirinya. Hal ini dilakukan saat dia login ke aplikasi, menyetujui pembelian, dan lainnya."</string>
+ <string name="security_settings_face_enroll_introduction_consent_message_0_class3" product="device" msgid="8943878265098867810">"Izinkan anak Anda menggunakan wajahnya untuk membuka kunci perangkat atau memverifikasi dirinya. Hal ini dilakukan saat dia login ke aplikasi, menyetujui pembelian, dan lainnya."</string>
<string name="security_settings_face_enroll_introduction_consent_message" product="default" msgid="3698558920963989416">"Menggunakan wajah anak Anda untuk membuka kunci ponselnya mungkin kurang aman dibandingkan dengan pola atau PIN yang kuat."</string>
<string name="security_settings_face_enroll_introduction_consent_message" product="tablet" msgid="2689983368730833505">"Menggunakan wajah anak Anda untuk membuka kunci tabletnya mungkin kurang aman dibandingkan dengan pola atau PIN yang kuat."</string>
<string name="security_settings_face_enroll_introduction_consent_message" product="device" msgid="5768077532130409820">"Menggunakan wajah anak Anda untuk membuka kunci perangkatnya mungkin kurang aman dibandingkan dengan pola atau PIN yang kuat."</string>
diff --git a/res-product/values-kk/strings.xml b/res-product/values-kk/strings.xml
index c30ab21..f3b1b58 100644
--- a/res-product/values-kk/strings.xml
+++ b/res-product/values-kk/strings.xml
@@ -58,12 +58,9 @@
<string name="security_settings_face_enroll_introduction_consent_message_0" product="default" msgid="9086377203303858619">"Балаңыз өз телефонын ашуы үшін, оған Бет тану функциясын қолдануына рұқсат беріңіз."</string>
<string name="security_settings_face_enroll_introduction_consent_message_0" product="tablet" msgid="4560949471246282574">"Балаңыз өз планшетін ашуы үшін, оған Бет тану функциясын қолдануына рұқсат беріңіз."</string>
<string name="security_settings_face_enroll_introduction_consent_message_0" product="device" msgid="1156063265854416046">"Балаңыз өз құрылғысын ашуы үшін, оған Бет тану функциясын қолдануына рұқсат беріңіз."</string>
- <!-- no translation found for security_settings_face_enroll_introduction_consent_message_0_class3 (5082581184108528408) -->
- <skip />
- <!-- no translation found for security_settings_face_enroll_introduction_consent_message_0_class3 (5932555218164668532) -->
- <skip />
- <!-- no translation found for security_settings_face_enroll_introduction_consent_message_0_class3 (8943878265098867810) -->
- <skip />
+ <string name="security_settings_face_enroll_introduction_consent_message_0_class3" product="default" msgid="5082581184108528408">"Балаңыз өз телефонын ашуы немесе жеке басын растауы үшін, оған бетін қолдануға рұқсат беріңіз. Ол қолданбаларға кіргенде, сатып алу транзакциясын мақұлдағанда және т.б. жағдайларда керек болады."</string>
+ <string name="security_settings_face_enroll_introduction_consent_message_0_class3" product="tablet" msgid="5932555218164668532">"Балаңыз өз планшетін ашуы немесе жеке басын растауы үшін, оған бетін қолдануға рұқсат беріңіз. Ол қолданбаларға кіргенде, сатып алу транзакциясын мақұлдағанда және т.б. жағдайларда керек болады."</string>
+ <string name="security_settings_face_enroll_introduction_consent_message_0_class3" product="device" msgid="8943878265098867810">"Балаңыз өз құрылғысын ашуы немесе жеке басын растауы үшін, оған бетін қолдануға рұқсат беріңіз. Ол қолданбаларға кіргенде, сатып алу транзакциясын мақұлдағанда және т.б. жағдайларда керек болады."</string>
<string name="security_settings_face_enroll_introduction_consent_message" product="default" msgid="3698558920963989416">"Телефонның құлпын баланың бетін пайдаланып ашу, қауіпсіздік тұрғысынан күрделі өрнекке немесе PIN кодына қарағанда әлсіздеу болуы мүмкін."</string>
<string name="security_settings_face_enroll_introduction_consent_message" product="tablet" msgid="2689983368730833505">"Планшеттің құлпын баланың бетін пайдаланып ашу, қауіпсіздік тұрғысынан күрделі өрнекке немесе PIN кодына қарағанда әлсіздеу болуы мүмкін."</string>
<string name="security_settings_face_enroll_introduction_consent_message" product="device" msgid="5768077532130409820">"Құрылғының құлпын баланың бетін пайдаланып ашу, қауіпсіздік тұрғысынан күрделі өрнекке немесе PIN кодына қарағанда әлсіздеу болуы мүмкін."</string>
diff --git a/res-product/values-mn/strings.xml b/res-product/values-mn/strings.xml
index 8358046..1574ca7 100644
--- a/res-product/values-mn/strings.xml
+++ b/res-product/values-mn/strings.xml
@@ -58,12 +58,9 @@
<string name="security_settings_face_enroll_introduction_consent_message_0" product="default" msgid="9086377203303858619">"Хүүхдэдээ царайгаа ашиглан утасныхаа түгжээг тайлахыг зөвшөөрнө үү"</string>
<string name="security_settings_face_enroll_introduction_consent_message_0" product="tablet" msgid="4560949471246282574">"Хүүхдэдээ царайгаа ашиглан таблетынхаа түгжээг тайлахыг зөвшөөрнө үү"</string>
<string name="security_settings_face_enroll_introduction_consent_message_0" product="device" msgid="1156063265854416046">"Хүүхдэдээ царайгаа ашиглан төхөөрөмжийнхөө түгжээг тайлахыг зөвшөөрнө үү"</string>
- <!-- no translation found for security_settings_face_enroll_introduction_consent_message_0_class3 (5082581184108528408) -->
- <skip />
- <!-- no translation found for security_settings_face_enroll_introduction_consent_message_0_class3 (5932555218164668532) -->
- <skip />
- <!-- no translation found for security_settings_face_enroll_introduction_consent_message_0_class3 (8943878265098867810) -->
- <skip />
+ <string name="security_settings_face_enroll_introduction_consent_message_0_class3" product="default" msgid="5082581184108528408">"Хүүхдэдээ царайгаа ашиглан утасныхаа түгжээг тайлах эсвэл өөрийгөө мөн болохыг баталгаажуулахыг зөвшөөрнө үү. Энэ нь түүнийг аппуудад нэвтрэх, худалдан авалтуудыг зөвшөөрөх болон бусад зүйлийг хийх үед тохиолдоно."</string>
+ <string name="security_settings_face_enroll_introduction_consent_message_0_class3" product="tablet" msgid="5932555218164668532">"Хүүхдэдээ царайгаа ашиглан таблетынхаа түгжээг тайлах эсвэл өөрийгөө мөн болохыг баталгаажуулахыг зөвшөөрнө үү. Энэ нь түүнийг аппуудад нэвтрэх, худалдан авалтуудыг зөвшөөрөх болон бусад зүйлийг хийх үед тохиолдоно."</string>
+ <string name="security_settings_face_enroll_introduction_consent_message_0_class3" product="device" msgid="8943878265098867810">"Хүүхдэдээ царайгаа ашиглан төхөөрөмжийнхөө түгжээг тайлах эсвэл өөрийгөө мөн болохыг баталгаажуулахыг зөвшөөрнө үү. Энэ нь түүнийг аппуудад нэвтрэх, худалдан авалтуудыг зөвшөөрөх болон бусад зүйлийг хийх үед тохиолдоно."</string>
<string name="security_settings_face_enroll_introduction_consent_message" product="default" msgid="3698558920963989416">"Хүүхдийнхээ царайг утасных нь түгжээг тайлахад ашиглах нь сайн хээ эсвэл ПИН-ээс хамгаалалт сул байж магадгүй."</string>
<string name="security_settings_face_enroll_introduction_consent_message" product="tablet" msgid="2689983368730833505">"Хүүхдийнхээ царайг таблетынх нь түгжээг тайлахад ашиглах нь сайн хээ эсвэл ПИН-ээс хамгаалалт сул байж магадгүй."</string>
<string name="security_settings_face_enroll_introduction_consent_message" product="device" msgid="5768077532130409820">"Хүүхдийнхээ царайг төхөөрөмжийнх нь түгжээг тайлахад ашиглах нь сайн хээ эсвэл ПИН-ээс хамгаалалт сул байж магадгүй."</string>
diff --git a/res-product/values-sl/strings.xml b/res-product/values-sl/strings.xml
index 8b037f8..fa82030 100644
--- a/res-product/values-sl/strings.xml
+++ b/res-product/values-sl/strings.xml
@@ -58,12 +58,9 @@
<string name="security_settings_face_enroll_introduction_consent_message_0" product="default" msgid="9086377203303858619">"Otroku dovolite odklepanje telefona z obrazom."</string>
<string name="security_settings_face_enroll_introduction_consent_message_0" product="tablet" msgid="4560949471246282574">"Otroku dovolite odklepanje tabličnega računalnika z obrazom."</string>
<string name="security_settings_face_enroll_introduction_consent_message_0" product="device" msgid="1156063265854416046">"Otroku dovolite odklepanje naprave z obrazom."</string>
- <!-- no translation found for security_settings_face_enroll_introduction_consent_message_0_class3 (5082581184108528408) -->
- <skip />
- <!-- no translation found for security_settings_face_enroll_introduction_consent_message_0_class3 (5932555218164668532) -->
- <skip />
- <!-- no translation found for security_settings_face_enroll_introduction_consent_message_0_class3 (8943878265098867810) -->
- <skip />
+ <string name="security_settings_face_enroll_introduction_consent_message_0_class3" product="default" msgid="5082581184108528408">"Otroku dovolite, da bo z obrazom odklepal telefon ali potrjeval svojo identiteto. To se zgodi, ko se prijavi v aplikacije, odobri nakup in drugo."</string>
+ <string name="security_settings_face_enroll_introduction_consent_message_0_class3" product="tablet" msgid="5932555218164668532">"Otroku dovolite, da bo z obrazom odklepal tablični računalnik ali potrjeval svojo identiteto. To se zgodi, ko se prijavi v aplikacije, odobri nakup in drugo."</string>
+ <string name="security_settings_face_enroll_introduction_consent_message_0_class3" product="device" msgid="8943878265098867810">"Otroku dovolite, da bo z obrazom odklepal napravo ali potrjeval svojo identiteto. To se zgodi, ko se prijavi v aplikacije, odobri nakup in drugo."</string>
<string name="security_settings_face_enroll_introduction_consent_message" product="default" msgid="3698558920963989416">"Če otrok odklepa telefon z obrazom, je to morda manj varno od zapletenega vzorca ali kode PIN."</string>
<string name="security_settings_face_enroll_introduction_consent_message" product="tablet" msgid="2689983368730833505">"Če otrok odklepa tablični računalnik z obrazom, je to morda manj varno od zapletenega vzorca ali kode PIN."</string>
<string name="security_settings_face_enroll_introduction_consent_message" product="device" msgid="5768077532130409820">"Če otrok odklepa napravo z obrazom, je to morda manj varno od zapletenega vzorca ali kode PIN."</string>
diff --git a/res-product/values-sr/strings.xml b/res-product/values-sr/strings.xml
index ce61d07..7e8ffd1 100644
--- a/res-product/values-sr/strings.xml
+++ b/res-product/values-sr/strings.xml
@@ -58,12 +58,9 @@
<string name="security_settings_face_enroll_introduction_consent_message_0" product="default" msgid="9086377203303858619">"Дозволите детету да користи откључавање телефона лицем"</string>
<string name="security_settings_face_enroll_introduction_consent_message_0" product="tablet" msgid="4560949471246282574">"Дозволите детету да користи откључавање таблета лицем"</string>
<string name="security_settings_face_enroll_introduction_consent_message_0" product="device" msgid="1156063265854416046">"Дозволите детету да користи откључавање уређаја лицем"</string>
- <!-- no translation found for security_settings_face_enroll_introduction_consent_message_0_class3 (5082581184108528408) -->
- <skip />
- <!-- no translation found for security_settings_face_enroll_introduction_consent_message_0_class3 (5932555218164668532) -->
- <skip />
- <!-- no translation found for security_settings_face_enroll_introduction_consent_message_0_class3 (8943878265098867810) -->
- <skip />
+ <string name="security_settings_face_enroll_introduction_consent_message_0_class3" product="default" msgid="5082581184108528408">"Дозволите детету да користи лице за откључавање телефона или потврду идентитета. Ово се дешава када се пријављује у апликације, одобрава куповину и у другим ситуацијама."</string>
+ <string name="security_settings_face_enroll_introduction_consent_message_0_class3" product="tablet" msgid="5932555218164668532">"Дозволите детету да користи лице за откључавање таблета или потврду идентитета. Ово се дешава када се пријављује у апликације, одобрава куповину и у другим ситуацијама."</string>
+ <string name="security_settings_face_enroll_introduction_consent_message_0_class3" product="device" msgid="8943878265098867810">"Дозволите детету да користи лице за откључавање уређаја или потврду идентитета. Ово се дешава када се пријављује у апликације, одобрава куповину и у другим ситуацијама."</string>
<string name="security_settings_face_enroll_introduction_consent_message" product="default" msgid="3698558920963989416">"Коришћење лица детета за откључавање телефона може да буде мање безбедно од јаког шаблона или PIN-а."</string>
<string name="security_settings_face_enroll_introduction_consent_message" product="tablet" msgid="2689983368730833505">"Коришћење лица детета за откључавање таблета може да буде мање безбедно од јаког шаблона или PIN-а."</string>
<string name="security_settings_face_enroll_introduction_consent_message" product="device" msgid="5768077532130409820">"Коришћење лица детета за откључавање уређаја може да буде мање безбедно од јаког шаблона или PIN-а."</string>
@@ -208,9 +205,9 @@
<string name="biometrics_unlock_title" product="device" msgid="3342994085226864170">"Можете да откључате уређај помоћу лица или отиска прста. Ова опција захтева резервни метод закључавања екрана из безбедносних разлога."</string>
<string name="encrypt_title" product="tablet" msgid="8915795247786124547">"Шифруј таблет"</string>
<string name="encrypt_title" product="default" msgid="511146128799853404">"Шифровање телефона"</string>
- <string name="suggested_lock_settings_summary" product="tablet" msgid="8821254377043173267">"Подесите закључавање екрана да бисте заштитили таблет"</string>
- <string name="suggested_lock_settings_summary" product="device" msgid="4863929838844014122">"Подесите закључавање екрана да бисте заштитили уређај"</string>
- <string name="suggested_lock_settings_summary" product="default" msgid="8050809409337082738">"Подесите закључавање екрана да бисте заштитили телефон"</string>
+ <string name="suggested_lock_settings_summary" product="tablet" msgid="8821254377043173267">"Подесите откључавање екрана да бисте заштитили таблет"</string>
+ <string name="suggested_lock_settings_summary" product="device" msgid="4863929838844014122">"Подесите откључавање екрана да бисте заштитили уређај"</string>
+ <string name="suggested_lock_settings_summary" product="default" msgid="8050809409337082738">"Подесите откључавање екрана да бисте заштитили телефон"</string>
<string name="suggested_fingerprint_lock_settings_summary" product="tablet" msgid="8565330205932332157"></string>
<string name="suggested_fingerprint_lock_settings_summary" product="device" msgid="8565330205932332157"></string>
<string name="suggested_fingerprint_lock_settings_summary" product="default" msgid="8565330205932332157"></string>
diff --git a/res-product/values-te/strings.xml b/res-product/values-te/strings.xml
index 4184745..f1c7ce6 100644
--- a/res-product/values-te/strings.xml
+++ b/res-product/values-te/strings.xml
@@ -119,13 +119,13 @@
<string name="security_settings_fingerprint_v2_enroll_introduction_footer_message_consent_3" product="default" msgid="6804981319922169283">"వారు Pixel Imprintను ఉపయోగించినప్పుడు, వారి వేలిముద్ర మోడల్ను అప్డేట్ చేయడానికి ఇమేజ్లు ఉపయోగించబడతాయి. మీ చిన్నారి వేలిముద్ర మోడల్ను క్రియేట్ చేయడానికి ఉపయోగించే ఇమేజ్లు ఎప్పుడూ స్టోర్ చేయబడవు, కానీ వేలిముద్ర మోడల్ ఫోన్లో సురక్షితంగా స్టోర్ చేయబడుతుంది, ఫోన్ నుండి బయటకు పంపబడదు. మొత్తం ప్రాసెస్ విధానం ఫోన్లో సురక్షితంగా జరుగుతుంది."</string>
<string name="security_settings_fingerprint_v2_enroll_introduction_footer_message_consent_3" product="tablet" msgid="1426913673720862863">"వారు Pixel Imprintను ఉపయోగించినప్పుడు, వారి వేలిముద్ర మోడల్ను అప్డేట్ చేయడానికి ఇమేజ్లు ఉపయోగించబడతాయి. మీ చిన్నారి వేలిముద్ర మోడల్ను క్రియేట్ చేయడానికి ఉపయోగించే ఇమేజ్లు ఎప్పుడూ స్టోర్ చేయబడవు, కానీ వేలిముద్ర మోడల్ టాబ్లెట్లో సురక్షితంగా స్టోర్ చేయబడుతుంది, టాబ్లెట్ నుండి బయటకు పంపబడదు. మొత్తం ప్రాసెస్ విధానం టాబ్లెట్లో సురక్షితంగా జరుగుతుంది."</string>
<string name="security_settings_fingerprint_v2_enroll_introduction_footer_message_consent_3" product="device" msgid="2631789126811300879">"వారు Pixel Imprintను ఉపయోగించినప్పుడు, వారి వేలిముద్ర మోడల్ను అప్డేట్ చేయడానికి ఇమేజ్లు ఉపయోగించబడతాయి. మీ చిన్నారి వేలిముద్ర మోడల్ను క్రియేట్ చేయడానికి ఉపయోగించే ఇమేజ్లు ఎప్పుడూ స్టోర్ చేయబడవు, కానీ వేలిముద్ర మోడల్ పరికరంలో సురక్షితంగా స్టోర్ చేయబడుతుంది, పరికరం నుండి బయటకు పంపబడదు. మొత్తం ప్రాసెస్ విధానం పరికరంలో సురక్షితంగా జరుగుతుంది."</string>
- <string name="security_settings_fingerprint_v2_enroll_introduction_footer_message_4" product="default" msgid="1354488801088258040">"మీరు సెట్టింగ్లలో ఎప్పుడైనా మీ వేలిముద్ర ఇమేజ్లను, మోడల్ను తొలగించవచ్చు లేదా \'వేలిముద్ర అన్లాక్\'ను ఆఫ్ చేయవచ్చు. వేలిముద్ర ఇమేజ్లు, మోడల్లు మీరు వాటిని తొలగించే వరకు ఫోన్లో స్టోర్ చేయబడతాయి."</string>
+ <string name="security_settings_fingerprint_v2_enroll_introduction_footer_message_4" product="default" msgid="1354488801088258040">"మీరు సెట్టింగ్లలో ఎప్పుడైనా మీ వేలిముద్ర ఇమేజ్లను, మోడల్ను తొలగించవచ్చు లేదా \'వేలిముద్ర అన్లాక్\'ను ఆఫ్ చేయవచ్చు. వేలిముద్ర ఇమేజ్లు, మోడల్లను మీరు తొలగించే వరకు ఫోన్లో స్టోర్ చేయబడతాయి."</string>
<string name="security_settings_fingerprint_v2_enroll_introduction_footer_message_4" product="tablet" msgid="8207309581266022275">"మీరు సెట్టింగ్లలో ఎప్పుడైనా మీ వేలిముద్ర ఇమేజ్లను, మోడల్ను తొలగించవచ్చు లేదా \'వేలిముద్ర అన్లాక్\'ను ఆఫ్ చేయవచ్చు. వేలిముద్ర ఇమేజ్లు, మోడల్లు మీరు వాటిని తొలగించే వరకు టాబ్లెట్లో స్టోర్ చేయబడతాయి."</string>
<string name="security_settings_fingerprint_v2_enroll_introduction_footer_message_4" product="device" msgid="2498580070051496133">"మీరు సెట్టింగ్లలో ఎప్పుడైనా మీ వేలిముద్ర ఇమేజ్లను, మోడల్ను తొలగించవచ్చు లేదా \'వేలిముద్ర అన్లాక్\'ను ఆఫ్ చేయవచ్చు. వేలిముద్ర ఇమేజ్లు, మోడల్లు మీరు వాటిని తొలగించే వరకు పరికరంలో స్టోర్ చేయబడతాయి."</string>
<string name="security_settings_fingerprint_v2_enroll_introduction_footer_message_consent_4" product="default" msgid="5003753461032107715">"మీరు మరియు మీ చిన్నారి వారి వేలిముద్ర ఇమేజ్లను, మోడల్ను తొలగించవచ్చు లేదా సెట్టింగ్లలో ఎప్పుడైనా వేలిముద్ర అన్లాక్ను ఆఫ్ చేయవచ్చు. వేలిముద్ర ఇమేజ్లు అలాగే మోడల్లు తొలగించబడే వరకు ఫోన్లో స్టోర్ చేయబడతాయి."</string>
<string name="security_settings_fingerprint_v2_enroll_introduction_footer_message_consent_4" product="tablet" msgid="8772005555323461143">"మీరు మరియు మీ చిన్నారి వారి వేలిముద్ర ఇమేజ్లను, మోడల్ను తొలగించవచ్చు లేదా సెట్టింగ్లలో ఎప్పుడైనా వేలిముద్ర అన్లాక్ను ఆఫ్ చేయవచ్చు. వేలిముద్ర ఇమేజ్లు అలాగే మోడల్లు తొలగించబడే వరకు టాబ్లెట్లో స్టోర్ చేయబడతాయి."</string>
<string name="security_settings_fingerprint_v2_enroll_introduction_footer_message_consent_4" product="device" msgid="7254955922685507093">"మీరు మరియు మీ చిన్నారి వారి వేలిముద్ర ఇమేజ్లను, మోడల్ను తొలగించవచ్చు లేదా సెట్టింగ్లలో ఎప్పుడైనా వేలిముద్ర అన్లాక్ను ఆఫ్ చేయవచ్చు. వేలిముద్ర ఇమేజ్లు అలాగే మోడల్లు తొలగించబడే వరకు పరికరంలో స్టోర్ చేయబడతాయి."</string>
- <string name="security_settings_fingerprint_v2_enroll_introduction_footer_message_5" product="default" msgid="6272159089589340181">"మీరు అనుకోని సందర్భాలలో కూడా, మీ వేలి వద్దకు స్క్రీన్ను తీసుకురావడం ద్వారా ఇతరులు కూడా అన్లాక్ చేయగలుగుతారు."</string>
+ <string name="security_settings_fingerprint_v2_enroll_introduction_footer_message_5" product="default" msgid="6272159089589340181">"మీకు తెలియకుండా ఇతరులు మీ వేలి వద్దకు ఫోన్ స్క్రీన్ను తెచ్చి తాకించి దానిని అన్లాక్ చేయగలుగుతారు."</string>
<string name="security_settings_fingerprint_v2_enroll_introduction_footer_message_5" product="tablet" msgid="2420109998272019149">"మీ టాబ్లెట్ను మీరు అన్లాక్ చేయకూడదనుకున్నప్పుడు కూడా ఎవరైనా దానిని మీ వేలి వద్దకు స్క్రీన్ను తీసుకురావడం ద్వారా అన్లాక్ చేయవచ్చు."</string>
<string name="security_settings_fingerprint_v2_enroll_introduction_footer_message_5" product="device" msgid="5915844445830045866">"మీ పరికరాన్ని మీరు అన్లాక్ చేయకూడదనుకున్నప్పుడు కూడా ఎవరైనా దానిని మీ వేలి వద్దకు స్క్రీన్ను తీసుకురావడం ద్వారా అన్లాక్ చేయవచ్చు."</string>
<string name="security_settings_fingerprint_v2_enroll_introduction_footer_message_consent_5" product="default" msgid="6556725426776167791">"కొంతమంది వారి వేలిని పట్టుకొని దాని దగ్గరకు తీసుకొచ్చినట్లు, మీ చిన్నారి ఫోన్ను ఉద్దేశం లేకపోయినప్పటికీ అన్లాక్ చేసే అవకాశం ఉండవచ్చు."</string>
diff --git a/res-product/values-zh-rTW/strings.xml b/res-product/values-zh-rTW/strings.xml
index a06b68e..cea25ce 100644
--- a/res-product/values-zh-rTW/strings.xml
+++ b/res-product/values-zh-rTW/strings.xml
@@ -145,7 +145,7 @@
<string name="lock_screen_intro_skip_dialog_text" product="default" msgid="8970036878014302990">"裝置保護功能將不會開啟。在這種情況下,你無法在這支手機遺失或遭竊時,防止其他人使用手機。"</string>
<string name="security_settings_sfps_enroll_find_sensor_message" product="tablet" msgid="2006739081527422127">"指紋感應器在電源鍵上。電源鍵形狀扁平,位在平板電腦側邊的調高音量按鈕旁。"</string>
<string name="security_settings_sfps_enroll_find_sensor_message" product="device" msgid="1209233633252372907">"指紋感應器在電源鍵上。電源鍵形狀扁平,位在裝置側邊的調高音量按鈕旁。"</string>
- <string name="security_settings_sfps_enroll_find_sensor_message" product="default" msgid="6862493139500275821">"指紋感應器在電源鍵上。電源鍵形狀扁平,位在手機側邊的調高音量按鈕旁。"</string>
+ <string name="security_settings_sfps_enroll_find_sensor_message" product="default" msgid="6862493139500275821">"指紋感應器在電源鍵上。電源鍵形狀扁平,位在手機側邊的調高音量鍵旁。"</string>
<string name="security_settings_fingerprint_enroll_finish_v2_message" product="tablet" msgid="2012126789397819713">"現在只要使用自己的指紋就能解鎖平板電腦或驗證身分,以便執行某些特定操作,例如登入應用程式或核准購買交易"</string>
<string name="security_settings_fingerprint_enroll_finish_v2_message" product="device" msgid="7119860465479161782">"現在只要使用自己的指紋就能解鎖裝置或驗證身分,以便執行某些特定操作,例如登入應用程式或核准購買交易"</string>
<string name="security_settings_fingerprint_enroll_finish_v2_message" product="default" msgid="8255422287180693200">"現在只要使用自己的指紋就能解鎖手機或驗證身分,以便執行某些特定操作,例如登入應用程式或核准購買交易"</string>
diff --git a/res/color/color_accent_selector.xml b/res/color/color_accent_selector.xml
new file mode 100644
index 0000000..3ccb640
--- /dev/null
+++ b/res/color/color_accent_selector.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2023 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:color="?android:attr/colorAccent"/>
+</selector>
diff --git a/res/color/color_battery_anomaly_yellow_selector.xml b/res/color/color_battery_anomaly_yellow_selector.xml
new file mode 100644
index 0000000..0dd79c2
--- /dev/null
+++ b/res/color/color_battery_anomaly_yellow_selector.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2023 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:color="@color/palette_list_color_yellow"/>
+</selector>
diff --git a/res/drawable-night/ic_app_aspect_ratio_16_9.xml b/res/drawable-night/ic_app_aspect_ratio_16_9.xml
new file mode 100644
index 0000000..069003e
--- /dev/null
+++ b/res/drawable-night/ic_app_aspect_ratio_16_9.xml
@@ -0,0 +1,66 @@
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="412dp"
+ android:height="300dp"
+ android:viewportWidth="412"
+ android:viewportHeight="300">
+ <group>
+ <clip-path
+ android:pathData="M0,0h412v300h-412z"/>
+ <path
+ android:pathData="M384.18,300H27.82C12.53,300 0,287.17 0,271.52V28.48C0,12.83 12.53,0 27.82,0H384.29C399.47,0 412,12.83 412,28.48V271.63C412,287.17 399.47,300 384.18,300Z"
+ android:fillColor="#000000"/>
+ <path
+ android:pathData="M321.83,134.76V134C322.96,133.96 323.87,132.78 323.87,131.32V116.86C323.87,115.4 322.96,114.22 321.83,114.18V58.55C321.83,52.85 317.2,48.22 311.49,48.22H213.79C211.62,48.22 209.49,48.77 207.6,49.82C206.71,50.18 205.73,50.18 204.85,49.83L204.81,49.82C202.92,48.77 200.79,48.22 198.62,48.22H101.22C95.51,48.22 90.88,52.85 90.88,58.55V242.05C90.88,247.76 95.51,252.38 101.22,252.38H198.84C201,252.38 203.13,251.83 205.03,250.78C205.86,250.45 206.78,250.44 207.63,250.73L207.73,250.78C209.62,251.83 213.04,252.38 215.2,252.38H311.49C317.2,252.38 321.83,247.76 321.83,242.05V181.69C322.96,181.65 323.87,180.47 323.87,179.01V152.1C323.87,150.65 322.96,149.46 321.83,149.43V134.76ZM319.45,242.43C319.45,246.61 315.67,250.01 311.49,250.01H101.22C97.04,250.01 93.26,246.61 93.26,242.43V58.55C93.26,54.38 97.04,50.6 101.22,50.6H311.49C315.67,50.6 319.45,54.38 319.45,58.55V242.43Z"
+ android:fillColor="#80868B"/>
+ <path
+ android:pathData="M155,53L259,53A4,4 0,0 1,263 57L263,243A4,4 0,0 1,259 247L155,247A4,4 0,0 1,151 243L151,57A4,4 0,0 1,155 53z"
+ android:fillColor="#669DF6"/>
+ <path
+ android:pathData="M157,57L257,57A2,2 0,0 1,259 59L259,241A2,2 0,0 1,257 243L157,243A2,2 0,0 1,155 241L155,59A2,2 0,0 1,157 57z"
+ android:fillColor="#000000"/>
+ <path
+ android:pathData="M171.48,237H161V226.42"
+ android:strokeLineJoin="round"
+ android:strokeWidth="4"
+ android:fillColor="#00000000"
+ android:strokeColor="#669DF6"
+ android:strokeLineCap="round"/>
+ <path
+ android:pathData="M161,237L176,222"
+ android:strokeLineJoin="round"
+ android:strokeWidth="4"
+ android:fillColor="#00000000"
+ android:strokeColor="#669DF6"
+ android:strokeLineCap="round"/>
+ <path
+ android:pathData="M242.52,63L253,63L253,73.58"
+ android:strokeLineJoin="round"
+ android:strokeWidth="4"
+ android:fillColor="#00000000"
+ android:strokeColor="#669DF6"
+ android:strokeLineCap="round"/>
+ <path
+ android:pathData="M253,63L238,78"
+ android:strokeLineJoin="round"
+ android:strokeWidth="4"
+ android:fillColor="#00000000"
+ android:strokeColor="#669DF6"
+ android:strokeLineCap="round"/>
+ </group>
+</vector>
diff --git a/res/drawable-night/ic_app_aspect_ratio_3_2.xml b/res/drawable-night/ic_app_aspect_ratio_3_2.xml
new file mode 100644
index 0000000..22c0969
--- /dev/null
+++ b/res/drawable-night/ic_app_aspect_ratio_3_2.xml
@@ -0,0 +1,66 @@
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="412dp"
+ android:height="300dp"
+ android:viewportWidth="412"
+ android:viewportHeight="300">
+ <group>
+ <clip-path
+ android:pathData="M0,0h412v300h-412z"/>
+ <path
+ android:pathData="M384.18,300H27.82C12.53,300 0,287.17 0,271.52V28.48C0,12.83 12.53,0 27.82,0H384.29C399.47,0 412,12.83 412,28.48V271.63C412,287.17 399.47,300 384.18,300Z"
+ android:fillColor="#000000"/>
+ <path
+ android:pathData="M321.83,134.76V134C322.96,133.96 323.87,132.78 323.87,131.32V116.86C323.87,115.4 322.96,114.22 321.83,114.18V58.55C321.83,52.85 317.2,48.22 311.49,48.22H213.79C211.62,48.22 209.49,48.77 207.6,49.82C206.71,50.18 205.73,50.18 204.85,49.83L204.81,49.82C202.92,48.77 200.79,48.22 198.62,48.22H101.22C95.51,48.22 90.88,52.85 90.88,58.55V242.05C90.88,247.76 95.51,252.38 101.22,252.38H198.84C201,252.38 203.13,251.83 205.03,250.78C205.86,250.45 206.78,250.44 207.63,250.73L207.73,250.78C209.62,251.83 213.04,252.38 215.2,252.38H311.49C317.2,252.38 321.83,247.76 321.83,242.05V181.69C322.96,181.65 323.87,180.47 323.87,179.01V152.1C323.87,150.65 322.96,149.46 321.83,149.43V134.76ZM319.45,242.43C319.45,246.61 315.67,250.01 311.49,250.01H101.22C97.04,250.01 93.26,246.61 93.26,242.43V58.55C93.26,54.38 97.04,50.6 101.22,50.6H311.49C315.67,50.6 319.45,54.38 319.45,58.55V242.43Z"
+ android:fillColor="#80868B"/>
+ <path
+ android:pathData="M144,53L268,53A4,4 0,0 1,272 57L272,243A4,4 0,0 1,268 247L144,247A4,4 0,0 1,140 243L140,57A4,4 0,0 1,144 53z"
+ android:fillColor="#669DF6"/>
+ <path
+ android:pathData="M146,57L266,57A2,2 0,0 1,268 59L268,241A2,2 0,0 1,266 243L146,243A2,2 0,0 1,144 241L144,59A2,2 0,0 1,146 57z"
+ android:fillColor="#000000"/>
+ <path
+ android:pathData="M160.48,237H150V226.42"
+ android:strokeLineJoin="round"
+ android:strokeWidth="4"
+ android:fillColor="#00000000"
+ android:strokeColor="#669DF6"
+ android:strokeLineCap="round"/>
+ <path
+ android:pathData="M150,237L165,222"
+ android:strokeLineJoin="round"
+ android:strokeWidth="4"
+ android:fillColor="#00000000"
+ android:strokeColor="#669DF6"
+ android:strokeLineCap="round"/>
+ <path
+ android:pathData="M251.52,63L262,63L262,73.58"
+ android:strokeLineJoin="round"
+ android:strokeWidth="4"
+ android:fillColor="#00000000"
+ android:strokeColor="#669DF6"
+ android:strokeLineCap="round"/>
+ <path
+ android:pathData="M262,63L247,78"
+ android:strokeLineJoin="round"
+ android:strokeWidth="4"
+ android:fillColor="#00000000"
+ android:strokeColor="#669DF6"
+ android:strokeLineCap="round"/>
+ </group>
+</vector>
diff --git a/res/drawable-night/ic_app_aspect_ratio_4_3.xml b/res/drawable-night/ic_app_aspect_ratio_4_3.xml
new file mode 100644
index 0000000..0238311
--- /dev/null
+++ b/res/drawable-night/ic_app_aspect_ratio_4_3.xml
@@ -0,0 +1,67 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="412dp"
+ android:height="300dp"
+ android:viewportWidth="412"
+ android:viewportHeight="300">
+ <group>
+ <clip-path
+ android:pathData="M0,0h412v300h-412z"/>
+ <path
+ android:pathData="M384.18,300H27.82C12.53,300 0,287.17 0,271.52V28.48C0,12.83 12.53,0 27.82,0H384.29C399.47,0 412,12.83 412,28.48V271.63C412,287.17 399.47,300 384.18,300Z"
+ android:fillColor="#000000"/>
+ <path
+ android:pathData="M321.83,134.76V134C322.96,133.96 323.87,132.78 323.87,131.32V116.86C323.87,115.4 322.96,114.22 321.83,114.18V58.55C321.83,52.85 317.2,48.22 311.49,48.22H213.79C211.62,48.22 209.49,48.77 207.6,49.82C206.71,50.18 205.73,50.18 204.85,49.83L204.81,49.82C202.92,48.77 200.79,48.22 198.62,48.22H101.22C95.51,48.22 90.88,52.85 90.88,58.55V242.05C90.88,247.76 95.51,252.38 101.22,252.38H198.84C201,252.38 203.13,251.83 205.03,250.78C205.86,250.45 206.78,250.44 207.63,250.73L207.73,250.78C209.62,251.83 213.04,252.38 215.2,252.38H311.49C317.2,252.38 321.83,247.76 321.83,242.05V181.69C322.96,181.65 323.87,180.47 323.87,179.01V152.1C323.87,150.65 322.96,149.46 321.83,149.43V134.76ZM319.45,242.43C319.45,246.61 315.67,250.01 311.49,250.01H101.22C97.04,250.01 93.26,246.61 93.26,242.43V58.55C93.26,54.38 97.04,50.6 101.22,50.6H311.49C315.67,50.6 319.45,54.38 319.45,58.55V242.43Z"
+ android:fillColor="#80868B"/>
+ <path
+ android:pathData="M136,53L276,53A4,4 0,0 1,280 57L280,243A4,4 0,0 1,276 247L136,247A4,4 0,0 1,132 243L132,57A4,4 0,0 1,136 53z"
+ android:fillColor="#669DF6"/>
+ <path
+ android:pathData="M138,57L274,57A2,2 0,0 1,276 59L276,241A2,2 0,0 1,274 243L138,243A2,2 0,0 1,136 241L136,59A2,2 0,0 1,138 57z"
+ android:fillColor="#000000"/>
+ <path
+ android:pathData="M152.48,237H142V226.42"
+ android:strokeLineJoin="round"
+ android:strokeWidth="4"
+ android:fillColor="#00000000"
+ android:strokeColor="#669DF6"
+ android:strokeLineCap="round"/>
+ <path
+ android:pathData="M142,237L157,222"
+ android:strokeLineJoin="round"
+ android:strokeWidth="4"
+ android:fillColor="#00000000"
+ android:strokeColor="#669DF6"
+ android:strokeLineCap="round"/>
+ <path
+ android:pathData="M259.52,63L270,63L270,73.58"
+ android:strokeLineJoin="round"
+ android:strokeWidth="4"
+ android:fillColor="#00000000"
+ android:strokeColor="#669DF6"
+ android:strokeLineCap="round"/>
+ <path
+ android:pathData="M270,63L255,78"
+ android:strokeLineJoin="round"
+ android:strokeWidth="4"
+ android:fillColor="#00000000"
+ android:strokeColor="#669DF6"
+ android:strokeLineCap="round"/>
+ </group>
+</vector>
diff --git a/res/drawable-night/ic_app_aspect_ratio_display_size.xml b/res/drawable-night/ic_app_aspect_ratio_display_size.xml
new file mode 100644
index 0000000..91626d7
--- /dev/null
+++ b/res/drawable-night/ic_app_aspect_ratio_display_size.xml
@@ -0,0 +1,66 @@
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="412dp"
+ android:height="300dp"
+ android:viewportWidth="412"
+ android:viewportHeight="300">
+ <group>
+ <clip-path
+ android:pathData="M0,0h412v300h-412z"/>
+ <path
+ android:pathData="M384.18,300H27.82C12.53,300 0,287.17 0,271.52V28.48C0,12.83 12.53,0 27.82,0H384.29C399.47,0 412,12.83 412,28.48V271.63C412,287.17 399.47,300 384.18,300Z"
+ android:fillColor="#000000"/>
+ <path
+ android:pathData="M321.83,134.76V134C322.96,133.96 323.87,132.78 323.87,131.32V116.86C323.87,115.4 322.96,114.22 321.83,114.18V58.55C321.83,52.85 317.2,48.22 311.49,48.22H213.79C211.62,48.22 209.49,48.77 207.6,49.82C206.71,50.18 205.73,50.18 204.85,49.83L204.81,49.82C202.92,48.77 200.79,48.22 198.62,48.22H101.22C95.51,48.22 90.88,52.85 90.88,58.55V242.05C90.88,247.76 95.51,252.38 101.22,252.38H198.84C201,252.38 203.13,251.83 205.03,250.78C205.86,250.45 206.78,250.44 207.63,250.73L207.73,250.78C209.62,251.83 213.04,252.38 215.2,252.38H311.49C317.2,252.38 321.83,247.76 321.83,242.05V181.69C322.96,181.65 323.87,180.47 323.87,179.01V152.1C323.87,150.65 322.96,149.46 321.83,149.43V134.76ZM319.45,242.43C319.45,246.61 315.67,250.01 311.49,250.01H101.22C97.04,250.01 93.26,246.61 93.26,242.43V58.55C93.26,54.38 97.04,50.6 101.22,50.6H311.49C315.67,50.6 319.45,54.38 319.45,58.55V242.43Z"
+ android:fillColor="#80868B"/>
+ <path
+ android:pathData="M128,53L284,53A4,4 0,0 1,288 57L288,243A4,4 0,0 1,284 247L128,247A4,4 0,0 1,124 243L124,57A4,4 0,0 1,128 53z"
+ android:fillColor="#669DF6"/>
+ <path
+ android:pathData="M130,57L282,57A2,2 0,0 1,284 59L284,241A2,2 0,0 1,282 243L130,243A2,2 0,0 1,128 241L128,59A2,2 0,0 1,130 57z"
+ android:fillColor="#000000"/>
+ <path
+ android:pathData="M144.48,237H134V226.42"
+ android:strokeLineJoin="round"
+ android:strokeWidth="4"
+ android:fillColor="#00000000"
+ android:strokeColor="#669DF6"
+ android:strokeLineCap="round"/>
+ <path
+ android:pathData="M134,237L149,222"
+ android:strokeLineJoin="round"
+ android:strokeWidth="4"
+ android:fillColor="#00000000"
+ android:strokeColor="#669DF6"
+ android:strokeLineCap="round"/>
+ <path
+ android:pathData="M267.52,63L278,63L278,73.58"
+ android:strokeLineJoin="round"
+ android:strokeWidth="4"
+ android:fillColor="#00000000"
+ android:strokeColor="#669DF6"
+ android:strokeLineCap="round"/>
+ <path
+ android:pathData="M278,63L263,78"
+ android:strokeLineJoin="round"
+ android:strokeWidth="4"
+ android:fillColor="#00000000"
+ android:strokeColor="#669DF6"
+ android:strokeLineCap="round"/>
+ </group>
+</vector>
diff --git a/res/drawable-night/ic_app_aspect_ratio_fullscreen.xml b/res/drawable-night/ic_app_aspect_ratio_fullscreen.xml
new file mode 100644
index 0000000..aecc8f0
--- /dev/null
+++ b/res/drawable-night/ic_app_aspect_ratio_fullscreen.xml
@@ -0,0 +1,66 @@
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="412dp"
+ android:height="300dp"
+ android:viewportWidth="412"
+ android:viewportHeight="300">
+ <group>
+ <clip-path
+ android:pathData="M0,0h412v300h-412z"/>
+ <path
+ android:pathData="M384.18,300H27.82C12.53,300 0,287.17 0,271.52V28.48C0,12.83 12.53,0 27.82,0H384.29C399.47,0 412,12.83 412,28.48V271.63C412,287.17 399.47,300 384.18,300Z"
+ android:fillColor="#000000"/>
+ <path
+ android:pathData="M321.83,134.76V134C322.96,133.96 323.87,132.78 323.87,131.32V116.86C323.87,115.4 322.96,114.22 321.83,114.18V58.55C321.83,52.85 317.2,48.22 311.49,48.22H213.79C211.62,48.22 209.49,48.77 207.6,49.82C206.71,50.18 205.73,50.18 204.85,49.83L204.81,49.82C202.92,48.77 200.79,48.22 198.62,48.22H101.22C95.51,48.22 90.88,52.85 90.88,58.55V242.05C90.88,247.76 95.51,252.38 101.22,252.38H198.84C201,252.38 203.13,251.83 205.03,250.78C205.86,250.45 206.78,250.44 207.63,250.73L207.73,250.78C209.62,251.83 213.04,252.38 215.2,252.38H311.49C317.2,252.38 321.83,247.76 321.83,242.05V181.69C322.96,181.65 323.87,180.47 323.87,179.01V152.1C323.87,150.65 322.96,149.46 321.83,149.43V134.76ZM319.45,242.43C319.45,246.61 315.67,250.01 311.49,250.01H101.22C97.04,250.01 93.26,246.61 93.26,242.43V58.55C93.26,54.38 97.04,50.6 101.22,50.6H311.49C315.67,50.6 319.45,54.38 319.45,58.55V242.43Z"
+ android:fillColor="#80868B"/>
+ <path
+ android:pathData="M100,53L312,53A4,4 0,0 1,316 57L316,243A4,4 0,0 1,312 247L100,247A4,4 0,0 1,96 243L96,57A4,4 0,0 1,100 53z"
+ android:fillColor="#669DF6"/>
+ <path
+ android:pathData="M102,57L310,57A2,2 0,0 1,312 59L312,241A2,2 0,0 1,310 243L102,243A2,2 0,0 1,100 241L100,59A2,2 0,0 1,102 57z"
+ android:fillColor="#000000"/>
+ <path
+ android:pathData="M116.48,237H106V226.42"
+ android:strokeLineJoin="round"
+ android:strokeWidth="4"
+ android:fillColor="#00000000"
+ android:strokeColor="#669DF6"
+ android:strokeLineCap="round"/>
+ <path
+ android:pathData="M106,237L121,222"
+ android:strokeLineJoin="round"
+ android:strokeWidth="4"
+ android:fillColor="#00000000"
+ android:strokeColor="#669DF6"
+ android:strokeLineCap="round"/>
+ <path
+ android:pathData="M295.52,63L306,63L306,73.58"
+ android:strokeLineJoin="round"
+ android:strokeWidth="4"
+ android:fillColor="#00000000"
+ android:strokeColor="#669DF6"
+ android:strokeLineCap="round"/>
+ <path
+ android:pathData="M306,63L291,78"
+ android:strokeLineJoin="round"
+ android:strokeWidth="4"
+ android:fillColor="#00000000"
+ android:strokeColor="#669DF6"
+ android:strokeLineCap="round"/>
+ </group>
+</vector>
diff --git a/res/drawable-night/ic_app_aspect_ratio_half_screen.xml b/res/drawable-night/ic_app_aspect_ratio_half_screen.xml
new file mode 100644
index 0000000..af533ea
--- /dev/null
+++ b/res/drawable-night/ic_app_aspect_ratio_half_screen.xml
@@ -0,0 +1,66 @@
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="412dp"
+ android:height="300dp"
+ android:viewportWidth="412"
+ android:viewportHeight="300">
+ <group>
+ <clip-path
+ android:pathData="M0,0h412v300h-412z"/>
+ <path
+ android:pathData="M384.18,300H27.82C12.53,300 0,287.17 0,271.52V28.48C0,12.83 12.53,0 27.82,0H384.29C399.47,0 412,12.83 412,28.48V271.63C412,287.17 399.47,300 384.18,300Z"
+ android:fillColor="#000000"/>
+ <path
+ android:pathData="M321.83,134.76V134C322.96,133.96 323.87,132.78 323.87,131.32V116.86C323.87,115.4 322.96,114.22 321.83,114.18V58.55C321.83,52.85 317.2,48.22 311.49,48.22H213.79C211.62,48.22 209.49,48.77 207.6,49.82C206.71,50.18 205.73,50.18 204.85,49.83L204.81,49.82C202.92,48.77 200.79,48.22 198.62,48.22H101.22C95.51,48.22 90.88,52.85 90.88,58.55V242.05C90.88,247.76 95.51,252.38 101.22,252.38H198.84C201,252.38 203.13,251.83 205.03,250.78C205.86,250.45 206.78,250.44 207.63,250.73L207.73,250.78C209.62,251.83 213.04,252.38 215.2,252.38H311.49C317.2,252.38 321.83,247.76 321.83,242.05V181.69C322.96,181.65 323.87,180.47 323.87,179.01V152.1C323.87,150.65 322.96,149.46 321.83,149.43V134.76ZM319.45,242.43C319.45,246.61 315.67,250.01 311.49,250.01H101.22C97.04,250.01 93.26,246.61 93.26,242.43V58.55C93.26,54.38 97.04,50.6 101.22,50.6H311.49C315.67,50.6 319.45,54.38 319.45,58.55V242.43Z"
+ android:fillColor="#80868B"/>
+ <path
+ android:pathData="M148,53L264,53A4,4 0,0 1,268 57L268,243A4,4 0,0 1,264 247L148,247A4,4 0,0 1,144 243L144,57A4,4 0,0 1,148 53z"
+ android:fillColor="#669DF6"/>
+ <path
+ android:pathData="M150,57L262,57A2,2 0,0 1,264 59L264,241A2,2 0,0 1,262 243L150,243A2,2 0,0 1,148 241L148,59A2,2 0,0 1,150 57z"
+ android:fillColor="#000000"/>
+ <path
+ android:pathData="M164.48,237H154V226.42"
+ android:strokeLineJoin="round"
+ android:strokeWidth="4"
+ android:fillColor="#00000000"
+ android:strokeColor="#669DF6"
+ android:strokeLineCap="round"/>
+ <path
+ android:pathData="M154,237L169,222"
+ android:strokeLineJoin="round"
+ android:strokeWidth="4"
+ android:fillColor="#00000000"
+ android:strokeColor="#669DF6"
+ android:strokeLineCap="round"/>
+ <path
+ android:pathData="M247.52,63L258,63L258,73.58"
+ android:strokeLineJoin="round"
+ android:strokeWidth="4"
+ android:fillColor="#00000000"
+ android:strokeColor="#669DF6"
+ android:strokeLineCap="round"/>
+ <path
+ android:pathData="M258,63L243,78"
+ android:strokeLineJoin="round"
+ android:strokeWidth="4"
+ android:fillColor="#00000000"
+ android:strokeColor="#669DF6"
+ android:strokeLineCap="round"/>
+ </group>
+</vector>
diff --git a/res/drawable/action_button_bg.xml b/res/drawable/action_button_bg.xml
new file mode 100644
index 0000000..b50cc41
--- /dev/null
+++ b/res/drawable/action_button_bg.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+<ripple xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+ android:color="?android:attr/colorControlHighlight">
+ <item>
+ <inset
+ android:insetLeft="0dp"
+ android:insetTop="8dp"
+ android:insetRight="0dp"
+ android:insetBottom="8dp">
+ <shape android:shape="rectangle">
+ <corners android:radius="8dp" />
+ <stroke android:width="1dp"
+ android:color="?androidprv:attr/colorAccentPrimaryVariant"/>
+ </shape>
+ </inset>
+ </item>
+</ripple>
+
diff --git a/res/drawable/battery_hints_chip_bg.xml b/res/drawable/battery_hints_chip_bg.xml
new file mode 100644
index 0000000..e7d1d0f
--- /dev/null
+++ b/res/drawable/battery_hints_chip_bg.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2023 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle">
+ <solid android:color="@color/settingslib_dialog_background" />
+ <corners android:radius="@dimen/battery_hints_chip_corner_radius" />
+</shape>
\ No newline at end of file
diff --git a/res/drawable/battery_hints_chip_bg_ripple.xml b/res/drawable/battery_hints_chip_bg_ripple.xml
new file mode 100644
index 0000000..a8bd0b37
--- /dev/null
+++ b/res/drawable/battery_hints_chip_bg_ripple.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2023 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<ripple xmlns:android="http://schemas.android.com/apk/res/android"
+ android:color="?android:attr/colorControlHighlight">
+ <item android:drawable="@drawable/battery_hints_chip_bg"/>
+</ripple>
\ No newline at end of file
diff --git a/res/drawable/battery_tips_all_rounded_bg.xml b/res/drawable/battery_tips_all_rounded_bg.xml
new file mode 100644
index 0000000..4f61f54
--- /dev/null
+++ b/res/drawable/battery_tips_all_rounded_bg.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2023 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle">
+ <solid android:color="@color/settingslib_dialog_background" />
+ <corners android:radius="@dimen/battery_tips_card_corner_radius_normal" />
+</shape>
\ No newline at end of file
diff --git a/res/drawable/battery_tips_all_rounded_bg_ripple.xml b/res/drawable/battery_tips_all_rounded_bg_ripple.xml
new file mode 100644
index 0000000..3180570
--- /dev/null
+++ b/res/drawable/battery_tips_all_rounded_bg_ripple.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2023 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<ripple xmlns:android="http://schemas.android.com/apk/res/android"
+ android:color="?android:attr/colorControlHighlight">
+ <item android:drawable="@drawable/battery_tips_all_rounded_bg"/>
+</ripple>
\ No newline at end of file
diff --git a/res/drawable/battery_tips_half_rounded_bottom_bg.xml b/res/drawable/battery_tips_half_rounded_bottom_bg.xml
new file mode 100644
index 0000000..7766de6
--- /dev/null
+++ b/res/drawable/battery_tips_half_rounded_bottom_bg.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2023 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle">
+ <solid android:color="@color/settingslib_dialog_background"/>
+ <corners
+ android:topLeftRadius="@dimen/battery_tips_card_corner_radius_small"
+ android:topRightRadius="@dimen/battery_tips_card_corner_radius_small"
+ android:bottomLeftRadius="@dimen/battery_tips_card_corner_radius_normal"
+ android:bottomRightRadius="@dimen/battery_tips_card_corner_radius_normal"
+ />
+</shape>
\ No newline at end of file
diff --git a/res/drawable/battery_tips_half_rounded_top_bg.xml b/res/drawable/battery_tips_half_rounded_top_bg.xml
new file mode 100644
index 0000000..aba1a4f
--- /dev/null
+++ b/res/drawable/battery_tips_half_rounded_top_bg.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2023 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle">
+ <solid android:color="@color/settingslib_dialog_background"/>
+ <corners
+ android:topLeftRadius="@dimen/battery_tips_card_corner_radius_normal"
+ android:topRightRadius="@dimen/battery_tips_card_corner_radius_normal"
+ android:bottomLeftRadius="@dimen/battery_tips_card_corner_radius_small"
+ android:bottomRightRadius="@dimen/battery_tips_card_corner_radius_small"
+ />
+</shape>
\ No newline at end of file
diff --git a/res/drawable/ic_app_aspect_ratio_16_9.xml b/res/drawable/ic_app_aspect_ratio_16_9.xml
new file mode 100644
index 0000000..2300f3d
--- /dev/null
+++ b/res/drawable/ic_app_aspect_ratio_16_9.xml
@@ -0,0 +1,66 @@
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="412dp"
+ android:height="300dp"
+ android:viewportWidth="412"
+ android:viewportHeight="300">
+ <group>
+ <clip-path
+ android:pathData="M0,0h412v300h-412z"/>
+ <path
+ android:pathData="M384.18,300H27.82C12.53,300 0,287.17 0,271.52V28.48C0,12.83 12.53,0 27.82,0H384.29C399.47,0 412,12.83 412,28.48V271.63C412,287.17 399.47,300 384.18,300Z"
+ android:fillColor="#ffffff"/>
+ <path
+ android:pathData="M321.83,134.76V134C322.96,133.96 323.87,132.78 323.87,131.32V116.86C323.87,115.4 322.96,114.22 321.83,114.18V58.55C321.83,52.85 317.2,48.22 311.49,48.22H213.79C211.62,48.22 209.49,48.77 207.6,49.82C206.71,50.18 205.73,50.18 204.84,49.83L204.81,49.82C202.92,48.77 200.79,48.22 198.62,48.22H101.22C95.51,48.22 90.88,52.85 90.88,58.55V242.05C90.88,247.76 95.51,252.38 101.22,252.38H198.84C201,252.38 203.13,251.83 205.03,250.78C205.86,250.45 206.78,250.44 207.63,250.73L207.73,250.78C209.62,251.83 213.04,252.38 215.2,252.38H311.49C317.2,252.38 321.83,247.76 321.83,242.05V181.69C322.96,181.65 323.87,180.47 323.87,179.01V152.1C323.87,150.65 322.96,149.46 321.83,149.43V134.76ZM319.45,242.43C319.45,246.61 315.67,250.01 311.49,250.01H101.21C97.04,250.01 93.26,246.61 93.26,242.43V58.55C93.26,54.38 97.04,50.6 101.21,50.6H311.49C315.67,50.6 319.45,54.38 319.45,58.55V242.43Z"
+ android:fillColor="#DADCE0"/>
+ <path
+ android:pathData="M155,53L259,53A4,4 0,0 1,263 57L263,243A4,4 0,0 1,259 247L155,247A4,4 0,0 1,151 243L151,57A4,4 0,0 1,155 53z"
+ android:fillColor="#1A73E8"/>
+ <path
+ android:pathData="M157,57L257,57A2,2 0,0 1,259 59L259,241A2,2 0,0 1,257 243L157,243A2,2 0,0 1,155 241L155,59A2,2 0,0 1,157 57z"
+ android:fillColor="#ffffff"/>
+ <path
+ android:pathData="M171.48,237H161V226.42"
+ android:strokeLineJoin="round"
+ android:strokeWidth="4"
+ android:fillColor="#00000000"
+ android:strokeColor="#1A73E8"
+ android:strokeLineCap="round"/>
+ <path
+ android:pathData="M161,237L176,222"
+ android:strokeLineJoin="round"
+ android:strokeWidth="4"
+ android:fillColor="#00000000"
+ android:strokeColor="#1A73E8"
+ android:strokeLineCap="round"/>
+ <path
+ android:pathData="M242.52,63L253,63L253,73.58"
+ android:strokeLineJoin="round"
+ android:strokeWidth="4"
+ android:fillColor="#00000000"
+ android:strokeColor="#1A73E8"
+ android:strokeLineCap="round"/>
+ <path
+ android:pathData="M253,63L238,78"
+ android:strokeLineJoin="round"
+ android:strokeWidth="4"
+ android:fillColor="#00000000"
+ android:strokeColor="#1A73E8"
+ android:strokeLineCap="round"/>
+ </group>
+</vector>
diff --git a/res/drawable/ic_app_aspect_ratio_3_2.xml b/res/drawable/ic_app_aspect_ratio_3_2.xml
new file mode 100644
index 0000000..b28bdd4
--- /dev/null
+++ b/res/drawable/ic_app_aspect_ratio_3_2.xml
@@ -0,0 +1,66 @@
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="412dp"
+ android:height="300dp"
+ android:viewportWidth="412"
+ android:viewportHeight="300">
+ <group>
+ <clip-path
+ android:pathData="M0,0h412v300h-412z"/>
+ <path
+ android:pathData="M384.18,300H27.82C12.53,300 0,287.17 0,271.52V28.48C0,12.83 12.53,0 27.82,0H384.29C399.47,0 412,12.83 412,28.48V271.63C412,287.17 399.47,300 384.18,300Z"
+ android:fillColor="#ffffff"/>
+ <path
+ android:pathData="M321.83,134.76V134C322.96,133.96 323.87,132.78 323.87,131.32V116.86C323.87,115.4 322.96,114.22 321.83,114.18V58.55C321.83,52.85 317.2,48.22 311.49,48.22H213.79C211.62,48.22 209.49,48.77 207.6,49.82C206.71,50.18 205.73,50.18 204.84,49.83L204.81,49.82C202.92,48.77 200.79,48.22 198.62,48.22H101.22C95.51,48.22 90.88,52.85 90.88,58.55V242.05C90.88,247.76 95.51,252.38 101.22,252.38H198.84C201,252.38 203.13,251.83 205.03,250.78C205.86,250.45 206.78,250.44 207.63,250.73L207.73,250.78C209.62,251.83 213.04,252.38 215.2,252.38H311.49C317.2,252.38 321.83,247.76 321.83,242.05V181.69C322.96,181.65 323.87,180.47 323.87,179.01V152.1C323.87,150.65 322.96,149.46 321.83,149.43V134.76ZM319.45,242.43C319.45,246.61 315.67,250.01 311.49,250.01H101.21C97.04,250.01 93.26,246.61 93.26,242.43V58.55C93.26,54.38 97.04,50.6 101.21,50.6H311.49C315.67,50.6 319.45,54.38 319.45,58.55V242.43Z"
+ android:fillColor="#DADCE0"/>
+ <path
+ android:pathData="M144,53L268,53A4,4 0,0 1,272 57L272,243A4,4 0,0 1,268 247L144,247A4,4 0,0 1,140 243L140,57A4,4 0,0 1,144 53z"
+ android:fillColor="#1A73E8"/>
+ <path
+ android:pathData="M146,57L266,57A2,2 0,0 1,268 59L268,241A2,2 0,0 1,266 243L146,243A2,2 0,0 1,144 241L144,59A2,2 0,0 1,146 57z"
+ android:fillColor="#ffffff"/>
+ <path
+ android:pathData="M160.48,237H150V226.42"
+ android:strokeLineJoin="round"
+ android:strokeWidth="4"
+ android:fillColor="#00000000"
+ android:strokeColor="#1A73E8"
+ android:strokeLineCap="round"/>
+ <path
+ android:pathData="M150,237L165,222"
+ android:strokeLineJoin="round"
+ android:strokeWidth="4"
+ android:fillColor="#00000000"
+ android:strokeColor="#1A73E8"
+ android:strokeLineCap="round"/>
+ <path
+ android:pathData="M251.52,63L262,63L262,73.58"
+ android:strokeLineJoin="round"
+ android:strokeWidth="4"
+ android:fillColor="#00000000"
+ android:strokeColor="#1A73E8"
+ android:strokeLineCap="round"/>
+ <path
+ android:pathData="M262,63L247,78"
+ android:strokeLineJoin="round"
+ android:strokeWidth="4"
+ android:fillColor="#00000000"
+ android:strokeColor="#1A73E8"
+ android:strokeLineCap="round"/>
+ </group>
+</vector>
diff --git a/res/drawable/ic_app_aspect_ratio_4_3.xml b/res/drawable/ic_app_aspect_ratio_4_3.xml
new file mode 100644
index 0000000..ba875e9
--- /dev/null
+++ b/res/drawable/ic_app_aspect_ratio_4_3.xml
@@ -0,0 +1,66 @@
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="412dp"
+ android:height="300dp"
+ android:viewportWidth="412"
+ android:viewportHeight="300">
+ <group>
+ <clip-path
+ android:pathData="M0,0h412v300h-412z"/>
+ <path
+ android:pathData="M384.18,300H27.82C12.53,300 0,287.17 0,271.52V28.48C0,12.83 12.53,0 27.82,0H384.29C399.47,0 412,12.83 412,28.48V271.63C412,287.17 399.47,300 384.18,300Z"
+ android:fillColor="#ffffff"/>
+ <path
+ android:pathData="M321.83,134.76V134C322.96,133.96 323.87,132.78 323.87,131.32V116.86C323.87,115.4 322.96,114.22 321.83,114.18V58.55C321.83,52.85 317.2,48.22 311.49,48.22H213.79C211.62,48.22 209.49,48.77 207.6,49.82C206.71,50.18 205.73,50.18 204.84,49.83L204.81,49.82C202.92,48.77 200.79,48.22 198.62,48.22H101.22C95.51,48.22 90.88,52.85 90.88,58.55V242.05C90.88,247.76 95.51,252.38 101.22,252.38H198.84C201,252.38 203.13,251.83 205.03,250.78C205.86,250.45 206.78,250.44 207.63,250.73L207.73,250.78C209.62,251.83 213.04,252.38 215.2,252.38H311.49C317.2,252.38 321.83,247.76 321.83,242.05V181.69C322.96,181.65 323.87,180.47 323.87,179.01V152.1C323.87,150.65 322.96,149.46 321.83,149.43V134.76ZM319.45,242.43C319.45,246.61 315.67,250.01 311.49,250.01H101.21C97.04,250.01 93.26,246.61 93.26,242.43V58.55C93.26,54.38 97.04,50.6 101.21,50.6H311.49C315.67,50.6 319.45,54.38 319.45,58.55V242.43Z"
+ android:fillColor="#DADCE0"/>
+ <path
+ android:pathData="M136,53L276,53A4,4 0,0 1,280 57L280,243A4,4 0,0 1,276 247L136,247A4,4 0,0 1,132 243L132,57A4,4 0,0 1,136 53z"
+ android:fillColor="#1A73E8"/>
+ <path
+ android:pathData="M138,57L274,57A2,2 0,0 1,276 59L276,241A2,2 0,0 1,274 243L138,243A2,2 0,0 1,136 241L136,59A2,2 0,0 1,138 57z"
+ android:fillColor="#ffffff"/>
+ <path
+ android:pathData="M152.48,237H142V226.42"
+ android:strokeLineJoin="round"
+ android:strokeWidth="4"
+ android:fillColor="#00000000"
+ android:strokeColor="#1A73E8"
+ android:strokeLineCap="round"/>
+ <path
+ android:pathData="M142,237L157,222"
+ android:strokeLineJoin="round"
+ android:strokeWidth="4"
+ android:fillColor="#00000000"
+ android:strokeColor="#1A73E8"
+ android:strokeLineCap="round"/>
+ <path
+ android:pathData="M259.52,63L270,63L270,73.58"
+ android:strokeLineJoin="round"
+ android:strokeWidth="4"
+ android:fillColor="#00000000"
+ android:strokeColor="#1A73E8"
+ android:strokeLineCap="round"/>
+ <path
+ android:pathData="M270,63L255,78"
+ android:strokeLineJoin="round"
+ android:strokeWidth="4"
+ android:fillColor="#00000000"
+ android:strokeColor="#1A73E8"
+ android:strokeLineCap="round"/>
+ </group>
+</vector>
diff --git a/res/drawable/ic_app_aspect_ratio_display_size.xml b/res/drawable/ic_app_aspect_ratio_display_size.xml
new file mode 100644
index 0000000..1122395
--- /dev/null
+++ b/res/drawable/ic_app_aspect_ratio_display_size.xml
@@ -0,0 +1,66 @@
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="412dp"
+ android:height="300dp"
+ android:viewportWidth="412"
+ android:viewportHeight="300">
+ <group>
+ <clip-path
+ android:pathData="M0,0h412v300h-412z"/>
+ <path
+ android:pathData="M384.18,300H27.82C12.53,300 0,287.17 0,271.52V28.48C0,12.83 12.53,0 27.82,0H384.29C399.47,0 412,12.83 412,28.48V271.63C412,287.17 399.47,300 384.18,300Z"
+ android:fillColor="#ffffff"/>
+ <path
+ android:pathData="M321.83,134.76V134C322.96,133.96 323.87,132.78 323.87,131.32V116.86C323.87,115.4 322.96,114.22 321.83,114.18V58.55C321.83,52.85 317.2,48.22 311.49,48.22H213.79C211.62,48.22 209.49,48.77 207.6,49.82C206.71,50.18 205.73,50.18 204.84,49.83L204.81,49.82C202.92,48.77 200.79,48.22 198.62,48.22H101.22C95.51,48.22 90.88,52.85 90.88,58.55V242.05C90.88,247.76 95.51,252.38 101.22,252.38H198.84C201,252.38 203.13,251.83 205.03,250.78C205.86,250.45 206.78,250.44 207.63,250.73L207.73,250.78C209.62,251.83 213.04,252.38 215.2,252.38H311.49C317.2,252.38 321.83,247.76 321.83,242.05V181.69C322.96,181.65 323.87,180.47 323.87,179.01V152.1C323.87,150.65 322.96,149.46 321.83,149.43V134.76ZM319.45,242.43C319.45,246.61 315.67,250.01 311.49,250.01H101.21C97.04,250.01 93.26,246.61 93.26,242.43V58.55C93.26,54.38 97.04,50.6 101.21,50.6H311.49C315.67,50.6 319.45,54.38 319.45,58.55V242.43Z"
+ android:fillColor="#DADCE0"/>
+ <path
+ android:pathData="M128,53L284,53A4,4 0,0 1,288 57L288,243A4,4 0,0 1,284 247L128,247A4,4 0,0 1,124 243L124,57A4,4 0,0 1,128 53z"
+ android:fillColor="#1A73E8"/>
+ <path
+ android:pathData="M130,57L282,57A2,2 0,0 1,284 59L284,241A2,2 0,0 1,282 243L130,243A2,2 0,0 1,128 241L128,59A2,2 0,0 1,130 57z"
+ android:fillColor="#ffffff"/>
+ <path
+ android:pathData="M144.48,237H134V226.42"
+ android:strokeLineJoin="round"
+ android:strokeWidth="4"
+ android:fillColor="#00000000"
+ android:strokeColor="#1A73E8"
+ android:strokeLineCap="round"/>
+ <path
+ android:pathData="M134,237L149,222"
+ android:strokeLineJoin="round"
+ android:strokeWidth="4"
+ android:fillColor="#00000000"
+ android:strokeColor="#1A73E8"
+ android:strokeLineCap="round"/>
+ <path
+ android:pathData="M267.52,63L278,63L278,73.58"
+ android:strokeLineJoin="round"
+ android:strokeWidth="4"
+ android:fillColor="#00000000"
+ android:strokeColor="#1A73E8"
+ android:strokeLineCap="round"/>
+ <path
+ android:pathData="M278,63L263,78"
+ android:strokeLineJoin="round"
+ android:strokeWidth="4"
+ android:fillColor="#00000000"
+ android:strokeColor="#1A73E8"
+ android:strokeLineCap="round"/>
+ </group>
+</vector>
diff --git a/res/drawable/ic_app_aspect_ratio_fullscreen.xml b/res/drawable/ic_app_aspect_ratio_fullscreen.xml
new file mode 100644
index 0000000..0e62fe5
--- /dev/null
+++ b/res/drawable/ic_app_aspect_ratio_fullscreen.xml
@@ -0,0 +1,66 @@
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="412dp"
+ android:height="300dp"
+ android:viewportWidth="412"
+ android:viewportHeight="300">
+ <group>
+ <clip-path
+ android:pathData="M0,0h412v300h-412z"/>
+ <path
+ android:pathData="M384.18,300H27.82C12.53,300 0,287.17 0,271.52V28.48C0,12.83 12.53,0 27.82,0H384.29C399.47,0 412,12.83 412,28.48V271.63C412,287.17 399.47,300 384.18,300Z"
+ android:fillColor="#ffffff"/>
+ <path
+ android:pathData="M321.83,134.76V134C322.96,133.96 323.87,132.78 323.87,131.32V116.86C323.87,115.4 322.96,114.22 321.83,114.18V58.55C321.83,52.85 317.2,48.22 311.49,48.22H213.79C211.62,48.22 209.49,48.77 207.6,49.82C206.71,50.18 205.73,50.18 204.84,49.83L204.81,49.82C202.92,48.77 200.79,48.22 198.62,48.22H101.22C95.51,48.22 90.88,52.85 90.88,58.55V242.05C90.88,247.76 95.51,252.38 101.22,252.38H198.84C201,252.38 203.13,251.83 205.03,250.78C205.86,250.45 206.78,250.44 207.63,250.73L207.73,250.78C209.62,251.83 213.04,252.38 215.2,252.38H311.49C317.2,252.38 321.83,247.76 321.83,242.05V181.69C322.96,181.65 323.87,180.47 323.87,179.01V152.1C323.87,150.65 322.96,149.46 321.83,149.43V134.76ZM319.45,242.43C319.45,246.61 315.67,250.01 311.49,250.01H101.21C97.04,250.01 93.26,246.61 93.26,242.43V58.55C93.26,54.38 97.04,50.6 101.21,50.6H311.49C315.67,50.6 319.45,54.38 319.45,58.55V242.43Z"
+ android:fillColor="#DADCE0"/>
+ <path
+ android:pathData="M100,53L312,53A4,4 0,0 1,316 57L316,243A4,4 0,0 1,312 247L100,247A4,4 0,0 1,96 243L96,57A4,4 0,0 1,100 53z"
+ android:fillColor="#1A73E8"/>
+ <path
+ android:pathData="M102,57L310,57A2,2 0,0 1,312 59L312,241A2,2 0,0 1,310 243L102,243A2,2 0,0 1,100 241L100,59A2,2 0,0 1,102 57z"
+ android:fillColor="#ffffff"/>
+ <path
+ android:pathData="M116.48,237H106V226.42"
+ android:strokeLineJoin="round"
+ android:strokeWidth="4"
+ android:fillColor="#00000000"
+ android:strokeColor="#1A73E8"
+ android:strokeLineCap="round"/>
+ <path
+ android:pathData="M106,237L121,222"
+ android:strokeLineJoin="round"
+ android:strokeWidth="4"
+ android:fillColor="#00000000"
+ android:strokeColor="#1A73E8"
+ android:strokeLineCap="round"/>
+ <path
+ android:pathData="M295.52,63L306,63L306,73.58"
+ android:strokeLineJoin="round"
+ android:strokeWidth="4"
+ android:fillColor="#00000000"
+ android:strokeColor="#1A73E8"
+ android:strokeLineCap="round"/>
+ <path
+ android:pathData="M306,63L291,78"
+ android:strokeLineJoin="round"
+ android:strokeWidth="4"
+ android:fillColor="#00000000"
+ android:strokeColor="#1A73E8"
+ android:strokeLineCap="round"/>
+ </group>
+</vector>
diff --git a/res/drawable/ic_app_aspect_ratio_half_screen.xml b/res/drawable/ic_app_aspect_ratio_half_screen.xml
new file mode 100644
index 0000000..43afce1
--- /dev/null
+++ b/res/drawable/ic_app_aspect_ratio_half_screen.xml
@@ -0,0 +1,66 @@
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="412dp"
+ android:height="300dp"
+ android:viewportWidth="412"
+ android:viewportHeight="300">
+ <group>
+ <clip-path
+ android:pathData="M0,0h412v300h-412z"/>
+ <path
+ android:pathData="M384.18,300H27.82C12.53,300 0,287.17 0,271.52V28.48C0,12.83 12.53,0 27.82,0H384.29C399.47,0 412,12.83 412,28.48V271.63C412,287.17 399.47,300 384.18,300Z"
+ android:fillColor="#ffffff"/>
+ <path
+ android:pathData="M321.83,134.76V134C322.96,133.96 323.87,132.78 323.87,131.32V116.86C323.87,115.4 322.96,114.22 321.83,114.18V58.55C321.83,52.85 317.2,48.22 311.49,48.22H213.79C211.62,48.22 209.49,48.77 207.6,49.82C206.71,50.18 205.73,50.18 204.84,49.83L204.81,49.82C202.92,48.77 200.79,48.22 198.62,48.22H101.22C95.51,48.22 90.88,52.85 90.88,58.55V242.05C90.88,247.76 95.51,252.38 101.22,252.38H198.84C201,252.38 203.13,251.83 205.03,250.78C205.86,250.45 206.78,250.44 207.63,250.73L207.73,250.78C209.62,251.83 213.04,252.38 215.2,252.38H311.49C317.2,252.38 321.83,247.76 321.83,242.05V181.69C322.96,181.65 323.87,180.47 323.87,179.01V152.1C323.87,150.65 322.96,149.46 321.83,149.43V134.76ZM319.45,242.43C319.45,246.61 315.67,250.01 311.49,250.01H101.21C97.04,250.01 93.26,246.61 93.26,242.43V58.55C93.26,54.38 97.04,50.6 101.21,50.6H311.49C315.67,50.6 319.45,54.38 319.45,58.55V242.43Z"
+ android:fillColor="#DADCE0"/>
+ <path
+ android:pathData="M148,53L264,53A4,4 0,0 1,268 57L268,243A4,4 0,0 1,264 247L148,247A4,4 0,0 1,144 243L144,57A4,4 0,0 1,148 53z"
+ android:fillColor="#1A73E8"/>
+ <path
+ android:pathData="M150,57L262,57A2,2 0,0 1,264 59L264,241A2,2 0,0 1,262 243L150,243A2,2 0,0 1,148 241L148,59A2,2 0,0 1,150 57z"
+ android:fillColor="#ffffff"/>
+ <path
+ android:pathData="M164.48,237H154V226.42"
+ android:strokeLineJoin="round"
+ android:strokeWidth="4"
+ android:fillColor="#00000000"
+ android:strokeColor="#1A73E8"
+ android:strokeLineCap="round"/>
+ <path
+ android:pathData="M154,237L169,222"
+ android:strokeLineJoin="round"
+ android:strokeWidth="4"
+ android:fillColor="#00000000"
+ android:strokeColor="#1A73E8"
+ android:strokeLineCap="round"/>
+ <path
+ android:pathData="M247.52,63L258,63L258,73.58"
+ android:strokeLineJoin="round"
+ android:strokeWidth="4"
+ android:fillColor="#00000000"
+ android:strokeColor="#1A73E8"
+ android:strokeLineCap="round"/>
+ <path
+ android:pathData="M258,63L243,78"
+ android:strokeLineJoin="round"
+ android:strokeWidth="4"
+ android:fillColor="#00000000"
+ android:strokeColor="#1A73E8"
+ android:strokeLineCap="round"/>
+ </group>
+</vector>
diff --git a/res/drawable/ic_battery_charger.xml b/res/drawable/ic_battery_charger.xml
new file mode 100644
index 0000000..4406a56
--- /dev/null
+++ b/res/drawable/ic_battery_charger.xml
@@ -0,0 +1,25 @@
+<!--
+Copyright (C) 2023 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="48dp"
+ android:height="48dp"
+ android:viewportWidth="960"
+ android:viewportHeight="960">
+ <path
+ android:fillColor="?android:attr/colorAccent"
+ android:pathData="M442,780L518,780L518,698L660,542L660,351Q660,351 660,351Q660,351 660,351L300,351Q300,351 300,351Q300,351 300,351L300,542L442,697.7L442,780ZM382,840L382,722L240,566L240,351Q240,326.25 257.63,308.63Q275.25,291 300,291L372,291L342,321L342,120L402,120L402,291L558,291L558,120L618,120L618,321L588,291L660,291Q684.75,291 702.38,308.63Q720,326.25 720,351L720,566L578,722L578,840L382,840ZM480,565L480,565L480,565L480,565Q480,565 480,565Q480,565 480,565L480,565Q480,565 480,565Q480,565 480,565L480,565L480,565L480,565L480,565Z"/>
+</vector>
+
diff --git a/res/drawable/ic_battery_full.xml b/res/drawable/ic_battery_full.xml
new file mode 100644
index 0000000..5dfac55
--- /dev/null
+++ b/res/drawable/ic_battery_full.xml
@@ -0,0 +1,25 @@
+<!--
+ Copyright (C) 2023 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="960"
+ android:viewportHeight="960"
+ android:tint="?android:attr/colorControlNormal">
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M320,880Q303,880 291.5,868.5Q280,857 280,840L280,200Q280,183 291.5,171.5Q303,160 320,160L400,160L400,80L560,80L560,160L640,160Q657,160 668.5,171.5Q680,183 680,200L680,840Q680,857 668.5,868.5Q657,880 640,880L320,880Z"/>
+</vector>
diff --git a/res/drawable/ic_battery_tips_close.xml b/res/drawable/ic_battery_tips_close.xml
new file mode 100644
index 0000000..7ef571b
--- /dev/null
+++ b/res/drawable/ic_battery_tips_close.xml
@@ -0,0 +1,25 @@
+<!--
+ Copyright (C) 2023 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="?android:attr/textColorSecondary"
+ android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12 19,6.41z"/>
+</vector>
diff --git a/res/drawable/ic_battery_tips_close_icon.xml b/res/drawable/ic_battery_tips_close_icon.xml
new file mode 100644
index 0000000..b766474
--- /dev/null
+++ b/res/drawable/ic_battery_tips_close_icon.xml
@@ -0,0 +1,32 @@
+<!--
+ Copyright (C) 2023 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp">
+ <item>
+ <shape android:shape="oval">
+ <size
+ android:width="24dp"
+ android:height="24dp" />
+ <solid android:color="?android:attr/colorBackground" />
+ </shape>
+ </item>
+ <item android:drawable="@drawable/ic_battery_tips_close"
+ android:gravity="center"
+ android:width="16dp"
+ android:height="16dp"/>
+</layer-list>
\ No newline at end of file
diff --git a/res/drawable/ic_battery_tips_lightbulb.xml b/res/drawable/ic_battery_tips_lightbulb.xml
new file mode 100644
index 0000000..6fffefc
--- /dev/null
+++ b/res/drawable/ic_battery_tips_lightbulb.xml
@@ -0,0 +1,25 @@
+<!--
+ Copyright (C) 2023 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="32dp"
+ android:height="32dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@color/color_accent_selector"
+ android:pathData="M7,20h4c0,1.1 -0.9,2 -2,2S7,21.1 7,20zM5,19h8v-2H5V19zM16.5,9.5c0,3.82 -2.66,5.86 -3.77,6.5H5.27C4.16,15.36 1.5,13.32 1.5,9.5C1.5,5.36 4.86,2 9,2S16.5,5.36 16.5,9.5zM14.5,9.5C14.5,6.47 12.03,4 9,4S3.5,6.47 3.5,9.5c0,2.47 1.49,3.89 2.35,4.5h6.3C13.01,13.39 14.5,11.97 14.5,9.5zM21.37,7.37L20,8l1.37,0.63L22,10l0.63,-1.37L24,8l-1.37,-0.63L22,6L21.37,7.37zM19,6l0.94,-2.06L22,3l-2.06,-0.94L19,0l-0.94,2.06L16,3l2.06,0.94L19,6z"/>
+</vector>
\ No newline at end of file
diff --git a/res/drawable/ic_battery_tips_thumb_down.xml b/res/drawable/ic_battery_tips_thumb_down.xml
new file mode 100644
index 0000000..cd7656b
--- /dev/null
+++ b/res/drawable/ic_battery_tips_thumb_down.xml
@@ -0,0 +1,25 @@
+<!--
+ Copyright (C) 2023 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="960"
+ android:viewportHeight="960">
+ <path
+ android:fillColor="?android:attr/colorAccent"
+ android:pathData="M242,120L686,120L686,632L408,920L369,889Q363,884 360,875Q357,866 357,853L357,843L402,632L103,632Q79,632 61,614Q43,596 43,572L43,490.16Q43,483 41.5,475.5Q40,468 43,461L169,171Q177.88,149.75 198.6,134.88Q219.31,120 242,120ZM626,180L229,180Q229,180 229,180Q229,180 229,180L103,479L103,572Q103,572 103,572Q103,572 103,572L476,572L423,821L626,607L626,180ZM626,607L626,607L626,572L626,572Q626,572 626,572Q626,572 626,572L626,479L626,180Q626,180 626,180Q626,180 626,180L626,180L626,607ZM686,632L686,572L819,572L819,180L686,180L686,120L879,120L879,632L686,632Z" />
+</vector>
\ No newline at end of file
diff --git a/res/drawable/ic_battery_tips_thumb_up.xml b/res/drawable/ic_battery_tips_thumb_up.xml
new file mode 100644
index 0000000..b1d4cb6
--- /dev/null
+++ b/res/drawable/ic_battery_tips_thumb_up.xml
@@ -0,0 +1,25 @@
+<!--
+ Copyright (C) 2023 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="960"
+ android:viewportHeight="960">
+ <path
+ android:fillColor="?android:attr/colorAccent"
+ android:pathData="M716,840L272,840L272,328L550,40L589,71Q595,76 598,85Q601,94 601,107L601,117L556,328L855,328Q879,328 897,346Q915,364 915,388L915,469.84Q915,477 916.5,484.5Q918,492 915,499L789,789Q780.12,810.25 759.41,825.13Q738.69,840 716,840ZM332,780L729,780Q729,780 729,780Q729,780 729,780L855,481L855,388Q855,388 855,388Q855,388 855,388L482,388L535,139L332,353L332,780ZM332,353L332,353L332,388L332,388Q332,388 332,388Q332,388 332,388L332,481L332,780Q332,780 332,780Q332,780 332,780L332,780L332,353ZM272,328L272,388L139,388L139,780L272,780L272,840L79,840L79,328L272,328Z" />
+</vector>
\ No newline at end of file
diff --git a/res/drawable/ic_battery_tips_warning_icon.xml b/res/drawable/ic_battery_tips_warning_icon.xml
new file mode 100644
index 0000000..c5df8a8
--- /dev/null
+++ b/res/drawable/ic_battery_tips_warning_icon.xml
@@ -0,0 +1,25 @@
+<!--
+ Copyright (C) 2023 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="32dp"
+ android:height="32dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@color/color_battery_anomaly_yellow_selector"
+ android:pathData="M1,21h22L12,2 1,21zM13,18h-2v-2h2v2zM13,14h-2v-4h2v4z"/>
+</vector>
\ No newline at end of file
diff --git a/res/drawable/ic_lock_none.xml b/res/drawable/ic_lock_none.xml
index 31069b7..54b9bb4 100644
--- a/res/drawable/ic_lock_none.xml
+++ b/res/drawable/ic_lock_none.xml
@@ -18,7 +18,8 @@
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
- android:viewportHeight="24">
+ android:viewportHeight="24"
+ android:tint="?android:attr/colorControlNormal">
<path
android:pathData="M18,8h-1L17,6c0,-2.76 -2.24,-5 -5,-5S7,3.24 7,6h2c0,-1.66 1.34,-3 3,-3s3,1.34 3,3v2L6,8c-1.1,0 -2,0.9 -2,2v10c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2L20,10c0,-1.1 -0.9,-2 -2,-2zM18,20L6,20L6,10h12v10z"
android:fillColor="?android:attr/colorAccent"/>
diff --git a/res/drawable/ic_lock_pin.xml b/res/drawable/ic_lock_pin.xml
index 587f49c..4614f53 100644
--- a/res/drawable/ic_lock_pin.xml
+++ b/res/drawable/ic_lock_pin.xml
@@ -18,7 +18,8 @@
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
- android:viewportHeight="24">
+ android:viewportHeight="24"
+ android:tint="?android:attr/colorControlNormal">
<path
android:pathData="M6,8c1.1,0 2,-0.9 2,-2s-0.9,-2 -2,-2 -2,0.9 -2,2 0.9,2 2,2zM12,20c1.1,0 2,-0.9 2,-2s-0.9,-2 -2,-2 -2,0.9 -2,2 0.9,2 2,2zM6,20c1.1,0 2,-0.9 2,-2s-0.9,-2 -2,-2 -2,0.9 -2,2 0.9,2 2,2zM6,14c1.1,0 2,-0.9 2,-2s-0.9,-2 -2,-2 -2,0.9 -2,2 0.9,2 2,2zM12,14c1.1,0 2,-0.9 2,-2s-0.9,-2 -2,-2 -2,0.9 -2,2 0.9,2 2,2zM16,6c0,1.1 0.9,2 2,2s2,-0.9 2,-2 -0.9,-2 -2,-2 -2,0.9 -2,2zM12,8c1.1,0 2,-0.9 2,-2s-0.9,-2 -2,-2 -2,0.9 -2,2 0.9,2 2,2zM18,14c1.1,0 2,-0.9 2,-2s-0.9,-2 -2,-2 -2,0.9 -2,2 0.9,2 2,2zM18,20c1.1,0 2,-0.9 2,-2s-0.9,-2 -2,-2 -2,0.9 -2,2 0.9,2 2,2z"
android:fillColor="?android:attr/colorAccent"/>
diff --git a/res/drawable/ic_lock_swipe.xml b/res/drawable/ic_lock_swipe.xml
index f7e78b8..fb8302d 100644
--- a/res/drawable/ic_lock_swipe.xml
+++ b/res/drawable/ic_lock_swipe.xml
@@ -18,7 +18,8 @@
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
- android:viewportHeight="24">
+ android:viewportHeight="24"
+ android:tint="?android:attr/colorControlNormal">
<path
android:pathData="M20.5,2v2.02C18.18,2.13 15.22,1 12,1S5.82,2.13 3.5,4.02V2H2v3.5V7h1.5H7V5.5H4.09c2.11,-1.86 4.88,-3 7.91,-3s5.79,1.14 7.91,3H17V7h3.5H22V5.5V2H20.5z"
android:fillColor="?android:attr/colorAccent"/>
diff --git a/res/drawable/ic_password.xml b/res/drawable/ic_password.xml
index 341e544..cf3b408 100644
--- a/res/drawable/ic_password.xml
+++ b/res/drawable/ic_password.xml
@@ -18,7 +18,8 @@
android:width="24dp"
android:height="24dp"
android:viewportHeight="24.0"
- android:viewportWidth="24.0">
+ android:viewportWidth="24.0"
+ android:tint="?android:attr/colorControlNormal">
<path
android:fillColor="?android:attr/colorAccent"
android:pathData="M21.5,9.39l-1.63,0l0.81,-1.42l-0.86,-0.5l-0.82,1.42l-0.82,-1.42l-0.86,0.5l0.81,1.42l-1.63,0l0,1l1.63,0l-0.81,1.41l0.86,0.5l0.82,-1.41l0.82,1.41l0.86,-0.5l-0.81,-1.41l1.63,0z" />
diff --git a/res/drawable/ic_pattern.xml b/res/drawable/ic_pattern.xml
index 788eaa7..e56fb00 100644
--- a/res/drawable/ic_pattern.xml
+++ b/res/drawable/ic_pattern.xml
@@ -18,7 +18,8 @@
android:width="24dp"
android:height="24dp"
android:viewportHeight="24.0"
- android:viewportWidth="24.0">
+ android:viewportWidth="24.0"
+ android:tint="?android:attr/colorControlNormal">
<path
android:fillColor="?android:attr/colorAccent"
android:pathData="M4,4m-2,0a2,2 0,1 1,4 0a2,2 0,1 1,-4 0" />
diff --git a/res/drawable/ic_pin.xml b/res/drawable/ic_pin.xml
index 682e934..8520ec1 100644
--- a/res/drawable/ic_pin.xml
+++ b/res/drawable/ic_pin.xml
@@ -18,7 +18,8 @@
android:width="24dp"
android:height="24dp"
android:viewportHeight="24.0"
- android:viewportWidth="24.0">
+ android:viewportWidth="24.0"
+ android:tint="?android:attr/colorControlNormal">
<path
android:fillColor="?android:attr/colorAccent"
android:pathData="M20,4L4,4A2,2 0,0 0,2 6L2,18a2,2 0,0 0,2 2L20,20a2,2 0,0 0,2 -2L22,6A2,2 0,0 0,20 4ZM7.1,15L5.9,15L5.9,10.2L4.7,10.2L4.7,9L7.1,9v6ZM13.2,11.4A1.2,1.2 0,0 1,12 12.6L10.8,12.6v1.2h2.4L13.2,15L9.6,15L9.6,12.6a1.2,1.2 0,0 1,1.2 -1.2L12,11.4L12,10.2L9.6,10.2L9.6,9L12,9a1.2,1.2 0,0 1,1.2 1.2v1.2ZM19.3,11.1a0.9,0.9 0,0 1,-0.9 0.9,0.9 0.9,0 0,1 0.9,0.9v0.9A1.2,1.2 0,0 1,18.1 15L15.7,15L15.7,13.8h2.4L18.1,12.6L16.9,12.6L16.9,11.4h1.2L18.1,10.2L15.7,10.2L15.7,9h2.4a1.2,1.2 0,0 1,1.2 1.2v0.9Z" />
diff --git a/res/layout-land/choose_lock_pattern_common.xml b/res/layout-land/choose_lock_pattern_common.xml
index 2913c5a..e440461 100644
--- a/res/layout-land/choose_lock_pattern_common.xml
+++ b/res/layout-land/choose_lock_pattern_common.xml
@@ -38,15 +38,6 @@
android:paddingRight="0dp"
android:paddingBottom="0dp">
- <!-- TODO b/249974175 Move into Glif header mixin -->
- <Button
- android:id="@+id/screen_lock_options"
- style="@style/SudGlifButton.Tertiary"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="@string/setup_lock_settings_options_button_label"
- android:visibility="gone"/>
-
<com.google.android.setupdesign.view.FillContentLayout
style="@style/LockPatternContainerStyle"
android:layout_width="wrap_content"
diff --git a/res/layout-land/udfps_enroll_enrolling.xml b/res/layout-land/udfps_enroll_enrolling.xml
deleted file mode 100644
index 743684f..0000000
--- a/res/layout-land/udfps_enroll_enrolling.xml
+++ /dev/null
@@ -1,101 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- Copyright (C) 2021 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.
--->
-
-<com.google.android.setupdesign.GlifLayout
- xmlns:android="http://schemas.android.com/apk/res/android"
- android:id="@+id/setup_wizard_layout"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:layout="@layout/sud_glif_blank_template"
- style="?attr/fingerprint_layout_theme">
-
- <LinearLayout
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:orientation="horizontal"
- android:clipToPadding="false"
- android:clipChildren="false">
-
- <!-- Both texts are kept as separate text views so it doesn't jump around in portrait.
- See layouts/fingerprint_enroll_enrolling_base.xml. -->
- <LinearLayout
- android:id="@+id/layout_container"
- android:layout_width="0dp"
- android:layout_weight="1"
- android:layout_height="match_parent"
- android:layout_marginStart="?attr/sudMarginStart"
- android:layout_marginEnd="@dimen/enroll_margin_end"
- android:layout_marginBottom="@dimen/sud_content_frame_padding_bottom"
- android:paddingStart="@dimen/enroll_padding_start"
- android:paddingEnd="@dimen/enroll_padding_end"
- android:clipChildren="false"
- android:clipToPadding="false"
- android:orientation="vertical">
-
- <ScrollView
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:clipChildren="false"
- android:clipToPadding="false"
- android:fillViewport="true">
-
- <LinearLayout
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:orientation="vertical"
- android:clipChildren="false"
- android:clipToPadding="false">
-
- <ImageView
- android:id="@+id/sud_layout_icon"
- style="@style/SudGlifIcon"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:scaleType="fitStart"
- android:layout_marginStart="0dp"
- android:layout_marginEnd="0dp"
- android:src="@drawable/ic_lock" />
-
- <TextView
- android:id="@+id/suc_layout_title"
- style="@style/SudGlifHeaderTitle"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_marginStart="0dp"
- android:layout_marginEnd="0dp" />
-
- <TextView
- style="@style/SudDescription.Glif"
- android:id="@+id/sud_layout_subtitle"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"/>
-
- <Space
- android:layout_width="0dp"
- android:layout_height="0dp"
- android:layout_weight="1" />
-
- </LinearLayout>
-
- </ScrollView>
-
- </LinearLayout>
-
- </LinearLayout>
-
- <include layout="@layout/udfps_enroll_view" />
-</com.google.android.setupdesign.GlifLayout>
\ No newline at end of file
diff --git a/res/layout/action_button.xml b/res/layout/action_button.xml
new file mode 100644
index 0000000..00fdc1e
--- /dev/null
+++ b/res/layout/action_button.xml
@@ -0,0 +1,32 @@
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+
+<Button xmlns:android="http://schemas.android.com/apk/res/android"
+ android:gravity="center_vertical|start"
+ android:paddingStart="12dp"
+ android:paddingEnd="12dp"
+ android:drawablePadding="8dp"
+ android:textColor="?android:attr/textColorPrimary"
+ android:textSize="12sp"
+ android:maxWidth="192dp"
+ android:singleLine="true"
+ android:clickable="true"
+ android:background="@drawable/action_button_bg"
+ android:drawableTint="?android:attr/textColorPrimary"
+ android:drawableTintMode="src_in"
+ style="?android:attr/borderlessButtonStyle"
+ />
+
diff --git a/res/layout/anomaly_app_item_preference.xml b/res/layout/anomaly_app_item_preference.xml
new file mode 100644
index 0000000..0a19849
--- /dev/null
+++ b/res/layout/anomaly_app_item_preference.xml
@@ -0,0 +1,67 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2023 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+
+ <include layout="@layout/preference_app"/>
+
+ <LinearLayout
+ android:id="@+id/warning_chip"
+ android:visibility="gone"
+ android:clickable="false"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="?android:attr/selectableItemBackground"
+ android:gravity="center_vertical"
+ android:orientation="horizontal"
+ android:paddingStart="?android:attr/listPreferredItemPaddingStart"
+ android:paddingEnd="?android:attr/listPreferredItemPaddingEnd">
+
+ <Space
+ android:layout_width="@dimen/secondary_app_icon_size"
+ android:layout_height="wrap_content" />
+
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:padding="8dp"
+ android:layout_marginStart="16dp"
+ android:background="@drawable/battery_hints_chip_bg_ripple">
+
+ <ImageView
+ android:layout_width="16dp"
+ android:layout_height="16dp"
+ android:layout_gravity="center_vertical|start"
+ android:contentDescription="@string/battery_hints_warning_icon_a11y"
+ android:src="@drawable/ic_battery_tips_warning_icon" />
+
+ <TextView
+ android:id="@+id/warning_info"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:paddingHorizontal="8dp"
+ android:layout_gravity="center_vertical|start"
+ android:textAlignment="viewStart"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:textColor="?android:attr/textColorPrimary"/>
+ </LinearLayout>
+
+ </LinearLayout>
+</LinearLayout>
\ No newline at end of file
diff --git a/res/layout/battery_chart_graph.xml b/res/layout/battery_chart_graph.xml
index f116c8e..9e816ed 100644
--- a/res/layout/battery_chart_graph.xml
+++ b/res/layout/battery_chart_graph.xml
@@ -27,6 +27,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginVertical="16dp"
+ android:textAlignment="viewStart"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="?android:attr/textColorSecondary"
android:text="@string/battery_usage_chart_graph_hint_last_full_charge" />
@@ -40,7 +41,7 @@
<com.android.settings.fuelgauge.batteryusage.BatteryChartView
android:id="@+id/daily_battery_chart"
android:layout_width="match_parent"
- android:layout_height="170dp"
+ android:layout_height="@dimen/chartview_layout_height"
android:layout_marginBottom="16dp"
android:visibility="gone"
android:contentDescription="@string/daily_battery_usage_chart"
@@ -50,7 +51,7 @@
<com.android.settings.fuelgauge.batteryusage.BatteryChartView
android:id="@+id/hourly_battery_chart"
android:layout_width="match_parent"
- android:layout_height="170dp"
+ android:layout_height="@dimen/chartview_layout_height"
android:layout_marginBottom="16dp"
android:visibility="visible"
android:contentDescription="@string/hourly_battery_usage_chart"
diff --git a/res/layout/battery_tips_card.xml b/res/layout/battery_tips_card.xml
new file mode 100644
index 0000000..c9a00bc
--- /dev/null
+++ b/res/layout/battery_tips_card.xml
@@ -0,0 +1,112 @@
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:id="@+id/battery_tips_card"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:paddingStart="?android:attr/listPreferredItemPaddingStart"
+ android:paddingEnd="?android:attr/listPreferredItemPaddingEnd">
+
+ <LinearLayout
+ android:id="@+id/tips_card"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="@drawable/battery_tips_all_rounded_bg_ripple"
+ android:orientation="vertical"
+ android:padding="24dp">
+
+ <ImageView
+ android:id="@+id/icon"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical|start"
+ android:src="@drawable/ic_battery_tips_lightbulb" />
+
+ <TextView
+ android:id="@+id/title"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="8dp"
+ android:textAlignment="viewStart"
+ android:textAppearance="?android:attr/textAppearanceLarge"
+ android:textColor="?android:attr/textColorPrimary" />
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:layout_marginTop="8dp"
+ android:gravity="end">
+
+ <com.google.android.material.button.MaterialButton
+ android:id="@+id/dismiss_button"
+ style="@style/Widget.Material3.Button.TextButton"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="end|center_vertical"
+ android:paddingHorizontal="16dp"
+ android:layout_marginEnd="8dp"
+ android:text="@string/battery_tips_card_dismiss_button"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:textColor="?android:attr/textColorPrimary" />
+
+ <com.google.android.material.button.MaterialButton
+ android:id="@+id/main_button"
+ style="@style/Widget.Material3.Button.OutlinedButton"
+ android:paddingHorizontal="16dp"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="end|center_vertical"
+ android:text="@string/battery_tips_card_action_button"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:textColor="?android:attr/textColorPrimary"
+ app:strokeColor="@color/color_accent_selector"
+ app:strokeWidth="1dp" />
+ </LinearLayout>
+ </LinearLayout>
+
+ <Space
+ android:layout_width="0dp"
+ android:layout_height="1dp"/>
+
+ <LinearLayout
+ android:id="@+id/feedback_card"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="@drawable/battery_tips_half_rounded_bottom_bg"
+ android:gravity="center_vertical|start"
+ android:orientation="horizontal"
+ android:paddingHorizontal="24dp"
+ android:paddingVertical="16dp"
+ android:visibility="gone">
+
+ <TextView
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="0dp"
+ android:layout_marginEnd="20dp"
+ android:layout_weight="1"
+ android:text="@string/battery_tips_card_feedback_info"
+ android:textAlignment="viewStart"
+ android:textColor="?android:attr/textColorPrimary"
+ android:textStyle="bold"/>
+
+ <ImageButton
+ android:id="@+id/thumb_up"
+ style="@style/Banner.Dismiss.SettingsLib"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical|end"
+ android:layout_marginEnd="20dp"
+ android:src="@drawable/ic_battery_tips_thumb_up" />
+
+ <ImageButton
+ android:id="@+id/thumb_down"
+ style="@style/Banner.Dismiss.SettingsLib"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical|end"
+ android:src="@drawable/ic_battery_tips_thumb_down" />
+ </LinearLayout>
+</LinearLayout>
\ No newline at end of file
diff --git a/res/layout/choose_lock_password.xml b/res/layout/choose_lock_password.xml
index 5819774..c2eb13a 100644
--- a/res/layout/choose_lock_password.xml
+++ b/res/layout/choose_lock_password.xml
@@ -61,12 +61,6 @@
android:imeOptions="actionNext|flagNoExtractUi|flagForceAscii"
style="@style/TextAppearance.PasswordEntry"/>
- <androidx.recyclerview.widget.RecyclerView
- android:layout_marginTop="8dp"
- android:id="@+id/password_requirements_view"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"/>
-
<CheckBox
android:id="@+id/auto_pin_confirm_enabler"
android:layout_marginTop="8dp"
@@ -91,14 +85,6 @@
android:textSize="16sp"
android:visibility="gone" />
- <Button
- android:id="@+id/screen_lock_options"
- style="@style/SudGlifButton.Tertiary"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="@string/setup_lock_settings_options_button_label"
- android:visibility="gone" />
-
</LinearLayout>
</com.google.android.setupdesign.GlifLayout>
diff --git a/res/layout/choose_lock_pattern_common.xml b/res/layout/choose_lock_pattern_common.xml
index 774f5cd..ddfa046 100644
--- a/res/layout/choose_lock_pattern_common.xml
+++ b/res/layout/choose_lock_pattern_common.xml
@@ -36,14 +36,6 @@
android:paddingLeft="0dp"
android:paddingRight="0dp">
- <Button
- android:id="@+id/screen_lock_options"
- style="@style/LockPatternButtonStyle"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="@string/setup_lock_settings_options_button_label"
- android:visibility="gone"/>
-
<com.google.android.setupdesign.view.FillContentLayout
style="@style/LockPatternContainerStyle"
android:layout_width="wrap_content"
diff --git a/res/layout/layout_color_selector.xml b/res/layout/layout_color_selector.xml
index c366add..a6b9cc8 100644
--- a/res/layout/layout_color_selector.xml
+++ b/res/layout/layout_color_selector.xml
@@ -14,161 +14,167 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/color_selector_root_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:minHeight="?android:attr/listPreferredItemHeight"
- android:orientation="vertical">
-
+ android:padding="20dp"
+ android:clipToPadding="false"
+ android:scrollbarStyle="outsideOverlay">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:layout_marginBottom="10dp"
- android:orientation="horizontal">
-
- <RadioButton
- android:id="@+id/color_radio_button_00"
- android:layout_width="@dimen/screen_flash_color_button_frame_size"
- android:layout_height="@dimen/screen_flash_color_button_frame_size"
- android:button="@drawable/screen_flash_color_01_selector"
- android:contentDescription="@string/screen_flash_color_blue" />
-
- <Space
- android:layout_width="0dp"
+ android:minHeight="?android:attr/listPreferredItemHeight"
+ android:orientation="vertical">
+ <LinearLayout
+ android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:layout_weight="1" />
+ android:layout_marginBottom="10dp"
+ android:orientation="horizontal">
- <RadioButton
- android:id="@+id/color_radio_button_01"
- android:layout_width="@dimen/screen_flash_color_button_frame_size"
- android:layout_height="@dimen/screen_flash_color_button_frame_size"
- android:button="@drawable/screen_flash_color_02_selector"
- android:contentDescription="@string/screen_flash_color_azure" />
+ <RadioButton
+ android:id="@+id/color_radio_button_00"
+ android:layout_width="@dimen/screen_flash_color_button_frame_size"
+ android:layout_height="@dimen/screen_flash_color_button_frame_size"
+ android:button="@drawable/screen_flash_color_01_selector"
+ android:contentDescription="@string/screen_flash_color_blue" />
- <Space
- android:layout_width="0dp"
+ <Space
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1" />
+
+ <RadioButton
+ android:id="@+id/color_radio_button_01"
+ android:layout_width="@dimen/screen_flash_color_button_frame_size"
+ android:layout_height="@dimen/screen_flash_color_button_frame_size"
+ android:button="@drawable/screen_flash_color_02_selector"
+ android:contentDescription="@string/screen_flash_color_azure" />
+
+ <Space
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1" />
+
+ <RadioButton
+ android:id="@+id/color_radio_button_02"
+ android:layout_width="@dimen/screen_flash_color_button_frame_size"
+ android:layout_height="@dimen/screen_flash_color_button_frame_size"
+ android:button="@drawable/screen_flash_color_03_selector"
+ android:contentDescription="@string/screen_flash_color_cyan" />
+
+ <Space
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1" />
+
+ <RadioButton
+ android:id="@+id/color_radio_button_03"
+ android:layout_width="@dimen/screen_flash_color_button_frame_size"
+ android:layout_height="@dimen/screen_flash_color_button_frame_size"
+ android:button="@drawable/screen_flash_color_04_selector"
+ android:contentDescription="@string/screen_flash_color_spring_green" />
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:layout_weight="1" />
+ android:layout_marginBottom="10dp"
+ android:orientation="horizontal">
- <RadioButton
- android:id="@+id/color_radio_button_02"
- android:layout_width="@dimen/screen_flash_color_button_frame_size"
- android:layout_height="@dimen/screen_flash_color_button_frame_size"
- android:button="@drawable/screen_flash_color_03_selector"
- android:contentDescription="@string/screen_flash_color_cyan" />
+ <RadioButton
+ android:id="@+id/color_radio_button_04"
+ android:layout_width="@dimen/screen_flash_color_button_frame_size"
+ android:layout_height="@dimen/screen_flash_color_button_frame_size"
+ android:button="@drawable/screen_flash_color_05_selector"
+ android:contentDescription="@string/screen_flash_color_green" />
- <Space
- android:layout_width="0dp"
+ <Space
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1" />
+
+ <RadioButton
+ android:id="@+id/color_radio_button_05"
+ android:layout_width="@dimen/screen_flash_color_button_frame_size"
+ android:layout_height="@dimen/screen_flash_color_button_frame_size"
+ android:button="@drawable/screen_flash_color_06_selector"
+ android:contentDescription="@string/screen_flash_color_chartreuse_green" />
+
+
+ <Space
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1" />
+ <RadioButton
+ android:id="@+id/color_radio_button_06"
+ android:layout_width="@dimen/screen_flash_color_button_frame_size"
+ android:layout_height="@dimen/screen_flash_color_button_frame_size"
+ android:button="@drawable/screen_flash_color_07_selector"
+ android:contentDescription="@string/screen_flash_color_yellow" />
+
+ <Space
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1" />
+
+ <RadioButton
+ android:id="@+id/color_radio_button_07"
+ android:layout_width="@dimen/screen_flash_color_button_frame_size"
+ android:layout_height="@dimen/screen_flash_color_button_frame_size"
+ android:button="@drawable/screen_flash_color_08_selector"
+ android:contentDescription="@string/screen_flash_color_orange" />
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:layout_weight="1" />
+ android:layout_marginBottom="10dp"
+ android:orientation="horizontal">
- <RadioButton
- android:id="@+id/color_radio_button_03"
- android:layout_width="@dimen/screen_flash_color_button_frame_size"
- android:layout_height="@dimen/screen_flash_color_button_frame_size"
- android:button="@drawable/screen_flash_color_04_selector"
- android:contentDescription="@string/screen_flash_color_spring_green" />
+ <RadioButton
+ android:id="@+id/color_radio_button_08"
+ android:layout_width="@dimen/screen_flash_color_button_frame_size"
+ android:layout_height="@dimen/screen_flash_color_button_frame_size"
+ android:button="@drawable/screen_flash_color_09_selector"
+ android:contentDescription="@string/screen_flash_color_red" />
+
+ <Space
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1" />
+
+ <RadioButton
+ android:id="@+id/color_radio_button_09"
+ android:layout_width="@dimen/screen_flash_color_button_frame_size"
+ android:layout_height="@dimen/screen_flash_color_button_frame_size"
+ android:button="@drawable/screen_flash_color_10_selector"
+ android:contentDescription="@string/screen_flash_color_rose" />
+
+ <Space
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1" />
+
+ <RadioButton
+ android:id="@+id/color_radio_button_10"
+ android:layout_width="@dimen/screen_flash_color_button_frame_size"
+ android:layout_height="@dimen/screen_flash_color_button_frame_size"
+ android:button="@drawable/screen_flash_color_11_selector"
+ android:contentDescription="@string/screen_flash_color_magenta" />
+
+ <Space
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1" />
+
+ <RadioButton
+ android:id="@+id/color_radio_button_11"
+ android:layout_width="@dimen/screen_flash_color_button_frame_size"
+ android:layout_height="@dimen/screen_flash_color_button_frame_size"
+ android:button="@drawable/screen_flash_color_12_selector"
+ android:contentDescription="@string/screen_flash_color_violet" />
+
+ </LinearLayout>
</LinearLayout>
-
- <LinearLayout
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_marginBottom="10dp"
- android:orientation="horizontal">
-
- <RadioButton
- android:id="@+id/color_radio_button_04"
- android:layout_width="@dimen/screen_flash_color_button_frame_size"
- android:layout_height="@dimen/screen_flash_color_button_frame_size"
- android:button="@drawable/screen_flash_color_05_selector"
- android:contentDescription="@string/screen_flash_color_green" />
-
- <Space
- android:layout_width="0dp"
- android:layout_height="wrap_content"
- android:layout_weight="1" />
-
- <RadioButton
- android:id="@+id/color_radio_button_05"
- android:layout_width="@dimen/screen_flash_color_button_frame_size"
- android:layout_height="@dimen/screen_flash_color_button_frame_size"
- android:button="@drawable/screen_flash_color_06_selector"
- android:contentDescription="@string/screen_flash_color_chartreuse_green" />
-
-
- <Space
- android:layout_width="0dp"
- android:layout_height="wrap_content"
- android:layout_weight="1" />
- <RadioButton
- android:id="@+id/color_radio_button_06"
- android:layout_width="@dimen/screen_flash_color_button_frame_size"
- android:layout_height="@dimen/screen_flash_color_button_frame_size"
- android:button="@drawable/screen_flash_color_07_selector"
- android:contentDescription="@string/screen_flash_color_yellow" />
-
- <Space
- android:layout_width="0dp"
- android:layout_height="wrap_content"
- android:layout_weight="1" />
-
- <RadioButton
- android:id="@+id/color_radio_button_07"
- android:layout_width="@dimen/screen_flash_color_button_frame_size"
- android:layout_height="@dimen/screen_flash_color_button_frame_size"
- android:button="@drawable/screen_flash_color_08_selector"
- android:contentDescription="@string/screen_flash_color_orange" />
- </LinearLayout>
-
- <LinearLayout
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_marginBottom="10dp"
- android:orientation="horizontal">
-
- <RadioButton
- android:id="@+id/color_radio_button_08"
- android:layout_width="@dimen/screen_flash_color_button_frame_size"
- android:layout_height="@dimen/screen_flash_color_button_frame_size"
- android:button="@drawable/screen_flash_color_09_selector"
- android:contentDescription="@string/screen_flash_color_red" />
-
- <Space
- android:layout_width="0dp"
- android:layout_height="wrap_content"
- android:layout_weight="1" />
-
- <RadioButton
- android:id="@+id/color_radio_button_09"
- android:layout_width="@dimen/screen_flash_color_button_frame_size"
- android:layout_height="@dimen/screen_flash_color_button_frame_size"
- android:button="@drawable/screen_flash_color_10_selector"
- android:contentDescription="@string/screen_flash_color_rose" />
-
- <Space
- android:layout_width="0dp"
- android:layout_height="wrap_content"
- android:layout_weight="1" />
-
- <RadioButton
- android:id="@+id/color_radio_button_10"
- android:layout_width="@dimen/screen_flash_color_button_frame_size"
- android:layout_height="@dimen/screen_flash_color_button_frame_size"
- android:button="@drawable/screen_flash_color_11_selector"
- android:contentDescription="@string/screen_flash_color_magenta" />
-
- <Space
- android:layout_width="0dp"
- android:layout_height="wrap_content"
- android:layout_weight="1" />
-
- <RadioButton
- android:id="@+id/color_radio_button_11"
- android:layout_width="@dimen/screen_flash_color_button_frame_size"
- android:layout_height="@dimen/screen_flash_color_button_frame_size"
- android:button="@drawable/screen_flash_color_12_selector"
- android:contentDescription="@string/screen_flash_color_violet" />
-
- </LinearLayout>
-</LinearLayout>
\ No newline at end of file
+</ScrollView>
diff --git a/res/layout/layout_color_selector_dialog.xml b/res/layout/layout_color_selector_dialog.xml
index 70d4509..e107689 100644
--- a/res/layout/layout_color_selector_dialog.xml
+++ b/res/layout/layout_color_selector_dialog.xml
@@ -17,16 +17,12 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
- android:orientation="vertical"
- android:paddingBottom="24dp">
+ android:gravity="center_horizontal"
+ android:orientation="vertical">
<com.android.settings.accessibility.ColorSelectorLayout
android:id="@+id/color_selector_preference"
android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_marginBottom="2dp"
- android:layout_marginHorizontal="25dp"
- android:layout_marginTop="21dp"
- android:orientation="vertical" />
+ android:layout_height="wrap_content"/>
</LinearLayout>
\ No newline at end of file
diff --git a/res/layout/locale_order_list.xml b/res/layout/locale_order_list.xml
index 5c1db15..da1eb62 100644
--- a/res/layout/locale_order_list.xml
+++ b/res/layout/locale_order_list.xml
@@ -27,11 +27,11 @@
android:clipChildren="true"
android:orientation="vertical">
- <com.android.settings.localepicker.LocaleRecyclerView
+ <androidx.recyclerview.widget.RecyclerView
android:id="@+id/dragList"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:scrollbars="vertical"/>
+ android:scrollbars="none"/>
<Button
android:id="@+id/add_language"
diff --git a/res/layout/modifier_key_item.xml b/res/layout/modifier_key_item.xml
index a189479..683f631 100644
--- a/res/layout/modifier_key_item.xml
+++ b/res/layout/modifier_key_item.xml
@@ -19,8 +19,7 @@
android:layout_marginTop="8dip"
android:layout_marginBottom="8dip"
android:minHeight="?android:attr/listPreferredItemHeight"
- android:paddingEnd="?android:attr/scrollbarSize"
- android:layout_weight="1">
+ android:paddingEnd="?android:attr/scrollbarSize">
<ImageView
android:id="@+id/modifier_key_check_icon"
@@ -36,7 +35,7 @@
<TextView
android:id="@+id/modifier_key_text"
- android:layout_width="match_parent"
+ android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:textDirection="locale"
@@ -46,4 +45,38 @@
android:ellipsize="marquee"
android:fadingEdge="horizontal" />
+ <TextView
+ android:id="@+id/modifier_key_left_bracket"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_centerVertical="true"
+ android:textDirection="locale"
+ android:padding="1dp"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:layout_toEndOf="@+id/modifier_key_text"
+ android:fadingEdge="horizontal" />
+
+ <ImageView
+ android:id="@+id/modifier_key_action_key_icon"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_centerVertical="true"
+ android:layout_toEndOf="@+id/modifier_key_left_bracket"
+ android:fadingEdge="horizontal"
+ android:tint="?android:attr/textColorPrimary"/>
+
+ <TextView
+ android:id="@+id/modifier_key_right_bracket"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_centerVertical="true"
+ android:textDirection="locale"
+ android:padding="1dp"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:layout_toEndOf="@+id/modifier_key_action_key_icon"
+ android:fadingEdge="horizontal" />
+
+ <View android:layout_width="wrap_content"
+ android:layout_height="match_parent" />
+
</RelativeLayout>
diff --git a/res/layout/modifier_keys_custom_key.xml b/res/layout/modifier_keys_custom_key.xml
new file mode 100644
index 0000000..f390c00
--- /dev/null
+++ b/res/layout/modifier_keys_custom_key.xml
@@ -0,0 +1,107 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2023 The Android Open Source Project
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+ http://www.apache.org/licenses/LICENSE-2.0
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:minHeight="?android:attr/listPreferredItemHeight"
+ android:gravity="center_vertical"
+ android:paddingStart="?android:attr/listPreferredItemPaddingStart"
+ android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
+ android:background="?android:attr/selectableItemBackground">
+
+ <FrameLayout
+ android:id="@+id/icon_frame"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content">
+ <androidx.preference.internal.PreferenceImageView
+ android:id="@android:id/icon"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ app:maxWidth="48dp"
+ app:maxHeight="48dp" />
+ </FrameLayout>
+
+ <RelativeLayout
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:paddingTop="16dp"
+ android:paddingBottom="16dp"
+ android:layout_weight="1">
+
+ <TextView android:id="@+id/title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:singleLine="true"
+ android:textAppearance="?android:attr/textAppearanceLarge"
+ android:textColor="?android:attr/textColorPrimary"
+ android:fadingEdge="horizontal" />
+
+ <TextView android:id="@+id/summary"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@+id/title"
+ android:layout_alignStart="@+id/title"
+ android:layout_alignLeft="@+id/title"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:textColor="?android:attr/textColorSecondary"
+ android:maxLines="4" />
+
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:paddingStart="15dp"
+ android:layout_toEndOf="@+id/title"
+ android:gravity="center_vertical"
+ android:orientation="horizontal">
+
+ <TextView
+ android:id="@+id/modifier_key_left_bracket"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textDirection="locale"
+ android:paddingStart="1dp"
+ android:paddingEnd="1dp"
+ android:textAppearance="?android:attr/textAppearanceLarge"
+ android:textColor="?android:attr/textColorPrimary"
+ android:fadingEdge="horizontal" />
+
+ <ImageView
+ android:id="@+id/modifier_key_action_key_icon"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:fadingEdge="horizontal"
+ android:tint="?android:attr/textColorPrimary"/>
+
+ <TextView
+ android:id="@+id/modifier_key_right_bracket"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textDirection="locale"
+ android:paddingStart="1dp"
+ android:paddingEnd="1dp"
+ android:textAppearance="?android:attr/textAppearanceLarge"
+ android:textColor="?android:attr/textColorPrimary"
+ android:fadingEdge="horizontal" />
+ </LinearLayout>
+ </RelativeLayout>
+
+ <!-- Preference should place its actual preference widget here. -->
+ <LinearLayout android:id="@android:id/widget_frame"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:gravity="center_vertical"
+ android:orientation="vertical" />
+</LinearLayout>
diff --git a/res/layout/preference_check_icon.xml b/res/layout/preference_check_icon.xml
index 1b759fc..bd0dd79 100644
--- a/res/layout/preference_check_icon.xml
+++ b/res/layout/preference_check_icon.xml
@@ -20,4 +20,5 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
- android:layout_marginHorizontal="16dp"/>
\ No newline at end of file
+ android:layout_marginHorizontal="16dp"
+ android:contentDescription="@*android:string/checked"/>
\ No newline at end of file
diff --git a/res/layout/preference_external_action_icon.xml b/res/layout/preference_external_action_icon.xml
new file mode 100644
index 0000000..fcec430
--- /dev/null
+++ b/res/layout/preference_external_action_icon.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2023 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<ImageView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:src="@drawable/ic_chevron_right_24dp"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginHorizontal="8dp" />
\ No newline at end of file
diff --git a/res/layout/radio_with_image_preference.xml b/res/layout/radio_with_image_preference.xml
new file mode 100644
index 0000000..fcd0e26
--- /dev/null
+++ b/res/layout/radio_with_image_preference.xml
@@ -0,0 +1,75 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="?android:attr/selectableItemBackground"
+ android:gravity="center_vertical"
+ android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
+ android:paddingTop="16dp"
+ android:paddingBottom="16dp">
+
+ <LinearLayout
+ android:id="@android:id/widget_frame"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:paddingHorizontal="20dp"
+ android:gravity="center_horizontal"
+ android:minWidth="56dp"
+ android:orientation="vertical"/>
+
+ <LinearLayout
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:orientation="vertical"
+ android:paddingEnd="?android:attr/listPreferredItemPaddingEnd">
+
+ <TextView
+ android:id="@android:id/title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:maxLines="2"
+ android:textAppearance="?android:attr/textAppearanceListItem"/>
+
+ <LinearLayout
+ android:id="@+id/summary_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:visibility="gone">
+ <TextView
+ android:id="@android:id/summary"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:textAlignment="viewStart"
+ android:textColor="?android:attr/textColorSecondary"/>
+ </LinearLayout>
+
+ <ImageView
+ android:id="@android:id/icon"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:paddingTop="@dimen/settingslib_illustration_padding"
+ android:adjustViewBounds="true"
+ android:maxWidth="@dimen/settingslib_illustration_width"
+ android:maxHeight="@dimen/settingslib_illustration_height" />
+ </LinearLayout>
+</LinearLayout>
\ No newline at end of file
diff --git a/res/layout/udfps_enroll_enrolling.xml b/res/layout/udfps_enroll_enrolling.xml
index 05556ff..366a87c 100644
--- a/res/layout/udfps_enroll_enrolling.xml
+++ b/res/layout/udfps_enroll_enrolling.xml
@@ -15,7 +15,7 @@
~ limitations under the License.
-->
-<com.google.android.setupdesign.GlifLayout
+<com.android.settings.biometrics.fingerprint.UdfpsEnrollEnrollingView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
@@ -78,4 +78,4 @@
</LinearLayout>
</LinearLayout>
-</com.google.android.setupdesign.GlifLayout>
+</com.android.settings.biometrics.fingerprint.UdfpsEnrollEnrollingView>
diff --git a/res/layout/udfps_enroll_view.xml b/res/layout/udfps_enroll_view.xml
index 6bf339b..bd62609 100644
--- a/res/layout/udfps_enroll_view.xml
+++ b/res/layout/udfps_enroll_view.xml
@@ -18,7 +18,8 @@
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/udfps_animation_view"
android:layout_width="match_parent"
- android:layout_height="match_parent">
+ android:layout_height="match_parent"
+ android:visibility="gone">
<ImageView
android:id="@+id/udfps_enroll_animation_fp_progress_view"
diff --git a/res/raw/user_aspect_ratio_education.json b/res/raw/user_aspect_ratio_education.json
new file mode 100644
index 0000000..ab74b45
--- /dev/null
+++ b/res/raw/user_aspect_ratio_education.json
@@ -0,0 +1 @@
+{"v":"5.12.0","fr":60,"ip":0,"op":226,"w":412,"h":300,"nm":"AppCompat_Felix_DT","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":".blue400","cl":"blue400","parent":3,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"k":[{"s":[38.5,-79.5,0],"t":15,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[38.661,-79.5,0],"t":16,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[39.181,-79.5,0],"t":17,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[40.183,-79.5,0],"t":18,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[41.974,-79.5,0],"t":19,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[45.7,-79.5,0],"t":20,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[49.943,-79.5,0],"t":21,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[51.81,-79.5,0],"t":22,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[52.893,-79.5,0],"t":23,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[53.628,-79.5,0],"t":24,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[54.171,-79.5,0],"t":25,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[54.59,-79.5,0],"t":26,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[54.925,-79.5,0],"t":27,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[55.197,-79.5,0],"t":28,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[55.423,-79.5,0],"t":29,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[55.611,-79.5,0],"t":30,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[55.77,-79.5,0],"t":31,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[55.904,-79.5,0],"t":32,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[56.017,-79.5,0],"t":33,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[56.113,-79.5,0],"t":34,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[56.195,-79.5,0],"t":35,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[56.263,-79.5,0],"t":36,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[56.32,-79.5,0],"t":37,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[56.368,-79.5,0],"t":38,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[56.406,-79.5,0],"t":39,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[56.461,-79.5,0],"t":41,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[56.5,-79.5,0],"t":60,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[56.823,-79.5,0],"t":61,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[57.861,-79.5,0],"t":62,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[59.865,-79.5,0],"t":63,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[63.449,-79.5,0],"t":64,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[70.9,-79.5,0],"t":65,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[79.385,-79.5,0],"t":66,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[83.121,-79.5,0],"t":67,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[85.286,-79.5,0],"t":68,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[86.757,-79.5,0],"t":69,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[87.841,-79.5,0],"t":70,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[88.68,-79.5,0],"t":71,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[89.349,-79.5,0],"t":72,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[89.894,-79.5,0],"t":73,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[90.345,-79.5,0],"t":74,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[90.722,-79.5,0],"t":75,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[91.039,-79.5,0],"t":76,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[91.307,-79.5,0],"t":77,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[91.534,-79.5,0],"t":78,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[91.727,-79.5,0],"t":79,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[91.889,-79.5,0],"t":80,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[92.026,-79.5,0],"t":81,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[92.141,-79.5,0],"t":82,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[92.236,-79.5,0],"t":83,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[92.313,-79.5,0],"t":84,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[92.375,-79.5,0],"t":85,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[92.458,-79.5,0],"t":87,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[92.5,-79.5,0],"t":105,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[92.177,-79.5,0],"t":106,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[91.139,-79.5,0],"t":107,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[89.135,-79.5,0],"t":108,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[85.551,-79.5,0],"t":109,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[78.1,-79.5,0],"t":110,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[69.615,-79.5,0],"t":111,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[65.879,-79.5,0],"t":112,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[63.714,-79.5,0],"t":113,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[62.243,-79.5,0],"t":114,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[61.159,-79.5,0],"t":115,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[60.32,-79.5,0],"t":116,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[59.651,-79.5,0],"t":117,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[59.106,-79.5,0],"t":118,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[58.655,-79.5,0],"t":119,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[58.278,-79.5,0],"t":120,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[57.961,-79.5,0],"t":121,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[57.693,-79.5,0],"t":122,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[57.466,-79.5,0],"t":123,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[57.273,-79.5,0],"t":124,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[57.111,-79.5,0],"t":125,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[56.974,-79.5,0],"t":126,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[56.859,-79.5,0],"t":127,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[56.764,-79.5,0],"t":128,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[56.687,-79.5,0],"t":129,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[56.625,-79.5,0],"t":130,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[56.577,-79.5,0],"t":131,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[56.542,-79.5,0],"t":132,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[56.5,-79.5,0],"t":150,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[56.339,-79.5,0],"t":151,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[55.819,-79.5,0],"t":152,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[54.817,-79.5,0],"t":153,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[53.026,-79.5,0],"t":154,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[49.3,-79.5,0],"t":155,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[45.057,-79.5,0],"t":156,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[43.19,-79.5,0],"t":157,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[42.107,-79.5,0],"t":158,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[41.372,-79.5,0],"t":159,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[40.829,-79.5,0],"t":160,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[40.41,-79.5,0],"t":161,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[40.075,-79.5,0],"t":162,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[39.803,-79.5,0],"t":163,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[39.577,-79.5,0],"t":164,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[39.389,-79.5,0],"t":165,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[39.23,-79.5,0],"t":166,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[39.096,-79.5,0],"t":167,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[38.983,-79.5,0],"t":168,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[38.887,-79.5,0],"t":169,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[38.805,-79.5,0],"t":170,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[38.737,-79.5,0],"t":171,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[38.68,-79.5,0],"t":172,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[38.632,-79.5,0],"t":173,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[38.594,-79.5,0],"t":174,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[38.563,-79.5,0],"t":175,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[38.539,-79.5,0],"t":176,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[38.502,-79.5,0],"t":179,"i":{"x":1,"y":1},"o":{"x":0,"y":0}}],"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[7.5,3.077],[7.5,-7.5],[-2.981,-7.5]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-7.5,7.5],[7.5,-7.5]],"c":false},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"st","c":{"a":0,"k":[0.40000000596,0.615686297417,0.964705884457,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Vector","np":4,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":2916,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":".blue400","cl":"blue400","parent":3,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"k":[{"s":[-38.5,79.5,0],"t":15,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[-38.661,79.5,0],"t":16,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[-39.181,79.5,0],"t":17,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[-40.183,79.5,0],"t":18,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[-41.974,79.5,0],"t":19,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[-45.7,79.5,0],"t":20,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[-49.943,79.5,0],"t":21,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[-51.81,79.5,0],"t":22,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[-52.893,79.5,0],"t":23,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[-53.628,79.5,0],"t":24,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[-54.171,79.5,0],"t":25,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[-54.59,79.5,0],"t":26,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[-54.925,79.5,0],"t":27,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[-55.197,79.5,0],"t":28,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[-55.423,79.5,0],"t":29,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[-55.611,79.5,0],"t":30,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[-55.77,79.5,0],"t":31,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[-55.904,79.5,0],"t":32,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[-56.017,79.5,0],"t":33,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[-56.113,79.5,0],"t":34,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[-56.195,79.5,0],"t":35,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[-56.263,79.5,0],"t":36,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[-56.32,79.5,0],"t":37,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[-56.368,79.5,0],"t":38,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[-56.406,79.5,0],"t":39,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[-56.461,79.5,0],"t":41,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[-56.5,79.5,0],"t":60,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[-56.823,79.5,0],"t":61,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[-57.861,79.5,0],"t":62,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[-59.865,79.5,0],"t":63,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[-63.449,79.5,0],"t":64,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[-70.9,79.5,0],"t":65,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[-79.385,79.5,0],"t":66,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[-83.121,79.5,0],"t":67,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[-85.286,79.5,0],"t":68,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[-86.757,79.5,0],"t":69,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[-87.841,79.5,0],"t":70,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[-88.68,79.5,0],"t":71,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[-89.349,79.5,0],"t":72,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[-89.894,79.5,0],"t":73,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[-90.345,79.5,0],"t":74,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[-90.722,79.5,0],"t":75,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[-91.039,79.5,0],"t":76,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[-91.307,79.5,0],"t":77,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[-91.534,79.5,0],"t":78,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[-91.727,79.5,0],"t":79,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[-91.889,79.5,0],"t":80,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[-92.026,79.5,0],"t":81,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[-92.141,79.5,0],"t":82,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[-92.236,79.5,0],"t":83,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[-92.313,79.5,0],"t":84,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[-92.375,79.5,0],"t":85,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[-92.458,79.5,0],"t":87,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[-92.5,79.5,0],"t":105,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[-92.177,79.5,0],"t":106,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[-91.139,79.5,0],"t":107,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[-89.135,79.5,0],"t":108,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[-85.551,79.5,0],"t":109,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[-78.1,79.5,0],"t":110,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[-69.615,79.5,0],"t":111,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[-65.879,79.5,0],"t":112,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[-63.714,79.5,0],"t":113,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[-62.243,79.5,0],"t":114,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[-61.159,79.5,0],"t":115,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[-60.32,79.5,0],"t":116,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[-59.651,79.5,0],"t":117,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[-59.106,79.5,0],"t":118,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[-58.655,79.5,0],"t":119,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[-58.278,79.5,0],"t":120,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[-57.961,79.5,0],"t":121,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[-57.693,79.5,0],"t":122,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[-57.466,79.5,0],"t":123,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[-57.273,79.5,0],"t":124,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[-57.111,79.5,0],"t":125,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[-56.974,79.5,0],"t":126,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[-56.859,79.5,0],"t":127,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[-56.764,79.5,0],"t":128,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[-56.687,79.5,0],"t":129,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[-56.625,79.5,0],"t":130,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[-56.577,79.5,0],"t":131,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[-56.542,79.5,0],"t":132,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[-56.5,79.5,0],"t":150,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[-56.339,79.5,0],"t":151,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[-55.819,79.5,0],"t":152,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[-54.817,79.5,0],"t":153,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[-53.026,79.5,0],"t":154,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[-49.3,79.5,0],"t":155,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[-45.057,79.5,0],"t":156,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[-43.19,79.5,0],"t":157,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[-42.107,79.5,0],"t":158,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[-41.372,79.5,0],"t":159,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[-40.829,79.5,0],"t":160,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[-40.41,79.5,0],"t":161,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[-40.075,79.5,0],"t":162,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[-39.803,79.5,0],"t":163,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[-39.577,79.5,0],"t":164,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[-39.389,79.5,0],"t":165,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[-39.23,79.5,0],"t":166,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[-39.096,79.5,0],"t":167,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[-38.983,79.5,0],"t":168,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[-38.887,79.5,0],"t":169,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[-38.805,79.5,0],"t":170,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[-38.737,79.5,0],"t":171,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[-38.68,79.5,0],"t":172,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[-38.632,79.5,0],"t":173,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[-38.594,79.5,0],"t":174,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[-38.563,79.5,0],"t":175,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[-38.539,79.5,0],"t":176,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[-38.502,79.5,0],"t":179,"i":{"x":1,"y":1},"o":{"x":0,"y":0}}],"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[-7.5,-3.077],[-7.5,7.5],[2.981,7.5]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[7.5,-7.5],[-7.5,7.5]],"c":false},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"st","c":{"a":0,"k":[0.40000000596,0.615686297417,0.964705884457,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Vector","np":4,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":2916,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":".black","cl":"black","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[206,150,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":1,"k":[{"i":{"x":[0.8,0.8],"y":[0.15,1]},"o":{"x":[0.3,0.3],"y":[0,0]},"t":15,"s":[104,186]},{"i":{"x":[0.1,0.1],"y":[1,1]},"o":{"x":[0.05,0.05],"y":[0.7,0]},"t":20,"s":[118.4,186]},{"i":{"x":[0.1,0.1],"y":[1,1]},"o":{"x":[0.167,0.167],"y":[0,0]},"t":45,"s":[140,186]},{"i":{"x":[0.8,0.8],"y":[0.15,1]},"o":{"x":[0.3,0.3],"y":[0,0]},"t":60,"s":[140,186]},{"i":{"x":[0.1,0.1],"y":[1,1]},"o":{"x":[0.05,0.05],"y":[0.7,0]},"t":65,"s":[168.8,186]},{"i":{"x":[0.1,0.1],"y":[1,1]},"o":{"x":[0.167,0.167],"y":[0,0]},"t":90,"s":[212,186]},{"i":{"x":[0.8,0.8],"y":[0.15,1]},"o":{"x":[0.3,0.3],"y":[0,0]},"t":105,"s":[212,186]},{"i":{"x":[0.1,0.1],"y":[1,1]},"o":{"x":[0.05,0.05],"y":[0.7,0]},"t":110,"s":[183.2,186]},{"i":{"x":[0.1,0.1],"y":[1,1]},"o":{"x":[0.3,0.3],"y":[0,0]},"t":135,"s":[140,186]},{"i":{"x":[0.8,0.8],"y":[0.15,1]},"o":{"x":[0.3,0.3],"y":[0,0]},"t":150,"s":[140,186]},{"i":{"x":[0.1,0.1],"y":[1,1]},"o":{"x":[0.05,0.05],"y":[0.7,0]},"t":155,"s":[125.6,186]},{"t":180,"s":[104,186]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":2,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"inside","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":2916,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":".blue400","cl":"blue400","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[206,150,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":1,"k":[{"i":{"x":[0.8,0.8],"y":[0.15,1]},"o":{"x":[0.3,0.3],"y":[0,0]},"t":15,"s":[112,194]},{"i":{"x":[0.1,0.1],"y":[1,1]},"o":{"x":[0.05,0.05],"y":[0.7,0]},"t":20,"s":[126.4,194]},{"i":{"x":[0.1,0.1],"y":[1,1]},"o":{"x":[0.167,0.167],"y":[0,0]},"t":45,"s":[148,194]},{"i":{"x":[0.8,0.8],"y":[0.15,1]},"o":{"x":[0.3,0.3],"y":[0,0]},"t":60,"s":[148,194]},{"i":{"x":[0.1,0.1],"y":[1,1]},"o":{"x":[0.05,0.05],"y":[0.7,0]},"t":65,"s":[176.8,194]},{"i":{"x":[0.1,0.1],"y":[1,1]},"o":{"x":[0.167,0.167],"y":[0,0]},"t":90,"s":[220,194]},{"i":{"x":[0.8,0.8],"y":[0.15,1]},"o":{"x":[0.3,0.3],"y":[0,0]},"t":105,"s":[220,194]},{"i":{"x":[0.1,0.1],"y":[1,1]},"o":{"x":[0.05,0.05],"y":[0.7,0]},"t":110,"s":[191.2,194]},{"i":{"x":[0.1,0.1],"y":[1,1]},"o":{"x":[0.3,0.3],"y":[0,0]},"t":135,"s":[148,194]},{"i":{"x":[0.8,0.8],"y":[0.15,1]},"o":{"x":[0.3,0.3],"y":[0,0]},"t":150,"s":[148,194]},{"i":{"x":[0.1,0.1],"y":[1,1]},"o":{"x":[0.05,0.05],"y":[0.7,0]},"t":155,"s":[133.6,194]},{"t":180,"s":[112,194]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":4,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[0.40000000596,0.615686297417,0.964705884457,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"outside","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":2916,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":".grey600","cl":"grey600","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[207.5,150.303,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[1.133,-0.038],[0,0],[0,0],[0,-1.46],[0,0],[1.133,-0.038],[0,0],[5.707,0],[0,0],[1.894,1.051],[0,0],[0.833,-0.334],[2.166,0],[0,0],[0,5.707],[0,0],[-5.707,0],[0,0],[-1.894,-1.05],[0,0],[-0.883,0.354],[-2.166,0],[0,0],[0,-5.707],[0,0],[0,-1.46],[0,0]],"o":[[0,0],[0,0],[1.133,0.038],[0,0],[0,1.46],[0,0],[0,5.706],[0,0],[-2.166,0],[0,0],[-0.846,-0.289],[-1.894,1.051],[0,0],[-5.707,0],[0,0],[0,-5.707],[0,0],[2.166,0],[0,0],[0.886,0.346],[1.894,-1.05],[0,0],[5.707,0],[0,0],[1.133,0.038],[0,0],[0,1.46]],"v":[[114.45,-16.3],[114.45,-15.539],[114.45,-0.877],[116.494,1.802],[116.494,28.704],[114.45,31.383],[114.45,91.749],[104.117,102.082],[7.828,102.082],[0.351,100.48],[0.25,100.424],[-2.349,100.48],[-8.539,102.082],[-106.16,102.082],[-116.494,91.748],[-116.494,-91.748],[-106.16,-102.082],[-8.754,-102.082],[-2.563,-100.48],[-2.532,-100.468],[0.221,-100.48],[6.411,-102.082],[104.116,-102.082],[114.45,-91.748],[114.45,-36.119],[116.494,-33.44],[116.494,-18.979]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,0],[0,4.177],[0,0],[4.177,0],[0,0],[0,-4.177],[0,0],[-4.177,0]],"o":[[4.177,0],[0,0],[0,-4.177],[0,0],[-4.177,0],[0,0],[0,4.177],[0,0]],"v":[[104.117,99.704],[112.072,92.128],[112.072,-91.748],[104.117,-99.704],[-106.161,-99.704],[-114.116,-91.748],[-114.116,92.128],[-106.161,99.704]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[0.501960813999,0.525490224361,0.54509806633,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"felix","np":4,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":2916,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":".black","cl":"black","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[206,150,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-15.291,0],[0,0],[0,15.544],[0,0],[15.185,0],[0,0],[0,-15.652],[0,0]],"o":[[0,0],[15.291,0],[0,0],[0,-15.652],[0,0],[-15.291,0],[0,0],[0,15.652]],"v":[[-178.179,150],[178.179,150],[206,121.63],[206,-121.522],[178.286,-150],[-178.179,-150],[-206,-121.522],[-206,121.522]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":".white","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false,"cl":"white"}],"ip":0,"op":2916,"st":0,"ct":1,"bm":0}],"markers":[{"tm":195,"cm":"Plus .5s hold for loop","dr":30}],"props":{}}
\ No newline at end of file
diff --git a/res/values-am/strings.xml b/res/values-am/strings.xml
index de6fd9c..3bfad97 100644
--- a/res/values-am/strings.xml
+++ b/res/values-am/strings.xml
@@ -4653,10 +4653,8 @@
<string name="ingress_rate_limit_no_limit_entry" msgid="8741098826008012163">"ምንም ገደብ የለም"</string>
<string name="disable_phantom_process_monitor_title" msgid="8348108346706188771">"ልጅ የሂደቶች ገደቦችን ያሰናክሉ"</string>
<string name="disable_phantom_process_monitor_summary" msgid="3044464635550256985">"የመተግበሪያው ልጅ ሂደቶቹ ላይ ያሉ የሥርዓት ንብረት አጠቃቀም ገደቦችን ያሰናክሉ"</string>
- <!-- no translation found for enable_notes_role_title (7662702013496114763) -->
- <skip />
- <!-- no translation found for enable_notes_role_summary (136916915155048249) -->
- <skip />
+ <string name="enable_notes_role_title" msgid="7662702013496114763">"የማስታወሻዎች ሚናን በግዳጅ ያንቁ"</string>
+ <string name="enable_notes_role_summary" msgid="136916915155048249">"በማስታወሻዎች ሚና በኩል ማስታወሻ የመውሰጃ ሥርዓት ውህደቶችን ያንቁ። የማስታወሻዎች ሚና አስቀድሞ ከነቃ ምንም አያድርጉ።"</string>
<string name="bluetooth_broadcast_dialog_title" msgid="9172775308463135884">"ስርጭት"</string>
<string name="bluetooth_broadcast_dialog_broadcast_app" msgid="1016617579194329005">"<xliff:g id="CURRENTAPP">%1$s</xliff:g>ን ያሰራጩ"</string>
<string name="bluetooth_broadcast_dialog_find_message" msgid="6621660851669953883">"በአቅራቢያዎ የሚጫወቱ ስርጭቶችን ያዳምጡ"</string>
diff --git a/res/values-ar/strings.xml b/res/values-ar/strings.xml
index efd22ae..0557846 100644
--- a/res/values-ar/strings.xml
+++ b/res/values-ar/strings.xml
@@ -3249,12 +3249,12 @@
<string name="notification_listener_security_warning_summary" msgid="1131986567509818121">"سيكون بإمكان \"<xliff:g id="NOTIFICATION_LISTENER_NAME">%1$s</xliff:g>\" قراءة جميع الإشعارات، بما في ذلك المعلومات الشخصية، مثلاً أسماء جهات الاتصال والصور ونصوص الرسائل التي تتلقّاها. وسيتمكن هذا التطبيق أيضًا من تأجيل الإشعارات أو إغلاقها أو اتخاذ إجراءات من خلال الأزرار في الإشعارات بما في ذلك الردّ على المكالمات الهاتفية. \n\nسيتيح هذا أيضًا للتطبيق إمكانية تفعيل ميزة \"عدم الإزعاج\" أو إيقافها وتغيير الإعدادات ذات الصلة."</string>
<string name="nls_warning_prompt" msgid="1486887096703743841">"سيتمكن التطبيق \"<xliff:g id="NOTIFICATION_LISTENER_NAME">%1$s</xliff:g>\" من إجراء ما يلي:"</string>
<string name="nls_feature_read_title" msgid="7629713268744220437">"قراءة الإشعارات"</string>
- <string name="nls_feature_read_summary" msgid="1064698238110273593">"يمكنها قراءة الإشعارات، بما في ذلك المعلومات الشخصية، مثل جهات الاتصال والرسائل والصور."</string>
+ <string name="nls_feature_read_summary" msgid="1064698238110273593">"يمكنه قراءة الإشعارات، بما في ذلك المعلومات الشخصية، مثل جهات الاتصال والرسائل والصور."</string>
<string name="nls_feature_reply_title" msgid="7925455553821362039">"الردّ على الرسائل"</string>
- <string name="nls_feature_reply_summary" msgid="4492543411395565556">"يمكنها الردّ على الرسائل واتخاذ إجراءات من خلال الأزرار في الإشعارات، بما في ذلك تأجيل الإشعارات أو إغلاقها والردّ على المكالمات."</string>
+ <string name="nls_feature_reply_summary" msgid="4492543411395565556">"يمكنه الردّ على الرسائل واتخاذ إجراءات من خلال الأزرار في الإشعارات، بما في ذلك تأجيل الإشعارات أو إغلاقها والردّ على المكالمات."</string>
<string name="nls_feature_settings_title" msgid="8208164329853194414">"تغيير الإعدادات"</string>
- <string name="nls_feature_settings_summary" msgid="3770028705648985689">"يمكنها تفعيل ميزة \"عدم الإزعاج\" أو إيقافها وتغيير الإعدادات ذات الصلة."</string>
- <string name="notification_listener_disable_warning_summary" msgid="8373396293802088961">"عند إيقاف الوصول إلى الإشعارات لخدمة <xliff:g id="NOTIFICATION_LISTENER_NAME">%1$s</xliff:g>، قد يتم إيقاف الوصول إلى ميزة \"عدم الإزعاج\" أيضًا."</string>
+ <string name="nls_feature_settings_summary" msgid="3770028705648985689">"يمكنه تفعيل ميزة \"عدم الإزعاج\" أو إيقافها وتغيير الإعدادات ذات الصلة."</string>
+ <string name="notification_listener_disable_warning_summary" msgid="8373396293802088961">"في حال إيقاف وصول \"<xliff:g id="NOTIFICATION_LISTENER_NAME">%1$s</xliff:g>\" إلى الإشعارات، قد يتم إيقاف الوصول إلى ميزة \"عدم الإزعاج\" أيضًا."</string>
<string name="notification_listener_disable_warning_confirm" msgid="841492108402184976">"إيقاف"</string>
<string name="notification_listener_disable_warning_cancel" msgid="8802784105045594324">"إلغاء"</string>
<string name="notif_type_ongoing" msgid="135675014223627555">"الوقت الفعلي"</string>
@@ -3275,7 +3275,7 @@
<string name="notif_listener_more_settings_desc" msgid="7995492074281663658">"تتوفّر إعدادات إضافية داخل التطبيق."</string>
<string name="vr_listeners_title" msgid="4960357292472540964">"خدمات مساعد الواقع الافتراضي"</string>
<string name="no_vr_listeners" msgid="8442646085375949755">"لم تطلب أي تطبيقات مثبَّتة أن يتم تشغيلها كخدمات مساعد واقع افتراضي."</string>
- <string name="vr_listener_security_warning_title" msgid="7026351795627615177">"هل تريد السماح لخدمة <xliff:g id="SERVICE">%1$s</xliff:g> بالوصول إلى خدمة الواقع الافتراضي؟"</string>
+ <string name="vr_listener_security_warning_title" msgid="7026351795627615177">"هل تريد السماح لخدمة \"<xliff:g id="SERVICE">%1$s</xliff:g>\" بالوصول إلى خدمة الواقع الافتراضي؟"</string>
<string name="vr_listener_security_warning_summary" msgid="1888843557687017791">"لن يتمكن <xliff:g id="VR_LISTENER_NAME">%1$s</xliff:g> من العمل عند استخدامك تطبيقات في وضع الواقع الافتراضي."</string>
<string name="display_vr_pref_title" msgid="4850474436291113569">"الجهاز في وضع الواقع الافتراضي"</string>
<string name="display_vr_pref_low_persistence" msgid="7039841277157739871">"خفض التعتيم (مستحسن)"</string>
@@ -3285,7 +3285,7 @@
<string name="picture_in_picture_keywords" msgid="3605379820551656253">"صورة، شاشة، صورة داخل صورة"</string>
<string name="picture_in_picture_app_detail_title" msgid="4442235098255164650">"نافذة ضمن النافذة"</string>
<string name="picture_in_picture_app_detail_switch" msgid="8544190716075624017">"السماح بعرض نافذة ضمن النافذة"</string>
- <string name="picture_in_picture_app_detail_summary" msgid="2503211101305358849">"يمكنك السماح لهذا التطبيق بإنشاء نافذة ضمن نافذة أثناء فتح التطبيق أو بعد مغادرته (على سبيل المثال، لمتابعة مشاهدة فيديو)، علمًا بأن هذه النافذة تظهر فوق التطبيقات الأخرى التي تستخدمها."</string>
+ <string name="picture_in_picture_app_detail_summary" msgid="2503211101305358849">"يمكنك السماح لهذا التطبيق بإنشاء نافذة ضمن نافذة أثناء فتح التطبيق أو بعد مغادرته (على سبيل المثال، لمتابعة مشاهدة فيديو)، علمًا بأنّ هذه النافذة تظهر فوق التطبيقات الأخرى التي تستخدمها."</string>
<string name="interact_across_profiles_title" msgid="7285906999927669971">"الربط بين تطبيقات العمل والتطبيقات الشخصية"</string>
<string name="interact_across_profiles_summary_allowed" msgid="1365881452153799092">"مرتبط"</string>
<string name="interact_across_profiles_summary_not_allowed" msgid="5802674212788171790">"غير مرتبط"</string>
@@ -3646,7 +3646,7 @@
<string name="zen_access_warning_dialog_title" msgid="6323325813123130154">"هل تسمح بوصول \"<xliff:g id="APP">%1$s</xliff:g>\" إلى إعداد \"عدم الإزعاج\"؟"</string>
<string name="zen_access_warning_dialog_summary" msgid="8468714854067428987">"سيكون التطبيق قادرًا على تفعيل/إيقاف ميزة \"عدم الإزعاج\" وإجراء تغييرات على الإعدادات ذات الصلة."</string>
<string name="zen_access_disabled_package_warning" msgid="6565908224294537889">"يجب أن يظل قيد التفعيل نظرًا لأن الوصول إلى الإشعارات قيد التفعيل"</string>
- <string name="zen_access_revoke_warning_dialog_title" msgid="7377261509261811449">"هل تريد إبطال إمكانية وصول تطبيق <xliff:g id="APP">%1$s</xliff:g> إلى قواعد \"عدم الإزعاج\"؟"</string>
+ <string name="zen_access_revoke_warning_dialog_title" msgid="7377261509261811449">"هل تريد إبطال إمكانية وصول تطبيق \"<xliff:g id="APP">%1$s</xliff:g>\" إلى إعداد \"عدم الإزعاج\"؟"</string>
<string name="zen_access_revoke_warning_dialog_summary" msgid="8689801842914183595">"ستتم إزالة كل قواعد \"عدم الإزعاج\" التي أنشأها هذا التطبيق."</string>
<string name="ignore_optimizations_on" msgid="6865583039303804932">"عدم تحسين"</string>
<string name="ignore_optimizations_off" msgid="9186557038453586295">"تحسين"</string>
@@ -3664,7 +3664,7 @@
<string name="filter_manage_external_storage" msgid="6751640571715343804">"يمكن الوصول إلى كل الملفات"</string>
<string name="full_screen_intent_title" msgid="1068024949389956404">"إدارة الرسائل بملء الشاشة"</string>
<string name="permit_full_screen_intent" msgid="2251949245519201421">"السماح للتطبيقات بإرسال رسائل بملء الشاشة"</string>
- <string name="footer_description_full_screen_intent" msgid="8322826300555418227">"السماح لهذا التطبيق بإرسال إشعارات رسائل بملء الشاشة تغطي الشاشة بأكملها"</string>
+ <string name="footer_description_full_screen_intent" msgid="8322826300555418227">"يمكنك السماح لهذا التطبيق بإرسال إشعارات رسائل بملء الشاشة تغطي الشاشة بأكملها."</string>
<string name="media_management_apps_title" msgid="8222942355578724582">"تطبيقات إدارة الوسائط"</string>
<string name="media_management_apps_toggle_label" msgid="166724270857067456">"السماح للتطبيق بإدارة الوسائط"</string>
<string name="media_management_apps_description" msgid="8000565658455268524">"في حال السماح لهذا التطبيق، يمكنه بدون طلب موافقتك تعديل أو حذف ملفات الوسائط التي يتم إنشاؤها باستخدام تطبيقات أخرى. يجب أن يتوفّر للتطبيق الإذن بالوصول إلى الملفات والوسائط."</string>
diff --git a/res/values-b+sr+Latn/strings.xml b/res/values-b+sr+Latn/strings.xml
index 7352ee9..528ecc3 100644
--- a/res/values-b+sr+Latn/strings.xml
+++ b/res/values-b+sr+Latn/strings.xml
@@ -481,9 +481,9 @@
<string name="fingerprint_last_delete_message_profile_challenge" msgid="4104208067277655068">"Nećete moći da koristite otisak prsta za otključavanje poslovnog profila, ovlašćivanje kupovina ili prijavljivanje u poslovne aplikacije."</string>
<string name="encryption_settings_title" msgid="2848716008695618360">"Šifrovanje"</string>
<string name="encrypted_summary" msgid="545623487587251207">"Šifrovano"</string>
- <string name="no_screen_lock_issue_title" msgid="1814109590692792891">"Podesite zaključavanje ekrana"</string>
+ <string name="no_screen_lock_issue_title" msgid="1814109590692792891">"Podesite otključavanje ekrana"</string>
<string name="no_screen_lock_issue_summary" msgid="2383217853510608406">"Da biste poboljšali bezbednost, podesite PIN, šablon ili lozinku za ovaj uređaj"</string>
- <string name="no_screen_lock_issue_action_label" msgid="2691229130486382863">"Podesi zaključavanje ekrana"</string>
+ <string name="no_screen_lock_issue_action_label" msgid="2691229130486382863">"Podesi otključavanje ekrana"</string>
<string name="no_screen_lock_issue_notification_title" msgid="5718363966239208505">"Uređaj nema zaključavanje ekrana"</string>
<string name="no_screen_lock_issue_notification_text" msgid="8696194459170873345">"Da biste poboljšali bezbednost, podesite PIN, šablon ili lozinku za ovaj uređaj"</string>
<string name="suggested_lock_settings_title" msgid="7836065447159730217">"Zaštitite telefon"</string>
@@ -4655,10 +4655,8 @@
<string name="ingress_rate_limit_no_limit_entry" msgid="8741098826008012163">"Bez ograničenja"</string>
<string name="disable_phantom_process_monitor_title" msgid="8348108346706188771">"Onemogući ograničenja za podređene procese"</string>
<string name="disable_phantom_process_monitor_summary" msgid="3044464635550256985">"Onemogućite ograničenja korišćenja resursa sistema za podređene procese aplikacije"</string>
- <!-- no translation found for enable_notes_role_title (7662702013496114763) -->
- <skip />
- <!-- no translation found for enable_notes_role_summary (136916915155048249) -->
- <skip />
+ <string name="enable_notes_role_title" msgid="7662702013496114763">"Prinudno omogući ulogu Beleške"</string>
+ <string name="enable_notes_role_summary" msgid="136916915155048249">"Omogućite integracije pravljenja beležaka u sistemu pomoću uloge Beleške. Ako je uloga Beleške već omogućena, ne preduzimajte ništa."</string>
<string name="bluetooth_broadcast_dialog_title" msgid="9172775308463135884">"Emitujte"</string>
<string name="bluetooth_broadcast_dialog_broadcast_app" msgid="1016617579194329005">"Emitujte <xliff:g id="CURRENTAPP">%1$s</xliff:g>"</string>
<string name="bluetooth_broadcast_dialog_find_message" msgid="6621660851669953883">"Slušajte emitovanja koja se puštaju u blizini"</string>
diff --git a/res/values-bg/strings.xml b/res/values-bg/strings.xml
index 85409f2..977f9a3 100644
--- a/res/values-bg/strings.xml
+++ b/res/values-bg/strings.xml
@@ -3247,7 +3247,7 @@
<string name="notification_assistant_security_warning_summary" msgid="4846559755787348129">"Адаптивните известия бяха заменени от функцията за подобрени известия в Android 12. Тя показва предложени действия и отговори и организира известията ви. \n\nФункцията може да осъществява достъп до съдържанието в известията, включително личната информация, като например имената на контактите и текстовите съобщения. Тя има възможност да отхвърля известията или да предприема действия в тях, като например приемане на телефонни обаждания или контролиране на режима „Не безпокойте“."</string>
<string name="notification_listener_security_warning_title" msgid="5791700876622858363">"Да се разреши ли достъпът до известията за „<xliff:g id="SERVICE">%1$s</xliff:g>“?"</string>
<string name="notification_listener_security_warning_summary" msgid="1131986567509818121">"Приложението <xliff:g id="NOTIFICATION_LISTENER_NAME">%1$s</xliff:g> ще може да чете всички известия, включително личната информация, като например имената на контактите, снимките и текста на съобщенията, които получавате. Това приложение ще може също да отлага или отхвърля известия и да взаимодейства с бутоните в тях, включително да отговаря на телефонни обаждания. \n\nПриложението ще може също да включва и изключва режима „Не безпокойте“, както и да променя свързаните с него настройки."</string>
- <string name="nls_warning_prompt" msgid="1486887096703743841">"Приложението <xliff:g id="NOTIFICATION_LISTENER_NAME">%1$s</xliff:g> ще може да:"</string>
+ <string name="nls_warning_prompt" msgid="1486887096703743841">"Приложението <xliff:g id="NOTIFICATION_LISTENER_NAME">%1$s</xliff:g> ще може следното:"</string>
<string name="nls_feature_read_title" msgid="7629713268744220437">"Четене на известия"</string>
<string name="nls_feature_read_summary" msgid="1064698238110273593">"Може да чете известията ви, включително личната информация, като например контактите, съобщенията и снимките."</string>
<string name="nls_feature_reply_title" msgid="7925455553821362039">"Отговаряне на съобщения"</string>
@@ -3664,7 +3664,7 @@
<string name="filter_manage_external_storage" msgid="6751640571715343804">"Може да осъществява достъп до всички файлове"</string>
<string name="full_screen_intent_title" msgid="1068024949389956404">"Управление на намеренията за цял екран"</string>
<string name="permit_full_screen_intent" msgid="2251949245519201421">"Разрешаване на приложенията да изпращат намерения за цял екран"</string>
- <string name="footer_description_full_screen_intent" msgid="8322826300555418227">"Разрешете на това приложение да изпраща намерения за известия, които заемат целия екран."</string>
+ <string name="footer_description_full_screen_intent" msgid="8322826300555418227">"Разрешете на това приложение да изпраща известия за намерения, които заемат целия екран."</string>
<string name="media_management_apps_title" msgid="8222942355578724582">"Приложения за управление на мултимедията"</string>
<string name="media_management_apps_toggle_label" msgid="166724270857067456">"Разрешаване на прил. да управл. мултимедията"</string>
<string name="media_management_apps_description" msgid="8000565658455268524">"Ако е разрешено, това приложение може да променя или изтрива мултимедийните файлове, създадени чрез други приложения, без да ви попита. То трябва да има разрешение за достъп до файловете и мултимедията."</string>
@@ -3812,7 +3812,7 @@
<string name="battery_saver_on_summary" msgid="4605146593966255848">"Вкл."</string>
<string name="battery_saver_off_scheduled_summary" msgid="2193875981740829819">"Ще се включи при <xliff:g id="BATTERY_PERCENTAGE">%1$s</xliff:g>"</string>
<string name="battery_saver_off_summary" msgid="4411561435493109261">"Изкл."</string>
- <string name="app_battery_usage_title" msgid="346558380609793334">"Използване на батерията от приложението"</string>
+ <string name="app_battery_usage_title" msgid="346558380609793334">"Използване на батерията от приложения"</string>
<string name="app_battery_usage_summary" msgid="6349965904306339539">"Задаване на използв. на батерията за приложенията"</string>
<string name="filter_battery_unrestricted_title" msgid="821027369424198223">"Неограничено"</string>
<string name="filter_battery_optimized_title" msgid="8236647176487754796">"Оптимизирано"</string>
@@ -4145,7 +4145,7 @@
<string name="my_device_info_device_identifiers_category_title" msgid="2197063484127704153">"Идентификатори на устройството"</string>
<string name="change_wifi_state_title" msgid="5629648102837821525">"Управление на Wi-Fi"</string>
<string name="change_wifi_state_app_detail_switch" msgid="1385358508267180745">"Разрешаване на приложението да управлява Wi-Fi"</string>
- <string name="change_wifi_state_app_detail_summary" msgid="8230854855584217111">"Разрешаване на това приложение да включва и изключва Wi-Fi, да сканира за Wi-Fi мрежи и да се свързва с тях, да добавя или премахва мрежи и да стартира точки за достъп само на локално ниво"</string>
+ <string name="change_wifi_state_app_detail_summary" msgid="8230854855584217111">"Разрешете на това приложение да включва и изключва Wi-Fi, да сканира за Wi-Fi мрежи и да се свързва с тях, да добавя или премахва мрежи и да стартира точки за достъп само на локално ниво."</string>
<string name="change_nfc_tag_apps_title" msgid="91514009058149617">"Стартиране чрез NFC"</string>
<string name="change_nfc_tag_apps_detail_switch" msgid="240286205725043561">"Разрешаване на стартирането при сканиране с NFC"</string>
<string name="change_nfc_tag_apps_detail_summary" msgid="7083666814715607078">"Разрешете на това приложение да се стартира при сканиране на маркер за NFC.\nАко предоставите това разрешение, приложението ще бъде налице като опция, когато сканирате маркер."</string>
@@ -4653,10 +4653,8 @@
<string name="ingress_rate_limit_no_limit_entry" msgid="8741098826008012163">"Няма ограничение"</string>
<string name="disable_phantom_process_monitor_title" msgid="8348108346706188771">"Деактивиране на ограниченията за дъщерните процеси"</string>
<string name="disable_phantom_process_monitor_summary" msgid="3044464635550256985">"Деактивиране на ограниченията за използването на системните ресурси за дъщерните процеси на приложението"</string>
- <!-- no translation found for enable_notes_role_title (7662702013496114763) -->
- <skip />
- <!-- no translation found for enable_notes_role_summary (136916915155048249) -->
- <skip />
+ <string name="enable_notes_role_title" msgid="7662702013496114763">"Принудително активиране на ролята на бележките"</string>
+ <string name="enable_notes_role_summary" msgid="136916915155048249">"Активиране на системните интегрирания за водене на бележки чрез ролята на бележките. Ако тази роля вече е активирана, не се извършва нищо."</string>
<string name="bluetooth_broadcast_dialog_title" msgid="9172775308463135884">"Предаване"</string>
<string name="bluetooth_broadcast_dialog_broadcast_app" msgid="1016617579194329005">"Предаване на <xliff:g id="CURRENTAPP">%1$s</xliff:g>"</string>
<string name="bluetooth_broadcast_dialog_find_message" msgid="6621660851669953883">"Слушайте предавания, които се възпроизвеждат в близост"</string>
diff --git a/res/values-bs/strings.xml b/res/values-bs/strings.xml
index 4daf299..18d54ce 100644
--- a/res/values-bs/strings.xml
+++ b/res/values-bs/strings.xml
@@ -3249,7 +3249,7 @@
<string name="nls_feature_reply_title" msgid="7925455553821362039">"odgovarati na poruke"</string>
<string name="nls_feature_reply_summary" msgid="4492543411395565556">"Može odgovarati na poruke i djelovati na dugmad u obavještenjima, uključujući odgodu ili odbacivanje obavještenja i odgovaranje na pozive."</string>
<string name="nls_feature_settings_title" msgid="8208164329853194414">"mijenjati postavke"</string>
- <string name="nls_feature_settings_summary" msgid="3770028705648985689">"Može da uključi ili isključi opciju Ne ometaj i da promijeni postavke povezane s tom opcijom."</string>
+ <string name="nls_feature_settings_summary" msgid="3770028705648985689">"Može uključiti ili isključiti opciju Ne ometaj i promijeniti postavke povezane s tom opcijom."</string>
<string name="notification_listener_disable_warning_summary" msgid="8373396293802088961">"Ako aplikaciji <xliff:g id="NOTIFICATION_LISTENER_NAME">%1$s</xliff:g> isključite pristup obavještenjima, može se isključiti i pristup načinu rada Ne ometaj."</string>
<string name="notification_listener_disable_warning_confirm" msgid="841492108402184976">"Isključi"</string>
<string name="notification_listener_disable_warning_cancel" msgid="8802784105045594324">"Otkaži"</string>
@@ -3271,7 +3271,7 @@
<string name="notif_listener_more_settings_desc" msgid="7995492074281663658">"Više postavki dostupno je unutar aplikacije"</string>
<string name="vr_listeners_title" msgid="4960357292472540964">"Usluge pomagača za VR"</string>
<string name="no_vr_listeners" msgid="8442646085375949755">"Nijedna instalirana aplikacija nije zatražila da bude pokrenuta kao usluga pomagača za VR."</string>
- <string name="vr_listener_security_warning_title" msgid="7026351795627615177">"Želite li dozvoliti pristup VR-a za uslugu <xliff:g id="SERVICE">%1$s</xliff:g>?"</string>
+ <string name="vr_listener_security_warning_title" msgid="7026351795627615177">"Dozvoliti pristup usluzi VR-a za uslugu <xliff:g id="SERVICE">%1$s</xliff:g>?"</string>
<string name="vr_listener_security_warning_summary" msgid="1888843557687017791">"<xliff:g id="VR_LISTENER_NAME">%1$s</xliff:g> će moći raditi kada aplikacije budete koristili u načinu rada virtuelne realnosti."</string>
<string name="display_vr_pref_title" msgid="4850474436291113569">"Kada je uređaj u VR načinu rada"</string>
<string name="display_vr_pref_low_persistence" msgid="7039841277157739871">"Smanjenje zamućenosti (preporučeno)"</string>
@@ -3280,7 +3280,7 @@
<string name="picture_in_picture_empty_text" msgid="9123600661268731579">"Nijedna instalirana aplikacija ne podržava način rada slike u slici"</string>
<string name="picture_in_picture_keywords" msgid="3605379820551656253">"slika u slici"</string>
<string name="picture_in_picture_app_detail_title" msgid="4442235098255164650">"Slika u slici"</string>
- <string name="picture_in_picture_app_detail_switch" msgid="8544190716075624017">"Dozvoli način rada slike u slici"</string>
+ <string name="picture_in_picture_app_detail_switch" msgid="8544190716075624017">"Dozvoli sliku u slici"</string>
<string name="picture_in_picture_app_detail_summary" msgid="2503211101305358849">"Dozvolite aplikaciji da kreira prozor sa slikom u slici dok je aplikacija otvorena ili nakon što je napustite (npr. da nastavite gledati videozapis). Ovaj prozor se prikazuje preko drugih aplikacija koje koristite."</string>
<string name="interact_across_profiles_title" msgid="7285906999927669971">"Povezane poslovne i lične aplikacije"</string>
<string name="interact_across_profiles_summary_allowed" msgid="1365881452153799092">"Povezano"</string>
@@ -3640,10 +3640,10 @@
<string name="memory_maximum_usage" msgid="2047013391595835607">"Maksimalno korištenje"</string>
<string name="no_data_usage" msgid="4665617440434654132">"Nema iskorištenih podataka"</string>
<string name="zen_access_warning_dialog_title" msgid="6323325813123130154">"Dozvoliti da <xliff:g id="APP">%1$s</xliff:g> pristupa načinu rada Ne ometaj?"</string>
- <string name="zen_access_warning_dialog_summary" msgid="8468714854067428987">"Aplikacija će biti u mogućnosti da uključi/isključi način rada Ne ometaj i da izmijeni povezane postavke."</string>
+ <string name="zen_access_warning_dialog_summary" msgid="8468714854067428987">"Aplikacija će biti u mogućnosti da uključi/isključi način rada Ne ometaj i izmijeni povezane postavke."</string>
<string name="zen_access_disabled_package_warning" msgid="6565908224294537889">"Mora ostati uključeno jer je uključen pristup obavještenjima"</string>
- <string name="zen_access_revoke_warning_dialog_title" msgid="7377261509261811449">"Opozvati pristup ka usluzi Ne ometaj za aplikaciju <xliff:g id="APP">%1$s</xliff:g>?"</string>
- <string name="zen_access_revoke_warning_dialog_summary" msgid="8689801842914183595">"Sva pravila za uslugu Ne ometaj koje je kreirala ova aplikacija, biti će uklonjena."</string>
+ <string name="zen_access_revoke_warning_dialog_title" msgid="7377261509261811449">"Opozvati pristup funkciji Ne ometaj za aplikaciju <xliff:g id="APP">%1$s</xliff:g>?"</string>
+ <string name="zen_access_revoke_warning_dialog_summary" msgid="8689801842914183595">"Sva pravila za funkciju Ne ometaj koje je kreirala ova aplikacija će se ukloniti."</string>
<string name="ignore_optimizations_on" msgid="6865583039303804932">"Ne optimiziraj"</string>
<string name="ignore_optimizations_off" msgid="9186557038453586295">"Optimizuj"</string>
<string name="ignore_optimizations_on_desc" msgid="1280043916460939932">"Baterija se može brže isprazniti. Aplikaciji više neće biti ograničena potrošnja baterije u pozadini."</string>
@@ -3659,7 +3659,7 @@
<string name="allow_manage_external_storage_description" msgid="5707948153603253225">"Dozvolite ovoj aplikaciji da čita, mijenja i briše sve fajlove na ovom uređaju ili svim povezanim uređajima za pohranu. Ako to dozvolite, aplikacija može pristupati fajlovima bez vašeg znanja."</string>
<string name="filter_manage_external_storage" msgid="6751640571715343804">"Može da pristupa svim fajlovima"</string>
<string name="full_screen_intent_title" msgid="1068024949389956404">"Upravljanje prikazom preko cijelog ekrana"</string>
- <string name="permit_full_screen_intent" msgid="2251949245519201421">"Dozvoli aplikacijama da šalju namjere preko cijelog ekrana"</string>
+ <string name="permit_full_screen_intent" msgid="2251949245519201421">"Dozvoli aplikacijama prikaz preko cijelog ekrana"</string>
<string name="footer_description_full_screen_intent" msgid="8322826300555418227">"Dozvolite aplikaciji da šalje obavještenja o namjeri preko cijelog ekrana."</string>
<string name="media_management_apps_title" msgid="8222942355578724582">"Aplikacije za upravljanje medijima"</string>
<string name="media_management_apps_toggle_label" msgid="166724270857067456">"Dozvoli apl. da upravlja medijskim fajlovima"</string>
diff --git a/res/values-cs/strings.xml b/res/values-cs/strings.xml
index cf3ffc7..411b8c4 100644
--- a/res/values-cs/strings.xml
+++ b/res/values-cs/strings.xml
@@ -4657,10 +4657,8 @@
<string name="ingress_rate_limit_no_limit_entry" msgid="8741098826008012163">"Bez omezení"</string>
<string name="disable_phantom_process_monitor_title" msgid="8348108346706188771">"Deaktivace omezení podřízených procesů"</string>
<string name="disable_phantom_process_monitor_summary" msgid="3044464635550256985">"Deaktivovat omezení využití zdrojů systému podřízenými procesy aplikace"</string>
- <!-- no translation found for enable_notes_role_title (7662702013496114763) -->
- <skip />
- <!-- no translation found for enable_notes_role_summary (136916915155048249) -->
- <skip />
+ <string name="enable_notes_role_title" msgid="7662702013496114763">"Vynutit aktivování role Poznámky"</string>
+ <string name="enable_notes_role_summary" msgid="136916915155048249">"Aktivovat integrace systému psaní poznámek prostřednictvím role Poznámky. Pokud je role Poznámky už aktivována, nic se nestane."</string>
<string name="bluetooth_broadcast_dialog_title" msgid="9172775308463135884">"Vysílání"</string>
<string name="bluetooth_broadcast_dialog_broadcast_app" msgid="1016617579194329005">"Vysílat v aplikaci <xliff:g id="CURRENTAPP">%1$s</xliff:g>"</string>
<string name="bluetooth_broadcast_dialog_find_message" msgid="6621660851669953883">"Poslouchejte vysílání v okolí"</string>
diff --git a/res/values-en-rAU/strings.xml b/res/values-en-rAU/strings.xml
index 865769c..cd858c5 100644
--- a/res/values-en-rAU/strings.xml
+++ b/res/values-en-rAU/strings.xml
@@ -3640,7 +3640,7 @@
<string name="memory_maximum_usage" msgid="2047013391595835607">"Maximum usage"</string>
<string name="no_data_usage" msgid="4665617440434654132">"No data used"</string>
<string name="zen_access_warning_dialog_title" msgid="6323325813123130154">"Allow access to Do Not Disturb for <xliff:g id="APP">%1$s</xliff:g>?"</string>
- <string name="zen_access_warning_dialog_summary" msgid="8468714854067428987">"The app will be able to turn on/off Do Not Disturb and make changes to related settings."</string>
+ <string name="zen_access_warning_dialog_summary" msgid="8468714854067428987">"The app will be able to turn Do Not Disturb on or off, and make changes to related settings."</string>
<string name="zen_access_disabled_package_warning" msgid="6565908224294537889">"Must stay turned on because notification access is on"</string>
<string name="zen_access_revoke_warning_dialog_title" msgid="7377261509261811449">"Revoke access to Do Not Disturb for <xliff:g id="APP">%1$s</xliff:g>?"</string>
<string name="zen_access_revoke_warning_dialog_summary" msgid="8689801842914183595">"All Do Not Disturb rules created by this app will be removed."</string>
diff --git a/res/values-en-rGB/strings.xml b/res/values-en-rGB/strings.xml
index 0aa4669..7f2bc25 100644
--- a/res/values-en-rGB/strings.xml
+++ b/res/values-en-rGB/strings.xml
@@ -3640,7 +3640,7 @@
<string name="memory_maximum_usage" msgid="2047013391595835607">"Maximum usage"</string>
<string name="no_data_usage" msgid="4665617440434654132">"No data used"</string>
<string name="zen_access_warning_dialog_title" msgid="6323325813123130154">"Allow access to Do Not Disturb for <xliff:g id="APP">%1$s</xliff:g>?"</string>
- <string name="zen_access_warning_dialog_summary" msgid="8468714854067428987">"The app will be able to turn on/off Do Not Disturb and make changes to related settings."</string>
+ <string name="zen_access_warning_dialog_summary" msgid="8468714854067428987">"The app will be able to turn Do Not Disturb on or off, and make changes to related settings."</string>
<string name="zen_access_disabled_package_warning" msgid="6565908224294537889">"Must stay turned on because notification access is on"</string>
<string name="zen_access_revoke_warning_dialog_title" msgid="7377261509261811449">"Revoke access to Do Not Disturb for <xliff:g id="APP">%1$s</xliff:g>?"</string>
<string name="zen_access_revoke_warning_dialog_summary" msgid="8689801842914183595">"All Do Not Disturb rules created by this app will be removed."</string>
diff --git a/res/values-en-rIN/strings.xml b/res/values-en-rIN/strings.xml
index a652a3b..e7be910 100644
--- a/res/values-en-rIN/strings.xml
+++ b/res/values-en-rIN/strings.xml
@@ -3640,7 +3640,7 @@
<string name="memory_maximum_usage" msgid="2047013391595835607">"Maximum usage"</string>
<string name="no_data_usage" msgid="4665617440434654132">"No data used"</string>
<string name="zen_access_warning_dialog_title" msgid="6323325813123130154">"Allow access to Do Not Disturb for <xliff:g id="APP">%1$s</xliff:g>?"</string>
- <string name="zen_access_warning_dialog_summary" msgid="8468714854067428987">"The app will be able to turn on/off Do Not Disturb and make changes to related settings."</string>
+ <string name="zen_access_warning_dialog_summary" msgid="8468714854067428987">"The app will be able to turn Do Not Disturb on or off, and make changes to related settings."</string>
<string name="zen_access_disabled_package_warning" msgid="6565908224294537889">"Must stay turned on because notification access is on"</string>
<string name="zen_access_revoke_warning_dialog_title" msgid="7377261509261811449">"Revoke access to Do Not Disturb for <xliff:g id="APP">%1$s</xliff:g>?"</string>
<string name="zen_access_revoke_warning_dialog_summary" msgid="8689801842914183595">"All Do Not Disturb rules created by this app will be removed."</string>
diff --git a/res/values-et/strings.xml b/res/values-et/strings.xml
index b262a3d..9b8c1a9 100644
--- a/res/values-et/strings.xml
+++ b/res/values-et/strings.xml
@@ -3171,8 +3171,8 @@
<string name="unseen_notifs_lock_screen" msgid="6910701117021324612">"Kuva lukustuskuval ainult uued märguanded"</string>
<string name="unseen_notifs_lock_screen_summary" msgid="5996722793868021391">"Eemalda varem vaadatud märguanded lukustuskuvalt automaatselt"</string>
<string name="lock_screen_notifs_title" msgid="3412042692317304449">"Märguanded lukustuskuval"</string>
- <string name="lock_screen_notifs_show_all_summary" msgid="4226586018375762117">"Kuva vestluse, vaike- ja vaiksed märguanded"</string>
- <string name="lock_screen_notifs_show_all" msgid="1300418674456749664">"Kuva vestluse, vaike- ja vaiksed märguanded"</string>
+ <string name="lock_screen_notifs_show_all_summary" msgid="4226586018375762117">"Kuva vestluse, vaike- ja hääletud märguanded"</string>
+ <string name="lock_screen_notifs_show_all" msgid="1300418674456749664">"Kuva vestluse, vaike- ja hääletud märguanded"</string>
<string name="lock_screen_notifs_show_alerting" msgid="6584682657382684566">"Peida hääletud vestlused ja märguanded"</string>
<string name="lock_screen_notifs_show_none" msgid="1941044980403067101">"Ära kuva märguandeid"</string>
<string name="lock_screen_notifs_redact" msgid="9024158855454642296">"Tundlikud märguanded"</string>
@@ -3258,7 +3258,7 @@
<string name="notif_type_conversation" msgid="4383931408641374979">"Vestlused"</string>
<string name="notif_type_conversation_summary" msgid="179142405410217101">"SMS-id, tekstsõnumid ja muud suhtlusviisid"</string>
<string name="notif_type_alerting" msgid="4713073696855718576">"Märguanded"</string>
- <string name="notif_type_alerting_summary" msgid="4681068287836313604">"Võib seadete põhjal heliseda või vibreerida"</string>
+ <string name="notif_type_alerting_summary" msgid="4681068287836313604">"Võivad seadete põhjal heliseda või vibreerida"</string>
<string name="notif_type_silent" msgid="6273951794420331010">"Hääletu"</string>
<string name="notif_type_silent_summary" msgid="7820923063105060844">"Märguanded, mis ei tee kunagi häält ega vibreeri"</string>
<string name="notification_listener_allowed" msgid="5536962633536318551">"Lubatud"</string>
@@ -3272,7 +3272,7 @@
<string name="vr_listeners_title" msgid="4960357292472540964">"VR-abilise teenused"</string>
<string name="no_vr_listeners" msgid="8442646085375949755">"Ükski installitud rakendus pole taotlenud VR-abilise teenusena käitamist."</string>
<string name="vr_listener_security_warning_title" msgid="7026351795627615177">"Kas lubada VR-teenuse juurdepääs teenusele <xliff:g id="SERVICE">%1$s</xliff:g>?"</string>
- <string name="vr_listener_security_warning_summary" msgid="1888843557687017791">"Virtuaalreaalses režiimis kuulajat <xliff:g id="VR_LISTENER_NAME">%1$s</xliff:g> saab käitada, kui kasutate rakendusi virtuaalreaalses režiimis."</string>
+ <string name="vr_listener_security_warning_summary" msgid="1888843557687017791">"Teenust <xliff:g id="VR_LISTENER_NAME">%1$s</xliff:g> saab käitada, kui kasutate rakendusi virtuaalreaalsuse režiimis."</string>
<string name="display_vr_pref_title" msgid="4850474436291113569">"Kui seade on VR-režiimis"</string>
<string name="display_vr_pref_low_persistence" msgid="7039841277157739871">"Vähenda hägusust (soovitatav)"</string>
<string name="display_vr_pref_off" msgid="4008841566387432721">"Vähenda värelemist"</string>
@@ -4141,7 +4141,7 @@
<string name="my_device_info_device_identifiers_category_title" msgid="2197063484127704153">"Seadme identifikaatorid"</string>
<string name="change_wifi_state_title" msgid="5629648102837821525">"WiFi-seadete juhtimine"</string>
<string name="change_wifi_state_app_detail_switch" msgid="1385358508267180745">"Luba rakendusel juhtida WiFi-t"</string>
- <string name="change_wifi_state_app_detail_summary" msgid="8230854855584217111">"Lubage sellel rakendusel WiFi sisse või välja lülitada, otsida WiFi-võrke ja nendega ühendus luua, võrke lisada või eemaldada või luua kohalik kuumkoht"</string>
+ <string name="change_wifi_state_app_detail_summary" msgid="8230854855584217111">"Lubage sellel rakendusel WiFi sisse või välja lülitada, otsida WiFi-võrke ja nendega ühendus luua, võrke lisada või eemaldada või luua kohalik kuumkoht."</string>
<string name="change_nfc_tag_apps_title" msgid="91514009058149617">"Käivitamine NFC kaudu"</string>
<string name="change_nfc_tag_apps_detail_switch" msgid="240286205725043561">"Luba käivitamine NFC skannimisel"</string>
<string name="change_nfc_tag_apps_detail_summary" msgid="7083666814715607078">"Lubage see rakendus käivitada, kui skannitakse NFC-kiip.\nKui see luba on sisse lülitatud, on rakendus kiibi tuvastamisel valikuna saadaval."</string>
diff --git a/res/values-fi/strings.xml b/res/values-fi/strings.xml
index f203e74..44ec910 100644
--- a/res/values-fi/strings.xml
+++ b/res/values-fi/strings.xml
@@ -4653,10 +4653,8 @@
<string name="ingress_rate_limit_no_limit_entry" msgid="8741098826008012163">"Ei rajoitusta"</string>
<string name="disable_phantom_process_monitor_title" msgid="8348108346706188771">"Poista käytöstä alatason prosessirajoitukset"</string>
<string name="disable_phantom_process_monitor_summary" msgid="3044464635550256985">"Poista käytöstä rajoitukset, jotka liittyvät järjestelmäresurssin käyttöön sovelluksen alatason prosesseissa"</string>
- <!-- no translation found for enable_notes_role_title (7662702013496114763) -->
- <skip />
- <!-- no translation found for enable_notes_role_summary (136916915155048249) -->
- <skip />
+ <string name="enable_notes_role_title" msgid="7662702013496114763">"Pakota Muistiinpanot-rooli käyttöön"</string>
+ <string name="enable_notes_role_summary" msgid="136916915155048249">"Ota käyttöön muistiinpanojärjestelmän integraatioita Muistiinpanot-roolin avulla. Jos Muistiinpanot-rooli on jo käytössä, tämä ei vaikuta mitenkään."</string>
<string name="bluetooth_broadcast_dialog_title" msgid="9172775308463135884">"Lähetä"</string>
<string name="bluetooth_broadcast_dialog_broadcast_app" msgid="1016617579194329005">"Lähetä <xliff:g id="CURRENTAPP">%1$s</xliff:g>"</string>
<string name="bluetooth_broadcast_dialog_find_message" msgid="6621660851669953883">"Kuuntele lähellä olevia lähetyksiä"</string>
diff --git a/res/values-fr/strings.xml b/res/values-fr/strings.xml
index e761beb..a3ee842 100644
--- a/res/values-fr/strings.xml
+++ b/res/values-fr/strings.xml
@@ -3477,7 +3477,7 @@
<string name="memtag_title" msgid="5096176296797727201">"Protection avancée mémoire (bêta)"</string>
<string name="memtag_toggle" msgid="8695028758462939212">"Protection avancée de la mémoire"</string>
<string name="memtag_intro" msgid="579408691329568953">"Cette fonctionnalité bêta vous aide à protéger votre appareil des bugs qui peuvent compromettre votre sécurité."</string>
- <string name="memtag_on" msgid="824938319141503923">"Activée"</string>
+ <string name="memtag_on" msgid="824938319141503923">"Activé"</string>
<string name="memtag_off" msgid="4835589640091709019">"Désactivée"</string>
<string name="memtag_on_pending" msgid="1592053425431532361">"Activé après le redémarrage"</string>
<string name="memtag_off_pending" msgid="1543177181383593726">"Désactivée après le redémarrage"</string>
diff --git a/res/values-gu/strings.xml b/res/values-gu/strings.xml
index 53caefe..66c69c4 100644
--- a/res/values-gu/strings.xml
+++ b/res/values-gu/strings.xml
@@ -909,10 +909,8 @@
<skip />
<string name="wifi_hotspot_speed_summary_unavailable" msgid="7276080644693388756">"તમારા દેશ અથવા પ્રદેશમાં ઉપલબ્ધ નથી"</string>
<string name="wifi_hotspot_speed_footer" msgid="8846939503916795002">"જો તમે પસંદ કરેલી ફ્રિકવન્સી ઉપલબ્ધ ન હોય, તો તમારું હૉટસ્પૉટ કોઈ અલગ ફ્રિકવન્સીનો ઉપયોગ કરી શકે છે. જો તમે ફ્રિકવન્સી બદલો, તો હૉટસ્પૉટના સુરક્ષા સેટિંગ બદલાઈ શકે છે."</string>
- <!-- no translation found for wifi_hotspot_security_summary_unavailable (117582979310345853) -->
- <skip />
- <!-- no translation found for wifi_hotspot_security_footer (4608329688744949796) -->
- <skip />
+ <string name="wifi_hotspot_security_summary_unavailable" msgid="117582979310345853">"6 GHz સાથે ઉપલબ્ધ નથી"</string>
+ <string name="wifi_hotspot_security_footer" msgid="4608329688744949796">"જો તમે હૉટસ્પૉટની ફ્રિકવન્સીને બદલો છો તો સુરક્ષા સેટિંગ બદલાઈ શકે છે"</string>
<string name="wifi_tether_starting" msgid="8879874184033857814">"હૉટસ્પૉટ ચાલુ કરી રહ્યું છે…"</string>
<string name="wifi_tether_stopping" msgid="4416492968019409188">"હૉટસ્પૉટ બંધ કરી રહ્યું છે…"</string>
<string name="wifi_tether_carrier_unsupport_dialog_title" msgid="3089432578433978073">"ઇન્ટરનેટ શેર કરવાની સુવિધા ઉપલબ્ધ નથી"</string>
@@ -2705,14 +2703,12 @@
<string name="enable_guest_calling" msgid="8300355036005240911">"અતિથિને ફોનનો ઉપયોગ કરવાની મંજૂરી આપો"</string>
<string name="enable_guest_calling_summary" msgid="4748224917641204782">"કૉલ ઇતિહાસ અતિથિ વપરાશકર્તા સાથે શેર કરવામાં આવશે"</string>
<string name="user_enable_calling_sms" msgid="8546430559552381324">"ફોન કૉલ અને SMS ચાલુ કરો"</string>
- <!-- no translation found for user_grant_admin (5942118263054572074) -->
- <skip />
+ <string name="user_grant_admin" msgid="5942118263054572074">"આ વપરાશકર્તાને ઍડમિન બનાવો"</string>
<string name="user_remove_user" msgid="8468203789739693845">"વપરાશકર્તાને ડિલીટ કરો"</string>
<string name="user_enable_calling_and_sms_confirm_title" msgid="4041510268838725520">"ફોન કૉલ અને SMS ચાલુ કરીએ?"</string>
<string name="user_enable_calling_and_sms_confirm_message" msgid="367792286597449922">"કૉલ અને SMS ઇતિહાસ આ વપરાશકર્તા સાથે શેર કરવામાં આવશે."</string>
<string name="user_revoke_admin_confirm_title" msgid="3057842401861731863">"ઍડમિનના વિશેષાધિકારો કાઢી નાખીએ?"</string>
- <!-- no translation found for user_revoke_admin_confirm_message (9207187319308572958) -->
- <skip />
+ <string name="user_revoke_admin_confirm_message" msgid="9207187319308572958">"જો તમે આ વપરાશકર્તાના ઍડમિનના અધિકારોને કાઢી નાખશો, તો તમે અથવા બીજા ઍડમિન પછી તેમને તે પાછા આપી શકે છે."</string>
<string name="emergency_info_title" msgid="8233682750953695582">"ઇમર્જન્સીની માહિતી"</string>
<string name="emergency_info_summary" msgid="8463622253016757697">"<xliff:g id="USER_NAME">%1$s</xliff:g> માટે માહિતી અને સંપર્કો"</string>
<string name="open_app_button" msgid="5025229765547191710">"<xliff:g id="APP_NAME">%1$s</xliff:g> ખોલો"</string>
diff --git a/res/values-hr/strings.xml b/res/values-hr/strings.xml
index a985d0f..8f556f5 100644
--- a/res/values-hr/strings.xml
+++ b/res/values-hr/strings.xml
@@ -346,7 +346,7 @@
<string name="setup_fingerprint_enroll_skip_after_adding_lock_text" msgid="2412645723804450304">"Postavljanje otisaka prstiju traje samo jednu ili dvije minute. Ako to preskočite, otiske prstiju možete dodati kasnije u postavkama."</string>
<string name="security_settings_fingerprint_v2_enroll_introduction_message_setup" msgid="6255210343107484206">"Kada se prikaže ta ikona, upotrijebite otisak prsta za autentifikaciju, na primjer radi prijave u aplikacije ili odobravanja kupnje"</string>
<string name="security_settings_fingerprint_v2_enroll_introduction_footer_title_1" msgid="4360262371633254407">"Napomena"</string>
- <string name="security_settings_fingerprint_v2_enroll_introduction_footer_title_2" msgid="2580899232734177771">"Način funkcioniranja"</string>
+ <string name="security_settings_fingerprint_v2_enroll_introduction_footer_title_2" msgid="2580899232734177771">"Kako to funkcionira"</string>
<string name="security_settings_fingerprint_v2_enroll_introduction_footer_message_2" msgid="5909924864816776516">"Otključavanje otiskom prsta izrađuje jedinstveni model vašeg otiska prsta radi potvrde vašeg identiteta. Da biste izradili model otiska prsta tijekom postavljanja, snimit ćete slike svojeg otiska prsta iz različitih položaja."</string>
<string name="security_settings_fingerprint_v2_enroll_introduction_footer_message_consent_2" msgid="3493356605815124807">"Otključavanje otiskom prsta izrađuje jedinstveni model otiska prsta vašeg djeteta radi potvrđivanja njegovog identiteta. Da bi izradilo model otiska prsta tijekom postavljanja, dijete će snimiti slike svojeg otiska prsta iz različitih položaja."</string>
<string name="security_settings_fingerprint_v2_enroll_introduction_footer_message_6" msgid="5314031490467481499">"Za najbolje rezultate koristite zaštitu zaslona s certifikatom Made for Google. Vaš otisak prsta možda neće funkcionirati s drugim zaštitama zaslona."</string>
@@ -2137,7 +2137,7 @@
<string name="print_feature_state_off" msgid="1466195699995209446">"Isključeno"</string>
<string name="print_menu_item_add_service" msgid="1549091062463044676">"Dodajte uslugu"</string>
<string name="print_menu_item_add_printer" msgid="8711630848324870892">"Dodajte pisač"</string>
- <string name="print_menu_item_search" msgid="5989979785203603169">"Pretraži"</string>
+ <string name="print_menu_item_search" msgid="5989979785203603169">"Pretražite"</string>
<string name="print_searching_for_printers" msgid="5401413178028348800">"Traženje pisača"</string>
<string name="print_service_disabled" msgid="9185935228930987786">"Usluga je onemogućena"</string>
<string name="print_print_jobs" msgid="2605944855933091183">"Zadaci ispisa"</string>
@@ -3119,7 +3119,7 @@
<string name="notification_dashboard_summary" msgid="7530169251902320652">"Povijest obavijesti, razgovori"</string>
<string name="conversation_notifs_category" msgid="2549844862379963273">"Razgovor"</string>
<string name="general_notification_header" msgid="3669031068980713359">"Upravljanje"</string>
- <string name="app_notification_field" msgid="3208079070539894909">"Obavijesti aplikacije"</string>
+ <string name="app_notification_field" msgid="3208079070539894909">"Obavijesti aplikacija"</string>
<string name="app_notification_field_summary" msgid="5981393613897713471">"Upravljajte obavijestima pojedinačnih aplikacija"</string>
<string name="advanced_section_header" msgid="6478709678084326738">"Općenito"</string>
<string name="profile_section_header" msgid="4970209372372610799">"Obavijesti poslovnog profila"</string>
@@ -3148,7 +3148,7 @@
<string name="notification_bubbles_title" msgid="5681506665322329301">"Oblačići"</string>
<string name="bubbles_app_toggle_title" msgid="5319021259954576150">"Oblačići"</string>
<string name="bubbles_conversation_toggle_title" msgid="5225039214083311316">"Prikaži ovaj razgovor u oblačiću"</string>
- <string name="bubbles_conversation_toggle_summary" msgid="720229032254323578">"Prikaži pomičnu ikonu povrh aplikacija"</string>
+ <string name="bubbles_conversation_toggle_summary" msgid="720229032254323578">"Prikazuje pomičnu ikonu povrh aplikacija"</string>
<string name="bubbles_feature_disabled_dialog_title" msgid="1794193899792284007">"Uključiti oblačiće za uređaj?"</string>
<string name="bubbles_feature_disabled_dialog_text" msgid="5275666953364031055">"Uključivanjem oblačića za ovu aplikaciju uključit će se i oblačići za vaš uređaj.\n\nTo utječe na druge aplikacije ili razgovore kojima je dopušteno otvaranje oblačića."</string>
<string name="bubbles_feature_disabled_button_approve" msgid="2042628067101419871">"Uključi"</string>
@@ -3247,11 +3247,11 @@
<string name="notification_assistant_security_warning_summary" msgid="4846559755787348129">"U Androidu 12 poboljšane obavijesti zamjenjuju prilagodljive obavijesti za Android. Ta značajka prikazuje predložene radnje i odgovore te organizira vaše obavijesti. \n\nPoboljšane obavijesti mogu pristupati sadržaju obavijesti, uključujući osobne podatke kao što su imena kontakata i poruke. Ta značajka može i odbacivati obavijesti ili poduzimati radnje u vezi s njima, na primjer može odgovarati na telefonske pozive i upravljati značajkom Ne uznemiravaj."</string>
<string name="notification_listener_security_warning_title" msgid="5791700876622858363">"Želite li usluzi <xliff:g id="SERVICE">%1$s</xliff:g> dopustiti da pristupa obavijestima?"</string>
<string name="notification_listener_security_warning_summary" msgid="1131986567509818121">"Aplikacija <xliff:g id="NOTIFICATION_LISTENER_NAME">%1$s</xliff:g> moći će čitati sve obavijesti, uključujući osobne podatke, na primjer imena kontakata, fotografije i tekstove poruka koje primate. Aplikacija će moći i odgoditi ili odbaciti obavijesti ili poduzeti radnje povezane s gumbima u obavijestima, uključujući odgovaranje na telefonske pozive. \n\nAplikacija će također moći uključiti ili isključiti značajku Ne uznemiravaj i promijeniti povezane postavke."</string>
- <string name="nls_warning_prompt" msgid="1486887096703743841">"Aplikacija <xliff:g id="NOTIFICATION_LISTENER_NAME">%1$s</xliff:g> moći će sljedeće:"</string>
+ <string name="nls_warning_prompt" msgid="1486887096703743841">"Aplikaciji <xliff:g id="NOTIFICATION_LISTENER_NAME">%1$s</xliff:g> omogućit će se:"</string>
<string name="nls_feature_read_title" msgid="7629713268744220437">"Čitanje vaših poruka"</string>
<string name="nls_feature_read_summary" msgid="1064698238110273593">"Može čitati vaše obavijesti, uključujući osobne podatke kao što su kontakti, poruke i fotografije."</string>
<string name="nls_feature_reply_title" msgid="7925455553821362039">"Odgovaranje na poruke"</string>
- <string name="nls_feature_reply_summary" msgid="4492543411395565556">"Može odgovarati na poruke i poduzeti radnje povezane s gumbima u obavijestima, uključujući odgađanje alarma, odbacivanje obavijesti i odgovaranje na pozive."</string>
+ <string name="nls_feature_reply_summary" msgid="4492543411395565556">"Može odgovarati na poruke i poduzimati radnje povezane s gumbima u obavijestima, uključujući odgađanje alarma, odbacivanje obavijesti i odgovaranje na pozive."</string>
<string name="nls_feature_settings_title" msgid="8208164329853194414">"Promjena postavki"</string>
<string name="nls_feature_settings_summary" msgid="3770028705648985689">"Može uključiti ili isključiti značajku Ne uznemiravaj i promijeniti povezane postavke."</string>
<string name="notification_listener_disable_warning_summary" msgid="8373396293802088961">"Ako isključite pristup obavijestima za <xliff:g id="NOTIFICATION_LISTENER_NAME">%1$s</xliff:g>, možda će se isključiti i pristup značajci Ne uznemiravaj."</string>
@@ -3643,10 +3643,10 @@
<string name="running_frequency" msgid="7260225121706316639">"Učestalost"</string>
<string name="memory_maximum_usage" msgid="2047013391595835607">"Maksimalna upotreba"</string>
<string name="no_data_usage" msgid="4665617440434654132">"Nema upotrebe podataka"</string>
- <string name="zen_access_warning_dialog_title" msgid="6323325813123130154">"Želite li dopustiti aplikaciji <xliff:g id="APP">%1$s</xliff:g> pristup opciji Ne uznemiravaj?"</string>
+ <string name="zen_access_warning_dialog_title" msgid="6323325813123130154">"Želite li aplikaciji <xliff:g id="APP">%1$s</xliff:g> dopustiti pristup značajci Ne uznemiravaj?"</string>
<string name="zen_access_warning_dialog_summary" msgid="8468714854067428987">"Aplikacija će moći uključiti ili isključiti opciju Ne uznemiravaj i mijenjati povezane postavke."</string>
<string name="zen_access_disabled_package_warning" msgid="6565908224294537889">"Mora ostati uključeno jer je uključen pristup obavijestima"</string>
- <string name="zen_access_revoke_warning_dialog_title" msgid="7377261509261811449">"Želite li opozvati pristup opciji Ne uznemiravaj za aplikaciju <xliff:g id="APP">%1$s</xliff:g>?"</string>
+ <string name="zen_access_revoke_warning_dialog_title" msgid="7377261509261811449">"Želite li opozvati pristup značajci Ne uznemiravaj za aplikaciju <xliff:g id="APP">%1$s</xliff:g>?"</string>
<string name="zen_access_revoke_warning_dialog_summary" msgid="8689801842914183595">"Uklonit će se sva pravila Ne uznemiravaj koja je postavila ova aplikacija."</string>
<string name="ignore_optimizations_on" msgid="6865583039303804932">"Ne optimiziraj"</string>
<string name="ignore_optimizations_off" msgid="9186557038453586295">"Optimizacija"</string>
@@ -3812,7 +3812,7 @@
<string name="battery_saver_on_summary" msgid="4605146593966255848">"Uključeno"</string>
<string name="battery_saver_off_scheduled_summary" msgid="2193875981740829819">"Uključit će se na <xliff:g id="BATTERY_PERCENTAGE">%1$s</xliff:g>"</string>
<string name="battery_saver_off_summary" msgid="4411561435493109261">"Isključeno"</string>
- <string name="app_battery_usage_title" msgid="346558380609793334">"Potrošnja baterije za aplikaciju"</string>
+ <string name="app_battery_usage_title" msgid="346558380609793334">"Potrošnja baterije za aplikacije"</string>
<string name="app_battery_usage_summary" msgid="6349965904306339539">"Postavite potrošnju baterije za aplikacije"</string>
<string name="filter_battery_unrestricted_title" msgid="821027369424198223">"Neograničeno"</string>
<string name="filter_battery_optimized_title" msgid="8236647176487754796">"Optimizirano"</string>
diff --git a/res/values-hu/strings.xml b/res/values-hu/strings.xml
index eb07144..02836d8 100644
--- a/res/values-hu/strings.xml
+++ b/res/values-hu/strings.xml
@@ -3275,7 +3275,7 @@
<string name="notif_listener_more_settings_desc" msgid="7995492074281663658">"Ebben az alkalmazásban további beállítások állnak rendelkezésre"</string>
<string name="vr_listeners_title" msgid="4960357292472540964">"Virtuálisvalóság-segédszolgáltatás"</string>
<string name="no_vr_listeners" msgid="8442646085375949755">"Egy telepített alkalmazás sem kérte, hogy virtuálisvalóság-segédszolgáltatásként legyen futtatva."</string>
- <string name="vr_listener_security_warning_title" msgid="7026351795627615177">"Engedélyezi a(z) <xliff:g id="SERVICE">%1$s</xliff:g> számára a hozzáférést a virtuálisvalóság-szolgáltatáshoz?"</string>
+ <string name="vr_listener_security_warning_title" msgid="7026351795627615177">"Hozzáférhet a(z) <xliff:g id="SERVICE">%1$s</xliff:g> a virtuálisvalóság-szolgáltatáshoz?"</string>
<string name="vr_listener_security_warning_summary" msgid="1888843557687017791">"A(z) <xliff:g id="VR_LISTENER_NAME">%1$s</xliff:g> futtatható lesz, ha Ön virtuális valóság módban használ alkalmazásokat."</string>
<string name="display_vr_pref_title" msgid="4850474436291113569">"Ha az eszköz VR módban van"</string>
<string name="display_vr_pref_low_persistence" msgid="7039841277157739871">"Homályosítás csökkentése (ajánlott)"</string>
@@ -3643,7 +3643,7 @@
<string name="running_frequency" msgid="7260225121706316639">"Gyakoriság"</string>
<string name="memory_maximum_usage" msgid="2047013391595835607">"Maximális használat"</string>
<string name="no_data_usage" msgid="4665617440434654132">"Nincs felhasznált adatmennyiség"</string>
- <string name="zen_access_warning_dialog_title" msgid="6323325813123130154">"Engedélyezi a(z) <xliff:g id="APP">%1$s</xliff:g> számára a hozzáférést a „Ne zavarjanak” funkcióhoz?"</string>
+ <string name="zen_access_warning_dialog_title" msgid="6323325813123130154">"Hozzáférhet a(z) <xliff:g id="APP">%1$s</xliff:g> a „Ne zavarjanak” funkcióhoz?"</string>
<string name="zen_access_warning_dialog_summary" msgid="8468714854067428987">"Az alkalmazás be- és kikapcsolhatja a „Ne zavarjanak” funkciót, és módosíthatja a vonatkozó beállításokat."</string>
<string name="zen_access_disabled_package_warning" msgid="6565908224294537889">"Bekapcsolva kell hagyni, mert az értesítési hozzáférés be van kapcsolva"</string>
<string name="zen_access_revoke_warning_dialog_title" msgid="7377261509261811449">"Visszavonja a(z) <xliff:g id="APP">%1$s</xliff:g> hozzáférését a „Ne zavarjanak” szabályhoz?"</string>
diff --git a/res/values-hy/strings.xml b/res/values-hy/strings.xml
index 72870f1..ee06335 100644
--- a/res/values-hy/strings.xml
+++ b/res/values-hy/strings.xml
@@ -2386,7 +2386,7 @@
<string name="vpn_settings_multiple_insecure_multiple_total" msgid="1706236062478680488">"<xliff:g id="VPN_COUNT">%d</xliff:g>-ն անվտանգ չէ"</string>
<string name="adaptive_connectivity_title" msgid="7464959640138428192">"Հարմարվող կապ"</string>
<string name="adaptive_connectivity_summary" msgid="3648731530666326885">"Ձեր ցանցային կապերն ավտոմատ կառավարելու միջոցով երկարացնում է մարտկոցի աշխատանքի տևողությունը և բարելավում սարքի արդյունավետությունը"</string>
- <string name="adaptive_connectivity_switch_on" msgid="3653067561620745493">"Միացնել"</string>
+ <string name="adaptive_connectivity_switch_on" msgid="3653067561620745493">"Միացված է"</string>
<string name="adaptive_connectivity_switch_off" msgid="5076172560836115265">"Անջատել"</string>
<string name="adaptive_connectivity_main_switch_title" msgid="261045483524512420">"Հարմարվող միացման օգտագործում"</string>
<string name="credentials_title" msgid="7535942196886123656">"Մուտքի տվյալների պահոց"</string>
diff --git a/res/values-in/strings.xml b/res/values-in/strings.xml
index f2861d9..0ab62ab 100644
--- a/res/values-in/strings.xml
+++ b/res/values-in/strings.xml
@@ -4653,10 +4653,8 @@
<string name="ingress_rate_limit_no_limit_entry" msgid="8741098826008012163">"Tak terbatas"</string>
<string name="disable_phantom_process_monitor_title" msgid="8348108346706188771">"Nonaktifkan batasan proses turunan"</string>
<string name="disable_phantom_process_monitor_summary" msgid="3044464635550256985">"Nonaktifkan batasan penggunaan resource sistem pada proses turunan aplikasi"</string>
- <!-- no translation found for enable_notes_role_title (7662702013496114763) -->
- <skip />
- <!-- no translation found for enable_notes_role_summary (136916915155048249) -->
- <skip />
+ <string name="enable_notes_role_title" msgid="7662702013496114763">"Aktifkan paksa peran Catatan"</string>
+ <string name="enable_notes_role_summary" msgid="136916915155048249">"Aktifkan integrasi sistem pembuatan catatan melalui peran Catatan. Jika peran Catatan telah diaktifkan, setelan ini tidak melakukan apa pun."</string>
<string name="bluetooth_broadcast_dialog_title" msgid="9172775308463135884">"Siaran"</string>
<string name="bluetooth_broadcast_dialog_broadcast_app" msgid="1016617579194329005">"Siarkan <xliff:g id="CURRENTAPP">%1$s</xliff:g>"</string>
<string name="bluetooth_broadcast_dialog_find_message" msgid="6621660851669953883">"Dengarkan siaran yang diputar di dekat Anda"</string>
diff --git a/res/values-ka/strings.xml b/res/values-ka/strings.xml
index 4bf64cf..a532a89 100644
--- a/res/values-ka/strings.xml
+++ b/res/values-ka/strings.xml
@@ -266,7 +266,7 @@
<string name="safety_center_summary" msgid="3554867379951053869">"აპების უსაფრთხოება, მოწყობილობის ჩაკეტვა, ნებართვები"</string>
<string name="security_settings_face_preference_summary" msgid="6675126437396914838">"სახე დამატებულია"</string>
<string name="security_settings_face_preference_summary_none" msgid="523320857738436024">"საჭიროა დაყენება"</string>
- <string name="security_settings_face_preference_title" msgid="2126625155005348417">"განბლოკვა სახით"</string>
+ <string name="security_settings_face_preference_title" msgid="2126625155005348417">"სახით განბლოკვა"</string>
<string name="security_settings_face_profile_preference_title" msgid="7519527436266375005">"სახით განბლოკვა სამსახურისთვის"</string>
<string name="security_settings_face_enroll_education_title" msgid="6448806884597691208">"როგორ უნდა დააყენოთ სახით განბლოკვა"</string>
<string name="security_settings_face_enroll_education_title_accessibility" msgid="3701874093226957891">"სახით განბლოკვის პარამეტრების დაყენება"</string>
@@ -3168,7 +3168,7 @@
<string name="silent_notifications_status_bar" msgid="6113307620588767516">"ჩუმი შეტყობინებების დამალვა სტატუსის ზოლში"</string>
<string name="notification_pulse_title" msgid="8013178454646671529">"სინათლის ციმციმი"</string>
<string name="lock_screen_notifications_title" msgid="2876323153692406203">"კონფიდენციალურობა"</string>
- <string name="lockscreen_bypass_title" msgid="6519964196744088573">"ჩაკეტილი ეკრანის გამოსატოვებლად"</string>
+ <string name="lockscreen_bypass_title" msgid="6519964196744088573">"ჩაკეტილი ეკრანის გამოტოვება"</string>
<string name="lockscreen_bypass_summary" msgid="4578154430436224161">"განბლოკვის შემდეგ პირდაპირ გადადით ბოლოს გამოყენებულ ეკრანზე. ჩაკეტილ ეკრანზე შეტყობინებები არ გამოჩნდება. გადაფურცლეთ ზემოდან ქვემოთ მათ სანახავად."</string>
<string name="keywords_lockscreen_bypass" msgid="41035425468915498">"ჩაკეტილი ეკრანი, დაბლოკილი ეკრანი, გამოტოვება, გვერდის ავლა"</string>
<string name="locked_work_profile_notification_title" msgid="279367321791301499">"დაბლოკილი სამს.პროფილისას"</string>
diff --git a/res/values-kk/strings.xml b/res/values-kk/strings.xml
index da57eee..0a47857 100644
--- a/res/values-kk/strings.xml
+++ b/res/values-kk/strings.xml
@@ -4653,10 +4653,8 @@
<string name="ingress_rate_limit_no_limit_entry" msgid="8741098826008012163">"Шексіз"</string>
<string name="disable_phantom_process_monitor_title" msgid="8348108346706188771">"Туынды процеске қатысты шектеулерді өшіру"</string>
<string name="disable_phantom_process_monitor_summary" msgid="3044464635550256985">"Қолданбаның туынды процестерінің жүйе ресурсын пайдалануына қатысты шектеулерді өшіру"</string>
- <!-- no translation found for enable_notes_role_title (7662702013496114763) -->
- <skip />
- <!-- no translation found for enable_notes_role_summary (136916915155048249) -->
- <skip />
+ <string name="enable_notes_role_title" msgid="7662702013496114763">"\"Ескертпелер\" жинағын мәжбүрлі түрде қосу"</string>
+ <string name="enable_notes_role_summary" msgid="136916915155048249">"\"Ескертпелер\" жинағы арқылы ескертпе жасау жүйесін қосады. \"Ескертпелер\" жинағы қосулы болса, ештеңе істемейді."</string>
<string name="bluetooth_broadcast_dialog_title" msgid="9172775308463135884">"Тарату"</string>
<string name="bluetooth_broadcast_dialog_broadcast_app" msgid="1016617579194329005">"<xliff:g id="CURRENTAPP">%1$s</xliff:g> қолданбасын тарату"</string>
<string name="bluetooth_broadcast_dialog_find_message" msgid="6621660851669953883">"Маңайыңызда таратылып жатқан медиамазмұндарды тыңдауға болады."</string>
diff --git a/res/values-kn/strings.xml b/res/values-kn/strings.xml
index 3e65d50..9287dc6 100644
--- a/res/values-kn/strings.xml
+++ b/res/values-kn/strings.xml
@@ -1517,7 +1517,7 @@
<string name="clear_cache_btn_text" msgid="8597272828928143723">"ಕ್ಯಾಷ್ ಅಳಿಸಿ"</string>
<string name="cache_size_label" msgid="313456088966822757">"ಕ್ಯಾಷ್"</string>
<string name="uri_permissions_text" msgid="8406345863117405105">"{count,plural, =1{1 ಐಟಂ}one{# ಐಟಂಗಳು}other{# ಐಟಂಗಳು}}"</string>
- <string name="clear_uri_btn_text" msgid="4828117421162495134">"ಪ್ರವೇಶ ತೆರವುಗೊಳಿಸು"</string>
+ <string name="clear_uri_btn_text" msgid="4828117421162495134">"ಆ್ಯಕ್ಸೆಸ್ ತೆರವುಗೊಳಿಸು"</string>
<string name="controls_label" msgid="8671492254263626383">"ನಿಯಂತ್ರಣಗಳು"</string>
<string name="force_stop" msgid="2681771622136916280">"ಬಲವಂತವಾಗಿ ನಿಲ್ಲಿಸಿ"</string>
<string name="total_size_label" msgid="2052185048749658866">"ಒಟ್ಟು"</string>
@@ -1751,7 +1751,7 @@
<string name="enabled_locales_keyboard_layout" msgid="3939886151098958639">"ಲೇಔಟ್"</string>
<string name="gadget_picker_title" msgid="7615902510050731400">"ಗ್ಯಾಜೆಟ್ ಆರಿಸಿ"</string>
<string name="widget_picker_title" msgid="7641298325488989676">"ವಿಜೆಟ್ ಅನ್ನು ಆರಿಸಿ"</string>
- <string name="allow_bind_app_widget_activity_allow_bind_title" msgid="3537968409832846255">"ವಿಜೆಟ್ ರಚಿಸಿ ಮತ್ತು ಪ್ರವೇಶ ಅನುಮಿತಿಸಬಹುದೇ?"</string>
+ <string name="allow_bind_app_widget_activity_allow_bind_title" msgid="3537968409832846255">"ವಿಜೆಟ್ ರಚಿಸಿ ಮತ್ತು ಆ್ಯಕ್ಸೆಸ್ ಅನುಮಿತಿಸಬಹುದೇ?"</string>
<string name="allow_bind_app_widget_activity_allow_bind" msgid="5825298768068148804">"ನೀವು ವಿಜೆಟ್ ಅನ್ನು ರಚಿಸಿದ ನಂತರ, ಪ್ರದರ್ಶಿಸುವ ಎಲ್ಲಾ ಡೇಟಾವನ್ನು ಆ್ಯಪ್ ಪ್ರವೇಶಿಸಬಹುದು.\n\nಆ್ಯಪ್: <xliff:g id="WIDGET_HOST_NAME">%1$s</xliff:g>\nವಿಜೆಟ್: <xliff:g id="WIDGET_LABEL">%2$s</xliff:g>\n"</string>
<string name="allow_bind_app_widget_activity_always_allow_bind" msgid="7268758525344468364">"ವಿಜೆಟ್ಗಳನ್ನು ರಚಿಸಲು ಮತ್ತು ಅವುಗಳ ಡೇಟಾವನ್ನು ಪ್ರವೇಶಿಸಲು <xliff:g id="WIDGET_HOST_NAME">%1$s</xliff:g> ಅನ್ನು ಯಾವಾಗಲೂ ಅನುಮತಿಸಿ"</string>
<string name="testing_usage_stats" msgid="4660643799010906365">"ಬಳಕೆಯ ಅಂಕಿಅಂಶಗಳು"</string>
@@ -2770,7 +2770,7 @@
<skip />
<string name="cell_broadcast_settings" msgid="2416980110093867199">"ವೈರ್ಲೆಸ್ ತುರ್ತು ಅಲರ್ಟ್ಗಳು"</string>
<string name="network_operators_settings" msgid="5105453353329748954">"ನೆಟ್ವರ್ಕ್ ಆಪರೇಟರ್ಗಳು"</string>
- <string name="access_point_names" msgid="5768430498022188057">"ಪ್ರವೇಶ ಕೇಂದ್ರದ ಹೆಸರುಗಳು"</string>
+ <string name="access_point_names" msgid="5768430498022188057">"ಆ್ಯಕ್ಸೆಸ್ ಕೇಂದ್ರದ ಹೆಸರುಗಳು"</string>
<string name="enhanced_4g_lte_mode_title" msgid="6624700245232361149">"VoLTE"</string>
<string name="enhanced_4g_lte_mode_title_advanced_calling" msgid="7066009898031465265">"ಸುಧಾರಿತ ಕರೆ ಮಾಡುವಿಕೆ"</string>
<string name="enhanced_4g_lte_mode_title_4g_calling" msgid="7445853566718786195">"4G ಕರೆ ಮಾಡುವಿಕೆ"</string>
@@ -2795,7 +2795,7 @@
<string name="auto_data_switch_title" msgid="5862200603753603464">"ಮೊಬೈಲ್ ಡೇಟಾವನ್ನು ಸ್ವಯಂಚಾಲಿತವಾಗಿ ಬದಲಿಸಿ"</string>
<string name="auto_data_switch_summary" msgid="1934340931995429057">"ಈ ನೆಟ್ವರ್ಕ್ ಉತ್ತಮ ಲಭ್ಯತೆಯನ್ನು ಹೊಂದಿರುವಾಗ ಇದನ್ನು ಬಳಸಿ"</string>
<string name="work_sim_title" msgid="8999872928646924429">"ಕೆಲಸದ ಸಿಮ್"</string>
- <string name="user_restrictions_title" msgid="4068914244980335993">"ಅಪ್ಲಿಕೇಶನ್ & ವಿಷಯ ಪ್ರವೇಶ"</string>
+ <string name="user_restrictions_title" msgid="4068914244980335993">"ಅಪ್ಲಿಕೇಶನ್ & ವಿಷಯ ಆ್ಯಕ್ಸೆಸ್"</string>
<string name="user_rename" msgid="8735940847878484249">"ಮರುಹೆಸರಿಸಿ"</string>
<string name="app_restrictions_custom_label" msgid="6949268049087435132">"ಅಪ್ಲಿಕೇಶನ್ ನಿರ್ಬಂಧಗಳನ್ನು ಹೊಂದಿಸಿ"</string>
<string name="user_restrictions_controlled_by" msgid="2821526006742851624">"<xliff:g id="APP">%1$s</xliff:g> ಮೂಲಕ ನಿಯಂತ್ರಿಸಲಾಗುತ್ತಿದೆ"</string>
@@ -2851,7 +2851,7 @@
<string name="nfc_and_payment_settings_payment_off_nfc_off_summary" msgid="7132040463607801625">"NFC ಆಫ್ ಆಗಿರುವ ಕಾರಣ ಲಭ್ಯವಿಲ್ಲ"</string>
<string name="nfc_and_payment_settings_no_payment_installed_summary" msgid="4879818114908207465">"ಬಳಸಲು, ಮೊದಲು ಪಾವತಿ ಆ್ಯಪ್ ಅನ್ನು ಇನ್ಸ್ಟಾಲ್ ಮಾಡಿ"</string>
<string name="app_and_notification_dashboard_summary" msgid="8047683010984186106">"ಇತ್ತೀಚಿನ ಆ್ಯಪ್ಗಳು, ಡೀಫಾಲ್ಟ್ ಆ್ಯಪ್ಗಳು"</string>
- <string name="notification_settings_work_profile" msgid="6076211850526353975">"ಕೆಲಸದ ಪ್ರೊಫೈಲ್ನಲ್ಲಿರುವ ಅಪ್ಲಿಕೇಶನ್ಗಳಿಗೆ ಅಧಿಸೂಚನೆ ಪ್ರವೇಶ ಲಭ್ಯವಿಲ್ಲ."</string>
+ <string name="notification_settings_work_profile" msgid="6076211850526353975">"ಕೆಲಸದ ಪ್ರೊಫೈಲ್ನಲ್ಲಿರುವ ಅಪ್ಲಿಕೇಶನ್ಗಳಿಗೆ ಅಧಿಸೂಚನೆ ಆ್ಯಕ್ಸೆಸ್ ಲಭ್ಯವಿಲ್ಲ."</string>
<string name="account_dashboard_title" msgid="8228773251948253914">"ಪಾಸ್ವರ್ಡ್ಗಳು ಮತ್ತು ಖಾತೆಗಳು"</string>
<string name="account_dashboard_default_summary" msgid="1730719656099599488">"ಉಳಿಸಲಾದ ಪಾಸ್ವರ್ಡ್ಗಳು, ಸ್ವಯಂ ಭರ್ತಿ, ಸಿಂಕ್ ಮಾಡಿದ ಖಾತೆಗಳು"</string>
<string name="app_default_dashboard_title" msgid="4071015747629103216">"ಡಿಫಾಲ್ಟ್ ಅಪ್ಲಿಕೇಶನ್ಗಳು"</string>
@@ -3254,7 +3254,7 @@
<string name="nls_feature_reply_summary" msgid="4492543411395565556">"ಇದು ಸಂದೇಶಗಳಿಗೆ ಪ್ರತ್ಯುತ್ತರ ನೀಡಬಹುದು ಮತ್ತು ಅಧಿಸೂಚನೆಗಳನ್ನು ಸ್ನೂಜ್ ಮಾಡುವುದು ಅಥವಾ ವಜಾಗೊಳಿಸುವುದು ಮತ್ತು ಕರೆಗಳಿಗೆ ಉತ್ತರಿಸುವುದು ಸೇರಿದಂತೆ ಅಧಿಸೂಚನೆಗಳಲ್ಲಿನ ಬಟನ್ಗಳ ಮೇಲೆ ಕ್ರಮ ತೆಗೆದುಕೊಳ್ಳಬಹುದು."</string>
<string name="nls_feature_settings_title" msgid="8208164329853194414">"ಸೆಟ್ಟಿಂಗ್ಗಳನ್ನು ಬದಲಾಯಿಸಿ"</string>
<string name="nls_feature_settings_summary" msgid="3770028705648985689">"ಇದು, ಅಡಚಣೆ ಮಾಡಬೇಡಿ ಫೀಚರ್ ಅನ್ನು ಆನ್ ಅಥವಾ ಆಫ್ ಮಾಡುವ ಮತ್ತು ಸಂಬಂಧಿತ ಸೆಟ್ಟಿಂಗ್ಗಳನ್ನು ಬದಲಾಯಿಸುವ ಸಾಮರ್ಥ್ಯವನ್ನು ಹೊಂದಿದೆ."</string>
- <string name="notification_listener_disable_warning_summary" msgid="8373396293802088961">"<xliff:g id="NOTIFICATION_LISTENER_NAME">%1$s</xliff:g> ಗೆ ನೀವು ಅಧಿಸೂಚನೆ ಪ್ರವೇಶವನ್ನು ಆಫ್ ಮಾಡಿದರೆ, ಅಡಚಣೆ ಮಾಡಬೇಡಿ ಪ್ರವೇಶ ಸಹ ಆಫ್ ಆಗಬಹುದು."</string>
+ <string name="notification_listener_disable_warning_summary" msgid="8373396293802088961">"<xliff:g id="NOTIFICATION_LISTENER_NAME">%1$s</xliff:g> ಗೆ ನೀವು ಅಧಿಸೂಚನೆ ಆ್ಯಕ್ಸೆಸ್ ಅನ್ನು ಆಫ್ ಮಾಡಿದರೆ, ಅಡಚಣೆ ಮಾಡಬೇಡಿ ಆ್ಯಕ್ಸೆಸ್ ಸಹ ಆಫ್ ಆಗಬಹುದು."</string>
<string name="notification_listener_disable_warning_confirm" msgid="841492108402184976">"ಆಫ್ ಮಾಡಿ"</string>
<string name="notification_listener_disable_warning_cancel" msgid="8802784105045594324">"ರದ್ದು ಮಾಡಿ"</string>
<string name="notif_type_ongoing" msgid="135675014223627555">"ನೈಜ ಸಮಯ"</string>
@@ -3559,7 +3559,7 @@
<string name="default_phone_title" msgid="7616730756650803827">"ಫೋನ್ ಅಪ್ಲಿಕೇಶನ್"</string>
<string name="system_app" msgid="1863291702508355041">"(ಸಿಸ್ಟಂ)"</string>
<string name="apps_storage" msgid="643866814746927111">"ಆ್ಯಪ್ಗಳ ಸಂಗ್ರಹ"</string>
- <string name="usage_access" msgid="5487993885373893282">"ಬಳಕೆ ಪ್ರವೇಶ"</string>
+ <string name="usage_access" msgid="5487993885373893282">"ಬಳಕೆ ಆ್ಯಕ್ಸೆಸ್"</string>
<string name="permit_usage_access" msgid="179630895262172674">"ಬಳಕೆ ಪ್ರವೇಶವನ್ನು ಅನುಮತಿಸಿ"</string>
<string name="time_spent_in_app_pref_title" msgid="25327097913383330">"ವೀಕ್ಷಣಾ ಅವಧಿ"</string>
<string name="usage_access_description" msgid="8547716253713890707">"ಬಳಕೆ ಪ್ರವೇಶವು ನೀವು ಇತರ ಯಾವ ಅಪ್ಲಿಕೇಶನ್ಗಳನ್ನು ಬಳಸುತ್ತಿರುವಿರಿ ಮತ್ತು ಎಷ್ಟು ಬಾರಿ ಎಂಬುದನ್ನು ಅಲ್ಲದೆ ನಿಮ್ಮ ವಾಹಕ, ಭಾಷೆ ಸೆಟ್ಟಿಂಗ್ಗಳು ಹಾಗೂ ಇತರ ವಿವರಗಳನ್ನು ಟ್ರ್ಯಾಕ್ ಮಾಡಲು ಅನುಮತಿಸುತ್ತದೆ."</string>
@@ -3623,7 +3623,7 @@
<string name="assist_access_screenshot_title" msgid="4395902231753643633">"ಸ್ಕ್ರೀನ್ಶಾಟ್ ಬಳಸಿ"</string>
<string name="assist_access_screenshot_summary" msgid="5276593070956201863">"ಸಹಾಯಕ ಅಪ್ಲಿಕೇಶನ್ಗೆ ಪರದೆಯ ಚಿತ್ರವನ್ನು ಪ್ರವೇಶಿಸಲು ಅನುಮತಿಸಿ"</string>
<string name="assist_flash_title" msgid="5449512572885550108">"ಫ್ಲ್ಯಾಶ್ ಪರದೆ"</string>
- <string name="assist_flash_summary" msgid="3032289860177784594">"ಪರದೆ ಅಥವಾ ಸ್ಕ್ರೀನ್ಶಾಟ್ನಿಂದ ಪಠ್ಯವನ್ನು ಸಹಾಯ ಅಪ್ಲಿಕೇಶನ್ ಪ್ರವೇಶ ಮಾಡಿದಾಗ ಪರದೆಯ ಅಂಚುಗಳು ಫ್ಲ್ಯಾಶ್ ಆಗುವುದು"</string>
+ <string name="assist_flash_summary" msgid="3032289860177784594">"ಪರದೆ ಅಥವಾ ಸ್ಕ್ರೀನ್ಶಾಟ್ನಿಂದ ಪಠ್ಯವನ್ನು ಸಹಾಯ ಅಪ್ಲಿಕೇಶನ್ ಆ್ಯಕ್ಸೆಸ್ ಮಾಡಿದಾಗ ಪರದೆಯ ಅಂಚುಗಳು ಫ್ಲ್ಯಾಶ್ ಆಗುವುದು"</string>
<string name="assist_footer" msgid="8248015363806299068">"ನೀವು ವೀಕ್ಷಿಸುತ್ತಿರುವ ಪರದೆಯ ಮಾಹಿತಿಯನ್ನು ಆಧರಿಸಿ ಸಹಾಯಕ ಅಪ್ಲಿಕೇಶನ್ಗಳು ನಿಮಗೆ ಸಹಾಯ ಮಾಡಬಹುದು. ನಿಮಗೆ ಸಂಪೂರ್ಣ ಸಹಾಯ ನೀಡಲು ಕೆಲವು ಅಪ್ಲಿಕೇಶನ್ಗಳು ಲಾಂಚರ್ ಮತ್ತು ಧ್ವನಿ ಇನ್ಪುಟ್ ಸೇವೆ ಎರಡನ್ನೂ ಬೆಂಬಲಿಸುತ್ತವೆ."</string>
<string name="average_memory_use" msgid="717313706368825388">"ಸರಾಸರಿ ಮೆಮೊರಿ ಬಳಕೆ"</string>
<string name="maximum_memory_use" msgid="2171779724001152933">"ಗರಿಷ್ಠ ಮೆಮೊರಿ ಬಳಕೆ"</string>
@@ -3643,7 +3643,7 @@
<string name="running_frequency" msgid="7260225121706316639">"ಫ್ರೀಕ್ವೆನ್ಸಿ"</string>
<string name="memory_maximum_usage" msgid="2047013391595835607">"ಗರಿಷ್ಠ ಬಳಕೆ"</string>
<string name="no_data_usage" msgid="4665617440434654132">"ಡೇಟಾ ಬಳಸಲಾಗಿಲ್ಲ"</string>
- <string name="zen_access_warning_dialog_title" msgid="6323325813123130154">"<xliff:g id="APP">%1$s</xliff:g> ಗೆ ಅಡಚಣೆ ಮಾಡಬೇಡಿಗೆ ಪ್ರವೇಶ ಅನುಮತಿಸುವುದೇ?"</string>
+ <string name="zen_access_warning_dialog_title" msgid="6323325813123130154">"<xliff:g id="APP">%1$s</xliff:g> ಗೆ ಅಡಚಣೆ ಮಾಡಬೇಡಿಗೆ ಆ್ಯಕ್ಸೆಸ್ ಅನುಮತಿಸುವುದೇ?"</string>
<string name="zen_access_warning_dialog_summary" msgid="8468714854067428987">"ಅಪ್ಲಿಕೇಶನ್ಗೆ ಅಡಚಣೆ ಮಾಡಬೇಡಿ ಆನ್/ಆಫ್ ಮಾಡಲು ಹಾಗೂ ಸಂಬಂಧಿಸಿದ ಸೆಟ್ಟಿಂಗ್ಗಳಿಗೆ ಬದಲಾವಣೆಗಳನ್ನು ಮಾಡಲು ಸಾಧ್ಯವಾಗುತ್ತದೆ."</string>
<string name="zen_access_disabled_package_warning" msgid="6565908224294537889">"ಅಧಿಸೂಚನೆ ಪ್ರವೇಶಗಳು ಆನ್ ಆಗಿರುವ ಕಾರಣ ಕಡ್ಡಾಯವಾಗಿ ಆನ್ ಆಗಿ ಉಳಿಯಬೇಕು"</string>
<string name="zen_access_revoke_warning_dialog_title" msgid="7377261509261811449">"<xliff:g id="APP">%1$s</xliff:g> ಅಪ್ಲಿಕೇಶನ್ಗೆ ಅಡಚಣೆ ಮಾಡಬೇಡಿಗೆ ಪ್ರವೇಶವನ್ನು ಹಿಂತೆಗೆದುಕೊಳ್ಳುವುದೇ?"</string>
@@ -3658,7 +3658,7 @@
<string name="system_alert_window_settings" msgid="6458633954424862521">"ಇತರ ಆ್ಯಪ್ಗಳ ಮೇಲೆ ಪ್ರದರ್ಶಿಸಿ"</string>
<string name="permit_draw_overlay" msgid="4468994037192804075">"ಇತರ ಆ್ಯಪ್ ಮೇಲೆ ಡಿಸ್ಪ್ಲೇ ಮಾಡಲು ಅನುಮತಿಸಿ"</string>
<string name="allow_overlay_description" msgid="1607235723669496298">"ನೀವು ಬಳಸುತ್ತಿರುವ ಇತರ ಆ್ಯಪ್ಗಳ ಮೇಲೆ ಈ ಆ್ಯಪ್ ಅನ್ನು ಪ್ರದರ್ಶಿಸಲು ಅನುಮತಿ ನೀಡಿ. ಪರದೆಯಲ್ಲಿ ನೀವು ಎಲ್ಲಿ ಟ್ಯಾಪ್ ಮಾಡುತ್ತೀರಿ ಎಂಬುದನ್ನು ಅಥವಾ ಏನನ್ನು ಪ್ರದರ್ಶಿಸಲಾಗುತ್ತಿದೆ ಎಂಬುದನ್ನು ನೋಡಲು ಈ ಆ್ಯಪ್ಗೆ ಸಾಧ್ಯವಾಗುತ್ತದೆ."</string>
- <string name="manage_external_storage_title" msgid="8024521099838816100">"ಎಲ್ಲಾ ಫೈಲ್ಗಳ ಪ್ರವೇಶ"</string>
+ <string name="manage_external_storage_title" msgid="8024521099838816100">"ಎಲ್ಲಾ ಫೈಲ್ಗಳ ಆ್ಯಕ್ಸೆಸ್"</string>
<string name="permit_manage_external_storage" msgid="6928847280689401761">"ಎಲ್ಲಾ ಫೈಲ್ ನಿರ್ವಹಿಸಲು, ಪ್ರವೇಶಕ್ಕೆ ಅನುಮತಿಸಿ"</string>
<string name="allow_manage_external_storage_description" msgid="5707948153603253225">"ಈ ಸಾಧನ ಅಥವಾ ಕನೆಕ್ಟ್ ಮಾಡಿದ ಶೇಖರಣಾ ವಾಲ್ಯೂಮ್ಗಳಲ್ಲಿ ಎಲ್ಲಾ ಫೈಲ್ಗಳನ್ನು ಓದಲು, ಮಾರ್ಪಡಿಸಲು ಮತ್ತು ಅಳಿಸಲು ಈ ಆ್ಯಪ್ಗೆ ಅನುಮತಿಸಿ. ಅನುಮತಿಸಿದರೆ, ಬಳಕೆದಾರರ ಪೂರ್ವಾನುಮತಿ ಇಲ್ಲದೆ ಆ್ಯಪ್ಗಳು ಫೈಲ್ಗಳನ್ನು ಪ್ರವೇಶಿಸಬಹುದು."</string>
<string name="filter_manage_external_storage" msgid="6751640571715343804">"ಎಲ್ಲಾ ಫೈಲ್ಗಳನ್ನು ಪ್ರವೇಶಿಸಬಹುದು"</string>
@@ -3714,9 +3714,9 @@
<string name="disabled_by_policy_parental_consent" msgid="9166060049019018978">"ಈ ಸೆಟ್ಟಿಂಗ್ ಅನ್ನು ಬದಲಾಯಿಸಲು ಅನುಮತಿಸಲು ಫೋನ್ ಅನ್ನು ನಿಮ್ಮ ಪೋಷಕರಿಗೆ ಹಸ್ತಾಂತರಿಸಿ."</string>
<string name="default_admin_support_msg" msgid="8816296554831532033">"ಹೆಚ್ಚಿನ ಮಾಹಿತಿಗಾಗಿ, ನಿಮ್ಮ IT ನಿರ್ವಾಹಕರನ್ನು ಸಂಪರ್ಕಿಸಿ"</string>
<string name="admin_support_more_info" msgid="8407433155725898290">"ಇನ್ನಷ್ಟು ವಿವರಗಳು"</string>
- <string name="admin_profile_owner_message" msgid="8860709969532649195">"ನಿಮ್ಮ ನಿರ್ವಾಹಕರು ಸೆಟ್ಟಿಂಗ್ಗಳು, ಅನುಮತಿಗಳು, ಕಾರ್ಪೊರೇಟ್ ಪ್ರವೇಶ, ನೆಟ್ವರ್ಕ್ ಚಟುವಟಿಕೆ ಮತ್ತು ಸಾಧನದ ಸ್ಥಳ ಮಾಹಿತಿಯನ್ನು ಒಳಗೊಂಡಂತೆ ನಿಮ್ಮ ಕೆಲಸದ ಪ್ರೊಫೈಲ್ಗೆ ಸಂಬಂಧಿಸಿದ ಅಪ್ಲಿಕೇಶನ್ಗಳು ಮತ್ತು ಡೇಟಾವನ್ನು ಮೇಲ್ವಿಚಾರಣೆ ಮಾಡಬಹುದು ಮತ್ತು ನಿರ್ವಹಿಸಬಹುದು."</string>
- <string name="admin_profile_owner_user_message" msgid="4929926887231544950">"ನಿಮ್ಮ ನಿರ್ವಾಹಕರು ಸೆಟ್ಟಿಂಗ್ಗಳು, ಅನುಮತಿಗಳು, ಕಾರ್ಪೊರೇಟ್ ಪ್ರವೇಶ, ನೆಟ್ವರ್ಕ್ ಚಟುವಟಿಕೆ ಮತ್ತು ಸಾಧನದ ಸ್ಥಳ ಮಾಹಿತಿಯನ್ನು ಒಳಗೊಂಡಂತೆ ಈ ಬಳಕೆದಾರರಿಗೆ ಸಂಬಂಧಿಸಿದ ಅಪ್ಲಿಕೇಶನ್ಗಳು ಮತ್ತು ಡೇಟಾವನ್ನು ಮೇಲ್ವಿಚಾರಣೆ ಮಾಡಬಹುದು ಮತ್ತು ನಿರ್ವಹಿಸಬಹುದು."</string>
- <string name="admin_device_owner_message" msgid="5503131744126520590">"ನಿಮ್ಮ ನಿರ್ವಾಹಕರು ಸೆಟ್ಟಿಂಗ್ಗಳು, ಅನುಮತಿಗಳು, ಕಾರ್ಪೊರೇಟ್ ಪ್ರವೇಶ, ನೆಟ್ವರ್ಕ್ ಚಟುವಟಿಕೆ ಮತ್ತು ಸಾಧನದ ಸ್ಥಳ ಮಾಹಿತಿಯನ್ನು ಒಳಗೊಂಡಂತೆ ಈ ಸಾಧನಕ್ಕೆ ಸಂಬಂಧಿಸಿದ ಅಪ್ಲಿಕೇಶನ್ಗಳು ಮತ್ತು ಡೇಟಾವನ್ನು ಮೇಲ್ವಿಚಾರಣೆ ಮಾಡಬಹುದು ಮತ್ತು ನಿರ್ವಹಿಸಬಹುದು."</string>
+ <string name="admin_profile_owner_message" msgid="8860709969532649195">"ನಿಮ್ಮ ನಿರ್ವಾಹಕರು ಸೆಟ್ಟಿಂಗ್ಗಳು, ಅನುಮತಿಗಳು, ಕಾರ್ಪೊರೇಟ್ ಆ್ಯಕ್ಸೆಸ್, ನೆಟ್ವರ್ಕ್ ಚಟುವಟಿಕೆ ಮತ್ತು ಸಾಧನದ ಸ್ಥಳ ಮಾಹಿತಿಯನ್ನು ಒಳಗೊಂಡಂತೆ ನಿಮ್ಮ ಕೆಲಸದ ಪ್ರೊಫೈಲ್ಗೆ ಸಂಬಂಧಿಸಿದ ಅಪ್ಲಿಕೇಶನ್ಗಳು ಮತ್ತು ಡೇಟಾವನ್ನು ಮೇಲ್ವಿಚಾರಣೆ ಮಾಡಬಹುದು ಮತ್ತು ನಿರ್ವಹಿಸಬಹುದು."</string>
+ <string name="admin_profile_owner_user_message" msgid="4929926887231544950">"ನಿಮ್ಮ ನಿರ್ವಾಹಕರು ಸೆಟ್ಟಿಂಗ್ಗಳು, ಅನುಮತಿಗಳು, ಕಾರ್ಪೊರೇಟ್ ಆ್ಯಕ್ಸೆಸ್, ನೆಟ್ವರ್ಕ್ ಚಟುವಟಿಕೆ ಮತ್ತು ಸಾಧನದ ಸ್ಥಳ ಮಾಹಿತಿಯನ್ನು ಒಳಗೊಂಡಂತೆ ಈ ಬಳಕೆದಾರರಿಗೆ ಸಂಬಂಧಿಸಿದ ಅಪ್ಲಿಕೇಶನ್ಗಳು ಮತ್ತು ಡೇಟಾವನ್ನು ಮೇಲ್ವಿಚಾರಣೆ ಮಾಡಬಹುದು ಮತ್ತು ನಿರ್ವಹಿಸಬಹುದು."</string>
+ <string name="admin_device_owner_message" msgid="5503131744126520590">"ನಿಮ್ಮ ನಿರ್ವಾಹಕರು ಸೆಟ್ಟಿಂಗ್ಗಳು, ಅನುಮತಿಗಳು, ಕಾರ್ಪೊರೇಟ್ ಆ್ಯಕ್ಸೆಸ್, ನೆಟ್ವರ್ಕ್ ಚಟುವಟಿಕೆ ಮತ್ತು ಸಾಧನದ ಸ್ಥಳ ಮಾಹಿತಿಯನ್ನು ಒಳಗೊಂಡಂತೆ ಈ ಸಾಧನಕ್ಕೆ ಸಂಬಂಧಿಸಿದ ಅಪ್ಲಿಕೇಶನ್ಗಳು ಮತ್ತು ಡೇಟಾವನ್ನು ಮೇಲ್ವಿಚಾರಣೆ ಮಾಡಬಹುದು ಮತ್ತು ನಿರ್ವಹಿಸಬಹುದು."</string>
<string name="admin_financed_message" msgid="1156197630834947884">"ನಿಮ್ಮ ಸಾಧನ ನಿರ್ವಾಹಕರಿಗೆ ಈ ಸಾಧನದ ಜೊತೆಗೆ ಸಂಯೋಜಿತವಾಗಿರುವ ಡೇಟಾವನ್ನು ಪ್ರವೇಶಿಸಲು, ಆ್ಯಪ್ಗಳನ್ನು ನಿರ್ವಹಿಸಲು ಮತ್ತು ಈ ಸಾಧನಗಳ ಸೆಟ್ಟಿಂಗ್ಗಳನ್ನು ಬದಲಾಯಿಸಲು ಸಾಧ್ಯವಾಗುತ್ತದೆ."</string>
<string name="condition_turn_off" msgid="402707350778441939">"ಆಫ್ ಮಾಡಿ"</string>
<string name="condition_turn_on" msgid="3911077299444314791">"ಆನ್ ಮಾಡಿ"</string>
@@ -3849,7 +3849,7 @@
<string name="display_cutout_emulation_keywords" msgid="4506580703807358127">"ಕಟ್ಔಟ್ ಪ್ರದರ್ಶನ, ನಾಚ್"</string>
<string name="overlay_option_device_default" msgid="7986355499809313848">"ಸಾಧನದ ಡೀಫಾಲ್ಟ್"</string>
<string name="overlay_toast_failed_to_apply" msgid="4839587811338164960">"ಓವರ್ಲೇ ಅನ್ನು ಅನ್ವಯಿಸಲು ವಿಫಲವಾಗಿದೆ"</string>
- <string name="special_access" msgid="1767980727423395147">"ವಿಶೇಷ ಆ್ಯಪ್ ಪ್ರವೇಶ"</string>
+ <string name="special_access" msgid="1767980727423395147">"ವಿಶೇಷ ಆ್ಯಪ್ ಆ್ಯಕ್ಸೆಸ್"</string>
<plurals name="special_access_summary" formatted="false" msgid="4995506406763570815">
<item quantity="one"><xliff:g id="COUNT">%d</xliff:g> ಅಪ್ಲಿಕೇಶನ್ಗಳು, ಅನಿರ್ಬಂಧಿತ ಡೇಟಾ ಬಳಸಬಹುದು</item>
<item quantity="other"><xliff:g id="COUNT">%d</xliff:g> ಅಪ್ಲಿಕೇಶನ್ಗಳು, ಅನಿರ್ಬಂಧಿತ ಡೇಟಾ ಬಳಸಬಹುದು</item>
@@ -3868,7 +3868,7 @@
<string name="developer_smallest_width" msgid="632354817870920911">"ಚಿಕ್ಕದಾದ ಅಗಲ"</string>
<string name="premium_sms_none" msgid="8737045049886416739">"ಯಾವುದೇ ಇನ್ಸ್ಟಾಲ್ ಮಾಡಿದ ಆ್ಯಪ್ಗಳು ಪ್ರೀಮಿಯಂ SMS ಪ್ರವೇಶವನ್ನು ವಿನಂತಿಸಿಲ್ಲ"</string>
<string name="premium_sms_warning" msgid="2192300872411073324">"ಪ್ರೀಮಿಯಂ SMS ನಿಮ್ಮ ವೆಚ್ಚಗಳಿಗೆ ಕಾರಣವಾಗಬಹುದು ಮತ್ತು ಇದನ್ನು ನಿಮ್ಮ ವಾಹಕ ಬಿಲ್ಗಳಿಗೆ ಸೇರಿಸಲಾಗುತ್ತದೆ. ನೀವು ಅಪ್ಲಿಕೇಶನ್ಗೆ ಅನುಮತಿಯನ್ನು ಸಕ್ರೀಯಗೊಳಿಸಿದರೆ, ನೀವು ಆ ಅಪ್ಲಿಕೇಶನ್ ಬಳಸಿಕೊಂಡು ಪ್ರೀಮಿಯಂ SMS ಕಳುಹಿಸಲು ಸಾಧ್ಯವಾಗುತ್ತದೆ."</string>
- <string name="premium_sms_access" msgid="5605970342699013212">"ಪ್ರೀಮಿಯಂ SMS ಪ್ರವೇಶ"</string>
+ <string name="premium_sms_access" msgid="5605970342699013212">"ಪ್ರೀಮಿಯಂ SMS ಆ್ಯಕ್ಸೆಸ್"</string>
<string name="bluetooth_disabled" msgid="835838280837359514">"ಆಫ್"</string>
<string name="bluetooth_connected_summary" msgid="8043167194934315712">"<xliff:g id="ID_1">%1$s</xliff:g> ಗೆ ಸಂಪರ್ಕಿಸಲಾಗಿದೆ"</string>
<string name="bluetooth_connected_multiple_devices_summary" msgid="2294954614327771844">"ಹಲವು ಸಾಧನಗಳಿಗೆ ಸಂಪರ್ಕಿಸಲಾಗಿದೆ"</string>
@@ -3976,10 +3976,10 @@
<string name="enterprise_privacy_settings" msgid="786350385374794180">"ನಿರ್ವಹಿಸುವ ಸಾಧನದ ಮಾಹಿತಿ"</string>
<string name="enterprise_privacy_settings_summary_generic" msgid="5471858290610344646">"ನಿಮ್ಮ ಸಂಸ್ಥೆಯು ನಿರ್ವಹಿಸುವ ಬದಲಾವಣೆಗಳು ಮತ್ತು ಸೆಟ್ಟಿಂಗ್ಗಳು"</string>
<string name="enterprise_privacy_settings_summary_with_name" msgid="1315413275836515937">"<xliff:g id="ORGANIZATION_NAME">%s</xliff:g> ನಿರ್ವಹಿಸುವ ಬದಲಾವಣೆಗಳು ಮತ್ತು ಸೆಟ್ಟಿಂಗ್ಗಳು"</string>
- <string name="enterprise_privacy_header" msgid="4626225398848641603">"ನಿಮ್ಮ ಕೆಲಸದ ಡೇಟಾಗೆ ಪ್ರವೇಶ ಒದಗಿಸಲು, ನಿಮ್ಮ ಸಂಸ್ಥೆಯು ನಿಮ್ಮ ಸಾಧನದಲ್ಲಿ ಸೆಟ್ಟಿಂಗ್ಗಳನ್ನು ಬದಲಾಯಿಸಬಹುದು ಮತ್ತು ಸಾಫ್ಟ್ವೇರ್ ಸ್ಥಾಪಿಸಬಹುದು.\n\nಹೆಚ್ಚಿನ ವಿವರಗಳಿಗಾಗಿ, ನಿಮ್ಮ ಸಂಸ್ಥೆಯ ನಿರ್ವಾಹಕರನ್ನು ಸಂಪರ್ಕಿಸಿ."</string>
+ <string name="enterprise_privacy_header" msgid="4626225398848641603">"ನಿಮ್ಮ ಕೆಲಸದ ಡೇಟಾಗೆ ಆ್ಯಕ್ಸೆಸ್ ಒದಗಿಸಲು, ನಿಮ್ಮ ಸಂಸ್ಥೆಯು ನಿಮ್ಮ ಸಾಧನದಲ್ಲಿ ಸೆಟ್ಟಿಂಗ್ಗಳನ್ನು ಬದಲಾಯಿಸಬಹುದು ಮತ್ತು ಸಾಫ್ಟ್ವೇರ್ ಸ್ಥಾಪಿಸಬಹುದು.\n\nಹೆಚ್ಚಿನ ವಿವರಗಳಿಗಾಗಿ, ನಿಮ್ಮ ಸಂಸ್ಥೆಯ ನಿರ್ವಾಹಕರನ್ನು ಸಂಪರ್ಕಿಸಿ."</string>
<string name="enterprise_privacy_exposure_category" msgid="2507761423540037308">"ನಿಮ್ಮ ಸಂಸ್ಥೆಯು ನೋಡಬಹುದಾದ ಮಾಹಿತಿಯ ವಿಧಗಳು"</string>
<string name="enterprise_privacy_exposure_changes_category" msgid="5459989751333816587">"ನಿಮ್ಮ ಸಂಸ್ಥೆಯ ನಿರ್ವಾಹಕರು ಮಾಡಿರುವ ಬದಲಾವಣೆಗಳು"</string>
- <string name="enterprise_privacy_device_access_category" msgid="140157499478630004">"ಈ ಸಾಧನಕ್ಕೆ ನಿಮ್ಮ ಪ್ರವೇಶ"</string>
+ <string name="enterprise_privacy_device_access_category" msgid="140157499478630004">"ಈ ಸಾಧನಕ್ಕೆ ನಿಮ್ಮ ಆ್ಯಕ್ಸೆಸ್"</string>
<string name="enterprise_privacy_enterprise_data" msgid="3963070078195245028">"ಇಮೇಲ್ ಮತ್ತು ಕ್ಯಾಲೆಂಡರ್ನಂತಹ ನಿಮ್ಮ ಕೆಲಸದ ಖಾತೆ ಜೊತೆಗೆ ಸಂಯೋಜಿತವಾಗಿರುವ ಡೇಟಾ"</string>
<string name="enterprise_privacy_installed_packages" msgid="6707006112254572820">"ನಿಮ್ಮ ಸಾಧನದಲ್ಲಿರುವ ಅಪ್ಲಿಕೇಶನ್ಗಳ ಪಟ್ಟಿ"</string>
<string name="enterprise_privacy_usage_stats" msgid="6328506963853465534">"ಪ್ರತಿ ಅಪ್ಲಿಕೇಶನ್ನಲ್ಲಿ ವ್ಯಯಿಸಿದ ಸಮಯ ಮತ್ತು ದಿನಾಂಕ"</string>
diff --git a/res/values-lt/strings.xml b/res/values-lt/strings.xml
index d6e323f..8d10452 100644
--- a/res/values-lt/strings.xml
+++ b/res/values-lt/strings.xml
@@ -909,10 +909,8 @@
<skip />
<string name="wifi_hotspot_speed_summary_unavailable" msgid="7276080644693388756">"Nepasiekiama jūsų šalyje ar regione"</string>
<string name="wifi_hotspot_speed_footer" msgid="8846939503916795002">"Jei pageidaujamas dažnis nepasiekiamas, viešosios interneto prieigos taškas gali naudoti kitą dažnį. Pakeitus dažnį gali pasikeisti viešosios interneto prieigos taško saugos nustatymai."</string>
- <!-- no translation found for wifi_hotspot_security_summary_unavailable (117582979310345853) -->
- <skip />
- <!-- no translation found for wifi_hotspot_security_footer (4608329688744949796) -->
- <skip />
+ <string name="wifi_hotspot_security_summary_unavailable" msgid="117582979310345853">"Nepasiekiama su 6 GHz"</string>
+ <string name="wifi_hotspot_security_footer" msgid="4608329688744949796">"Saugos nustatymai gali pasikeisti, jei pakeisite viešosios interneto prieigos taško dažnį"</string>
<string name="wifi_tether_starting" msgid="8879874184033857814">"Įjungiamas viešosios interneto prieigos taškas…"</string>
<string name="wifi_tether_stopping" msgid="4416492968019409188">"Išjungiamas viešosios interneto prieigos taškas…"</string>
<string name="wifi_tether_carrier_unsupport_dialog_title" msgid="3089432578433978073">"Įrenginio kaip modemo naudojimas nepasiekiamas"</string>
@@ -2705,14 +2703,12 @@
<string name="enable_guest_calling" msgid="8300355036005240911">"Leidimas svečiui naudoti telefoną"</string>
<string name="enable_guest_calling_summary" msgid="4748224917641204782">"Skambučių istorija bus bendrinama su naudotoju svečiu"</string>
<string name="user_enable_calling_sms" msgid="8546430559552381324">"Įjungti telefono skambučius ir SMS"</string>
- <!-- no translation found for user_grant_admin (5942118263054572074) -->
- <skip />
+ <string name="user_grant_admin" msgid="5942118263054572074">"Nustatyti šį naudotoją kaip administratorių"</string>
<string name="user_remove_user" msgid="8468203789739693845">"Naudotojo ištrynimas"</string>
<string name="user_enable_calling_and_sms_confirm_title" msgid="4041510268838725520">"Įjungti telefono skambučius ir SMS?"</string>
<string name="user_enable_calling_and_sms_confirm_message" msgid="367792286597449922">"Skambučių ir SMS istorija bus bendrinama su šiuo naudotoju."</string>
<string name="user_revoke_admin_confirm_title" msgid="3057842401861731863">"Pašalinti administratoriaus privilegijas?"</string>
- <!-- no translation found for user_revoke_admin_confirm_message (9207187319308572958) -->
- <skip />
+ <string name="user_revoke_admin_confirm_message" msgid="9207187319308572958">"Jei pašalinsite šio naudotojo administratoriaus privilegijas, jūs ar kitas administratorius galės jas vėl suteikti vėliau."</string>
<string name="emergency_info_title" msgid="8233682750953695582">"Kritinės padėties informacija"</string>
<string name="emergency_info_summary" msgid="8463622253016757697">"<xliff:g id="USER_NAME">%1$s</xliff:g> informacija ir kontaktai"</string>
<string name="open_app_button" msgid="5025229765547191710">"Atidaryti „<xliff:g id="APP_NAME">%1$s</xliff:g>“"</string>
diff --git a/res/values-ml/strings.xml b/res/values-ml/strings.xml
index cfd4b53..79c1db0 100644
--- a/res/values-ml/strings.xml
+++ b/res/values-ml/strings.xml
@@ -3244,11 +3244,11 @@
<string name="notification_listener_security_warning_title" msgid="5791700876622858363">"<xliff:g id="SERVICE">%1$s</xliff:g> എന്നതിന് അറിയിപ്പ് ആക്സസ് അനുവദിക്കണോ?"</string>
<string name="notification_listener_security_warning_summary" msgid="1131986567509818121">"കോൺടാക്റ്റ് പേരുകൾ, ഫോട്ടോകൾ, നിങ്ങൾക്ക് ലഭിക്കുന്ന സന്ദേശങ്ങളുടെ വാചകം എന്നിവ പോലുള്ള വ്യക്തിപരമായ വിവരങ്ങൾ ഉൾപ്പെടെ എല്ലാ അറിയിപ്പുകളും <xliff:g id="NOTIFICATION_LISTENER_NAME">%1$s</xliff:g> എന്ന ആപ്പിന് വായിക്കാൻ കഴിയും. അറിയിപ്പുകൾ ഡിസ്മിസ് ചെയ്യാനും സ്നൂസ് ചെയ്യാനും ഫോൺ കോളുകൾക്ക് മറുപടി നൽകുന്നതുൾപ്പെടെ അറിയിപ്പുകളിലെ ബട്ടണുകളിൽ നടപടിയെടുക്കാനും ഈ ആപ്പിന് കഴിയും. \n\n എന്ന ആപ്പിന് \'ശല്യപ്പെടുത്തരുത്\' ഓൺ അല്ലെങ്കിൽ ഓഫ് ആക്കാനും ബന്ധപ്പെട്ട ക്രമീകരണം മാറ്റാനുമുള്ള ശേഷിയും ഇത് നൽകും."</string>
<string name="nls_warning_prompt" msgid="1486887096703743841">"<xliff:g id="NOTIFICATION_LISTENER_NAME">%1$s</xliff:g> എന്ന ആപ്പിന് ഇനിപറയുന്നവ ചെയ്യാനാകും:"</string>
- <string name="nls_feature_read_title" msgid="7629713268744220437">"നിങ്ങളുടെ അറിയിപ്പുകൾ വായിക്കുക"</string>
+ <string name="nls_feature_read_title" msgid="7629713268744220437">"നിങ്ങളുടെ അറിയിപ്പുകൾ വായിക്കാം"</string>
<string name="nls_feature_read_summary" msgid="1064698238110273593">"നിങ്ങളുടെ കോൺടാക്റ്റുകൾ, സന്ദേശങ്ങൾ, ഫോട്ടോകൾ പോലുള്ള വ്യക്തിപരമായ വിവരങ്ങൾ ഉൾപ്പെടെയുള്ള അറിയിപ്പുകൾ ഇതിന് വായിക്കാൻ കഴിയും."</string>
- <string name="nls_feature_reply_title" msgid="7925455553821362039">"സന്ദേശങ്ങൾക്ക് മറുപടി നൽകുക"</string>
+ <string name="nls_feature_reply_title" msgid="7925455553821362039">"സന്ദേശങ്ങൾക്ക് മറുപടി നൽകാം"</string>
<string name="nls_feature_reply_summary" msgid="4492543411395565556">"സ്നൂസ് ചെയ്യൽ അല്ലെങ്കിൽ അറിയിപ്പുകൾ ഡിസ്മിസ് ചെയ്യൽ എന്നതും കോളുകൾക്ക് മറുപടി നൽകലും ഉൾപ്പെടെയുള്ള, അറിയിപ്പുകളിലെ ബട്ടണുകളിൽ നടപടിയെടുക്കാനും സന്ദേശങ്ങൾക്ക് മറുപടി നൽകാനും ഇതിന് കഴിയും."</string>
- <string name="nls_feature_settings_title" msgid="8208164329853194414">"ക്രമീകരണം മാറ്റുക"</string>
+ <string name="nls_feature_settings_title" msgid="8208164329853194414">"ക്രമീകരണം മാറ്റാം"</string>
<string name="nls_feature_settings_summary" msgid="3770028705648985689">"ഇതിന് \'ശല്യപ്പെടുത്തരുത്\' ഓൺ അല്ലെങ്കിൽ ഓഫ് ആക്കാനും ബന്ധപ്പെട്ട ക്രമീകരണം മാറ്റാനും കഴിയും."</string>
<string name="notification_listener_disable_warning_summary" msgid="8373396293802088961">"<xliff:g id="NOTIFICATION_LISTENER_NAME">%1$s</xliff:g> എന്നതിനായുള്ള അറിയിപ്പ് ആക്സസ് നിങ്ങൾ ഓഫാക്കുന്നുവെങ്കിൽ, \'ശല്യപ്പെടുത്തരുത്\' ആക്സസും ഓഫാക്കിയേക്കാം."</string>
<string name="notification_listener_disable_warning_confirm" msgid="841492108402184976">"ഓഫാക്കുക"</string>
@@ -3639,7 +3639,7 @@
<string name="running_frequency" msgid="7260225121706316639">"ഫ്രീക്വൻസി"</string>
<string name="memory_maximum_usage" msgid="2047013391595835607">"പരമാവധി ഉപയോഗം"</string>
<string name="no_data_usage" msgid="4665617440434654132">"ഡാറ്റയൊന്നും ഉപയോഗിച്ചില്ല"</string>
- <string name="zen_access_warning_dialog_title" msgid="6323325813123130154">"\'ശല്യപ്പെടുത്തരുത്\' എന്നതിലേക്ക് <xliff:g id="APP">%1$s</xliff:g>-ന് ആക്സസ് അനുവദിക്കണോ?"</string>
+ <string name="zen_access_warning_dialog_title" msgid="6323325813123130154">"\'ശല്യപ്പെടുത്തരുത്\' എന്നതിലേക്ക് <xliff:g id="APP">%1$s</xliff:g>-യ്ക്ക് ആക്സസ് അനുവദിക്കണോ?"</string>
<string name="zen_access_warning_dialog_summary" msgid="8468714854067428987">"\'ശല്യപ്പെടുത്തരുത്\' ഓൺ/ഓഫ് ആക്കാനും ബന്ധപ്പെട്ട ക്രമീകരണത്തിലേക്ക് മാറ്റങ്ങൾ വരുത്താനും ആപ്പിന് കഴിയും."</string>
<string name="zen_access_disabled_package_warning" msgid="6565908224294537889">"അറിയിപ്പ് ആക്സസ്സ് ഓണായിരിക്കുന്നതിനാൽ ഇതും ഓണായി തുടരണം"</string>
<string name="zen_access_revoke_warning_dialog_title" msgid="7377261509261811449">"\'ശല്യപ്പെടുത്തരുത്\' എന്നതിലേക്ക് <xliff:g id="APP">%1$s</xliff:g> ആപ്പിനുള്ള ആക്സസ് പിൻവലിക്കണോ?"</string>
diff --git a/res/values-mn/strings.xml b/res/values-mn/strings.xml
index a128813..295df75 100644
--- a/res/values-mn/strings.xml
+++ b/res/values-mn/strings.xml
@@ -909,10 +909,8 @@
<skip />
<string name="wifi_hotspot_speed_summary_unavailable" msgid="7276080644693388756">"Танай улс эсвэл бүс нутагт боломжгүй"</string>
<string name="wifi_hotspot_speed_footer" msgid="8846939503916795002">"Хэрэв таны сонгосон давтамж боломжгүй бол таны сүлжээний цэг өөрийг ашиглаж магадгүй. Хэрэв та давтамжийг өөрчилбөл сүлжээний цэгийн аюулгүй байдлын тохиргоо өөрчлөгдөж магадгүй."</string>
- <!-- no translation found for wifi_hotspot_security_summary_unavailable (117582979310345853) -->
- <skip />
- <!-- no translation found for wifi_hotspot_security_footer (4608329688744949796) -->
- <skip />
+ <string name="wifi_hotspot_security_summary_unavailable" msgid="117582979310345853">"6 ГГц-ээр боломжгүй"</string>
+ <string name="wifi_hotspot_security_footer" msgid="4608329688744949796">"Хэрэв та сүлжээний цэгийн давтамжийг өөрчилбөл аюулгүй байдлын тохиргоо өөрчлөгдөж магадгүй"</string>
<string name="wifi_tether_starting" msgid="8879874184033857814">"Сүлжээний цэгийг асааж байна…"</string>
<string name="wifi_tether_stopping" msgid="4416492968019409188">"Сүлжээний цэгийг унтрааж байна…"</string>
<string name="wifi_tether_carrier_unsupport_dialog_title" msgid="3089432578433978073">"Модем хийх боломжгүй байна"</string>
@@ -2705,14 +2703,12 @@
<string name="enable_guest_calling" msgid="8300355036005240911">"Зочинд утас ашиглахыг зөвшөөрөх"</string>
<string name="enable_guest_calling_summary" msgid="4748224917641204782">"Дуудлагын түүхийг зочин хэрэглэгчтэй хуваалцана"</string>
<string name="user_enable_calling_sms" msgid="8546430559552381324">"Утасны дуудлага & SMS авах"</string>
- <!-- no translation found for user_grant_admin (5942118263054572074) -->
- <skip />
+ <string name="user_grant_admin" msgid="5942118263054572074">"Энэ хэрэглэгчийг админ болгох"</string>
<string name="user_remove_user" msgid="8468203789739693845">"Хэрэглэгчийг устгах"</string>
<string name="user_enable_calling_and_sms_confirm_title" msgid="4041510268838725520">"Утасны дуудлага & SMS-г хүлээн авах уу?"</string>
<string name="user_enable_calling_and_sms_confirm_message" msgid="367792286597449922">"Энэ хэрэглэгчтэй утасны дуудлага болон SMS түүхийг хуваалцах болно."</string>
<string name="user_revoke_admin_confirm_title" msgid="3057842401861731863">"Админы эрхийг хасах уу?"</string>
- <!-- no translation found for user_revoke_admin_confirm_message (9207187319308572958) -->
- <skip />
+ <string name="user_revoke_admin_confirm_message" msgid="9207187319308572958">"Хэрэв та энэ хэрэглэгчийн админы эрхийг хасвал та эсвэл өөр админ түүнд дараа буцаан өгөх боломжтой."</string>
<string name="emergency_info_title" msgid="8233682750953695582">"Яаралтай тусламжийн мэдээлэл"</string>
<string name="emergency_info_summary" msgid="8463622253016757697">"<xliff:g id="USER_NAME">%1$s</xliff:g>-н мэдээлэл, харилцагч"</string>
<string name="open_app_button" msgid="5025229765547191710">"<xliff:g id="APP_NAME">%1$s</xliff:g>-г нээх"</string>
@@ -4653,10 +4649,8 @@
<string name="ingress_rate_limit_no_limit_entry" msgid="8741098826008012163">"Хязгааргүй"</string>
<string name="disable_phantom_process_monitor_title" msgid="8348108346706188771">"Дэд боловсруулалтын хязгаарлалтыг идэвхгүй болгох"</string>
<string name="disable_phantom_process_monitor_summary" msgid="3044464635550256985">"Аппын дэд боловсруулалтын системийн нөөцийн ашиглалтын хязгаарлалтыг идэвхгүй болгох"</string>
- <!-- no translation found for enable_notes_role_title (7662702013496114763) -->
- <skip />
- <!-- no translation found for enable_notes_role_summary (136916915155048249) -->
- <skip />
+ <string name="enable_notes_role_title" msgid="7662702013496114763">"Тэмдэглэлийн үүргийг хүчээр идэвхжүүлэх"</string>
+ <string name="enable_notes_role_summary" msgid="136916915155048249">"Тэмдэглэлийн үүргээр тэмдэглэл хөтлөх системийн нэгтгэлийг идэвхжүүлнэ үү. Хэрэв Тэмдэглэлийн үүргийг аль хэдийн идэвхжүүлсэн бол юу ч битгий хийгээрэй."</string>
<string name="bluetooth_broadcast_dialog_title" msgid="9172775308463135884">"Нэвтрүүлэлт"</string>
<string name="bluetooth_broadcast_dialog_broadcast_app" msgid="1016617579194329005">"<xliff:g id="CURRENTAPP">%1$s</xliff:g>-г нэвтрүүлэх"</string>
<string name="bluetooth_broadcast_dialog_find_message" msgid="6621660851669953883">"Таны ойролцоо тоглуулж буй нэвтрүүлэлтийг сонсоорой"</string>
diff --git a/res/values-mr/strings.xml b/res/values-mr/strings.xml
index 261f559..ca8f624 100644
--- a/res/values-mr/strings.xml
+++ b/res/values-mr/strings.xml
@@ -412,7 +412,7 @@
<string name="security_settings_fingerprint_enroll_start_message" msgid="5010227772754175346">"तुमचे बोट सेन्सरवर ठेवा आणि व्हायब्रेशन जाणवल्यावर काढा"</string>
<string name="security_settings_udfps_enroll_start_message" msgid="5032954588171487566">"तुम्हाला व्हायब्रेशन जाणवेपर्यंत तुमची फिंगरप्रिंट सेन्सरवर चपटी ठेवा"</string>
<string name="security_settings_sfps_enroll_start_message" msgid="9054672627477685212">"बटण न दाबता, तुम्हाला व्हायब्रेशन जाणवेपर्यंत तुमची फिंगरप्रिंट सेन्सरवर ठेवा.\n\nप्रत्येक वेळी तुमचे बोट किंचित हलवा. यामुळे तुमची फिंगरप्रिंट आणखी स्पष्टपणे कॅप्चर करण्यात मदत होते."</string>
- <string name="security_settings_fingerprint_enroll_udfps_title" msgid="6665610134560896895">"फिंगरप्रिंट सेन्सरला स्पर्श करा आणि धरून ठेवा"</string>
+ <string name="security_settings_fingerprint_enroll_udfps_title" msgid="6665610134560896895">"फिंगरप्रिंट सेन्सरला स्पर्श करून धरून ठेवा"</string>
<string name="security_settings_fingerprint_enroll_repeat_title" msgid="9172202128243545021">"उचला, नंतर पुन्हा स्पर्श करा"</string>
<string name="security_settings_udfps_enroll_title_one_more_time" msgid="424937043843482410">"आणखी एकदा"</string>
<string name="security_settings_udfps_enroll_repeat_title_touch_icon" msgid="4096344864386190335">"फिंगरप्रिंट आयकन फॉलो करा"</string>
diff --git a/res/values-pa/strings.xml b/res/values-pa/strings.xml
index 6a87fdd..ab94d3b 100644
--- a/res/values-pa/strings.xml
+++ b/res/values-pa/strings.xml
@@ -2317,8 +2317,8 @@
<string name="daily_battery_usage_chart" msgid="4176059567682992686">"ਰੋਜ਼ਾਨਾ ਬੈਟਰੀ ਵਰਤੋਂ ਚਾਰਟ"</string>
<string name="hourly_battery_usage_chart" msgid="3098314511076561272">"ਪ੍ਰਤੀ ਘੰਟਾ ਬੈਟਰੀ ਵਰਤੋਂ ਚਾਰਟ"</string>
<string name="battery_usage_breakdown_title_since_last_full_charge" msgid="435006273323199906">"ਪਿਛਲੀ ਵਾਰ ਪੂਰਾ ਚਾਰਜ ਕਰਨ ਤੋਂ ਬਾਅਦ ਬੈਟਰੀ ਵਰਤੋਂ"</string>
- <string name="battery_usage_breakdown_title_for_slot" msgid="4823179483667671406">"<xliff:g id="SLOT">%s</xliff:g> ਦੇ ਲਈ ਬੈਟਰੀ ਵਰਤੋਂ"</string>
- <string name="screen_time_category_last_full_charge" msgid="8856908320256057753">"ਪਿਛਲੀ ਵਾਰ ਪੂਰਾ ਚਾਰਜ ਕਰਨ ਤੋਂ ਬਾਅਦ ਵਰਤੋਂ ਸਕ੍ਰੀਨ ਸਮਾਂ"</string>
+ <string name="battery_usage_breakdown_title_for_slot" msgid="4823179483667671406">"<xliff:g id="SLOT">%s</xliff:g> ਲਈ ਬੈਟਰੀ ਵਰਤੋਂ"</string>
+ <string name="screen_time_category_last_full_charge" msgid="8856908320256057753">"ਪਿਛਲੀ ਵਾਰ ਪੂਰਾ ਚਾਰਜ ਕਰਨ ਤੋਂ ਬਾਅਦ ਸਕ੍ਰੀਨ ਸਮਾਂ"</string>
<string name="screen_time_category_for_slot" msgid="8287722270554654959">"<xliff:g id="SLOT">%s</xliff:g> ਲਈ ਸਕ੍ਰੀਨ ਸਮਾਂ"</string>
<string name="battery_usage_spinner_breakdown_by_apps" msgid="7746337368402445072">"ਐਪਾਂ ਮੁਤਾਬਕ ਵੰਡ"</string>
<string name="battery_usage_spinner_breakdown_by_system" msgid="4646952798665973464">"ਸਿਸਟਮ ਮੁਤਾਬਕ ਵੰਡ"</string>
diff --git a/res/values-pt-rBR/strings.xml b/res/values-pt-rBR/strings.xml
index 9a83d75..0af2507 100644
--- a/res/values-pt-rBR/strings.xml
+++ b/res/values-pt-rBR/strings.xml
@@ -3249,7 +3249,7 @@
<string name="nls_feature_reply_title" msgid="7925455553821362039">"Responder a mensagens"</string>
<string name="nls_feature_reply_summary" msgid="4492543411395565556">"Ele pode responder a mensagens e acionar botões nas notificações, como ativar a soneca, dispensar notificações ou atender ligações."</string>
<string name="nls_feature_settings_title" msgid="8208164329853194414">"Mudar configurações"</string>
- <string name="nls_feature_settings_summary" msgid="3770028705648985689">"Ele pode ativar ou ativar o modo Não perturbe e mudar as configurações relacionadas a ele."</string>
+ <string name="nls_feature_settings_summary" msgid="3770028705648985689">"Ele pode ativar e desativar o modo Não perturbe e mudar as configurações relacionadas a ele."</string>
<string name="notification_listener_disable_warning_summary" msgid="8373396293802088961">"Se você desativar o acesso a notificações para <xliff:g id="NOTIFICATION_LISTENER_NAME">%1$s</xliff:g>, é possível que o acesso ao Não perturbe também seja desativado."</string>
<string name="notification_listener_disable_warning_confirm" msgid="841492108402184976">"Desativar"</string>
<string name="notification_listener_disable_warning_cancel" msgid="8802784105045594324">"Cancelar"</string>
diff --git a/res/values-pt-rPT/strings.xml b/res/values-pt-rPT/strings.xml
index 296a1b2..e03d2b2 100644
--- a/res/values-pt-rPT/strings.xml
+++ b/res/values-pt-rPT/strings.xml
@@ -3118,7 +3118,7 @@
<string name="configure_notification_settings" msgid="1492820231694314376">"Notificações"</string>
<string name="notification_dashboard_summary" msgid="7530169251902320652">"Histórico de notificações, conversas"</string>
<string name="conversation_notifs_category" msgid="2549844862379963273">"Conversa"</string>
- <string name="general_notification_header" msgid="3669031068980713359">"Faça a gestão"</string>
+ <string name="general_notification_header" msgid="3669031068980713359">"Gerir"</string>
<string name="app_notification_field" msgid="3208079070539894909">"Notificações de apps"</string>
<string name="app_notification_field_summary" msgid="5981393613897713471">"Controle as notificações de apps individuais"</string>
<string name="advanced_section_header" msgid="6478709678084326738">"Geral"</string>
@@ -3134,7 +3134,7 @@
<string name="asst_importance_reset_summary" msgid="684794589254282667">"Reponha as definições de importância alteradas pelo utilizador e permita que o assistente de notificações defina prioridades."</string>
<string name="asst_capabilities_actions_replies_title" msgid="4392470465646394289">"Respostas e ações sugeridas"</string>
<string name="asst_capabilities_actions_replies_summary" msgid="416234323365645871">"São apresentadas automaticamente respostas e ações sugeridas."</string>
- <string name="notification_history_summary" msgid="5434741516307706892">"Apresentar notificações recentes e suspensas"</string>
+ <string name="notification_history_summary" msgid="5434741516307706892">"Veja as notificações recentes e suspensas"</string>
<string name="notification_history" msgid="8663811361243456201">"Histórico de notificações"</string>
<string name="notification_history_toggle" msgid="9093762294928569030">"Usar histórico de notificações"</string>
<string name="notification_history_off_title_extended" msgid="853807652537281601">"Histórico de notificações desativado"</string>
@@ -3148,7 +3148,7 @@
<string name="notification_bubbles_title" msgid="5681506665322329301">"Balões"</string>
<string name="bubbles_app_toggle_title" msgid="5319021259954576150">"Balões"</string>
<string name="bubbles_conversation_toggle_title" msgid="5225039214083311316">"Apresentar esta conversa como balão"</string>
- <string name="bubbles_conversation_toggle_summary" msgid="720229032254323578">"Mostrar ícone flutuante por cima das apps"</string>
+ <string name="bubbles_conversation_toggle_summary" msgid="720229032254323578">"Mostre ícone flutuante por cima das apps"</string>
<string name="bubbles_feature_disabled_dialog_title" msgid="1794193899792284007">"Ativar os balões para o dispositivo?"</string>
<string name="bubbles_feature_disabled_dialog_text" msgid="5275666953364031055">"A ativação dos balões para esta app também irá ativar os balões para o seu dispositivo.\n\nEsta ação afeta outras apps ou conversas que tenham autorização para aparecer em balões."</string>
<string name="bubbles_feature_disabled_button_approve" msgid="2042628067101419871">"Ativar"</string>
@@ -3177,13 +3177,13 @@
<string name="lock_screen_notifs_title" msgid="3412042692317304449">"Notificações no ecrã de bloqueio"</string>
<string name="lock_screen_notifs_show_all_summary" msgid="4226586018375762117">"Mostrar conversas predefinidas e silenciosas"</string>
<string name="lock_screen_notifs_show_all" msgid="1300418674456749664">"Mostrar conversas predefinidas e silenciosas"</string>
- <string name="lock_screen_notifs_show_alerting" msgid="6584682657382684566">"Ocultar notificações e conversas silenciosas"</string>
+ <string name="lock_screen_notifs_show_alerting" msgid="6584682657382684566">"Oculte notificações e conversas silenciosas"</string>
<string name="lock_screen_notifs_show_none" msgid="1941044980403067101">"Não mostrar notificações"</string>
<string name="lock_screen_notifs_redact" msgid="9024158855454642296">"Notificações confidenciais"</string>
<string name="lock_screen_notifs_redact_summary" msgid="1395483766035470612">"Mostrar conteúdo confidencial quando estiver bloqueado"</string>
<string name="lock_screen_notifs_redact_work" msgid="3833920196569208430">"Notificações confidenciais do perfil de trabalho"</string>
<string name="lock_screen_notifs_redact_work_summary" msgid="3238238380405430156">"Mostrar conteúdo confidencial do perfil de trabalho quando estiver bloqueado"</string>
- <string name="lock_screen_notifications_summary_show" msgid="6540443483088311328">"Mostrar os conteúdos de todas as notificações"</string>
+ <string name="lock_screen_notifications_summary_show" msgid="6540443483088311328">"Mostre conteúdos de todas as notificações"</string>
<string name="lock_screen_notifications_summary_hide" msgid="7837303171531166789">"Mostrar conteúdo confidencial apenas se desbloqueado"</string>
<string name="lock_screen_notifications_summary_disable" msgid="3388290397947365744">"Não mostrar notificações"</string>
<string name="lock_screen_notifications_interstitial_message" msgid="4688399629301178487">"Como quer que o ecrã de bloqueio seja apresentado?"</string>
@@ -3316,7 +3316,7 @@
<string name="app_notifications_not_send_desc" msgid="5683060986735070528">"Esta app não envia notificações"</string>
<string name="notification_channels" msgid="1502969522886493799">"Categorias"</string>
<string name="notification_channels_other" msgid="18159805343647908">"Outra"</string>
- <string name="no_channels" msgid="4716199078612071915">"Esta aplicação não publicou notificações"</string>
+ <string name="no_channels" msgid="4716199078612071915">"Esta app não publicou notificações"</string>
<string name="app_settings_link" msgid="6725453466705333311">"Definições adicionais na app"</string>
<string name="deleted_channels" msgid="8489800381509312964">"{count,plural, =1{# categoria apagada}other{# categorias apagadas}}"</string>
<string name="app_notification_block_title" msgid="3880322745749900296">"Bloquear tudo"</string>
diff --git a/res/values-pt/strings.xml b/res/values-pt/strings.xml
index 9a83d75..0af2507 100644
--- a/res/values-pt/strings.xml
+++ b/res/values-pt/strings.xml
@@ -3249,7 +3249,7 @@
<string name="nls_feature_reply_title" msgid="7925455553821362039">"Responder a mensagens"</string>
<string name="nls_feature_reply_summary" msgid="4492543411395565556">"Ele pode responder a mensagens e acionar botões nas notificações, como ativar a soneca, dispensar notificações ou atender ligações."</string>
<string name="nls_feature_settings_title" msgid="8208164329853194414">"Mudar configurações"</string>
- <string name="nls_feature_settings_summary" msgid="3770028705648985689">"Ele pode ativar ou ativar o modo Não perturbe e mudar as configurações relacionadas a ele."</string>
+ <string name="nls_feature_settings_summary" msgid="3770028705648985689">"Ele pode ativar e desativar o modo Não perturbe e mudar as configurações relacionadas a ele."</string>
<string name="notification_listener_disable_warning_summary" msgid="8373396293802088961">"Se você desativar o acesso a notificações para <xliff:g id="NOTIFICATION_LISTENER_NAME">%1$s</xliff:g>, é possível que o acesso ao Não perturbe também seja desativado."</string>
<string name="notification_listener_disable_warning_confirm" msgid="841492108402184976">"Desativar"</string>
<string name="notification_listener_disable_warning_cancel" msgid="8802784105045594324">"Cancelar"</string>
diff --git a/res/values-sl/strings.xml b/res/values-sl/strings.xml
index eae1931..3c9d068 100644
--- a/res/values-sl/strings.xml
+++ b/res/values-sl/strings.xml
@@ -4653,10 +4653,8 @@
<string name="ingress_rate_limit_no_limit_entry" msgid="8741098826008012163">"Brez omejitev"</string>
<string name="disable_phantom_process_monitor_title" msgid="8348108346706188771">"Onemogočanje omejitev za podrejene procese"</string>
<string name="disable_phantom_process_monitor_summary" msgid="3044464635550256985">"Onemogočanje omejitev uporabe sistemskih sredstev za podrejene procese aplikacije"</string>
- <!-- no translation found for enable_notes_role_title (7662702013496114763) -->
- <skip />
- <!-- no translation found for enable_notes_role_summary (136916915155048249) -->
- <skip />
+ <string name="enable_notes_role_title" msgid="7662702013496114763">"Vsiljenje omogočanja vloge »Zapiski«"</string>
+ <string name="enable_notes_role_summary" msgid="136916915155048249">"Omogočanje integracij sistema za ustvarjanje zapiskov prek vloge »Zapiski«. Če je vloga »Zapiski« že omogočena, se ne zgodi nič."</string>
<string name="bluetooth_broadcast_dialog_title" msgid="9172775308463135884">"Oddajanje"</string>
<string name="bluetooth_broadcast_dialog_broadcast_app" msgid="1016617579194329005">"Oddajaj aplikacijo <xliff:g id="CURRENTAPP">%1$s</xliff:g>"</string>
<string name="bluetooth_broadcast_dialog_find_message" msgid="6621660851669953883">"Poslušajte oddajanja, ki se predvajajo v bližini."</string>
diff --git a/res/values-sr/strings.xml b/res/values-sr/strings.xml
index 9cd9c36..cc3f51b 100644
--- a/res/values-sr/strings.xml
+++ b/res/values-sr/strings.xml
@@ -481,9 +481,9 @@
<string name="fingerprint_last_delete_message_profile_challenge" msgid="4104208067277655068">"Нећете моћи да користите отисак прста за откључавање пословног профила, овлашћивање куповина или пријављивање у пословне апликације."</string>
<string name="encryption_settings_title" msgid="2848716008695618360">"Шифровање"</string>
<string name="encrypted_summary" msgid="545623487587251207">"Шифровано"</string>
- <string name="no_screen_lock_issue_title" msgid="1814109590692792891">"Подесите закључавање екрана"</string>
+ <string name="no_screen_lock_issue_title" msgid="1814109590692792891">"Подесите откључавање екрана"</string>
<string name="no_screen_lock_issue_summary" msgid="2383217853510608406">"Да бисте побољшали безбедност, подесите PIN, шаблон или лозинку за овај уређај"</string>
- <string name="no_screen_lock_issue_action_label" msgid="2691229130486382863">"Подеси закључавање екрана"</string>
+ <string name="no_screen_lock_issue_action_label" msgid="2691229130486382863">"Подеси откључавање екрана"</string>
<string name="no_screen_lock_issue_notification_title" msgid="5718363966239208505">"Уређај нема закључавање екрана"</string>
<string name="no_screen_lock_issue_notification_text" msgid="8696194459170873345">"Да бисте побољшали безбедност, подесите PIN, шаблон или лозинку за овај уређај"</string>
<string name="suggested_lock_settings_title" msgid="7836065447159730217">"Заштитите телефон"</string>
@@ -4655,10 +4655,8 @@
<string name="ingress_rate_limit_no_limit_entry" msgid="8741098826008012163">"Без ограничења"</string>
<string name="disable_phantom_process_monitor_title" msgid="8348108346706188771">"Онемогући ограничења за подређене процесе"</string>
<string name="disable_phantom_process_monitor_summary" msgid="3044464635550256985">"Онемогућите ограничења коришћења ресурса система за подређене процесе апликације"</string>
- <!-- no translation found for enable_notes_role_title (7662702013496114763) -->
- <skip />
- <!-- no translation found for enable_notes_role_summary (136916915155048249) -->
- <skip />
+ <string name="enable_notes_role_title" msgid="7662702013496114763">"Принудно омогући улогу Белешке"</string>
+ <string name="enable_notes_role_summary" msgid="136916915155048249">"Омогућите интеграције прављења бележака у систему помоћу улоге Белешке. Ако је улога Белешке већ омогућена, не предузимајте ништа."</string>
<string name="bluetooth_broadcast_dialog_title" msgid="9172775308463135884">"Емитујте"</string>
<string name="bluetooth_broadcast_dialog_broadcast_app" msgid="1016617579194329005">"Емитујте <xliff:g id="CURRENTAPP">%1$s</xliff:g>"</string>
<string name="bluetooth_broadcast_dialog_find_message" msgid="6621660851669953883">"Слушајте емитовања која се пуштају у близини"</string>
diff --git a/res/values-te/strings.xml b/res/values-te/strings.xml
index 4cf790c..4ad13ce 100644
--- a/res/values-te/strings.xml
+++ b/res/values-te/strings.xml
@@ -3130,7 +3130,7 @@
<string name="asst_importance_reset_summary" msgid="684794589254282667">"యూజర్ మార్చిన ప్రాముఖ్యత సెట్టింగ్లను రీసెట్ చేయండి, నోటిఫికేషన్ అసిస్టెంట్ను ప్రాధాన్యత ఇవ్వడానికి అనుమతించండి"</string>
<string name="asst_capabilities_actions_replies_title" msgid="4392470465646394289">"సూచించబడిన చర్యలు, రిప్లయిలు"</string>
<string name="asst_capabilities_actions_replies_summary" msgid="416234323365645871">"సూచనాత్మక చర్యలను & రిప్లయిలను ఆటోమేటిక్గా చూపుతుంది"</string>
- <string name="notification_history_summary" msgid="5434741516307706892">"ఇటీవలి నోటిఫికేషన్లను, అలాగే స్నూజ్ చేసిన వాటిని చూడండి"</string>
+ <string name="notification_history_summary" msgid="5434741516307706892">"ఇటీవలి, అలాగే తాత్కాలికంగా వాయిదా వేసిన నోటిఫికేషన్లను చూడండి"</string>
<string name="notification_history" msgid="8663811361243456201">"నోటిఫికేషన్ హిస్టరీ"</string>
<string name="notification_history_toggle" msgid="9093762294928569030">"నోటిఫికేషన్ హిస్టరీ ఉపయోగించండి"</string>
<string name="notification_history_off_title_extended" msgid="853807652537281601">"నోటిఫికేషన్ హిస్టరీ ఆఫ్ చేయబడింది"</string>
@@ -3143,8 +3143,8 @@
<string name="notification_badging_title" msgid="5469616894819568917">"యాప్ చిహ్నంపై నోటిఫికేషన్ డాట్"</string>
<string name="notification_bubbles_title" msgid="5681506665322329301">"బబుల్స్"</string>
<string name="bubbles_app_toggle_title" msgid="5319021259954576150">"బబుల్స్"</string>
- <string name="bubbles_conversation_toggle_title" msgid="5225039214083311316">"ఈ సంభాషణను బబుల్ చేయి"</string>
- <string name="bubbles_conversation_toggle_summary" msgid="720229032254323578">"యాప్ల పైన తేలియాడే చిహ్నాన్ని చూపించు"</string>
+ <string name="bubbles_conversation_toggle_title" msgid="5225039214083311316">"ఈ సంభాషణను బబుల్ చేయండి"</string>
+ <string name="bubbles_conversation_toggle_summary" msgid="720229032254323578">"యాప్ల పైన తేలియాడే చిహ్నాన్ని చూపించండి"</string>
<string name="bubbles_feature_disabled_dialog_title" msgid="1794193899792284007">"పరికరానికి బబుల్స్ ఆన్ చేయాలా?"</string>
<string name="bubbles_feature_disabled_dialog_text" msgid="5275666953364031055">"ఈ యాప్నకు బబుల్స్ ఆన్ చేస్తే మీ పరికరానికి కూడా బబుల్స్ ఆన్ అవుతాయి.\n\nబబుల్ చేయడానికి అనుమతించబడిన ఇతర యాప్లు లేదా సంభాషణలను కూడా ఇది ప్రభావితం చేస్తుంది."</string>
<string name="bubbles_feature_disabled_button_approve" msgid="2042628067101419871">"ఆన్ చేయండి"</string>
@@ -3173,7 +3173,7 @@
<string name="lock_screen_notifs_title" msgid="3412042692317304449">"లాక్స్క్రీన్లో నోటిఫికేషన్లు"</string>
<string name="lock_screen_notifs_show_all_summary" msgid="4226586018375762117">"సంభాషణలు, ఆటోమేటిక్, నిశ్శబ్దం - ఈ నోటిఫికేషన్లను చూపండి"</string>
<string name="lock_screen_notifs_show_all" msgid="1300418674456749664">"సంభాషణలు, ఆటోమేటిక్, నిశ్శబ్దం - ఈ నోటిఫికేషన్లను చూపాలి"</string>
- <string name="lock_screen_notifs_show_alerting" msgid="6584682657382684566">"నిశ్శబ్ద సంభాషణలు, నోటిఫికేషన్లను దాచిపెట్టు"</string>
+ <string name="lock_screen_notifs_show_alerting" msgid="6584682657382684566">"నిశ్శబ్ద సంభాషణలు, నోటిఫికేషన్లను దాచిపెట్టండి"</string>
<string name="lock_screen_notifs_show_none" msgid="1941044980403067101">"ఏవిధమైన నోటిఫికేషన్లను చూపించవద్దు"</string>
<string name="lock_screen_notifs_redact" msgid="9024158855454642296">"గోప్యమైన నోటిఫికేషన్లు"</string>
<string name="lock_screen_notifs_redact_summary" msgid="1395483766035470612">"లాక్ అయినప్పుడు గోప్యమైన కంటెంట్ను చూపుతుంది"</string>
@@ -3211,8 +3211,8 @@
<string name="recent_convos_removed" msgid="2122932798895714203">"ఇటీవలి సంభాషణలు తీసివేయబడ్డాయి"</string>
<string name="recent_convo_removed" msgid="8686414146325958281">"సంభాషణ తీసివేయబడింది"</string>
<string name="clear" msgid="5092178335409471100">"క్లియర్ చేయండి"</string>
- <string name="conversation_onboarding_title" msgid="5194559958353468484">"ప్రముఖమైన, మార్పులు చేసిన సంభాషణలు ఇక్కడ కనిపిస్తాయి"</string>
- <string name="conversation_onboarding_summary" msgid="2484845363368486941">"మీరు ఒక సంభాషణను ఒక సారి ప్రముఖమైనదిగా మార్క్ చేసినా, లేదా సంభాషణలకు ఇతర మార్పులు ఏవైనా చేసినా, అవి ఇక్కడ కనిపిస్తాయి. \n\nసంభాషణ సెట్టింగ్లను ఇలా మార్చండి: \nకిందకు-లాగే షేడ్ను తెరవడానికి స్క్రీన్పై నుండి కిందకి స్వైప్ చేయండి, ఆపై ఒక సంభాషణను తాకి, అలాగే పట్టుకుని ఉండండి."</string>
+ <string name="conversation_onboarding_title" msgid="5194559958353468484">"ప్రాధాన్యతగల, అలాగే మార్పులు చేసిన సంభాషణలు ఇక్కడ కనిపిస్తాయి"</string>
+ <string name="conversation_onboarding_summary" msgid="2484845363368486941">"మీరు ఒక సంభాషణను ఒకసారి ప్రాధాన్యతగలదిగా మార్క్ చేసినా, లేదా సంభాషణలకు ఇతర మార్పులు ఏవైనా చేసినా, అవి ఇక్కడ కనిపిస్తాయి. \n\nసంభాషణ సెట్టింగ్లను ఇలా మార్చండి: \nకిందకు-లాగే షేడ్ను తెరవడానికి స్క్రీన్పై నుండి కిందకి స్వైప్ చేయండి, ఆపై ఒక సంభాషణను తాకి, అలాగే పట్టుకుని ఉండండి."</string>
<string name="notification_importance_min_title" msgid="7676541266705442501">"తక్కువ స్థాయికి తగ్గించండి"</string>
<string name="notification_importance_high_title" msgid="394129291760607808">"స్క్రీన్పై పాప్ అవుతుంది"</string>
<string name="notification_silence_title" msgid="4085829874452944989">"నిశ్శబ్దం"</string>
@@ -3223,10 +3223,10 @@
<string name="notification_channel_summary_priority" msgid="7225362351439076913">"సంభాషణ విభాగం ఎగువన ఉంటుంది, తేలుతున్న బబుల్లాగా కనిపిస్తుంది, లాక్ స్క్రీన్పై ప్రొఫైల్ ఫోటోను ప్రదర్శిస్తుంది"</string>
<string name="convo_not_supported_summary" msgid="4285471045268268048">"<xliff:g id="APP_NAME">%1$s</xliff:g> చాలా వరకు సంభాషణ ఫీచర్లను సపోర్ట్ చేయదు. మీరు సంభాషణను ప్రాధాన్యతగా సెట్ చేయలేరు, అలాగే సంభాషణలు తేలుతున్న బబుల్స్లా కనిపించవు."</string>
<string name="notification_channel_summary_min" msgid="8823399508450176842">"కిందకు-లాగే షేడ్లో, నోటిఫికేషన్లను ఒక లైన్కు కుదించవచ్చు"</string>
- <string name="notification_channel_summary_low" msgid="5549662596677692000">"శబ్దం లేదా వైబ్రేషన్లు ఏవీ లేవు"</string>
+ <string name="notification_channel_summary_low" msgid="5549662596677692000">"సౌండ్ లేదా వైబ్రేషన్లు ఏవీ ఉండవు"</string>
<string name="notification_conversation_summary_low" msgid="6352818857388412326">"శబ్దం లేదా వైబ్రేషన్ లేదు, సంభాషణ విభాగం దిగువన కనిపిస్తుంది"</string>
<string name="notification_channel_summary_default" msgid="3674057458265438896">"ఫోన్ సెట్టింగ్ల ఆధారంగా రింగ్ లేదా వైబ్రేట్ కావచ్చు"</string>
- <string name="notification_channel_summary_high" msgid="3411637309360617621">"పరికరాన్ని అన్లాక్ చేసినప్పుడు స్క్రీన్ పైభాగంలో ఒక బ్యానర్గా నోటిఫికేషన్లను చూపించు"</string>
+ <string name="notification_channel_summary_high" msgid="3411637309360617621">"పరికరాన్ని అన్లాక్ చేసినప్పుడు స్క్రీన్ పైభాగంలో ఒక బ్యానర్గా నోటిఫికేషన్లను చూపించండి"</string>
<string name="notification_switch_label" msgid="8029371325967501557">"\"<xliff:g id="APP_NAME">%1$s</xliff:g>\" నోటిఫికేషన్లన్నీ"</string>
<string name="notification_app_switch_label" msgid="4422902423925084193">"అన్ని <xliff:g id="APP_NAME">%1$s</xliff:g> నోటిఫికేషన్లు"</string>
<string name="notifications_sent_daily" msgid="10274479224185437">"{count,plural, =1{సుమారుగా ఒక రోజుకు # నోటిఫికేషన్}other{సుమారుగా ఒక రోజుకు # నోటిఫికేషన్లు}}"</string>
@@ -3317,11 +3317,11 @@
<string name="deleted_channels" msgid="8489800381509312964">"{count,plural, =1{# కేటగిరీ తొలగించబడింది}other{# కేటగిరీలు తొలగించబడ్డాయి}}"</string>
<string name="app_notification_block_title" msgid="3880322745749900296">"అన్నింటినీ బ్లాక్ చేయండి"</string>
<string name="app_notification_block_summary" msgid="1804611676339341551">"ఈ నోటిఫికేషన్లను ఎప్పుడూ చూపవద్దు"</string>
- <string name="notification_content_block_title" msgid="6689085826061361351">"నోటిఫికేషన్లను చూపు"</string>
+ <string name="notification_content_block_title" msgid="6689085826061361351">"నోటిఫికేషన్లను చూపండి"</string>
<string name="notification_content_block_summary" msgid="329171999992248925">"నోటిఫికేషన్లను ఎన్నడూ షేడ్లో లేదా అనుబంధ పరికరాల్లో చూపవద్దు"</string>
<string name="notification_badge_title" msgid="6854537463548411313">"నోటిఫికేషన్ డాట్ను అనుమతించండి"</string>
- <string name="notification_channel_badge_title" msgid="6505542437385640049">"నోటిఫికేషన్ డాట్ చూపించు"</string>
- <string name="app_notification_override_dnd_title" msgid="3769539356442226691">"\'అంతరాయం కలిగించవద్దు\'ను ఓవర్రైడ్ చేయి"</string>
+ <string name="notification_channel_badge_title" msgid="6505542437385640049">"నోటిఫికేషన్ డాట్ చూపించండి"</string>
+ <string name="app_notification_override_dnd_title" msgid="3769539356442226691">"\'అంతరాయం కలిగించవద్దు\'ను ఓవర్రైడ్ చేయండి"</string>
<string name="app_notification_override_dnd_summary" msgid="4894641191397562920">"అంతరాయం కలిగించవద్దు ఫీచర్ ఆన్లో ఉన్నా కూడా, నోటిఫికేషన్ వచ్చిన సంగతిని తెలియజెప్పడానికి ఈ నోటిఫికేషన్లను అనుమతిస్తుంది"</string>
<string name="app_notification_visibility_override_title" msgid="7778628150022065920">"లాక్ స్క్రీన్"</string>
<string name="app_notifications_dialog_done" msgid="573716608705273004">"పూర్తయింది"</string>
@@ -3642,8 +3642,8 @@
<string name="zen_access_warning_dialog_title" msgid="6323325813123130154">"<xliff:g id="APP">%1$s</xliff:g> కోసం అంతరాయం కలిగించవద్దు ఎంపికకు యాక్సెస్ను అనుమతించాలా?"</string>
<string name="zen_access_warning_dialog_summary" msgid="8468714854067428987">"యాప్ అంతరాయం కలిగించవద్దు ఎంపికను ఆన్/ఆఫ్ చేయగలదు మరియు సంబంధిత సెట్టింగ్లకు మార్పులు చేయగలదు."</string>
<string name="zen_access_disabled_package_warning" msgid="6565908224294537889">"నోటిఫికేషన్ యాక్సెస్ ఆన్లో ఉన్నందున తప్పనిసరిగా ఇది ఆన్లో ఉండాలి"</string>
- <string name="zen_access_revoke_warning_dialog_title" msgid="7377261509261811449">"<xliff:g id="APP">%1$s</xliff:g> అంతరాయం కలిగించవద్దు ఫీచర్కు యాక్సెస్ను ఉపసంహరించాలా?"</string>
- <string name="zen_access_revoke_warning_dialog_summary" msgid="8689801842914183595">"ఈ యాప్ ద్వారా రూపొందించిన అన్ని అంతరాయం కలిగించవద్దు నిబంధనలు తీసివేయబడతాయి."</string>
+ <string name="zen_access_revoke_warning_dialog_title" msgid="7377261509261811449">"<xliff:g id="APP">%1$s</xliff:g> కోసం అంతరాయం కలిగించవద్దు ఫీచర్కు ఉన్న యాక్సెస్ను ఉపసంహరించాలా?"</string>
+ <string name="zen_access_revoke_warning_dialog_summary" msgid="8689801842914183595">"ఈ యాప్ ద్వారా రూపొందించిన అన్ని అంతరాయం కలిగించవద్దు నియమాలు తీసివేయబడతాయి."</string>
<string name="ignore_optimizations_on" msgid="6865583039303804932">"ఆప్టిమైజ్ చేయవద్దు"</string>
<string name="ignore_optimizations_off" msgid="9186557038453586295">"ఆప్టిమైజ్ చేయి"</string>
<string name="ignore_optimizations_on_desc" msgid="1280043916460939932">"మీ బ్యాటరీ మరింత త్వరగా వినియోగించబడవచ్చు. ఇకపై ఈ యాప్ నేపథ్య బ్యాటరీ వినియోగం పరిమితం చేయబడదు."</string>
@@ -4143,7 +4143,7 @@
<string name="my_device_info_device_identifiers_category_title" msgid="2197063484127704153">"పరికర ఐడెంటిఫయర్లు"</string>
<string name="change_wifi_state_title" msgid="5629648102837821525">"Wi-Fi కంట్రోల్"</string>
<string name="change_wifi_state_app_detail_switch" msgid="1385358508267180745">"Wi-Fiని కంట్రోల్ చేయడానికి యాప్ను అనుమతించండి"</string>
- <string name="change_wifi_state_app_detail_summary" msgid="8230854855584217111">"Wi-Fiని ఆన్ లేదా ఆఫ్ చేయడానికి, Wi-Fi నెట్వర్క్లను స్కాన్ చేసి, కనెక్ట్ కావడానికి, నెట్వర్క్లను జోడించడానికి లేదా తీసివేయడానికి లేదా లోకల్-మాత్రమే హాట్స్పాట్ను ప్రారంభించడానికి ఈ యాప్ను అనుమతించండి"</string>
+ <string name="change_wifi_state_app_detail_summary" msgid="8230854855584217111">"Wi-Fiని ఆన్ లేదా ఆఫ్ చేయడానికి; Wi-Fi నెట్వర్క్లను స్కాన్ చేసి, కనెక్ట్ కావడానికి; నెట్వర్క్లను జోడించడానికి లేదా తీసివేయడానికి; లేదా లోకల్-మాత్రమే హాట్స్పాట్ను ప్రారంభించడానికి ఈ యాప్ను అనుమతించండి"</string>
<string name="change_nfc_tag_apps_title" msgid="91514009058149617">"NFC ద్వారా లాంచ్ అవ్వడం"</string>
<string name="change_nfc_tag_apps_detail_switch" msgid="240286205725043561">"NFC స్కాన్ చేసినప్పుడు లాంచ్ అవ్వగలిగేలా అనుమతించండి"</string>
<string name="change_nfc_tag_apps_detail_summary" msgid="7083666814715607078">"NFC ట్యాగ్ను స్కాన్ చేసినప్పుడు లాంచ్ అవ్వగలిగేలా ఈ యాప్ను అనుమతించండి.\nఈ అనుమతి ఆన్లో ఉన్నట్లయితే, ట్యాగ్ గుర్తించబడినప్పుడల్లా ఒక ఆప్షన్గా ఉపయోగించేందుకు ఈ యాప్ అందుబాటులో ఉంటుంది."</string>
@@ -4684,7 +4684,7 @@
<string name="flash_notifications_summary_on_camera" msgid="3286405833586333730">"ఆన్లో ఉంది / కెమెరా ఫ్లాష్"</string>
<string name="flash_notifications_summary_on_screen" msgid="9040640799633336219">"ఆన్లో ఉంది / స్క్రీన్ ఫ్లాష్"</string>
<string name="flash_notifications_summary_on_camera_and_screen" msgid="2326268141063768701">"ఆన్లో ఉంది / కెమెరా, స్క్రీన్ ఫ్లాష్"</string>
- <string name="flash_notifications_intro" msgid="8409873413480928249">"మీరు నోటిఫికేషన్లను స్వీకరించినప్పుడు లేదా అలారాలు సౌండ్ చేసినప్పుడు కెమెరా లైట్ లేదా స్క్రీన్ను ఫ్లాష్ చేయండి"</string>
+ <string name="flash_notifications_intro" msgid="8409873413480928249">"మీరు నోటిఫికేషన్లను స్వీకరించినప్పుడు లేదా అలారాలు సౌండ్ చేసినప్పుడు కెమెరా లైట్ లేదా స్క్రీన్ను ఫ్లాష్ చేస్తుంది"</string>
<string name="flash_notifications_intro_without_camera_flash" msgid="6297337174487793891">"మీరు నోటిఫికేషన్లను అందుకున్నప్పుడు లేదా అలారాలు మోగినప్పుడు స్క్రీన్ను ఫ్లాష్ చేయండి"</string>
<string name="flash_notifications_note" msgid="2426125248448055075">"మీరు లైట్ సెన్సిటివ్ అయితే, ఫ్లాష్ నోటిఫికేషన్లను జాగ్రత్తగా ఉపయోగించండి"</string>
<string name="flash_notifications_keywords" msgid="2458759275318514836">"ఫ్లాష్, కాంతి, వినడంలో సమస్య, వినికిడి ఎర్రర్"</string>
diff --git a/res/values-th/strings.xml b/res/values-th/strings.xml
index 5c2f9c1..c4dbf9d 100644
--- a/res/values-th/strings.xml
+++ b/res/values-th/strings.xml
@@ -909,10 +909,8 @@
<skip />
<string name="wifi_hotspot_speed_summary_unavailable" msgid="7276080644693388756">"ไม่มีให้บริการในประเทศหรือภูมิภาคของคุณ"</string>
<string name="wifi_hotspot_speed_footer" msgid="8846939503916795002">"หากไม่มีความถี่ที่ต้องการ ฮอตสปอตอาจใช้ความถี่อื่น การตั้งค่าความปลอดภัยฮอตสปอตอาจเปลี่ยนแปลงหากคุณเปลี่ยนความถี่"</string>
- <!-- no translation found for wifi_hotspot_security_summary_unavailable (117582979310345853) -->
- <skip />
- <!-- no translation found for wifi_hotspot_security_footer (4608329688744949796) -->
- <skip />
+ <string name="wifi_hotspot_security_summary_unavailable" msgid="117582979310345853">"ไม่พร้อมใช้งานคลื่นความถี่ 6 GHz"</string>
+ <string name="wifi_hotspot_security_footer" msgid="4608329688744949796">"การตั้งค่าความปลอดภัยอาจเปลี่ยนแปลงหากคุณเปลี่ยนคลื่นความถี่ของฮอตสปอต"</string>
<string name="wifi_tether_starting" msgid="8879874184033857814">"กำลังเปิดฮอตสปอต…"</string>
<string name="wifi_tether_stopping" msgid="4416492968019409188">"กำลังปิดฮอตสปอต…"</string>
<string name="wifi_tether_carrier_unsupport_dialog_title" msgid="3089432578433978073">"ไม่มีบริการเชื่อมต่ออินเทอร์เน็ตผ่านมือถือ"</string>
@@ -2705,14 +2703,12 @@
<string name="enable_guest_calling" msgid="8300355036005240911">"อนุญาตให้ผู้ใช้ชั่วคราวใช้โทรศัพท์"</string>
<string name="enable_guest_calling_summary" msgid="4748224917641204782">"ระบบจะแชร์ประวัติการโทรกับผู้ใช้ชั่วคราว"</string>
<string name="user_enable_calling_sms" msgid="8546430559552381324">"เปิดการโทรและ SMS"</string>
- <!-- no translation found for user_grant_admin (5942118263054572074) -->
- <skip />
+ <string name="user_grant_admin" msgid="5942118263054572074">"กำหนดให้ผู้ใช้รายนี้เป็นผู้ดูแลระบบ"</string>
<string name="user_remove_user" msgid="8468203789739693845">"ลบผู้ใช้"</string>
<string name="user_enable_calling_and_sms_confirm_title" msgid="4041510268838725520">"เปิดการโทรและ SMS ไหม"</string>
<string name="user_enable_calling_and_sms_confirm_message" msgid="367792286597449922">"ระบบจะแชร์ประวัติการโทรและ SMS กับผู้ใช้รายนี้"</string>
<string name="user_revoke_admin_confirm_title" msgid="3057842401861731863">"นำสิทธิ์ของผู้ดูแลระบบออกใช่ไหม"</string>
- <!-- no translation found for user_revoke_admin_confirm_message (9207187319308572958) -->
- <skip />
+ <string name="user_revoke_admin_confirm_message" msgid="9207187319308572958">"หากนำสิทธิ์ของผู้ดูแลระบบออกจากผู้ใช้รายนี้ คุณหรือผู้ดูแลระบบคนอื่นสามารถให้สิทธิ์ดังกล่าวอีกครั้งในภายหลังได้"</string>
<string name="emergency_info_title" msgid="8233682750953695582">"ข้อมูลสำหรับกรณีฉุกเฉิน"</string>
<string name="emergency_info_summary" msgid="8463622253016757697">"ข้อมูลและรายชื่อติดต่อสำหรับ <xliff:g id="USER_NAME">%1$s</xliff:g>"</string>
<string name="open_app_button" msgid="5025229765547191710">"เปิด <xliff:g id="APP_NAME">%1$s</xliff:g>"</string>
diff --git a/res/values-zh-rCN/strings.xml b/res/values-zh-rCN/strings.xml
index 851e3ea..6e706e2 100644
--- a/res/values-zh-rCN/strings.xml
+++ b/res/values-zh-rCN/strings.xml
@@ -909,10 +909,8 @@
<skip />
<string name="wifi_hotspot_speed_summary_unavailable" msgid="7276080644693388756">"您所在的国家或地区无法使用此功能"</string>
<string name="wifi_hotspot_speed_footer" msgid="8846939503916795002">"如果首选频率不可用,热点可能会使用其他频率。如果您更改频率,热点的安全设置可能也会改变。"</string>
- <!-- no translation found for wifi_hotspot_security_summary_unavailable (117582979310345853) -->
- <skip />
- <!-- no translation found for wifi_hotspot_security_footer (4608329688744949796) -->
- <skip />
+ <string name="wifi_hotspot_security_summary_unavailable" msgid="117582979310345853">"不适用于 6 GHz"</string>
+ <string name="wifi_hotspot_security_footer" msgid="4608329688744949796">"如果您更改热点频率,安全设置可能也会改变"</string>
<string name="wifi_tether_starting" msgid="8879874184033857814">"正在打开热点..."</string>
<string name="wifi_tether_stopping" msgid="4416492968019409188">"正在关闭热点..."</string>
<string name="wifi_tether_carrier_unsupport_dialog_title" msgid="3089432578433978073">"网络共享不可用"</string>
@@ -2705,14 +2703,12 @@
<string name="enable_guest_calling" msgid="8300355036005240911">"允许访客使用电话"</string>
<string name="enable_guest_calling_summary" msgid="4748224917641204782">"将与访客用户共享通话记录"</string>
<string name="user_enable_calling_sms" msgid="8546430559552381324">"开启通话和短信功能"</string>
- <!-- no translation found for user_grant_admin (5942118263054572074) -->
- <skip />
+ <string name="user_grant_admin" msgid="5942118263054572074">"将此用户设为管理员"</string>
<string name="user_remove_user" msgid="8468203789739693845">"删除用户"</string>
<string name="user_enable_calling_and_sms_confirm_title" msgid="4041510268838725520">"要开启通话和短信功能吗?"</string>
<string name="user_enable_calling_and_sms_confirm_message" msgid="367792286597449922">"将与此用户共享通话记录和短信记录。"</string>
<string name="user_revoke_admin_confirm_title" msgid="3057842401861731863">"要撤消管理员权限吗?"</string>
- <!-- no translation found for user_revoke_admin_confirm_message (9207187319308572958) -->
- <skip />
+ <string name="user_revoke_admin_confirm_message" msgid="9207187319308572958">"如果您撤消该用户的管理员权限,您或其他管理员之后可以重新授予其管理员权限。"</string>
<string name="emergency_info_title" msgid="8233682750953695582">"急救信息"</string>
<string name="emergency_info_summary" msgid="8463622253016757697">"<xliff:g id="USER_NAME">%1$s</xliff:g>的相关信息和联系人信息"</string>
<string name="open_app_button" msgid="5025229765547191710">"打开“<xliff:g id="APP_NAME">%1$s</xliff:g>”"</string>
diff --git a/res/values-zh-rTW/strings.xml b/res/values-zh-rTW/strings.xml
index c0f00c1..8159eca 100644
--- a/res/values-zh-rTW/strings.xml
+++ b/res/values-zh-rTW/strings.xml
@@ -1477,7 +1477,7 @@
<string name="lockpattern_settings_enhanced_pin_privacy_summary" msgid="8639588868341114740">"輸入 PIN 碼時停用動畫"</string>
<string name="lockpattern_settings_enable_visible_pattern_title_profile" msgid="5138189101808127489">"顯示設定檔解鎖圖案"</string>
<string name="lockpattern_settings_enable_tactile_feedback_title" msgid="2273374883831956787">"輕觸時震動"</string>
- <string name="lockpattern_settings_enable_power_button_instantly_locks" msgid="1638619728773344099">"按下電源按鈕立即鎖定"</string>
+ <string name="lockpattern_settings_enable_power_button_instantly_locks" msgid="1638619728773344099">"按下電源鍵立即鎖定"</string>
<string name="lockpattern_settings_power_button_instantly_locks_summary" msgid="2202430156268094229">"由 <xliff:g id="TRUST_AGENT_NAME">%1$s</xliff:g> 維持解鎖狀態時除外"</string>
<string name="lockpattern_settings_choose_lock_pattern" msgid="2193588309557281466">"設定解鎖圖案"</string>
<string name="lockpattern_settings_change_lock_pattern" msgid="7614155083815661347">"變更解鎖圖案"</string>
@@ -3893,7 +3893,7 @@
<string name="automatic_storage_manager_primary_switch_title" msgid="9131959126462101994">"使用儲存空間管理工具"</string>
<string name="gesture_preference_title" msgid="8291899281322647187">"手勢"</string>
<string name="double_tap_power_for_camera_title" msgid="7982364144330923683">"快速開啟相機"</string>
- <string name="double_tap_power_for_camera_summary" msgid="1100926048598415509">"只要在任何畫面中按兩下電源按鈕,即可快速開啟相機。"</string>
+ <string name="double_tap_power_for_camera_summary" msgid="1100926048598415509">"只要在任何畫面中按兩下電源鍵,即可快速開啟相機。"</string>
<string name="double_twist_for_camera_mode_title" msgid="472455236910935684">"翻轉相機切換自拍模式"</string>
<string name="double_twist_for_camera_mode_summary" msgid="592503740044744951"></string>
<string name="system_navigation_title" msgid="4890381153527184636">"操作模式"</string>
@@ -3938,7 +3938,7 @@
<string name="ambient_display_tap_screen_summary" msgid="4480489179996521405">"如要查看時間、通知和其他資訊,請輕觸螢幕。"</string>
<string name="emergency_gesture_screen_title" msgid="3280543310204360902">"緊急求救"</string>
<string name="emergency_gesture_switchbar_title" msgid="7421353963329899514">"使用緊急求救功能"</string>
- <string name="emergency_gesture_screen_summary" msgid="6640521030845132507">"快速按電源按鈕 5 次以上即可觸發以下動作"</string>
+ <string name="emergency_gesture_screen_summary" msgid="6640521030845132507">"快速按電源鍵 5 次以上即可觸發以下動作"</string>
<string name="emergency_gesture_sound_setting_title" msgid="7153948164862156536">"播放倒數計時警報"</string>
<string name="emergency_gesture_sound_setting_summary" msgid="6573377104470235173">"觸發緊急求救功能時,播放高分貝的音效"</string>
<string name="emergency_gesture_category_call_for_help_title" msgid="1680040129478289510">"求救通知"</string>
@@ -4413,8 +4413,8 @@
<string name="power_menu_summary_long_press_for_assistant" msgid="32706459458422952">"存取數位助理"</string>
<string name="power_menu_summary_long_press_for_power_menu" msgid="7617247135239683710">"存取電源鍵選單"</string>
<string name="lockscreen_privacy_not_secure" msgid="3251276389681975912">"必須先設定螢幕鎖定才能使用"</string>
- <string name="power_menu_power_volume_up_hint" msgid="5619917593676125759">"電源鍵選單:\n同時按下電源鍵和調高音量按鈕"</string>
- <string name="power_menu_power_prevent_ringing_hint" msgid="1169955014711158873">"暫停響鈴:\n按下音量按鈕即可顯示捷徑"</string>
+ <string name="power_menu_power_volume_up_hint" msgid="5619917593676125759">"電源鍵選單:\n同時按下電源鍵和調高音量鍵"</string>
+ <string name="power_menu_power_prevent_ringing_hint" msgid="1169955014711158873">"暫停響鈴:\n按下音量鍵即可顯示捷徑"</string>
<string name="power_menu_long_press_for_assist_sensitivity_title" msgid="1626808509158422185">"按住電源鍵的時間長度"</string>
<string name="power_menu_long_press_for_assist_sensitivity_summary" msgid="7550610071666801935">"選擇按住電源鍵的時間長度即可調整靈敏度"</string>
<string name="power_menu_long_press_for_assist_sensitivity_low_label" msgid="3430099983480845635">"短"</string>
diff --git a/res/values/arrays.xml b/res/values/arrays.xml
index 787163e..1ab9876 100644
--- a/res/values/arrays.xml
+++ b/res/values/arrays.xml
@@ -1409,4 +1409,60 @@
<integer-array name="network_mode_3g_deprecated_carrier_id" translatable="false">
</integer-array>
+ <!-- The following 2 arrays are for battery tips card. Please keep them the same size. -->
+ <string-array name="battery_tips_card_icons" translatable="false">
+ <item>ic_battery_tips_lightbulb</item>
+ <item>ic_battery_tips_warning_icon</item>
+ </string-array>
+
+ <string-array name="battery_tips_card_colors" translatable="false">
+ <item>color_accent_selector</item>
+ <item>color_battery_anomaly_yellow_selector</item>
+ </string-array>
+
+ <!-- The following 4 arrays are for power anomaly tips card. Please keep them the same size. -->
+ <string-array name="power_anomaly_title_ids" translatable="false">
+ <item>battery_tips_settings_summary_brightness</item>
+ <item>battery_tips_settings_summary_screen_timeout</item>
+ <item>battery_tips_apps_summary_always_high</item>
+ <item>battery_tips_apps_summary_higher_than_usual</item>
+ <item>battery_tips_apps_summary_always_high_in_background</item>
+ <item>battery_tips_apps_summary_higher_than_usual_in_background</item>
+ <item>battery_tips_apps_summary_always_high_in_foreground</item>
+ <item>battery_tips_apps_summary_higher_than_usual_in_foreground</item>
+ </string-array>
+
+ <string-array name="power_anomaly_main_btn_strings" translatable="false">
+ <item>@string/battery_tips_card_action_button</item>
+ <item>@string/battery_tips_card_action_button</item>
+ <item>@string/battery_tips_card_action_button_check</item>
+ <item>@string/battery_tips_card_action_button_check</item>
+ <item>@string/battery_tips_card_action_button_check</item>
+ <item>@string/battery_tips_card_action_button_check</item>
+ <item>@string/battery_tips_card_action_button_check</item>
+ <item>@string/battery_tips_card_action_button_check</item>
+ </string-array>
+
+ <string-array name="power_anomaly_dismiss_btn_strings" translatable="false">
+ <item>@string/battery_tips_card_dismiss_button</item>
+ <item>@string/battery_tips_card_dismiss_button</item>
+ <item>@string/battery_tips_card_dismiss_button</item>
+ <item>@string/battery_tips_card_dismiss_button</item>
+ <item>@string/battery_tips_card_dismiss_button</item>
+ <item>@string/battery_tips_card_dismiss_button</item>
+ <item>@string/battery_tips_card_dismiss_button</item>
+ <item>@string/battery_tips_card_dismiss_button</item>
+ </string-array>
+
+ <string-array name="power_anomaly_hint_messages" translatable="false">
+ <item></item>
+ <item></item>
+ <item>@string/battery_app_item_hint</item>
+ <item>@string/battery_app_item_hint</item>
+ <item>@string/battery_app_item_hint_in_bg</item>
+ <item>@string/battery_app_item_hint_in_bg</item>
+ <item>@string/battery_app_item_hint_in_fg</item>
+ <item>@string/battery_app_item_hint_in_fg</item>
+ </string-array>
+
</resources>
diff --git a/res/values/config.xml b/res/values/config.xml
index 2c683ea..d6f7c08 100755
--- a/res/values/config.xml
+++ b/res/values/config.xml
@@ -345,6 +345,9 @@
<bool name="config_show_manual">false</bool>
<!-- Whether to show a preference item for regulatory information in About phone -->
<bool name="config_show_regulatory_info">false</bool>
+ <!-- Package name of regulatory information overlay which provides mapping and contents.
+ Fetch resource from overlay package directly if this is set. -->
+ <string name="config_regulatory_info_overlay_package_name" translatable="false" />
<!-- Whether to show a preference item for mobile plan -->
<bool name="config_show_mobile_plan">true</bool>
@@ -364,7 +367,7 @@
<bool name="config_show_wifi_hotspot_settings">true</bool>
<!-- Whether Wi-Fi hotspot speed should be shown or not. -->
- <bool name="config_show_wifi_hotspot_speed">false</bool>
+ <bool name="config_show_wifi_hotspot_speed">true</bool>
<!-- Whether toggle_airplane is available or not. -->
<bool name="config_show_toggle_airplane">true</bool>
@@ -605,6 +608,31 @@
<item>3</item>
</integer-array>
+ <!-- App aspect ratio settings screen, user aspect ratio override options. Must be the same
+ length and order as config_userAspectRatioOverrideValues below. -->
+ <string-array name="config_userAspectRatioOverrideEntries" translatable="false">
+ <item>@string/user_aspect_ratio_app_default</item>
+ <item>@string/user_aspect_ratio_fullscreen</item>
+ <item>@string/user_aspect_ratio_half_screen</item>
+ <item>@string/user_aspect_ratio_device_size</item>
+ <item>@string/user_aspect_ratio_16_9</item>
+ <item>@string/user_aspect_ratio_4_3</item>
+ <item>@string/user_aspect_ratio_3_2</item>
+ </string-array>
+
+ <!-- App aspect ratio settings screen, user aspect ratio override options. Must be the same
+ length and order as config_userAspectRatioOverrideEntries above. The values must
+ correspond to PackageManager.UserMinAspectRatio -->
+ <integer-array name="config_userAspectRatioOverrideValues" translatable="false">
+ <item>0</item> <!-- USER_MIN_ASPECT_RATIO_UNSET -->
+ <item>6</item> <!-- USER_MIN_ASPECT_RATIO_FULLSCREEN -->
+ <item>1</item> <!-- USER_MIN_ASPECT_RATIO_SPLIT_SCREEN -->
+ <item>2</item> <!-- USER_MIN_ASPECT_RATIO_DISPLAY_SIZE -->
+ <item>4</item> <!-- USER_MIN_ASPECT_RATIO_16_9 -->
+ <item>3</item> <!-- USER_MIN_ASPECT_RATIO_4_3 -->
+ <item>5</item> <!-- USER_MIN_ASPECT_RATIO_3_2 -->
+ </integer-array>
+
<!-- The settings/preference description for each settable device state defined in the array
"config_perDeviceStateRotationLockDefaults".
The item in position "i" describes the auto-rotation setting for the device state also in
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index de33ec7..9703124 100755
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -229,6 +229,15 @@
<!-- Minimum height for setting a lock pattern -->
<dimen name="choose_lockscreen_min_height">200dp</dimen>
+ <!-- Choose lock Password requirement dimensions -->
+ <dimen name="password_requirement_view_margin_top">16dp</dimen>
+
+ <!-- Screen lock option button dimensions -->
+ <dimen name="screen_lock_options_button_margin_top">32dp</dimen>
+
+ <!-- Choose lock Password requirement font size -->
+ <dimen name="password_requirement_font_size">16sp</dimen>
+
<!-- Select dialog -->
<dimen name="select_dialog_padding_start">20dp</dimen>
<dimen name="select_dialog_item_margin_start">12dp</dimen>
@@ -360,10 +369,21 @@
<dimen name="chartview_text_padding">6dp</dimen>
<dimen name="chartview_divider_width">1dp</dimen>
<dimen name="chartview_divider_height">4dp</dimen>
+ <dimen name="chartview_transom_width">4dp</dimen>
+ <dimen name="chartview_transom_radius">4dp</dimen>
+ <dimen name="chartview_transom_icon_size">12dp</dimen>
+ <dimen name="chartview_transom_padding_top">2dp</dimen>
+ <dimen name="chartview_transom_layout_height">12dp</dimen>
+ <dimen name="chartview_layout_height">182dp</dimen>
<dimen name="chartview_trapezoid_radius">5dp</dimen>
<dimen name="chartview_trapezoid_margin_start">1dp</dimen>
<dimen name="chartview_trapezoid_margin_bottom">2dp</dimen>
+ <!-- Battery tips card view component -->
+ <dimen name="battery_tips_card_corner_radius_small">4dp</dimen>
+ <dimen name="battery_tips_card_corner_radius_normal">24dp</dimen>
+ <dimen name="battery_hints_chip_corner_radius">8dp</dimen>
+
<!-- Dimensions for Dream settings cards -->
<dimen name="dream_item_min_column_width">174dp</dimen>
<dimen name="dream_item_corner_radius">28dp</dimen>
@@ -397,6 +417,9 @@
<!-- Margin for SD card setup completion Image -->
<dimen name="setup_completion_margin_top">88dp</dimen>
+ <!-- QR code action button -->
+ <dimen name="action_button_icon_size">18dp</dimen>
+
<!-- Biometrics Face enroll education dimensions-->
<dimen name="face_enroll_icon_large_width">300dp</dimen>
<dimen name="face_enroll_icon_large_height">300dp</dimen>
diff --git a/res/values/ids.xml b/res/values/ids.xml
index c1cfe2e..211a707 100644
--- a/res/values/ids.xml
+++ b/res/values/ids.xml
@@ -25,6 +25,8 @@
<item type="id" name="encrypt_dont_require_password" />
+ <item type="id" name="tag_row_view" />
+
<!-- Used for custom accessibility actions in the Drag-and-Drop locale list -->
<item type="id" name="action_drag_move_up" />
<item type="id" name="action_drag_move_down" />
@@ -40,4 +42,8 @@
<!-- For a layout container to add AppLocaleDetails into -->
<item type="id" name="layout_app_locale_details" />
+
+ <!-- For screen lock options button -->
+ <item type="id" name="screen_lock_options" />
+
</resources>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index d625122..7fe1daa 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -80,6 +80,19 @@
<!-- Description for the button that makes interface elements larger. [CHAR_LIMIT=NONE] -->
<string name="font_size_make_larger_desc">Make larger</string>
+ <!-- Title for stay awake on fold radio button. [CHAR_LIMIT=NONE] -->
+ <string name="stay_awake_on_fold_title">Always</string>
+ <!-- Summary for stay awake on fold radio button. [CHAR_LIMIT=NONE] -->
+ <string name="stay_awake_on_fold_summary">Front display turns on when you fold your device</string>
+ <!-- Title for selective stay awake radio button. [CHAR_LIMIT=NONE] -->
+ <string name="selective_stay_awake_title">Only games, videos, and more</string>
+ <!-- Summary for selective stay awake radio button. [CHAR_LIMIT=NONE] -->
+ <string name="selective_stay_awake_summary">Front display turns on for apps that stop your screen going idle</string>
+ <!-- Title for sleep on fold radio button. [CHAR_LIMIT=NONE] -->
+ <string name="sleep_on_fold_title">Never</string>
+ <!-- Summary for sleep on fold radio button. [CHAR_LIMIT=NONE] -->
+ <string name="sleep_on_fold_summary">Front display locks when you fold your device</string>
+
<!-- Auto rotate switchbar title. [CHAR_LIMIT=NONE] -->
<string name="auto_rotate_settings_primary_switch_title">Use auto-rotate</string>
@@ -258,8 +271,10 @@
<!-- Title for stylus device details page [CHAR LIMIT=50] -->
<string name="stylus_device_details_title">Stylus</string>
- <!-- Preference title for setting the default note taking app [CHAR LIMIT=none] -->
- <string name="stylus_default_notes_app">Default notes app</string>
+ <!-- Preference title for setting the app that opens user presses stylus button [CHAR LIMIT=none] -->
+ <string name="stylus_default_notes_app">Tail button press</string>
+ <!-- Summary for the app that opens when user presses stylus tail button, if set to a work profile app [CHAR LIMIT=none] -->
+ <string name="stylus_default_notes_summary_work"><xliff:g id="app_name" example="Mail">%s</xliff:g> (Work profile)</string>
<!-- Preference title for toggling whether handwriting in textfields is enabled [CHAR LIMIT=none] -->
<string name="stylus_textfield_handwriting">Write in text fields</string>
<!-- Preference title for toggling whether stylus button presses are ignored [CHAR LIMIT=none] -->
@@ -411,7 +426,7 @@
<!-- The title of the menu entry of Numbers system preference. [CHAR LIMIT=50] -->
<string name="numbers_preferences_title">Numbers preferences</string>
<!-- The summary of default string for each regional preference. [CHAR LIMIT=50] -->
- <string name="default_string_of_regional_preference">Use app default</string>
+ <string name="default_string_of_regional_preference">Use default</string>
<!-- The title of Celsius for preference of temperature unit. [CHAR LIMIT=50] -->
<string name="celsius_temperature_unit">Celsius (\u00B0C)</string>
<!-- The title of Fahrenheit for preference of temperature unit. [CHAR LIMIT=50] -->
@@ -744,6 +759,10 @@
<string name="security_settings_face_settings_remove_dialog_details">Your face model will be permanently and securely deleted.\n\nAfter deletion, you will need your PIN, pattern, or password to unlock your phone or for authentication in apps.</string>
<!-- Dialog contents shown when the user removes an enrollment when configured as a convenience [CHAR LIMIT=NONE] -->
<string name="security_settings_face_settings_remove_dialog_details_convenience">Your face model will be permanently and securely deleted.\n\nAfter deletion, you will need your PIN, pattern, or password to unlock your phone.</string>
+ <!-- Dialog contents shown when the user removes an enrollment [CHAR LIMIT=NONE] -->
+ <string name="security_settings_face_remove_dialog_details_fingerprint">Your face model will be permanently and securely deleted.\n\nAfter deletion, you will need your fingerprint, PIN, pattern, or password to unlock your phone or for authentication in apps.</string>
+ <!-- Dialog contents shown when the user removes an enrollment when configured as a convenience [CHAR LIMIT=NONE] -->
+ <string name="security_settings_face_remove_dialog_details_fingerprint_conv">Your face model will be permanently and securely deleted.\n\nAfter deletion, you will need your fingerprint, PIN, pattern, or password to unlock your phone.</string>
<!-- Subtitle shown for contextual setting face enrollment [CHAR LIMIT=NONE] -->
<string name="security_settings_face_settings_context_subtitle">Use Face Unlock to unlock your phone</string>
@@ -859,7 +878,7 @@
<!-- Biometric settings --><skip />
<!-- Title shown for menu item that launches biometric settings. [CHAR LIMIT=66] -->
- <string name="security_settings_biometric_preference_title">Face & Fingerprint Unlock</string>
+ <string name="security_settings_biometric_preference_title">Fingerprint & Face Unlock</string>
<!-- Title shown for work menu item that launches biometric settings. [CHAR LIMIT=66] -->
<string name="security_settings_work_biometric_preference_title">Face & Fingerprint Unlock for work</string>
<!-- Message shown in summary field of biometric settings. [CHAR LIMIT=66] -->
@@ -1211,14 +1230,8 @@
<!-- Title for preference that guides the user to skip Face Unlock setup [CHAR LIMIT=60]-->
<string name="face_unlock_skip_face">Continue without Face Unlock</string>
- <!-- Title for preference that guides the user through creating a backup unlock pattern for biometrics unlock [CHAR LIMIT=45]-->
- <string name="biometrics_unlock_set_unlock_pattern">Pattern \u2022 Face \u2022 Fingerprint</string>
- <!-- Title for preference that guides the user through creating a backup unlock PIN for biometrics unlock [CHAR LIMIT=45]-->
- <string name="biometrics_unlock_set_unlock_pin">PIN \u2022 Face \u2022 Fingerprint</string>
- <!-- Title for preference that guides the user through creating a backup unlock password for biometrics unlock [CHAR LIMIT=45]-->
- <string name="biometrics_unlock_set_unlock_password">Password \u2022 Face \u2022 Fingerprint</string>
<!-- Title for preference that guides the user to skip face unlock setup [CHAR LIMIT=60]-->
- <string name="biometrics_unlock_skip_biometrics">Continue without face or fingerprint</string>
+ <string name="biometrics_unlock_skip_biometrics">Continue without fingerprint or face</string>
<!-- Summary for "Configure lockscreen" when lock screen is off [CHAR LIMIT=45] -->
<string name="unlock_set_unlock_mode_off">None</string>
@@ -1852,7 +1865,7 @@
<!-- Title for the fragment to show that the QR code is for sharing Wi-Fi hotspot network [CHAR LIMIT=50] -->
<string name="wifi_dpp_share_hotspot">Share hotspot</string>
<!-- Title for Wi-Fi DPP lockscreen title [CHAR LIMIT=50] -->
- <string name="wifi_dpp_lockscreen_title">Verify that it\u0027s you</string>
+ <string name="wifi_dpp_lockscreen_title">Verify it\u0027s you</string>
<!-- Hint for Wi-Fi password [CHAR LIMIT=50] -->
<string name="wifi_dpp_wifi_password">Wi\u2011Fi password: <xliff:g id="password" example="my password">%1$s</xliff:g></string>
<!-- Hint for Wi-Fi hotspot password [CHAR LIMIT=50] -->
@@ -1980,6 +1993,23 @@
<!-- Wifi details preference category title for IPv6 information -->
<string name="wifi_details_ipv6_address_header">IPv6 addresses</string>
+ <!-- Hotspot device details preference category title in Network details [CHAR LIMIT=NONE]-->
+ <string name="hotspot_device_details_category">Hotspot device details</string>
+ <!-- Internet source preference in Hotspot device details preference category [CHAR LIMIT=NONE]-->
+ <string name="hotspot_device_details_internet_source">Internet source</string>
+ <!-- Wi-Fi summary in Internet source preference [CHAR LIMIT=NONE]-->
+ <string name="internet_source_wifi">Wi\u2011Fi</string>
+ <!-- Mobile data summary in Internet source preference [CHAR LIMIT=NONE]-->
+ <string name="internet_source_mobile_data">Mobile data</string>
+ <!-- Ethernet summary in Internet source preference [CHAR LIMIT=NONE]-->
+ <string name="internet_source_ethernet">Ethernet</string>
+ <!-- Hotspot device details battery charging summary [CHAR LIMIT=NONE]-->
+ <string name="hotspot_battery_charging_summary"><xliff:g id="battery_percentage" example="80%">%s</xliff:g> \u2011 Charging</string>
+ <!-- Hotspot device details preference category title in Network details [CHAR LIMIT=NONE]-->
+ <string name="hotspot_connection_category">Hotspot connection</string>
+ <!-- Connection strength preference in Hotspot connection preference category [CHAR LIMIT=NONE]-->
+ <string name="hotspot_connection_strength">Connection strength</string>
+
<!-- Wifi saved access points. Used as a label under the shortcut icon that goes to Wifi saved access points. [CHAR LIMIT=20] -->
<string name="wifi_saved_access_points_label">Saved networks</string>
<!-- Tab title for showing subscribed WiFi access points. [CHAR LIMIT=20] -->
@@ -2098,6 +2128,13 @@
<!-- The footer message for Wi-Fi hotspot security settings [CHAR LIMIT=NONE] -->
<string name="wifi_hotspot_security_footer">Security settings may change if you change the hotspot’s frequency</string>
+ <!-- Title for the instant hotspot state [CHAR LIMIT=NONE]-->
+ <string name="wifi_hotspot_instant_title">Instant hotspot</string>
+ <!-- Summary text when instant hotspot is turned on -->
+ <string name="wifi_hotspot_instant_summary_on">On</string>
+ <!-- Summary text when instant hotspot is turned off -->
+ <string name="wifi_hotspot_instant_summary_off">Off</string>
+
<!-- Summary text when turning hotspot on -->
<string name="wifi_tether_starting">Turning hotspot on\u2026</string>
<!-- Summary text when turning hotspot off -->
@@ -2357,6 +2394,8 @@
<string name="display_white_balance_title">Display white balance</string>
<!-- Display settings screen, display white balance settings summary [CHAR LIMIT=NONE] -->
<string name="display_white_balance_summary"></string>
+ <!-- Display settings screen, setting option name to change Fold setting -->
+ <string name="fold_lock_behavior_title">Continue using apps on fold</string>
<!-- Display settings screen, peak refresh rate settings title [CHAR LIMIT=30] -->
<string name="peak_refresh_rate_title">Smooth Display</string>
<!-- Display settings screen, peak refresh rate settings summary [CHAR LIMIT=NONE] -->
@@ -2654,6 +2693,8 @@
<string name="build_number">Build number</string>
<!-- About phone screen, tapping this button will take user to a seperate UI to check Google Play system update [CHAR LIMIT=60] -->
<string name="module_version">Google Play system update</string>
+ <!-- About phone screen, show a list of battery information [CHAR LIMIT=60] -->
+ <string name="battery_info">Battery information</string>
<!-- About phone screen, show when a value of some status item is unavailable. -->
<string name="device_info_not_available">Not available</string>
@@ -2725,6 +2766,16 @@
<string name="status_serial_number">Serial number</string>
<!-- About phone, status item title. How long the device has been running since its last reboot. -->
<string name="status_up_time">Up time</string>
+
+ <!-- About phone, status item title. The battery manufacture date. [CHAR LIMIT=60]-->
+ <string name="battery_manufacture_date">Manufacture date</string>
+ <!-- About phone, status item title. Date of first use of the battery. [CHAR LIMIT=60]-->
+ <string name="battery_first_use_date">Date of first use</string>
+ <!-- About phone, status item title. Count of battery full charge/discharge cycles [CHAR LIMIT=60]-->
+ <string name="battery_cycle_count">Cycle count</string>
+ <!-- About phone, status item title. The status summary for cycle count that's not available. [CHAR LIMIT=40] -->
+ <string name="battery_cycle_count_not_available">Unavailable</string>
+
<!-- SD card & phone storage settings summary. Displayed when the total memory usage is being calculated. Will be replaced with a number like "12.3 GB" when finished calucating. [CHAR LIMIT=30] -->
<string name="memory_calculating_size">Calculating\u2026</string>
@@ -3008,8 +3059,6 @@
<string name="reset_bluetooth_wifi_complete_toast">Bluetooth & Wi\u2011Fi have been reset</string>
<!-- Erase Euicc -->
- <!-- Confirmation button of dialog to confirm resetting user's app preferences [CHAR LIMIT=NONE] -->
- <string name="erase_euicc_data_button">Erase</string>
<!-- Erase Euicc dialog and SD card & phone storage settings screen, title for the menu option and checkbox to let user decide whether erase eSIM data together [CHAR LIMIT=50] -->
<string name="reset_esim_title">Erase eSIMs</string>
<!-- Erase Euicc dialog and SD card & phone storage settings screen, message for the checkbox to let user decide whether erase eSIM data together [CHAR LIMIT=NONE] -->
@@ -3409,16 +3458,16 @@
<!-- Message to be used to explain the users that they need to enter their pattern to continue a
particular operation. [CHAR LIMIT=70]-->
- <string name="lockpassword_confirm_your_pattern_generic">Use your device pattern to continue</string>
+ <string name="lockpassword_confirm_your_pattern_generic">Draw your pattern to continue</string>
<!-- Message to be used to explain the users that they need to enter their PIN to continue a
particular operation. [CHAR LIMIT=70]-->
- <string name="lockpassword_confirm_your_pin_generic">Enter your device PIN to continue</string>
+ <string name="lockpassword_confirm_your_pin_generic">Enter your PIN to continue</string>
<!-- Message to be used to explain the users that they need to enter their password to continue a
particular operation. [CHAR LIMIT=70]-->
- <string name="lockpassword_confirm_your_password_generic">Enter your device password to continue</string>
+ <string name="lockpassword_confirm_your_password_generic">Enter your password to continue</string>
<!-- Message to be used to explain the users that they need to enter their work pattern to continue a
particular operation. [CHAR LIMIT=70]-->
- <string name="lockpassword_confirm_your_pattern_generic_profile">Use your work pattern to continue</string>
+ <string name="lockpassword_confirm_your_pattern_generic_profile">Draw your work pattern to continue</string>
<!-- Message to be used to explain the users that they need to enter their work PIN to continue a
particular operation. [CHAR LIMIT=70]-->
<string name="lockpassword_confirm_your_pin_generic_profile">Enter your work PIN to continue</string>
@@ -3479,6 +3528,18 @@
<!-- Checkbox label to set password as new screen lock if remote device credential validation succeeds. [CHAR LIMIT=43] -->
<string name="lockpassword_remote_validation_set_password_as_screenlock">Also use password to unlock this device</string>
+ <!-- Header shown when pattern needs to be solved before the device exits repair mode. [CHAR LIMIT=40] -->
+ <string name="lockpassword_confirm_repair_mode_pattern_header">Verify pattern</string>
+ <!-- Header shown when the pin needs to be solved before the device exits repair mode. [CHAR LIMIT=40] -->
+ <string name="lockpassword_confirm_repair_mode_pin_header">Verify PIN</string>
+ <!-- Header shown when the password needs to be solved before the device exits repair mode. [CHAR LIMIT=40] -->
+ <string name="lockpassword_confirm_repair_mode_password_header">Verify password</string>
+ <!-- An explanation text that the pattern needs to be solved before the device exits repair mode. [CHAR LIMIT=100] -->
+ <string name="lockpassword_confirm_repair_mode_pattern_details">Use your device pattern to continue</string>
+ <!-- An explanation text that the PIN needs to be solved before the device exits repair mode. [CHAR LIMIT=100] -->
+ <string name="lockpassword_confirm_repair_mode_pin_details">Enter your device PIN to continue</string>
+ <!-- An explanation text that the password needs to be solved before the device exits repair mode. [CHAR LIMIT=100] -->
+ <string name="lockpassword_confirm_repair_mode_password_details">Enter your device password to continue</string>
<!-- Security & location settings screen, change security method screen instruction if user
enters incorrect PIN [CHAR LIMIT=30] -->
@@ -4559,6 +4620,8 @@
</ol>
]]>
</string>
+ <!-- suffixed to click action texts "Double-tap to " -->
+ <string name="accessibility_action_label_panel_slice">enter settings</string>
<!-- Title for accessibility preference for configuring feature that performs click action soon after mouse/trackpad pointer stops moving. [CHAR LIMIT=NONE] -->
<string name="accessibility_autoclick_preference_title">Autoclick (dwell timing)</string>
<!-- Title for accessibility dwell timing footer. [CHAR LIMIT=NONE] -->
@@ -5507,6 +5570,8 @@
<string name="battery_usage_less_than_percent">< <xliff:g id="percentage">%1$s</xliff:g></string>
<!-- Process Stats strings -->
<skip />
+ <!-- Description of battery information footer text. [CHAR LIMIT=NONE] -->
+ <string name="battery_cycle_count_footer">Due to quality inspections before shipping, the cycle count may not be zero on first use</string>
<!-- [CHAR LIMIT=NONE] Activity title for Process Stats summary -->
<string name="process_stats_summary_title">Process Stats</string>
@@ -6401,7 +6466,7 @@
<!-- Search keywords for the "Delete Guest Activity" section in Multiple Users Screen. [CHAR LIMIT=NONE] -->
<string name="remove_guest_on_exit_keywords">delete, guest, activity, remove, data, visitor, erase</string>
<!-- Title of preference to enable guest calling[CHAR LIMIT=40] -->
- <string name="enable_guest_calling">Allow guest to use phone</string>
+ <string name="enable_guest_calling">Allow guest to make phone calls</string>
<!-- Summary of preference to enable guest calling [CHAR LIMIT=NONE] -->
<string name="enable_guest_calling_summary">Call history will be shared with guest user</string>
@@ -6940,7 +7005,7 @@
<!-- Summary text for system preference title, showing important setting items under system setting [CHAR LIMIT=NONE]-->
<string name="system_dashboard_summary">Languages, gestures, time, backup</string>
<!-- Summary text for language preference title, showing important setting items under language setting [CHAR LIMIT=NONE]-->
- <string name="languages_setting_summary">System languages, app languages, speech</string>
+ <string name="languages_setting_summary">System languages, app languages, regional preferences, speech</string>
<!--Search Keywords [CHAR LIMIT=NONE]-->
<string name="keywords_wifi">wifi, wi-fi, network connection, internet, wireless, data, wi fi</string>
@@ -7004,6 +7069,9 @@
<string name="keywords_app_pinning">screen pinning</string>
<string name="keywords_profile_challenge">work challenge, work, profile</string>
<string name="keywords_unification">work profile, managed profile, unify, unification, work, profile</string>
+ <string name="keywords_fold_lock_behavior">
+ awake, sleep, do not lock, stay unlocked on fold, folding, closing, fold, close, screen off
+ </string>
<string name="keywords_gesture">gestures</string>
<string name="keywords_wallet">wallet</string>
<string name="keywords_payment_settings">pay, tap, payments</string>
@@ -7018,6 +7086,7 @@
<string name="keywords_sim_status_iccid_esim">network, mobile network state, service state, signal strength, mobile network type, roaming, iccid, eid</string>
<string name="keywords_esim_eid">eid</string>
<string name="keywords_model_and_hardware">serial number, hardware version</string>
+ <string name="keywords_battery_info">battery info, manufacture date, cycle count, first use</string>
<string name="keywords_android_version">android security patch level, baseband version, kernel version</string>
<!-- Search keywords for dark mode settings [CHAR LIMIT=NONE] -->
<string name="keywords_dark_ui_mode">theme, light, dark, mode, light sensitivity, photophobia, make darker, darken, dark mode, migraine</string>
@@ -7185,7 +7254,7 @@
<string name="vibrate_when_ringing_option_ramping_ringer">Vibrate first then ring gradually</string>
<!-- Sound: Title for the option enabling spatializer effect. [CHAR LIMIT=30] -->
- <string name="spatial_audio_title">Spatial audio</string>
+ <string name="spatial_audio_title">Spatial Audio</string>
<!-- Sound: Other sounds: Title for the option enabling touch sounds for dial pad tones. [CHAR LIMIT=30] -->
<string name="dial_pad_tones_title">Dial pad tones</string>
@@ -7233,7 +7302,11 @@
<string name="live_caption_summary">Automatically caption media</string>
<!-- Output device type for the phone speaker that is available for spatializer effect. [CHAR LIMIT=NONE]-->
- <string name="spatial_audio_speaker">Phone speaker</string>
+ <string name="spatial_audio_speaker" product="default">Phone speakers</string>
+ <!-- Output device type for the phone speaker that is available for spatializer effect. [CHAR LIMIT=NONE]-->
+ <string name="spatial_audio_speaker" product="tablet">Tablet speakers</string>
+ <!-- Output device type for the phone speaker that is available for spatializer effect. [CHAR LIMIT=NONE]-->
+ <string name="spatial_audio_speaker" product="device">Device speakers</string>
<!-- Output device type for the wired headphones that is available for spatializer effect. [CHAR LIMIT=NONE]-->
<string name="spatial_audio_wired_headphones">Wired headphones</string>
@@ -9618,6 +9691,54 @@
<!-- Preference summary for battery usage list page[CHAR_LIMIT=50]-->
<string name="app_battery_usage_summary">Set battery usage for apps</string>
+ <!-- Label of action button in battery tips card [CHAR LIMIT=50] -->
+ <string name="battery_tips_card_action_button">View Settings</string>
+
+ <!-- Label of action button in battery tips card [CHAR LIMIT=50] -->
+ <string name="battery_tips_card_action_button_check">Check</string>
+
+ <!-- Label of dismiss button in battery tips card [CHAR LIMIT=50] -->
+ <string name="battery_tips_card_dismiss_button">Got it</string>
+
+ <!-- Feedback card message in battery tips card [CHAR LIMIT=NONE] -->
+ <string name="battery_tips_card_feedback_info">Is this message helpful?</string>
+
+ <!-- Content description for battery hints warning icon of app anomaly [CHAR LIMIT=NONE] -->
+ <string name="battery_hints_warning_icon_a11y">Battery tips warning icon</string>
+
+ <!-- Summary of settings anomaly for adaptive brightness [CHAR LIMIT=NONE] -->
+ <string name="battery_tips_settings_summary_brightness">Turn on adaptive brightness to extend battery life</string>
+
+ <!-- Summary of settings anomaly for screen timeout [CHAR LIMIT=NONE] -->
+ <string name="battery_tips_settings_summary_screen_timeout">Reduce screen timeout to extend battery life</string>
+
+ <!-- Summary of apps anomaly for always high [CHAR LIMIT=NONE] -->
+ <string name="battery_tips_apps_summary_always_high"><xliff:g id="app_label" example="Pokemon Go">%1$s</xliff:g> used more battery</string>
+
+ <!-- Summary of apps anomaly for higher than usual [CHAR LIMIT=NONE] -->
+ <string name="battery_tips_apps_summary_higher_than_usual"><xliff:g id="app_label" example="Pokemon Go">%1$s</xliff:g> used more battery than usual</string>
+
+ <!-- Summary of apps anomaly for always high in background [CHAR LIMIT=NONE] -->
+ <string name="battery_tips_apps_summary_always_high_in_background"><xliff:g id="app_label" example="Pokemon Go">%1$s</xliff:g> used more battery while in the background</string>
+
+ <!-- Summary of apps anomaly for higher than usual in background [CHAR LIMIT=NONE] -->
+ <string name="battery_tips_apps_summary_higher_than_usual_in_background"><xliff:g id="app_label" example="Pokemon Go">%1$s</xliff:g> used more battery than usual while in the background</string>
+
+ <!-- Summary of apps anomaly for always high in foreground [CHAR LIMIT=NONE] -->
+ <string name="battery_tips_apps_summary_always_high_in_foreground"><xliff:g id="app_label" example="Pokemon Go">%1$s</xliff:g> used more battery while in the foreground</string>
+
+ <!-- Summary of apps anomaly for higher than usual in foreground [CHAR LIMIT=NONE] -->
+ <string name="battery_tips_apps_summary_higher_than_usual_in_foreground"><xliff:g id="app_label" example="Pokemon Go">%1$s</xliff:g> used more battery than usual while in the foreground</string>
+
+ <!-- Label of hint for apps anomaly in battery usage [CHAR LIMIT=NONE] -->
+ <string name="battery_app_item_hint">High battery usage</string>
+
+ <!-- Label of hint for apps background anomaly in battery usage [CHAR LIMIT=NONE] -->
+ <string name="battery_app_item_hint_in_bg">High battery usage in the background</string>
+
+ <!-- Label of hint for apps foreground anomaly in battery usage [CHAR LIMIT=NONE] -->
+ <string name="battery_app_item_hint_in_fg">High battery usage in the foreground</string>
+
<!-- Filter title for battery unrestricted[CHAR_LIMIT=50]-->
<string name="filter_battery_unrestricted_title">Unrestricted</string>
@@ -9701,12 +9822,6 @@
<!-- [CHAR_LIMIT=60] Label for special access screen -->
<string name="special_access">Special app access</string>
- <!-- Summary for special access settings [CHAR_LIMIT=NONE] -->
- <plurals name="special_access_summary">
- <item quantity="one">1 app can use unrestricted data</item>
- <item quantity="other"><xliff:g id="count" example="10">%d</xliff:g> apps can use unrestricted data</item>
- </plurals>
-
<!-- Title for the See more preference item in Special app access settings [CHAR LIMIT=30] -->
<string name="special_access_more">See more</string>
@@ -9864,7 +9979,7 @@
<!-- Title text for edge to edge navigation [CHAR LIMIT=60] -->
<string name="edge_to_edge_navigation_title">Gesture navigation</string>
<!-- Summary text for edge to edge navigation [CHAR LIMIT=NONE] -->
- <string name="edge_to_edge_navigation_summary">To go Home, swipe up from the bottom of the screen. To switch apps, swipe up from the bottom, hold, then release. To go back, swipe from either the left or right edge.</string>
+ <string name="edge_to_edge_navigation_summary">To go home, swipe up from the bottom of the screen. To switch apps, swipe up from the bottom, hold, then release. To go back, swipe from either the left or right edge.</string>
<!-- Title text for 3-button navigation [CHAR LIMIT=60] -->
<string name="legacy_navigation_title">3-button navigation</string>
@@ -10523,8 +10638,6 @@
<string name="platform_compat_default_disabled_title">Default disabled changes</string>
<!-- Title for target SDK gated app compat changes category (do not translate 'targetSdkVersion') [CHAR LIMIT=50] -->
<string name="platform_compat_target_sdk_title">Enabled for targetSdkVersion >= <xliff:g id="number" example="29">%d</xliff:g></string>
- <!-- Title for the dialog shown when no debuggable apps are available [CHAR LIMIT=30] -->
- <string name="platform_compat_dialog_title_no_apps">No apps available</string>
<!-- Explanatory text shown when no debuggable apps are available [CHAR LIMIT=NONE] -->
<string name="platform_compat_dialog_text_no_apps">App compatibility changes can only be modified for debuggable apps. Install a debuggable app and try again.</string>
@@ -11313,10 +11426,10 @@
<string name="lockscreen_trivial_controls_setting_toggle">Use device controls</string>
<!-- Trivial Device disabled controls summary [CHAR LIMIT=NONE] -->
<string name="lockscreen_trivial_disabled_controls_summary">To use, first turn on \u0022Show device controls\u0022</string>
- <!-- Lockscreen double-line clock summary [CHAR LIMIT=NONE] -->
- <string name="lockscreen_double_line_clock_summary">Show double-line clock when available</string>
- <!-- Lockscreen double-line clock toggle [CHAR LIMIT=60] -->
- <string name="lockscreen_double_line_clock_setting_toggle">Double-line clock</string>
+ <!-- Lockscreen dynamic clock summary [CHAR LIMIT=NONE] -->
+ <string name="lockscreen_double_line_clock_summary">Clock size changes according to lock screen content</string>
+ <!-- Lockscreen dynamic clock toggle [CHAR LIMIT=60] -->
+ <string name="lockscreen_double_line_clock_setting_toggle">Dynamic clock</string>
<!-- Lock screen shortcuts preference [CHAR LIMIT=60] -->
<string name="lockscreen_quick_affordances_title">Shortcuts</string>
<!-- Summary for the lock screen button preference [CHAR LIMIT=60] -->
@@ -11966,6 +12079,26 @@
<!-- The summary of the head tracking [CHAR LIMIT=none] -->
<string name="bluetooth_details_head_tracking_summary">Audio changes as you move your head to sound more natural</string>
+ <!-- The title of CDM Permissions Sync -->
+ <string name="bluetooth_details_permissions_sync_title">Sync permissions</string>
+ <!-- The summary of CDM Permissions Sync -->
+ <string name="bluetooth_details_permissions_sync_summary">Give <xliff:g id="remote_device_name" example="Pixel Watch">%1$s</xliff:g> the same app permissions that you’ve allowed on <xliff:g id="local_device_name" example="Pixel 6">%2$s</xliff:g></string>
+
+ <!-- The title of the bluetooth audio device type selection [CHAR LIMIT=none] -->
+ <string name="bluetooth_details_audio_device_types_title">Audio Device Type</string>
+ <!-- The audio device type corresponding to unknown device type [CHAR LIMIT=none] -->
+ <string name="bluetooth_details_audio_device_type_unknown">Unknown</string>
+ <!-- The audio device type corresponding to speakers [CHAR LIMIT=none] -->
+ <string name="bluetooth_details_audio_device_type_speaker">Speaker</string>
+ <!-- The audio device type corresponding to headphones [CHAR LIMIT=none] -->
+ <string name="bluetooth_details_audio_device_type_headphones">Headphones</string>
+ <!-- The audio device type corresponding to hearing aid [CHAR LIMIT=none] -->
+ <string name="bluetooth_details_audio_device_type_hearing_aid">Hearing Aid</string>
+ <!-- The audio device type corresponding to car kit [CHAR LIMIT=none] -->
+ <string name="bluetooth_details_audio_device_type_carkit">Car Kit</string>
+ <!-- The audio device type corresponding to other device type [CHAR LIMIT=none] -->
+ <string name="bluetooth_details_audio_device_type_other">Other</string>
+
<!-- Developer Settings: Title for network bandwidth ingress rate limit [CHAR LIMIT=none] -->
<string name="ingress_rate_limit_title">Network download rate limit</string>
<!-- Developer Settings: Summary for network bandwidth ingress rate limit [CHAR LIMIT=none] -->
@@ -11983,7 +12116,7 @@
<!-- Developer settings: Title for force enabling Notes role. [CHAR LIMIT=50]-->
<string name="enable_notes_role_title">Force enable Notes role</string>
<!-- Developer settings: Summary for disabling phantom process monitoring. [CHAR LIMIT=NONE]-->
- <string name="enable_notes_role_summary">Enable note-taking system integrations via the Notes role. If the Notes role is already enabled, does nothing.</string>
+ <string name="enable_notes_role_summary">Enable note-taking system integrations via the Notes role. If the Notes role is already enabled, does nothing. Requires reboot.</string>
<!-- BT LE Audio Device: Media Broadcast -->
@@ -12057,6 +12190,54 @@
other {Apps installed more than # months ago}
}</string>
+ <!-- App Aspect Ratio (User Aspect Ratio Override) -->
+ <!-- [CHAR LIMIT=60] Aspect ratio title setting to choose app aspect ratio -->
+ <string name="aspect_ratio_title">Aspect ratio</string>
+ <!-- [CHAR LIMIT=NONE] Aspect ratio setting summary to choose aspect ratio for apps unoptimized for device -->
+ <string name="aspect_ratio_summary">Try a new aspect ratio to view this app if it hasn\'t been designed to fit your <xliff:g id="device_name">%1$s</xliff:g></string>
+ <!-- [CHAR LIMIT=NONE] Aspect ratio setting main summary on page to choose aspect ratio for apps unoptimized for device -->
+ <string name="aspect_ratio_main_summary">Try a new aspect ratio to view this app if it hasn\'t been designed to fit your <xliff:g id="device_name">%1$s</xliff:g>. Some apps may not be optimized for certain aspect ratios.</string>
+ <!-- [CHAR LIMIT=NONE] Aspect ratio setting summary to choose aspect ratio for apps unoptimized for device -->
+ <string name="aspect_ratio_summary_text">Try a new aspect ratio to view an app if it hasn\'t been designed to fit your <xliff:g id="device_name">%1$s</xliff:g></string>
+ <!-- [CHAR LIMIT=NONE] Aspect ratio setting main summary on page to choose aspect ratio for apps unoptimized for device -->
+ <string name="aspect_ratio_main_summary_text">Try a new aspect ratio to view an app if it hasn\'t been designed to fit your <xliff:g id="device_name">%1$s</xliff:g>. Some apps may not be optimized for certain aspect ratios.</string>
+ <!-- [CHAR LIMIT=NONE] Aspect ratio suggested apps filter label -->
+ <string name="user_aspect_ratio_suggested_apps_label">Suggested apps</string>
+ <!-- [CHAR LIMIT=14] Filter label for apps that have user aspect ratio changed -->
+ <string name="user_aspect_ratio_changed_apps_label">Changed apps</string>
+ <!-- [CHAR LIMIT=NONE] App default aspect ratio entry -->
+ <string name="user_aspect_ratio_app_default">App default</string>
+ <!-- [CHAR LIMIT=NONE] Fullscreen aspect ratio entry -->
+ <string name="user_aspect_ratio_fullscreen">Full screen</string>
+ <!-- [CHAR LIMIT=NONE] Half screen aspect ratio entry -->
+ <string name="user_aspect_ratio_half_screen">Half screen</string>
+ <!-- [CHAR LIMIT=NONE] Device display size aspect ratio entry -->
+ <string name="user_aspect_ratio_device_size">Device aspect ratio</string>
+ <!-- [CHAR LIMIT=NONE] 16:9 aspect ratio entry -->
+ <string name="user_aspect_ratio_16_9">16:9</string>
+ <!-- [CHAR LIMIT=NONE] 3:2 aspect ratio entry -->
+ <string name="user_aspect_ratio_3_2">3:2</string>
+ <!-- [CHAR LIMIT=NONE] 4:3 aspect ratio entry -->
+ <string name="user_aspect_ratio_4_3">4:3</string>
+ <!-- [CHAR LIMIT=NONE] Aspect ratio a11y message announced to replace colon in aspect ratio entry e.g. 3 by 2 -->
+ <string name="user_aspect_ratio_option_a11y"><xliff:g id="numerator">%1$s</xliff:g> by <xliff:g id="denominator">%2$s</xliff:g></string>
+ <!-- [CHAR LIMIT=NONE] Warning description for app info aspect ratio page -->
+ <string name="app_aspect_ratio_footer">The app will restart when you change aspect ratio. You may lose unsaved changes. Some apps may not be optimized for certain aspect ratios.</string>
+
+ <!-- TODO(b/300219974): Change aspect ratio title and clean up unused titles -->
+ <!-- [CHAR LIMIT=60] Aspect ratio experimental title settings to choose app aspect ratio -->
+ <string name="aspect_ratio_experimental_title">Aspect ratio (experimental)</string>
+ <!-- [CHAR LIMIT=60] Aspect ratio experiment title settings to choose app aspect ratio -->
+ <string name="aspect_ratio_experiment_title">Aspect ratio (experiment)</string>
+ <!-- [CHAR LIMIT=60] Aspect ratio labs title settings to choose app aspect ratio -->
+ <string name="aspect_ratio_labs_title">Aspect ratio (labs)</string>
+ <!-- [CHAR LIMIT=60] Aspect ratio experimental title label -->
+ <string name="aspect_ratio_experimental_label">Experimental</string>
+ <!-- [CHAR LIMIT=60] Aspect ratio experiment title label -->
+ <string name="aspect_ratio_experiment_label">Experiment</string>
+ <!-- [CHAR LIMIT=60] Aspect ratio labs title label -->
+ <string name="aspect_ratio_labs_label">Labs</string>
+
<!-- Accessibility label for fingerprint sensor [CHAR LIMIT=NONE] -->
<string name="accessibility_fingerprint_label">Fingerprint sensor</string>
diff --git a/res/values/styles.xml b/res/values/styles.xml
index fe15226..ee78a45 100644
--- a/res/values/styles.xml
+++ b/res/values/styles.xml
@@ -947,4 +947,10 @@
<item name="biometricsEnrollProgressHelp">@color/udfps_enroll_progress_help</item>
<item name="biometricsEnrollProgressHelpWithTalkback">@color/udfps_enroll_progress_help_with_talkback</item>
</style>
+
+ <style name="ScreenLockPasswordHintTextFontStyle">
+ <item name="android:textColor">?android:attr/textColorPrimary</item>
+ <item name="android:fontFamily">google-sans-text</item>
+ </style>
+
</resources>
diff --git a/res/xml/apps.xml b/res/xml/apps.xml
index ae51bae..c4313f6 100644
--- a/res/xml/apps.xml
+++ b/res/xml/apps.xml
@@ -105,7 +105,25 @@
android:key="special_access"
android:fragment="com.android.settings.applications.specialaccess.SpecialAccessSettings"
android:title="@string/special_access"
- android:order="20"
- settings:controller="com.android.settings.applications.SpecialAppAccessPreferenceController"/>
+ android:order="20"/>
+
+ <PreferenceCategory
+ android:key="advanced_category"
+ android:title="@string/advanced_apps"
+ android:order="21"
+ settings:searchable="false">
+
+ <Preference
+ android:key="aspect_ratio_apps"
+ android:title="@string/aspect_ratio_experimental_title"
+ android:summary="@string/summary_placeholder"
+ android:order="22"
+ settings:controller="com.android.settings.applications.appcompat.UserAspectRatioAppsPreferenceController"
+ android:fragment="com.android.settings.applications.manageapplications.ManageApplications">
+ <extra android:name="classname"
+ android:value="com.android.settings.Settings$UserAspectRatioAppListActivity"/>
+ <intent android:action="android.settings.MANAGE_USER_ASPECT_RATIO_SETTINGS"/>
+ </Preference>
+ </PreferenceCategory>
</PreferenceScreen>
diff --git a/res/xml/battery_info.xml b/res/xml/battery_info.xml
new file mode 100644
index 0000000..8e3c31f
--- /dev/null
+++ b/res/xml/battery_info.xml
@@ -0,0 +1,50 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2023 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<PreferenceScreen
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:settings="http://schemas.android.com/apk/res-auto"
+ android:title="@string/battery_info"
+ settings:keywords="@string/keywords_battery_info">
+
+ <Preference
+ android:key="battery_info_manufacture_date"
+ android:title="@string/battery_manufacture_date"
+ android:summary="@string/summary_placeholder"
+ settings:controller="com.android.settings.deviceinfo.batteryinfo.BatteryManufactureDatePreferenceController"
+ settings:enableCopying="true"/>
+
+ <Preference
+ android:key="battery_info_first_use_date"
+ android:title="@string/battery_first_use_date"
+ android:summary="@string/summary_placeholder"
+ settings:controller="com.android.settings.deviceinfo.batteryinfo.BatteryFirstUseDatePreferenceController"
+ settings:enableCopying="true"/>
+
+ <Preference
+ android:key="battery_info_cycle_count"
+ android:title="@string/battery_cycle_count"
+ android:summary="@string/summary_placeholder"
+ settings:controller="com.android.settings.deviceinfo.batteryinfo.BatteryCycleCountPreferenceController"
+ settings:enableCopying="true"/>
+
+ <com.android.settingslib.widget.FooterPreference
+ android:key="battery_info_footer"
+ android:title="@string/battery_cycle_count_footer"
+ android:selectable="false"
+ settings:searchable="false" />
+</PreferenceScreen>
diff --git a/res/xml/bluetooth_device_details_fragment.xml b/res/xml/bluetooth_device_details_fragment.xml
index 35359f7..12ed8eb 100644
--- a/res/xml/bluetooth_device_details_fragment.xml
+++ b/res/xml/bluetooth_device_details_fragment.xml
@@ -72,6 +72,9 @@
android:key="device_controls_general" />
<PreferenceCategory
+ android:key="bluetooth_audio_device_type_group"/>
+
+ <PreferenceCategory
android:key="spatial_audio_group"/>
<PreferenceCategory
@@ -89,6 +92,9 @@
settings:controller="com.android.settings.accessibility.LiveCaptionPreferenceController"/>
</PreferenceCategory>
+ <PreferenceCategory
+ android:key="data_sync_group"/>
+
<Preference
android:key="keyboard_settings"
android:persistent="false"
diff --git a/res/xml/development_settings.xml b/res/xml/development_settings.xml
index 68e4e78..32acac6 100644
--- a/res/xml/development_settings.xml
+++ b/res/xml/development_settings.xml
@@ -258,7 +258,7 @@
android:key="platform_compat_dashboard"
android:title="@string/platform_compat_dashboard_title"
android:summary="@string/platform_compat_dashboard_summary"
- android:fragment="com.android.settings.development.compat.PlatformCompatDashboard"
+ settings:controller="com.android.settings.spa.development.compat.PlatformCompatPreferenceController"
/>
<SwitchPreference
@@ -464,6 +464,11 @@
android:title="@string/pointer_location"
android:summary="@string/pointer_location_summary" />
+ <SwitchPreference
+ android:key="show_key_presses"
+ android:title="@string/show_key_presses"
+ android:summary="@string/show_key_presses_summary" />
+
</PreferenceCategory>
<PreferenceCategory
diff --git a/res/xml/display_settings.xml b/res/xml/display_settings.xml
index ad5236e..c5e559c 100644
--- a/res/xml/display_settings.xml
+++ b/res/xml/display_settings.xml
@@ -49,6 +49,13 @@
settings:controller="com.android.settings.security.screenlock.LockScreenPreferenceController"/>
<com.android.settingslib.RestrictedPreference
+ android:fragment="com.android.settings.display.FoldLockBehaviorSettings"
+ android:key="fold_lock_behavior"
+ android:title="@string/fold_lock_behavior_title"
+ settings:controller="com.android.settings.display.FoldLockBehaviorPreferenceController"
+ settings:keywords="@string/keywords_fold_lock_behavior" />
+
+ <com.android.settingslib.RestrictedPreference
android:key="screen_timeout"
android:title="@string/screen_timeout"
android:summary="@string/summary_placeholder"
diff --git a/res/xml/fold_lock_behavior_settings.xml b/res/xml/fold_lock_behavior_settings.xml
new file mode 100644
index 0000000..46a8917
--- /dev/null
+++ b/res/xml/fold_lock_behavior_settings.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2023 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<PreferenceScreen
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:title="@string/fold_lock_behavior_title"/>
diff --git a/res/xml/languages.xml b/res/xml/languages.xml
index 0f45540..5269d99 100644
--- a/res/xml/languages.xml
+++ b/res/xml/languages.xml
@@ -18,7 +18,7 @@
<PreferenceScreen
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:settings="http://schemas.android.com/apk/res-auto"
- android:title="@string/language_settings">
+ android:title="@string/language_picker_title">
<com.android.settingslib.widget.TopIntroPreference
android:title="@string/desc_introduction_of_language_picker"
diff --git a/res/xml/modifier_keys_settings.xml b/res/xml/modifier_keys_settings.xml
index 63e7ee1..25525ae 100644
--- a/res/xml/modifier_keys_settings.xml
+++ b/res/xml/modifier_keys_settings.xml
@@ -21,25 +21,22 @@
android:title="@string/modifier_keys_settings"
android:key="modifier_keys_all"
settings:controller="com.android.settings.inputmethod.ModifierKeysPreferenceController">
- <Preference
+
+ <com.android.settingslib.widget.LayoutPreference
android:key="modifier_keys_caps_lock"
- android:title="@string/modifier_keys_caps_lock"
- android:summary="@string/modifier_keys_default_summary"/>
+ android:layout="@layout/modifier_keys_custom_key" />
- <Preference
+ <com.android.settingslib.widget.LayoutPreference
android:key="modifier_keys_ctrl"
- android:title="@string/modifier_keys_ctrl"
- android:summary="@string/modifier_keys_default_summary"/>
+ android:layout="@layout/modifier_keys_custom_key" />
- <Preference
+ <com.android.settingslib.widget.LayoutPreference
android:key="modifier_keys_meta"
- android:title="@string/modifier_keys_meta"
- android:summary="@string/modifier_keys_default_summary"/>
+ android:layout="@layout/modifier_keys_custom_key" />
- <Preference
+ <com.android.settingslib.widget.LayoutPreference
android:key="modifier_keys_alt"
- android:title="@string/modifier_keys_alt"
- android:summary="@string/modifier_keys_default_summary"/>
+ android:layout="@layout/modifier_keys_custom_key" />
<Preference
android:key="modifier_keys_restore"
diff --git a/res/xml/my_device_info.xml b/res/xml/my_device_info.xml
index 4cbe13f..6576742 100644
--- a/res/xml/my_device_info.xml
+++ b/res/xml/my_device_info.xml
@@ -144,6 +144,14 @@
android:summary="@string/summary_placeholder"
android:fragment="com.android.settings.deviceinfo.firmwareversion.FirmwareVersionSettings"
settings:controller="com.android.settings.deviceinfo.firmwareversion.FirmwareVersionPreferenceController"/>
+
+ <!-- Battery information -->
+ <Preference
+ android:key="battery_info"
+ android:order="43"
+ android:title="@string/battery_info"
+ android:fragment="com.android.settings.deviceinfo.batteryinfo.BatteryInfoFragment"
+ settings:keywords="@string/keywords_battery_info"/>
</PreferenceCategory>
<PreferenceCategory
diff --git a/res/xml/power_usage_advanced.xml b/res/xml/power_usage_advanced.xml
index 2a1a23c..c129453 100644
--- a/res/xml/power_usage_advanced.xml
+++ b/res/xml/power_usage_advanced.xml
@@ -21,6 +21,18 @@
android:title="@string/advanced_battery_title"
settings:keywords="@string/keywords_battery_usage">
+ <PreferenceCategory
+ android:key="battery_tips_category"
+ settings:controller=
+ "com.android.settings.fuelgauge.batteryusage.BatteryTipsController"
+ settings:isPreferenceVisible="false">
+
+ <com.android.settings.fuelgauge.batteryusage.BatteryTipsCardPreference
+ android:key="battery_tips_card"
+ settings:isPreferenceVisible="false" />
+
+ </PreferenceCategory>
+
<com.android.settings.fuelgauge.batteryusage.BatteryHistoryPreference
android:key="battery_chart"
settings:controller=
diff --git a/res/xml/reset_dashboard_fragment.xml b/res/xml/reset_dashboard_fragment.xml
index 3bd7a13..08852c9 100644
--- a/res/xml/reset_dashboard_fragment.xml
+++ b/res/xml/reset_dashboard_fragment.xml
@@ -57,5 +57,13 @@
settings:keywords="@string/keywords_factory_data_reset"
settings:userRestriction="no_factory_reset"
settings:useAdminDisabledSummary="true"
+ settings:controller="com.android.settings.system.FactoryResetPreferenceController"
+ android:fragment="com.android.settings.MainClear" />
+
+ <Preference
+ android:key="factory_reset_demo_user"
+ android:title="@string/main_clear_title"
+ settings:keywords="@string/keywords_factory_data_reset"
+ settings:controller="com.android.settings.system.FactoryResetDemoUserPreferenceController"
android:fragment="com.android.settings.MainClear" />
</PreferenceScreen>
diff --git a/res/xml/security_settings_fingerprint_limbo.xml b/res/xml/security_settings_fingerprint_limbo.xml
new file mode 100644
index 0000000..02a3dfb
--- /dev/null
+++ b/res/xml/security_settings_fingerprint_limbo.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<PreferenceScreen
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:settings="http://schemas.android.com/apk/res-auto"
+ android:title="@string/security_settings_fingerprint_preference_title">
+
+ <PreferenceCategory
+ android:key="security_settings_fingerprints_enrolled"
+ settings:controller="com.android.settings.biometrics.fingerprint.FingerprintsEnrolledCategoryPreferenceController">
+ </PreferenceCategory>
+
+ <androidx.preference.Preference
+ android:icon="@drawable/ic_add_24dp"
+ android:key="key_fingerprint_add"
+ android:title="@string/fingerprint_add_title" />
+
+ <PreferenceCategory
+ android:key="security_settings_fingerprint_unlock_category"
+ android:title="@string/security_settings_fingerprint_settings_preferences_category"
+ android:visibility="gone">
+
+ <com.android.settingslib.RestrictedSwitchPreference
+ android:key="security_settings_require_screen_on_to_auth"
+ android:title="@string/security_settings_require_screen_on_to_auth_title"
+ android:summary="@string/security_settings_require_screen_on_to_auth_description"
+ settings:keywords="@string/security_settings_require_screen_on_to_auth_keywords"
+ settings:controller="com.android.settings.biometrics.fingerprint.FingerprintSettingsRequireScreenOnToAuthPreferenceController" />
+ </PreferenceCategory>
+
+ <PreferenceCategory
+ android:key="security_settings_fingerprint_footer">
+ </PreferenceCategory>
+
+</PreferenceScreen>
+
diff --git a/res/xml/shortcuts.xml b/res/xml/shortcuts.xml
index afb2104..058d22e 100644
--- a/res/xml/shortcuts.xml
+++ b/res/xml/shortcuts.xml
@@ -24,6 +24,7 @@
<shortcut
android:shortcutId="manifest-shortcut-data-usage"
android:icon="@drawable/ic_shortcut_data_usage"
+ android:enabled="@bool/config_show_sim_info"
android:shortcutShortLabel="@string/data_usage_summary_title">
<intent
android:action="android.intent.action.MAIN"
diff --git a/res/xml/stylus_usi_details_fragment.xml b/res/xml/stylus_usi_details_fragment.xml
index 8a1d036..639c284 100644
--- a/res/xml/stylus_usi_details_fragment.xml
+++ b/res/xml/stylus_usi_details_fragment.xml
@@ -30,4 +30,7 @@
<PreferenceCategory
android:key="device_stylus"/>
+ <PreferenceCategory
+ android:key="stylus_usb_firmware"
+ settings:controller="com.android.settings.connecteddevice.stylus.StylusUsbFirmwareController"/>
</PreferenceScreen>
\ No newline at end of file
diff --git a/res/xml/user_aspect_ratio_details.xml b/res/xml/user_aspect_ratio_details.xml
new file mode 100644
index 0000000..44a7589
--- /dev/null
+++ b/res/xml/user_aspect_ratio_details.xml
@@ -0,0 +1,69 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<PreferenceScreen
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:settings="http://schemas.android.com/apk/res-auto"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:title="@string/aspect_ratio_experimental_title">
+
+ <com.android.settingslib.widget.TopIntroPreference
+ android:key="app_aspect_ratio_summary"
+ android:order="-1001"
+ android:title="@string/summary_placeholder"
+ settings:searchable="false"/>
+
+ <com.android.settingslib.widget.ActionButtonsPreference
+ android:key="header_view" />
+
+ <com.android.settings.applications.appcompat.RadioWithImagePreference
+ android:key="app_default_pref"
+ android:title="@string/user_aspect_ratio_app_default"/>
+
+ <com.android.settings.applications.appcompat.RadioWithImagePreference
+ android:key="fullscreen_pref"
+ android:title="@string/user_aspect_ratio_fullscreen"
+ android:icon="@drawable/ic_app_aspect_ratio_fullscreen"/>
+
+ <com.android.settings.applications.appcompat.RadioWithImagePreference
+ android:key="half_screen_pref"
+ android:title="@string/user_aspect_ratio_half_screen"
+ android:icon="@drawable/ic_app_aspect_ratio_half_screen"/>
+
+ <com.android.settings.applications.appcompat.RadioWithImagePreference
+ android:key="display_size_pref"
+ android:title="@string/user_aspect_ratio_device_size"
+ android:icon="@drawable/ic_app_aspect_ratio_display_size"/>
+
+ <com.android.settings.applications.appcompat.RadioWithImagePreference
+ android:key="16_9_pref"
+ android:icon="@drawable/ic_app_aspect_ratio_16_9"/>
+
+ <com.android.settings.applications.appcompat.RadioWithImagePreference
+ android:key="4_3_pref"
+ android:icon="@drawable/ic_app_aspect_ratio_4_3"/>
+
+ <com.android.settings.applications.appcompat.RadioWithImagePreference
+ android:key="3_2_pref"
+ android:icon="@drawable/ic_app_aspect_ratio_3_2"/>
+
+ <com.android.settingslib.widget.FooterPreference
+ android:title="@string/app_aspect_ratio_footer"
+ android:selectable="false"
+ settings:searchable="false"/>
+
+</PreferenceScreen>
diff --git a/res/layout/wifi_api_test.xml b/res/xml/wifi_api_test.xml
similarity index 100%
rename from res/layout/wifi_api_test.xml
rename to res/xml/wifi_api_test.xml
diff --git a/res/xml/wifi_network_details_fragment2.xml b/res/xml/wifi_network_details_fragment2.xml
index eb9add1..0062474 100644
--- a/res/xml/wifi_network_details_fragment2.xml
+++ b/res/xml/wifi_network_details_fragment2.xml
@@ -40,6 +40,31 @@
android:key="buttons"
android:selectable="false"/>
+ <!-- Hotspot device details category -->
+ <PreferenceCategory
+ android:key="hotspot_device_details_category"
+ android:title="@string/hotspot_device_details_category"
+ settings:isPreferenceVisible="false">
+ <Preference
+ android:key="hotspot_device_details_internet_source"
+ android:title="@string/hotspot_device_details_internet_source"
+ android:selectable="false"
+ settings:enableCopying="true"/>
+ <Preference
+ android:key="hotspot_device_details_battery"
+ android:icon="@drawable/ic_battery_full"
+ android:title="@string/power_usage_summary_title"
+ android:selectable="false"
+ settings:enableCopying="true"/>
+ </PreferenceCategory>
+
+ <!-- Hotspot connection category -->
+ <PreferenceCategory
+ android:key="hotspot_connection_category"
+ android:title="@string/hotspot_connection_category"
+ settings:isPreferenceVisible="false">
+ </PreferenceCategory>
+
<!-- General Details Preferences -->
<Preference
android:key="signal_strength"
diff --git a/res/xml/wifi_tether_settings.xml b/res/xml/wifi_tether_settings.xml
index a85d9ea..b8b810f 100644
--- a/res/xml/wifi_tether_settings.xml
+++ b/res/xml/wifi_tether_settings.xml
@@ -59,4 +59,10 @@
android:summary="@string/summary_placeholder"
android:fragment="com.android.settings.wifi.tether.WifiHotspotSpeedSettings"
settings:isPreferenceVisible="@bool/config_show_wifi_hotspot_speed"/>
+
+ <Preference
+ android:key="wifi_hotspot_instant"
+ android:title="@string/wifi_hotspot_instant_title"
+ android:summary="@string/summary_placeholder"
+ settings:isPreferenceVisible="false"/>
</PreferenceScreen>
diff --git a/src/com/android/settings/MainClear.java b/src/com/android/settings/MainClear.java
index f706c78..8a441e2 100644
--- a/src/com/android/settings/MainClear.java
+++ b/src/com/android/settings/MainClear.java
@@ -569,7 +569,7 @@
UserHandle.myUserId());
if (disallow && !Utils.isDemoUser(context)) {
return inflater.inflate(R.layout.main_clear_disallowed_screen, null);
- } else if (admin != null) {
+ } else if (admin != null && !Utils.isDemoUser(context)) {
new ActionDisabledByAdminDialogHelper(getActivity())
.prepareDialogBuilder(UserManager.DISALLOW_FACTORY_RESET, admin)
.setOnDismissListener(__ -> getActivity().finish())
diff --git a/src/com/android/settings/RegulatoryInfoDisplayActivity.java b/src/com/android/settings/RegulatoryInfoDisplayActivity.java
deleted file mode 100644
index 8f65051..0000000
--- a/src/com/android/settings/RegulatoryInfoDisplayActivity.java
+++ /dev/null
@@ -1,171 +0,0 @@
-/*
- * Copyright (C) 2013 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.settings;
-
-import android.app.Activity;
-import android.content.DialogInterface;
-import android.content.res.Resources;
-import android.graphics.Bitmap;
-import android.graphics.BitmapFactory;
-import android.graphics.drawable.Drawable;
-import android.os.Bundle;
-import android.os.SystemProperties;
-import android.text.TextUtils;
-import android.view.Gravity;
-import android.view.View;
-import android.widget.ImageView;
-import android.widget.TextView;
-
-import androidx.annotation.VisibleForTesting;
-import androidx.appcompat.app.AlertDialog;
-
-import java.util.Locale;
-
-/**
- * {@link Activity} that displays regulatory information for the "Regulatory information"
- * preference item, and when "*#07#" is dialed on the Phone keypad. To enable this feature,
- * set the "config_show_regulatory_info" boolean to true in a device overlay resource, and in the
- * same overlay, either add a drawable named "regulatory_info.png" containing a graphical version
- * of the required regulatory info (If ro.bootloader.hardware.sku property is set use
- * "regulatory_info_<sku>.png where sku is ro.bootloader.hardware.sku property value in lowercase"),
- * or add a string resource named "regulatory_info_text" with an HTML version of the required
- * information (text will be centered in the dialog).
- */
-public class RegulatoryInfoDisplayActivity extends Activity implements
- DialogInterface.OnDismissListener {
-
- private final String REGULATORY_INFO_RESOURCE = "regulatory_info";
- private static final String DEFAULT_REGULATORY_INFO_FILEPATH =
- "/data/misc/elabel/regulatory_info.png";
- private static final String REGULATORY_INFO_FILEPATH_TEMPLATE =
- "/data/misc/elabel/regulatory_info_%s.png";
-
- /**
- * Display the regulatory info graphic in a dialog window.
- */
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- AlertDialog.Builder builder = new AlertDialog.Builder(this)
- .setTitle(R.string.regulatory_labels)
- .setOnDismissListener(this)
- .setPositiveButton(android.R.string.ok, null /* onClickListener */);
-
- boolean regulatoryInfoDrawableExists = false;
-
- final String regulatoryInfoFile = getRegulatoryInfoImageFileName();
- final Bitmap regulatoryInfoBitmap = BitmapFactory.decodeFile(regulatoryInfoFile);
-
- if (regulatoryInfoBitmap != null) {
- regulatoryInfoDrawableExists = true;
- }
-
- int resId = 0;
- if (!regulatoryInfoDrawableExists) {
- resId = getResourceId();
- }
- if (resId != 0) {
- try {
- Drawable d = getDrawable(resId);
- // set to false if the width or height is <= 2
- // (missing PNG can return an empty 2x2 pixel Drawable)
- regulatoryInfoDrawableExists = (d.getIntrinsicWidth() > 2
- && d.getIntrinsicHeight() > 2);
- } catch (Resources.NotFoundException ignored) {
- regulatoryInfoDrawableExists = false;
- }
- }
-
- CharSequence regulatoryText = getResources()
- .getText(R.string.regulatory_info_text);
-
- if (regulatoryInfoDrawableExists) {
- View view = getLayoutInflater().inflate(R.layout.regulatory_info, null);
- ImageView image = view.findViewById(R.id.regulatoryInfo);
- if (regulatoryInfoBitmap != null) {
- image.setImageBitmap(regulatoryInfoBitmap);
- } else {
- image.setImageResource(resId);
- }
- builder.setView(view);
- builder.show();
- } else if (regulatoryText.length() > 0) {
- builder.setMessage(regulatoryText);
- AlertDialog dialog = builder.show();
- // we have to show the dialog first, or the setGravity() call will throw a NPE
- TextView messageText = (TextView) dialog.findViewById(android.R.id.message);
- messageText.setGravity(Gravity.CENTER);
- } else {
- // neither drawable nor text resource exists, finish activity
- finish();
- }
- }
-
- @VisibleForTesting
- int getResourceId() {
- // Use regulatory_info by default.
- int resId = getResources().getIdentifier(
- REGULATORY_INFO_RESOURCE, "drawable", getPackageName());
-
- // When hardware sku property exists, use regulatory_info_<sku> resource if valid.
- final String sku = getSku();
- if (!TextUtils.isEmpty(sku)) {
- String regulatory_info_res = REGULATORY_INFO_RESOURCE + "_" + sku.toLowerCase();
- int id = getResources().getIdentifier(
- regulatory_info_res, "drawable", getPackageName());
- if (id != 0) {
- resId = id;
- }
- }
-
- // When hardware coo property exists, use regulatory_info_<sku>_<coo> resource if valid.
- final String coo = getCoo();
- if (!TextUtils.isEmpty(coo) && !TextUtils.isEmpty(sku)) {
- final String regulatory_info_coo_res =
- REGULATORY_INFO_RESOURCE + "_" + sku.toLowerCase() + "_" + coo.toLowerCase();
- final int id = getResources().getIdentifier(
- regulatory_info_coo_res, "drawable", getPackageName());
- if (id != 0) {
- resId = id;
- }
- }
- return resId;
- }
-
- @Override
- public void onDismiss(DialogInterface dialog) {
- finish(); // close the activity
- }
-
- private String getCoo() {
- return SystemProperties.get("ro.boot.hardware.coo", "");
- }
-
- private String getSku() {
- return SystemProperties.get("ro.boot.hardware.sku", "");
- }
-
- private String getRegulatoryInfoImageFileName() {
- final String sku = getSku();
- if (TextUtils.isEmpty(sku)) {
- return DEFAULT_REGULATORY_INFO_FILEPATH;
- } else {
- return String.format(Locale.US, REGULATORY_INFO_FILEPATH_TEMPLATE,
- sku.toLowerCase());
- }
- }
-}
diff --git a/src/com/android/settings/RegulatoryInfoDisplayActivity.kt b/src/com/android/settings/RegulatoryInfoDisplayActivity.kt
new file mode 100644
index 0000000..fdf66c3
--- /dev/null
+++ b/src/com/android/settings/RegulatoryInfoDisplayActivity.kt
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings
+
+import android.app.Activity
+import android.os.Bundle
+import android.view.Gravity
+import android.widget.ImageView
+import android.widget.TextView
+import androidx.appcompat.app.AlertDialog
+import com.android.settings.deviceinfo.regulatory.RegulatoryInfo.getRegulatoryInfo
+
+/**
+ * [Activity] that displays regulatory information for the "Regulatory information"
+ * preference item, and when "*#07#" is dialed on the Phone keypad. To enable this feature,
+ * set the "config_show_regulatory_info" boolean to true in a device overlay resource, and in the
+ * same overlay, either add a drawable named "regulatory_info.png" containing a graphical version
+ * of the required regulatory info (If ro.bootloader.hardware.sku property is set use
+ * "regulatory_info_<sku>.png where sku is ro.bootloader.hardware.sku property value in lowercase"),
+ * or add a string resource named "regulatory_info_text" with an HTML version of the required
+ * information (text will be centered in the dialog).
+ */
+class RegulatoryInfoDisplayActivity : Activity() {
+
+ /** Display the regulatory info graphic in a dialog window. */
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ val builder = AlertDialog.Builder(this)
+ .setTitle(R.string.regulatory_labels)
+ .setOnDismissListener { finish() } // close the activity
+ .setPositiveButton(android.R.string.ok, null)
+
+ getRegulatoryInfo()?.let {
+ val view = layoutInflater.inflate(R.layout.regulatory_info, null)
+ val image = view.requireViewById<ImageView>(R.id.regulatoryInfo)
+ image.setImageDrawable(it)
+ builder.setView(view)
+ builder.show()
+ return
+ }
+
+ val regulatoryText = resources.getText(R.string.regulatory_info_text)
+ if (regulatoryText.isNotEmpty()) {
+ builder.setMessage(regulatoryText)
+ val dialog = builder.show()
+ // we have to show the dialog first, or the setGravity() call will throw a NPE
+ dialog.findViewById<TextView>(android.R.id.message)?.gravity = Gravity.CENTER
+ } else {
+ // neither drawable nor text resource exists, finish activity
+ finish()
+ }
+ }
+}
diff --git a/src/com/android/settings/Settings.java b/src/com/android/settings/Settings.java
index 14d978b..a929384 100644
--- a/src/com/android/settings/Settings.java
+++ b/src/com/android/settings/Settings.java
@@ -52,27 +52,11 @@
public static class AssistGestureSettingsActivity extends SettingsActivity { /* empty */}
public static class BluetoothSettingsActivity extends SettingsActivity { /* empty */ }
public static class CreateShortcutActivity extends SettingsActivity { /* empty */ }
- public static class FaceSettingsActivity extends SettingsActivity {
- @Override
- protected void onCreate(Bundle savedState) {
- setTheme(SetupWizardUtils.getTheme(this, getIntent()));
- setTheme(R.style.SettingsPreferenceTheme_SetupWizard);
- ThemeHelper.trySetDynamicColor(this);
- super.onCreate(savedState);
- }
- }
+ public static class FaceSettingsActivity extends SettingsActivity { /* empty */ }
/** Container for {@link FaceSettings} to use with a pre-defined task affinity. */
- public static class FaceSettingsInternalActivity extends SettingsActivity {
- @Override
- protected void onCreate(Bundle savedState) {
- setTheme(SetupWizardUtils.getTheme(this, getIntent()));
- setTheme(R.style.SettingsPreferenceTheme_SetupWizard);
- ThemeHelper.trySetDynamicColor(this);
- super.onCreate(savedState);
- }
- }
-
+ public static class FaceSettingsInternalActivity extends SettingsActivity { /* empty */ }
public static class FingerprintSettingsActivity extends SettingsActivity { /* empty */ }
+ public static class FingerprintSettingsActivityV2 extends SettingsActivity { /* empty */ }
public static class CombinedBiometricSettingsActivity extends SettingsActivity { /* empty */ }
public static class CombinedBiometricProfileSettingsActivity extends SettingsActivity { /* empty */ }
public static class TetherSettingsActivity extends SettingsActivity {
@@ -376,6 +360,10 @@
public static class NotificationAppListActivity extends SettingsActivity { /* empty */ }
/** Activity to manage Cloned Apps page */
public static class ClonedAppsListActivity extends SettingsActivity { /* empty */ }
+ /** Activity to manage Aspect Ratio app list page */
+ public static class UserAspectRatioAppListActivity extends SettingsActivity { /* empty */ }
+ /** Activity to manage Aspect Ratio app page */
+ public static class UserAspectRatioAppActivity extends SettingsActivity { /* empty */ }
public static class NotificationReviewPermissionsActivity extends SettingsActivity { /* empty */ }
public static class AppNotificationSettingsActivity extends SettingsActivity { /* empty */ }
public static class ChannelNotificationSettingsActivity extends SettingsActivity { /* empty */ }
diff --git a/src/com/android/settings/SettingsActivityUtil.kt b/src/com/android/settings/SettingsActivityUtil.kt
index cac341f..65d26de 100644
--- a/src/com/android/settings/SettingsActivityUtil.kt
+++ b/src/com/android/settings/SettingsActivityUtil.kt
@@ -35,6 +35,7 @@
import com.android.settings.spa.app.specialaccess.InstallUnknownAppsListProvider
import com.android.settings.spa.app.specialaccess.MediaManagementAppsAppListProvider
import com.android.settings.spa.app.specialaccess.ModifySystemSettingsAppListProvider
+import com.android.settings.spa.app.specialaccess.NfcTagAppsSettingsProvider
import com.android.settings.spa.app.specialaccess.PictureInPictureListProvider
import com.android.settings.spa.app.specialaccess.WifiControlAppListProvider
import com.android.settings.wifi.ChangeWifiStateDetails
@@ -62,6 +63,8 @@
MediaManagementAppsAppListProvider.getAppInfoRoutePrefix(),
ChangeWifiStateDetails::class.qualifiedName to
WifiControlAppListProvider.getAppInfoRoutePrefix(),
+ NfcTagAppsSettingsProvider::class.qualifiedName to
+ NfcTagAppsSettingsProvider.getAppInfoRoutePrefix(),
)
@JvmStatic
diff --git a/src/com/android/settings/Utils.java b/src/com/android/settings/Utils.java
index 68b1a48..910ab81 100644
--- a/src/com/android/settings/Utils.java
+++ b/src/com/android/settings/Utils.java
@@ -16,6 +16,9 @@
package com.android.settings;
+import static android.app.admin.DevicePolicyResources.Strings.Settings.WORK_PROFILE_CONFIRM_PASSWORD;
+import static android.app.admin.DevicePolicyResources.Strings.Settings.WORK_PROFILE_CONFIRM_PATTERN;
+import static android.app.admin.DevicePolicyResources.Strings.Settings.WORK_PROFILE_CONFIRM_PIN;
import static android.content.Intent.EXTRA_USER;
import static android.content.Intent.EXTRA_USER_ID;
import static android.text.format.DateUtils.FORMAT_ABBREV_MONTH;
@@ -97,7 +100,6 @@
import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
-import androidx.annotation.StringRes;
import androidx.annotation.VisibleForTesting;
import androidx.core.graphics.drawable.IconCompat;
import androidx.core.graphics.drawable.RoundedBitmapDrawable;
@@ -708,9 +710,13 @@
final int userId = bundle.getInt(Intent.EXTRA_USER_ID, UserHandle.myUserId());
if (userId == LockPatternUtils.USER_FRP) {
return allowAnyUser ? userId : checkUserOwnsFrpCredential(context, userId);
- } else {
- return allowAnyUser ? userId : enforceSameOwner(context, userId);
}
+ if (userId == LockPatternUtils.USER_REPAIR_MODE) {
+ enforceRepairModeActive(context);
+ // any users can exit repair mode
+ return userId;
+ }
+ return allowAnyUser ? userId : enforceSameOwner(context, userId);
}
/**
@@ -730,6 +736,16 @@
}
/**
+ * Throws {@link SecurityException} if repair mode is not active on the device.
+ */
+ private static void enforceRepairModeActive(Context context) {
+ if (LockPatternUtils.isRepairModeActive(context)) {
+ return;
+ }
+ throw new SecurityException("Repair mode is not active on the device.");
+ }
+
+ /**
* Returns the given user id if it belongs to the current user.
*
* @throws SecurityException if the given userId does not belong to the current user group.
@@ -768,6 +784,47 @@
return lpu.getCredentialTypeForUser(userId);
}
+ /**
+ * Returns the confirmation credential string of the given user id.
+ */
+ @Nullable public static String getConfirmCredentialStringForUser(@NonNull Context context,
+ int userId, @LockPatternUtils.CredentialType int credentialType) {
+ final int effectiveUserId = UserManager.get(context).getCredentialOwnerProfile(userId);
+ final boolean isEffectiveUserManagedProfile = UserManager.get(context)
+ .isManagedProfile(effectiveUserId);
+ final DevicePolicyManager devicePolicyManager = context
+ .getSystemService(DevicePolicyManager.class);
+ switch (credentialType) {
+ case LockPatternUtils.CREDENTIAL_TYPE_PIN:
+ if (isEffectiveUserManagedProfile) {
+ return devicePolicyManager.getResources().getString(WORK_PROFILE_CONFIRM_PIN,
+ () -> context.getString(
+ R.string.lockpassword_confirm_your_pin_generic_profile));
+ }
+
+ return context.getString(R.string.lockpassword_confirm_your_pin_generic);
+ case LockPatternUtils.CREDENTIAL_TYPE_PATTERN:
+ if (isEffectiveUserManagedProfile) {
+ return devicePolicyManager.getResources().getString(
+ WORK_PROFILE_CONFIRM_PATTERN,
+ () -> context.getString(
+ R.string.lockpassword_confirm_your_pattern_generic_profile));
+ }
+
+ return context.getString(R.string.lockpassword_confirm_your_pattern_generic);
+ case LockPatternUtils.CREDENTIAL_TYPE_PASSWORD:
+ if (isEffectiveUserManagedProfile) {
+ return devicePolicyManager.getResources().getString(
+ WORK_PROFILE_CONFIRM_PASSWORD,
+ () -> context.getString(
+ R.string.lockpassword_confirm_your_password_generic_profile));
+ }
+
+ return context.getString(R.string.lockpassword_confirm_your_password_generic);
+ }
+ return null;
+ }
+
private static final StringBuilder sBuilder = new StringBuilder(50);
private static final java.util.Formatter sFormatter = new java.util.Formatter(
sBuilder, Locale.getDefault());
@@ -1295,4 +1352,5 @@
com.android.internal.R.bool.config_dreamsOnlyEnabledForDockUser);
return dreamsSupported && (!dreamsOnlyEnabledForDockUser || canCurrentUserDream(context));
}
+
}
diff --git a/src/com/android/settings/accessibility/AccessibilityQuickSettingsPrimarySwitchPreferenceController.java b/src/com/android/settings/accessibility/AccessibilityQuickSettingsPrimarySwitchPreferenceController.java
index 9681a42..e82cd96 100644
--- a/src/com/android/settings/accessibility/AccessibilityQuickSettingsPrimarySwitchPreferenceController.java
+++ b/src/com/android/settings/accessibility/AccessibilityQuickSettingsPrimarySwitchPreferenceController.java
@@ -66,6 +66,10 @@
@Override
public void onDestroy() {
mHandler.removeCallbacksAndMessages(null);
+ final boolean isTooltipWindowShowing = mTooltipWindow != null && mTooltipWindow.isShowing();
+ if (isTooltipWindowShowing) {
+ mTooltipWindow.dismiss();
+ }
}
@Override
@@ -126,10 +130,17 @@
return;
}
- mTooltipWindow = new AccessibilityQuickSettingsTooltipWindow(mContext);
- mTooltipWindow.setup(getTileTooltipContent(),
- R.drawable.accessibility_auto_added_qs_tooltip_illustration);
- mTooltipWindow.showAtTopCenter(mPreference.getSwitch());
+ // TODO (287728819): Move tooltip showing to SystemUI
+ // Since the lifecycle of controller is independent of that of the preference, doing
+ // null check on switch is a temporary solution for the case that switch view
+ // is not ready when we would like to show the tooltip. If the switch is not ready,
+ // we give up showing the tooltip and also do not reshow it in the future.
+ if (mPreference.getSwitch() != null) {
+ mTooltipWindow = new AccessibilityQuickSettingsTooltipWindow(mContext);
+ mTooltipWindow.setup(getTileTooltipContent(),
+ R.drawable.accessibility_auto_added_qs_tooltip_illustration);
+ mTooltipWindow.showAtTopCenter(mPreference.getSwitch());
+ }
AccessibilityQuickSettingUtils.optInValueToSharedPreferences(mContext, tileComponentName);
mNeedsQSTooltipReshow = false;
}
diff --git a/src/com/android/settings/accessibility/AvailableHearingDeviceUpdater.java b/src/com/android/settings/accessibility/AvailableHearingDeviceUpdater.java
index b3d3715..f600b03 100644
--- a/src/com/android/settings/accessibility/AvailableHearingDeviceUpdater.java
+++ b/src/com/android/settings/accessibility/AvailableHearingDeviceUpdater.java
@@ -16,7 +16,6 @@
package com.android.settings.accessibility;
-import android.bluetooth.BluetoothDevice;
import android.content.Context;
import com.android.settings.bluetooth.AvailableMediaBluetoothDeviceUpdater;
@@ -37,11 +36,9 @@
@Override
public boolean isFilterMatched(CachedBluetoothDevice cachedDevice) {
- final BluetoothDevice device = cachedDevice.getDevice();
- final boolean isConnectedHearingAidDevice = (cachedDevice.isConnectedHearingAidDevice()
- && (device.getBondState() == BluetoothDevice.BOND_BONDED));
-
- return isConnectedHearingAidDevice && isDeviceInCachedDevicesList(cachedDevice);
+ return cachedDevice.isHearingAidDevice()
+ && isDeviceConnected(cachedDevice)
+ && isDeviceInCachedDevicesList(cachedDevice);
}
@Override
diff --git a/src/com/android/settings/accessibility/HearingAidHelper.java b/src/com/android/settings/accessibility/HearingAidHelper.java
index 66a37f8..1b9bdc4 100644
--- a/src/com/android/settings/accessibility/HearingAidHelper.java
+++ b/src/com/android/settings/accessibility/HearingAidHelper.java
@@ -56,7 +56,8 @@
* @return a list of hearing aids {@link BluetoothDevice} objects
*/
public List<BluetoothDevice> getConnectedHearingAidDeviceList() {
- if (!isHearingAidSupported()) {
+ if (mBluetoothAdapter == null || !mBluetoothAdapter.isEnabled()
+ || !isHearingAidSupported()) {
return new ArrayList<>();
}
final List<BluetoothDevice> deviceList = new ArrayList<>();
@@ -88,9 +89,6 @@
* supported.
*/
public boolean isHearingAidSupported() {
- if (mBluetoothAdapter == null || !mBluetoothAdapter.isEnabled()) {
- return false;
- }
final List<Integer> supportedList = mBluetoothAdapter.getSupportedProfiles();
return supportedList.contains(BluetoothProfile.HEARING_AID)
|| supportedList.contains(BluetoothProfile.HAP_CLIENT);
diff --git a/src/com/android/settings/accessibility/HearingAidUtils.java b/src/com/android/settings/accessibility/HearingAidUtils.java
index 42484f9..4315093 100644
--- a/src/com/android/settings/accessibility/HearingAidUtils.java
+++ b/src/com/android/settings/accessibility/HearingAidUtils.java
@@ -23,6 +23,7 @@
import com.android.settings.bluetooth.HearingAidPairingDialogFragment;
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
+import com.android.settingslib.bluetooth.CsipSetCoordinatorProfile;
import com.android.settingslib.bluetooth.HearingAidInfo;
/** Provides utility methods related hearing aids. */
@@ -40,6 +41,11 @@
*/
public static void launchHearingAidPairingDialog(FragmentManager fragmentManager,
@NonNull CachedBluetoothDevice device) {
+ // No need to show the pair another ear dialog if the device supports and enables CSIP.
+ // CSIP will pair other devices in the same set automatically.
+ if (isCsipSupportedAndEnabled(device)) {
+ return;
+ }
if (device.isConnectedAshaHearingAidDevice()
&& device.getDeviceMode() == HearingAidInfo.DeviceMode.MODE_BINAURAL
&& device.getSubDevice() == null) {
@@ -56,4 +62,10 @@
HearingAidPairingDialogFragment.newInstance(device.getAddress()).show(fragmentManager,
HearingAidPairingDialogFragment.TAG);
}
+
+ private static boolean isCsipSupportedAndEnabled(@NonNull CachedBluetoothDevice device) {
+ return device.getProfiles().stream().anyMatch(
+ profile -> (profile instanceof CsipSetCoordinatorProfile)
+ && (profile.isEnabled(device.getDevice())));
+ }
}
diff --git a/src/com/android/settings/accessibility/HearingDevicePairingDetail.java b/src/com/android/settings/accessibility/HearingDevicePairingDetail.java
index de86dcf..117a8ed 100644
--- a/src/com/android/settings/accessibility/HearingDevicePairingDetail.java
+++ b/src/com/android/settings/accessibility/HearingDevicePairingDetail.java
@@ -28,7 +28,8 @@
import com.android.settings.bluetooth.BluetoothDevicePairingDetailBase;
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
-import java.util.Collections;
+import java.util.ArrayList;
+import java.util.List;
/**
* HearingDevicePairingDetail is a page to scan hearing devices. This page shows scanning icons and
@@ -42,10 +43,16 @@
public HearingDevicePairingDetail() {
super();
- final ScanFilter filter = new ScanFilter.Builder()
- .setServiceData(BluetoothUuid.HEARING_AID, new byte[]{0}, new byte[]{0})
- .build();
- setFilter(Collections.singletonList(filter));
+ final List<ScanFilter> filterList = new ArrayList<>();
+ // Filters for ASHA hearing aids
+ filterList.add(new ScanFilter.Builder().setServiceUuid(BluetoothUuid.HEARING_AID).build());
+ filterList.add(new ScanFilter.Builder()
+ .setServiceData(BluetoothUuid.HEARING_AID, new byte[0]).build());
+ // Filters for LE audio hearing aids
+ filterList.add(new ScanFilter.Builder().setServiceUuid(BluetoothUuid.HAS).build());
+ filterList.add(new ScanFilter.Builder()
+ .setServiceData(BluetoothUuid.HAS, new byte[0]).build());
+ setFilter(filterList);
}
@Override
diff --git a/src/com/android/settings/accessibility/PreviewSizeSeekBarController.java b/src/com/android/settings/accessibility/PreviewSizeSeekBarController.java
index 4c860eb..6bd8747 100644
--- a/src/com/android/settings/accessibility/PreviewSizeSeekBarController.java
+++ b/src/com/android/settings/accessibility/PreviewSizeSeekBarController.java
@@ -28,7 +28,6 @@
import com.android.settings.R;
import com.android.settings.core.BasePreferenceController;
import com.android.settings.widget.LabeledSeekBarPreference;
-import com.android.settings.widget.SeekBarPreference;
import com.android.settingslib.core.lifecycle.LifecycleObserver;
import com.android.settingslib.core.lifecycle.events.OnCreate;
import com.android.settingslib.core.lifecycle.events.OnDestroy;
@@ -111,6 +110,10 @@
public void onDestroy() {
// remove runnables in the queue.
mHandler.removeCallbacksAndMessages(null);
+ final boolean isTooltipWindowShowing = mTooltipWindow != null && mTooltipWindow.isShowing();
+ if (isTooltipWindowShowing) {
+ mTooltipWindow.dismiss();
+ }
}
@Override
@@ -210,11 +213,19 @@
return;
}
- mTooltipWindow = new AccessibilityQuickSettingsTooltipWindow(mContext);
- mTooltipWindow.setup(getTileTooltipContent(),
- R.drawable.accessibility_auto_added_qs_tooltip_illustration);
- mTooltipWindow.showAtTopCenter(mSeekBarPreference.getSeekbar());
- AccessibilityQuickSettingUtils.optInValueToSharedPreferences(mContext, tileComponentName);
+ // TODO (287728819): Move tooltip showing to SystemUI
+ // Since the lifecycle of controller is independent of that of the preference, doing
+ // null check on seekbar is a temporary solution for the case that seekbar view
+ // is not ready when we would like to show the tooltip. If the seekbar is not ready,
+ // we give up showing the tooltip and also do not reshow it in the future.
+ if (mSeekBarPreference.getSeekbar() != null) {
+ mTooltipWindow = new AccessibilityQuickSettingsTooltipWindow(mContext);
+ mTooltipWindow.setup(getTileTooltipContent(),
+ R.drawable.accessibility_auto_added_qs_tooltip_illustration);
+ mTooltipWindow.showAtTopCenter(mSeekBarPreference.getSeekbar());
+ }
+ AccessibilityQuickSettingUtils.optInValueToSharedPreferences(mContext,
+ tileComponentName);
mNeedsQSTooltipReshow = false;
}
diff --git a/src/com/android/settings/accessibility/ToggleFeaturePreferenceFragment.java b/src/com/android/settings/accessibility/ToggleFeaturePreferenceFragment.java
index edbd120..6a4344f 100644
--- a/src/com/android/settings/accessibility/ToggleFeaturePreferenceFragment.java
+++ b/src/com/android/settings/accessibility/ToggleFeaturePreferenceFragment.java
@@ -296,6 +296,10 @@
public void onDestroyView() {
super.onDestroyView();
removeActionBarToggleSwitch();
+ final boolean isTooltipWindowShowing = mTooltipWindow != null && mTooltipWindow.isShowing();
+ if (isTooltipWindowShowing) {
+ mTooltipWindow.dismiss();
+ }
}
@Override
diff --git a/src/com/android/settings/applications/AdvancedAppsPreferenceCategoryController.java b/src/com/android/settings/applications/AdvancedAppsPreferenceCategoryController.java
new file mode 100644
index 0000000..6a518fd
--- /dev/null
+++ b/src/com/android/settings/applications/AdvancedAppsPreferenceCategoryController.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.applications;
+
+import android.content.Context;
+
+import androidx.annotation.NonNull;
+
+import com.android.settings.widget.PreferenceCategoryController;
+
+/**
+ * Preference category controller for Advanced category under Apps page
+ */
+public class AdvancedAppsPreferenceCategoryController extends PreferenceCategoryController {
+
+ public AdvancedAppsPreferenceCategoryController(@NonNull Context context,
+ @NonNull String preferenceKey) {
+ super(context, preferenceKey);
+ }
+}
diff --git a/src/com/android/settings/applications/AppDashboardFragment.java b/src/com/android/settings/applications/AppDashboardFragment.java
index 7e203b0..41160d8 100644
--- a/src/com/android/settings/applications/AppDashboardFragment.java
+++ b/src/com/android/settings/applications/AppDashboardFragment.java
@@ -20,9 +20,12 @@
import android.content.Context;
import android.provider.SearchIndexableResource;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.settings.R;
+import com.android.settings.applications.appcompat.UserAspectRatioAppsPreferenceController;
import com.android.settings.dashboard.DashboardFragment;
import com.android.settings.search.BaseSearchIndexProvider;
+import com.android.settings.widget.PreferenceCategoryController;
import com.android.settingslib.core.AbstractPreferenceController;
import com.android.settingslib.search.SearchIndexable;
@@ -35,11 +38,21 @@
public class AppDashboardFragment extends DashboardFragment {
private static final String TAG = "AppDashboardFragment";
+ private static final String ADVANCED_CATEGORY_KEY = "advanced_category";
+ private static final String ASPECT_RATIO_PREF_KEY = "aspect_ratio_apps";
private AppsPreferenceController mAppsPreferenceController;
private static List<AbstractPreferenceController> buildPreferenceControllers(Context context) {
final List<AbstractPreferenceController> controllers = new ArrayList<>();
controllers.add(new AppsPreferenceController(context));
+
+ final UserAspectRatioAppsPreferenceController aspectRatioAppsPreferenceController =
+ new UserAspectRatioAppsPreferenceController(context, ASPECT_RATIO_PREF_KEY);
+ final AdvancedAppsPreferenceCategoryController advancedCategoryController =
+ new AdvancedAppsPreferenceCategoryController(context, ADVANCED_CATEGORY_KEY);
+ advancedCategoryController.setChildren(List.of(aspectRatioAppsPreferenceController));
+ controllers.add(advancedCategoryController);
+
return controllers;
}
@@ -66,7 +79,6 @@
@Override
public void onAttach(Context context) {
super.onAttach(context);
- use(SpecialAppAccessPreferenceController.class).setSession(getSettingsLifecycle());
mAppsPreferenceController = use(AppsPreferenceController.class);
mAppsPreferenceController.setFragment(this /* fragment */);
getSettingsLifecycle().addObserver(mAppsPreferenceController);
@@ -76,6 +88,11 @@
getSettingsLifecycle().addObserver(hibernatedAppsPreferenceController);
}
+ @VisibleForTesting
+ PreferenceCategoryController getAdvancedAppsPreferenceCategoryController() {
+ return use(AdvancedAppsPreferenceCategoryController.class);
+ }
+
@Override
protected List<AbstractPreferenceController> createPreferenceControllers(Context context) {
return buildPreferenceControllers(context);
diff --git a/src/com/android/settings/applications/SpecialAppAccessPreferenceController.java b/src/com/android/settings/applications/SpecialAppAccessPreferenceController.java
deleted file mode 100644
index 42f5930..0000000
--- a/src/com/android/settings/applications/SpecialAppAccessPreferenceController.java
+++ /dev/null
@@ -1,154 +0,0 @@
-/*
- * Copyright (C) 2017 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.settings.applications;
-
-import android.app.Application;
-import android.content.Context;
-
-import androidx.annotation.VisibleForTesting;
-import androidx.preference.Preference;
-import androidx.preference.PreferenceScreen;
-
-import com.android.settings.R;
-import com.android.settings.core.BasePreferenceController;
-import com.android.settings.datausage.AppStateDataUsageBridge;
-import com.android.settings.datausage.AppStateDataUsageBridge.DataUsageState;
-import com.android.settings.datausage.DataSaverBackend;
-import com.android.settingslib.applications.ApplicationsState;
-import com.android.settingslib.core.lifecycle.Lifecycle;
-import com.android.settingslib.core.lifecycle.LifecycleObserver;
-import com.android.settingslib.core.lifecycle.events.OnDestroy;
-import com.android.settingslib.core.lifecycle.events.OnStart;
-import com.android.settingslib.core.lifecycle.events.OnStop;
-
-import java.util.ArrayList;
-
-public class SpecialAppAccessPreferenceController extends BasePreferenceController implements
- AppStateBaseBridge.Callback, ApplicationsState.Callbacks, LifecycleObserver, OnStart,
- OnStop, OnDestroy {
-
- @VisibleForTesting
- ApplicationsState.Session mSession;
-
- private final ApplicationsState mApplicationsState;
- private final AppStateDataUsageBridge mDataUsageBridge;
- private final DataSaverBackend mDataSaverBackend;
-
- private Preference mPreference;
- private boolean mExtraLoaded;
-
-
- public SpecialAppAccessPreferenceController(Context context, String key) {
- super(context, key);
- mApplicationsState = ApplicationsState.getInstance(
- (Application) context.getApplicationContext());
- mDataSaverBackend = new DataSaverBackend(context);
- mDataUsageBridge = new AppStateDataUsageBridge(mApplicationsState, this, mDataSaverBackend);
- }
-
- public void setSession(Lifecycle lifecycle) {
- mSession = mApplicationsState.newSession(this, lifecycle);
- }
-
- @Override
- public int getAvailabilityStatus() {
- return AVAILABLE;
- }
-
- @Override
- public void displayPreference(PreferenceScreen screen) {
- super.displayPreference(screen);
- mPreference = screen.findPreference(getPreferenceKey());
- }
-
- @Override
- public void onStart() {
- mDataUsageBridge.resume(true /* forceLoadAllApps */);
- }
-
- @Override
- public void onStop() {
- mDataUsageBridge.pause();
- }
-
- @Override
- public void onDestroy() {
- mDataUsageBridge.release();
- }
-
- @Override
- public void updateState(Preference preference) {
- updateSummary();
- }
-
- @Override
- public void onExtraInfoUpdated() {
- mExtraLoaded = true;
- updateSummary();
- }
-
- private void updateSummary() {
- if (!mExtraLoaded || mPreference == null) {
- return;
- }
-
- final ArrayList<ApplicationsState.AppEntry> allApps = mSession.getAllApps();
- int count = 0;
- for (ApplicationsState.AppEntry entry : allApps) {
- if (!ApplicationsState.FILTER_DOWNLOADED_AND_LAUNCHER.filterApp(entry)) {
- continue;
- }
- if (entry.extraInfo instanceof DataUsageState
- && ((DataUsageState) entry.extraInfo).isDataSaverAllowlisted) {
- count++;
- }
- }
- mPreference.setSummary(mContext.getResources().getQuantityString(
- R.plurals.special_access_summary, count, count));
- }
-
- @Override
- public void onRunningStateChanged(boolean running) {
- }
-
- @Override
- public void onPackageListChanged() {
- }
-
- @Override
- public void onRebuildComplete(ArrayList<ApplicationsState.AppEntry> apps) {
- }
-
- @Override
- public void onPackageIconChanged() {
- }
-
- @Override
- public void onPackageSizeChanged(String packageName) {
- }
-
- @Override
- public void onAllSizesComputed() {
- }
-
- @Override
- public void onLauncherInfoChanged() {
- // when the value of the AppEntry.hasLauncherEntry was changed.
- updateSummary();
- }
-
- @Override
- public void onLoadEntriesCompleted() {
- }
-}
diff --git a/src/com/android/settings/applications/appcompat/RadioWithImagePreference.java b/src/com/android/settings/applications/appcompat/RadioWithImagePreference.java
new file mode 100644
index 0000000..77cd86c
--- /dev/null
+++ b/src/com/android/settings/applications/appcompat/RadioWithImagePreference.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.applications.appcompat;
+
+import android.content.Context;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.view.View;
+
+import androidx.preference.CheckBoxPreference;
+import androidx.preference.PreferenceViewHolder;
+
+import com.android.settings.R;
+
+/**
+ * Radio button preference with image at the bottom.
+ *
+ * <p>Layout should stay the same as
+ * {@link com.android.settingslib.widget.SelectorWithWidgetPreference} for consistency.
+ */
+public class RadioWithImagePreference extends CheckBoxPreference {
+
+ /**
+ * Interface definition for a callback to be invoked when the preference is clicked.
+ */
+ public interface OnClickListener {
+ /**
+ * Called when a preference has been clicked.
+ *
+ * @param emiter The clicked preference
+ */
+ void onRadioButtonClicked(RadioWithImagePreference emiter);
+ }
+
+ private OnClickListener mListener = null;
+
+ /**
+ * Performs inflation from XML and apply a class-specific base style.
+ *
+ * @param context The {@link Context} this is associated with, through which it can
+ * access the current theme, resources, {@link SharedPreferences}, etc.
+ * @param attrs The attributes of the XML tag that is inflating the preference
+ * @param defStyle An attribute in the current theme that contains a reference to a style
+ * resource that supplies default values for the view. Can be 0 to not
+ * look for defaults.
+ */
+ public RadioWithImagePreference(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ init();
+ }
+
+ /**
+ * Performs inflation from XML and apply a class-specific base style.
+ *
+ * @param context The {@link Context} this is associated with, through which it can
+ * access the current theme, resources, {@link SharedPreferences}, etc.
+ * @param attrs The attributes of the XML tag that is inflating the preference
+ */
+ public RadioWithImagePreference(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ init();
+ }
+
+ /**
+ * Constructor to create a preference.
+ *
+ * @param context The Context this is associated with.
+ */
+ public RadioWithImagePreference(Context context) {
+ this(context, null);
+ }
+
+ /**
+ * Sets the callback to be invoked when this preference is clicked by the user.
+ *
+ * @param listener The callback to be invoked
+ */
+ public void setOnClickListener(OnClickListener listener) {
+ mListener = listener;
+ }
+
+ /**
+ * Processes a click on the preference.
+ */
+ @Override
+ public void onClick() {
+ if (mListener != null) {
+ mListener.onRadioButtonClicked(this);
+ }
+ }
+
+ /**
+ * Binds the created View to the data for this preference.
+ *
+ * <p>This is a good place to grab references to custom Views in the layout and set
+ * properties on them.
+ *
+ * <p>Make sure to call through to the superclass's implementation.
+ *
+ * @param holder The ViewHolder that provides references to the views to fill in. These views
+ * will be recycled, so you should not hold a reference to them after this method
+ * returns.
+ */
+ @Override
+ public void onBindViewHolder(PreferenceViewHolder holder) {
+ super.onBindViewHolder(holder);
+
+ View summaryContainer = holder.findViewById(R.id.summary_container);
+ if (summaryContainer != null) {
+ summaryContainer.setVisibility(
+ TextUtils.isEmpty(getSummary()) ? View.GONE : View.VISIBLE);
+ }
+ }
+
+ private void init() {
+ setWidgetLayoutResource(com.android.settingslib.R.layout.preference_widget_radiobutton);
+ setLayoutResource(R.layout.radio_with_image_preference);
+ setIconSpaceReserved(false);
+ }
+}
diff --git a/src/com/android/settings/applications/appcompat/UserAspectRatioAppsPreferenceController.java b/src/com/android/settings/applications/appcompat/UserAspectRatioAppsPreferenceController.java
new file mode 100644
index 0000000..4211424
--- /dev/null
+++ b/src/com/android/settings/applications/appcompat/UserAspectRatioAppsPreferenceController.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.applications.appcompat;
+
+import android.content.Context;
+import android.os.Build;
+
+import androidx.annotation.NonNull;
+
+import com.android.settings.R;
+import com.android.settings.core.BasePreferenceController;
+
+/**
+ * Preference controller for
+ * {@link com.android.settings.spa.app.appcompat.UserAspectRatioAppsPageProvider}
+ */
+public class UserAspectRatioAppsPreferenceController extends BasePreferenceController {
+
+ public UserAspectRatioAppsPreferenceController(@NonNull Context context,
+ @NonNull String preferenceKey) {
+ super(context, preferenceKey);
+ }
+
+ @Override
+ public int getAvailabilityStatus() {
+ return UserAspectRatioManager.isFeatureEnabled(mContext)
+ ? AVAILABLE : CONDITIONALLY_UNAVAILABLE;
+ }
+
+ @Override
+ public CharSequence getSummary() {
+ return mContext.getResources().getString(R.string.aspect_ratio_summary_text, Build.MODEL);
+ }
+}
diff --git a/src/com/android/settings/applications/appcompat/UserAspectRatioDetails.java b/src/com/android/settings/applications/appcompat/UserAspectRatioDetails.java
new file mode 100644
index 0000000..dfb583c
--- /dev/null
+++ b/src/com/android/settings/applications/appcompat/UserAspectRatioDetails.java
@@ -0,0 +1,251 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.applications.appcompat;
+
+import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP;
+import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
+import static android.content.pm.PackageManager.USER_MIN_ASPECT_RATIO_16_9;
+import static android.content.pm.PackageManager.USER_MIN_ASPECT_RATIO_3_2;
+import static android.content.pm.PackageManager.USER_MIN_ASPECT_RATIO_4_3;
+import static android.content.pm.PackageManager.USER_MIN_ASPECT_RATIO_DISPLAY_SIZE;
+import static android.content.pm.PackageManager.USER_MIN_ASPECT_RATIO_FULLSCREEN;
+import static android.content.pm.PackageManager.USER_MIN_ASPECT_RATIO_SPLIT_SCREEN;
+import static android.content.pm.PackageManager.USER_MIN_ASPECT_RATIO_UNSET;
+
+import android.app.ActivityManager;
+import android.app.IActivityManager;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.appcompat.app.AlertDialog;
+import androidx.preference.Preference;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.settings.R;
+import com.android.settings.Utils;
+import com.android.settings.applications.AppInfoBase;
+import com.android.settings.widget.EntityHeaderController;
+import com.android.settingslib.applications.AppUtils;
+import com.android.settingslib.widget.ActionButtonsPreference;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * App specific activity to show aspect ratio overrides
+ */
+public class UserAspectRatioDetails extends AppInfoBase implements
+ RadioWithImagePreference.OnClickListener {
+ private static final String TAG = UserAspectRatioDetails.class.getSimpleName();
+
+ private static final String KEY_HEADER_SUMMARY = "app_aspect_ratio_summary";
+ private static final String KEY_HEADER_BUTTONS = "header_view";
+ private static final String KEY_PREF_FULLSCREEN = "fullscreen_pref";
+ private static final String KEY_PREF_HALF_SCREEN = "half_screen_pref";
+ private static final String KEY_PREF_DISPLAY_SIZE = "display_size_pref";
+ private static final String KEY_PREF_16_9 = "16_9_pref";
+ private static final String KEY_PREF_4_3 = "4_3_pref";
+ @VisibleForTesting
+ static final String KEY_PREF_DEFAULT = "app_default_pref";
+ @VisibleForTesting
+ static final String KEY_PREF_3_2 = "3_2_pref";
+
+ private final List<RadioWithImagePreference> mAspectRatioPreferences = new ArrayList<>();
+
+ @NonNull private UserAspectRatioManager mUserAspectRatioManager;
+ @NonNull private String mSelectedKey = KEY_PREF_DEFAULT;
+
+ @Override
+ public void onCreate(@NonNull Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ mUserAspectRatioManager = new UserAspectRatioManager(getContext());
+ initPreferences();
+ try {
+ final int userAspectRatio = mUserAspectRatioManager
+ .getUserMinAspectRatioValue(mPackageName, mUserId);
+ mSelectedKey = getSelectedKey(userAspectRatio);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Unable to get user min aspect ratio");
+ }
+ refreshUi();
+ }
+
+ @Override
+ public void onRadioButtonClicked(@NonNull RadioWithImagePreference selected) {
+ final String selectedKey = selected.getKey();
+ if (mSelectedKey.equals(selectedKey)) {
+ return;
+ }
+ final int userAspectRatio = getSelectedUserMinAspectRatio(selectedKey);
+ try {
+ getAspectRatioManager().setUserMinAspectRatio(mPackageName, mUserId, userAspectRatio);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Unable to set user min aspect ratio");
+ return;
+ }
+ // Only update to selected aspect ratio if nothing goes wrong
+ mSelectedKey = selectedKey;
+ updateAllPreferences(mSelectedKey);
+ Log.d(TAG, "Killing application process " + mPackageName);
+ try {
+ final IActivityManager am = ActivityManager.getService();
+ am.stopAppForUser(mPackageName, mUserId);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Unable to stop application " + mPackageName);
+ }
+ }
+
+ @Override
+ public int getMetricsCategory() {
+ // TODO(b/292566895): add metrics for logging
+ return 0;
+ }
+
+ @Override
+ protected boolean refreshUi() {
+ if (mPackageInfo == null || mPackageInfo.applicationInfo == null) {
+ return false;
+ }
+ updateAllPreferences(mSelectedKey);
+ return true;
+ }
+
+ @Override
+ protected AlertDialog createDialog(int id, int errorCode) {
+ return null;
+ }
+
+ private void launchApplication() {
+ Intent launchIntent = mPm.getLaunchIntentForPackage(mPackageName)
+ .addFlags(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_CLEAR_TOP);
+ if (launchIntent != null) {
+ getContext().startActivityAsUser(launchIntent, new UserHandle(mUserId));
+ }
+ }
+
+ @PackageManager.UserMinAspectRatio
+ private int getSelectedUserMinAspectRatio(@NonNull String selectedKey) {
+ switch (selectedKey) {
+ case KEY_PREF_FULLSCREEN:
+ return USER_MIN_ASPECT_RATIO_FULLSCREEN;
+ case KEY_PREF_HALF_SCREEN:
+ return USER_MIN_ASPECT_RATIO_SPLIT_SCREEN;
+ case KEY_PREF_DISPLAY_SIZE:
+ return USER_MIN_ASPECT_RATIO_DISPLAY_SIZE;
+ case KEY_PREF_3_2:
+ return USER_MIN_ASPECT_RATIO_3_2;
+ case KEY_PREF_4_3:
+ return USER_MIN_ASPECT_RATIO_4_3;
+ case KEY_PREF_16_9:
+ return USER_MIN_ASPECT_RATIO_16_9;
+ default:
+ return USER_MIN_ASPECT_RATIO_UNSET;
+ }
+ }
+
+ @NonNull
+ private String getSelectedKey(@PackageManager.UserMinAspectRatio int userMinAspectRatio) {
+ switch (userMinAspectRatio) {
+ case USER_MIN_ASPECT_RATIO_FULLSCREEN:
+ return KEY_PREF_FULLSCREEN;
+ case USER_MIN_ASPECT_RATIO_SPLIT_SCREEN:
+ return KEY_PREF_HALF_SCREEN;
+ case USER_MIN_ASPECT_RATIO_DISPLAY_SIZE:
+ return KEY_PREF_DISPLAY_SIZE;
+ case USER_MIN_ASPECT_RATIO_3_2:
+ return KEY_PREF_3_2;
+ case USER_MIN_ASPECT_RATIO_4_3:
+ return KEY_PREF_4_3;
+ case USER_MIN_ASPECT_RATIO_16_9:
+ return KEY_PREF_16_9;
+ default:
+ return KEY_PREF_DEFAULT;
+ }
+ }
+
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+ final Preference pref = EntityHeaderController
+ .newInstance(getActivity(), this, null /* header */)
+ .setIcon(Utils.getBadgedIcon(getContext(), mPackageInfo.applicationInfo))
+ .setLabel(mPackageInfo.applicationInfo.loadLabel(mPm))
+ .setIsInstantApp(AppUtils.isInstant(mPackageInfo.applicationInfo))
+ .setPackageName(mPackageName)
+ .setUid(mPackageInfo.applicationInfo.uid)
+ .setHasAppInfoLink(true)
+ .setButtonActions(EntityHeaderController.ActionType.ACTION_NONE,
+ EntityHeaderController.ActionType.ACTION_NONE)
+ .done(getActivity(), getPrefContext());
+
+ getPreferenceScreen().addPreference(pref);
+ }
+
+ private void initPreferences() {
+ addPreferencesFromResource(R.xml.user_aspect_ratio_details);
+
+ final String summary = getContext().getResources().getString(
+ R.string.aspect_ratio_main_summary, Build.MODEL);
+ findPreference(KEY_HEADER_SUMMARY).setTitle(summary);
+
+ ((ActionButtonsPreference) findPreference(KEY_HEADER_BUTTONS))
+ .setButton1Text(R.string.launch_instant_app)
+ .setButton1Icon(R.drawable.ic_settings_open)
+ .setButton1OnClickListener(v -> launchApplication());
+
+ addPreference(KEY_PREF_DEFAULT, USER_MIN_ASPECT_RATIO_UNSET);
+ addPreference(KEY_PREF_FULLSCREEN, USER_MIN_ASPECT_RATIO_FULLSCREEN);
+ addPreference(KEY_PREF_DISPLAY_SIZE, USER_MIN_ASPECT_RATIO_DISPLAY_SIZE);
+ addPreference(KEY_PREF_HALF_SCREEN, USER_MIN_ASPECT_RATIO_SPLIT_SCREEN);
+ addPreference(KEY_PREF_16_9, USER_MIN_ASPECT_RATIO_16_9);
+ addPreference(KEY_PREF_4_3, USER_MIN_ASPECT_RATIO_4_3);
+ addPreference(KEY_PREF_3_2, USER_MIN_ASPECT_RATIO_3_2);
+ }
+
+ private void addPreference(@NonNull String key,
+ @PackageManager.UserMinAspectRatio int aspectRatio) {
+ final RadioWithImagePreference pref = findPreference(key);
+ if (pref == null) {
+ return;
+ }
+ if (!mUserAspectRatioManager.hasAspectRatioOption(aspectRatio, mPackageName)) {
+ pref.setVisible(false);
+ return;
+ }
+ pref.setTitle(mUserAspectRatioManager.getAccessibleEntry(aspectRatio, mPackageName));
+ pref.setOnClickListener(this);
+ mAspectRatioPreferences.add(pref);
+ }
+
+ private void updateAllPreferences(@NonNull String selectedKey) {
+ for (RadioWithImagePreference pref : mAspectRatioPreferences) {
+ pref.setChecked(selectedKey.equals(pref.getKey()));
+ }
+ }
+
+ @VisibleForTesting
+ UserAspectRatioManager getAspectRatioManager() {
+ return mUserAspectRatioManager;
+ }
+}
diff --git a/src/com/android/settings/applications/appcompat/UserAspectRatioManager.java b/src/com/android/settings/applications/appcompat/UserAspectRatioManager.java
new file mode 100644
index 0000000..b940dc8
--- /dev/null
+++ b/src/com/android/settings/applications/appcompat/UserAspectRatioManager.java
@@ -0,0 +1,274 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.applications.appcompat;
+
+import static android.os.UserHandle.getUserHandleForUid;
+import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_FULLSCREEN_OVERRIDE;
+import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_OVERRIDE;
+
+import static java.lang.Boolean.FALSE;
+
+import android.app.AppGlobals;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.IPackageManager;
+import android.content.pm.LauncherApps;
+import android.content.pm.PackageManager;
+import android.os.RemoteException;
+import android.provider.DeviceConfig;
+import android.util.ArrayMap;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.settings.R;
+import com.android.settings.Utils;
+
+import com.google.common.annotations.VisibleForTesting;
+
+import java.util.Map;
+
+/**
+ * Helper class for handling app aspect ratio override
+ * {@link PackageManager.UserMinAspectRatio} set by user
+ */
+public class UserAspectRatioManager {
+ private static final Intent LAUNCHER_ENTRY_INTENT =
+ new Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_LAUNCHER);
+
+ // TODO(b/288142656): Enable user aspect ratio settings by default
+ private static final boolean DEFAULT_VALUE_ENABLE_USER_ASPECT_RATIO_SETTINGS = true;
+ @VisibleForTesting
+ static final String KEY_ENABLE_USER_ASPECT_RATIO_SETTINGS =
+ "enable_app_compat_aspect_ratio_user_settings";
+ static final String KEY_ENABLE_USER_ASPECT_RATIO_FULLSCREEN =
+ "enable_app_compat_user_aspect_ratio_fullscreen";
+ private static final boolean DEFAULT_VALUE_ENABLE_USER_ASPECT_RATIO_FULLSCREEN = true;
+
+ private final Context mContext;
+ private final IPackageManager mIPm;
+ /** Apps that have launcher entry defined in manifest */
+ private final Map<Integer, String> mUserAspectRatioMap;
+ private final Map<Integer, CharSequence> mUserAspectRatioA11yMap;
+
+ public UserAspectRatioManager(@NonNull Context context) {
+ mContext = context;
+ mIPm = AppGlobals.getPackageManager();
+ mUserAspectRatioA11yMap = new ArrayMap<>();
+ mUserAspectRatioMap = getUserMinAspectRatioMapping();
+ }
+
+ /**
+ * Whether user aspect ratio settings is enabled for device.
+ */
+ public static boolean isFeatureEnabled(Context context) {
+ final boolean isBuildTimeFlagEnabled = context.getResources().getBoolean(
+ com.android.internal.R.bool.config_appCompatUserAppAspectRatioSettingsIsEnabled);
+ return getValueFromDeviceConfig(KEY_ENABLE_USER_ASPECT_RATIO_SETTINGS,
+ DEFAULT_VALUE_ENABLE_USER_ASPECT_RATIO_SETTINGS) && isBuildTimeFlagEnabled;
+ }
+
+ /**
+ * @return user-specific {@link PackageManager.UserMinAspectRatio} override for an app
+ */
+ @PackageManager.UserMinAspectRatio
+ public int getUserMinAspectRatioValue(@NonNull String packageName, int uid)
+ throws RemoteException {
+ final int aspectRatio = mIPm.getUserMinAspectRatio(packageName, uid);
+ return hasAspectRatioOption(aspectRatio, packageName)
+ ? aspectRatio : PackageManager.USER_MIN_ASPECT_RATIO_UNSET;
+ }
+
+ /**
+ * @return corresponding string for {@link PackageManager.UserMinAspectRatio} value
+ */
+ @NonNull
+ public String getUserMinAspectRatioEntry(@PackageManager.UserMinAspectRatio int aspectRatio,
+ String packageName) {
+ if (!hasAspectRatioOption(aspectRatio, packageName)) {
+ return mUserAspectRatioMap.get(PackageManager.USER_MIN_ASPECT_RATIO_UNSET);
+ }
+ return mUserAspectRatioMap.get(aspectRatio);
+ }
+
+ /**
+ * @return corresponding accessible string for {@link PackageManager.UserMinAspectRatio} value
+ */
+ @NonNull
+ public CharSequence getAccessibleEntry(@PackageManager.UserMinAspectRatio int aspectRatio,
+ String packageName) {
+ return mUserAspectRatioA11yMap.getOrDefault(aspectRatio,
+ getUserMinAspectRatioEntry(aspectRatio, packageName));
+ }
+
+ /**
+ * @return corresponding aspect ratio string for package name and user
+ */
+ @NonNull
+ public String getUserMinAspectRatioEntry(@NonNull String packageName, int uid)
+ throws RemoteException {
+ final int aspectRatio = getUserMinAspectRatioValue(packageName, uid);
+ return getUserMinAspectRatioEntry(aspectRatio, packageName);
+ }
+
+ /**
+ * Whether user aspect ratio option is specified in
+ * {@link R.array.config_userAspectRatioOverrideValues}
+ * and is enabled by device config
+ */
+ public boolean hasAspectRatioOption(@PackageManager.UserMinAspectRatio int option,
+ String packageName) {
+ if (option == PackageManager.USER_MIN_ASPECT_RATIO_FULLSCREEN
+ && !isFullscreenOptionEnabled(packageName)) {
+ return false;
+ }
+ return mUserAspectRatioMap.containsKey(option);
+ }
+
+ /**
+ * Sets user-specified {@link PackageManager.UserMinAspectRatio} override for an app
+ */
+ public void setUserMinAspectRatio(@NonNull String packageName, int uid,
+ @PackageManager.UserMinAspectRatio int aspectRatio) throws RemoteException {
+ mIPm.setUserMinAspectRatio(packageName, uid, aspectRatio);
+ }
+
+ /**
+ * Whether an app's aspect ratio can be overridden by user. Only apps with launcher entry
+ * will be overridable.
+ */
+ public boolean canDisplayAspectRatioUi(@NonNull ApplicationInfo app) {
+ Boolean appAllowsUserAspectRatioOverride = readComponentProperty(
+ mContext.getPackageManager(), app.packageName,
+ PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_OVERRIDE);
+ return !FALSE.equals(appAllowsUserAspectRatioOverride) && hasLauncherEntry(app);
+ }
+
+ /**
+ * Whether fullscreen option in per-app user aspect ratio settings is enabled
+ */
+ @VisibleForTesting
+ boolean isFullscreenOptionEnabled(String packageName) {
+ Boolean appAllowsFullscreenOption = readComponentProperty(mContext.getPackageManager(),
+ packageName, PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_FULLSCREEN_OVERRIDE);
+ final boolean isBuildTimeFlagEnabled = mContext.getResources().getBoolean(
+ com.android.internal.R.bool.config_appCompatUserAppAspectRatioFullscreenIsEnabled);
+ return !FALSE.equals(appAllowsFullscreenOption) && isBuildTimeFlagEnabled
+ && getValueFromDeviceConfig(KEY_ENABLE_USER_ASPECT_RATIO_FULLSCREEN,
+ DEFAULT_VALUE_ENABLE_USER_ASPECT_RATIO_FULLSCREEN);
+ }
+
+ LauncherApps getLauncherApps() {
+ return mContext.getSystemService(LauncherApps.class);
+ }
+
+ private boolean hasLauncherEntry(@NonNull ApplicationInfo app) {
+ return !getLauncherApps().getActivityList(app.packageName, getUserHandleForUid(app.uid))
+ .isEmpty();
+ }
+
+ private static boolean getValueFromDeviceConfig(String name, boolean defaultValue) {
+ return DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_WINDOW_MANAGER, name, defaultValue);
+ }
+
+ @NonNull
+ private Map<Integer, String> getUserMinAspectRatioMapping() {
+ final String[] userMinAspectRatioStrings = mContext.getResources().getStringArray(
+ R.array.config_userAspectRatioOverrideEntries);
+ final int[] userMinAspectRatioValues = mContext.getResources().getIntArray(
+ R.array.config_userAspectRatioOverrideValues);
+ if (userMinAspectRatioStrings.length != userMinAspectRatioValues.length) {
+ throw new RuntimeException(
+ "config_userAspectRatioOverride options cannot be different length");
+ }
+
+ final Map<Integer, String> userMinAspectRatioMap = new ArrayMap<>();
+ for (int i = 0; i < userMinAspectRatioValues.length; i++) {
+ final int aspectRatioVal = userMinAspectRatioValues[i];
+ final String aspectRatioString = getAspectRatioStringOrDefault(
+ userMinAspectRatioStrings[i], aspectRatioVal);
+ boolean containsColon = aspectRatioString.contains(":");
+ switch (aspectRatioVal) {
+ // Only map known values of UserMinAspectRatio and ignore unknown entries
+ case PackageManager.USER_MIN_ASPECT_RATIO_FULLSCREEN:
+ case PackageManager.USER_MIN_ASPECT_RATIO_UNSET:
+ case PackageManager.USER_MIN_ASPECT_RATIO_SPLIT_SCREEN:
+ case PackageManager.USER_MIN_ASPECT_RATIO_DISPLAY_SIZE:
+ case PackageManager.USER_MIN_ASPECT_RATIO_4_3:
+ case PackageManager.USER_MIN_ASPECT_RATIO_16_9:
+ case PackageManager.USER_MIN_ASPECT_RATIO_3_2:
+ if (containsColon) {
+ String[] aspectRatioDigits = aspectRatioString.split(":");
+ String accessibleString = getAccessibleOption(aspectRatioDigits[0],
+ aspectRatioDigits[1]);
+ final CharSequence accessibleSequence = Utils.createAccessibleSequence(
+ aspectRatioString, accessibleString);
+ mUserAspectRatioA11yMap.put(aspectRatioVal, accessibleSequence);
+ }
+ userMinAspectRatioMap.put(aspectRatioVal, aspectRatioString);
+ }
+ }
+ if (!userMinAspectRatioMap.containsKey(PackageManager.USER_MIN_ASPECT_RATIO_UNSET)) {
+ throw new RuntimeException("config_userAspectRatioOverrideValues options must have"
+ + " USER_MIN_ASPECT_RATIO_UNSET value");
+ }
+ return userMinAspectRatioMap;
+ }
+
+ @NonNull
+ private String getAccessibleOption(String numerator, String denominator) {
+ return mContext.getResources().getString(R.string.user_aspect_ratio_option_a11y,
+ numerator, denominator);
+ }
+
+ @NonNull
+ private String getAspectRatioStringOrDefault(@Nullable String aspectRatioString,
+ @PackageManager.UserMinAspectRatio int aspectRatioVal) {
+ if (aspectRatioString != null) {
+ return aspectRatioString;
+ }
+ // Options are customized per device and if strings are set to @null, use default
+ switch (aspectRatioVal) {
+ case PackageManager.USER_MIN_ASPECT_RATIO_FULLSCREEN:
+ return mContext.getString(R.string.user_aspect_ratio_fullscreen);
+ case PackageManager.USER_MIN_ASPECT_RATIO_SPLIT_SCREEN:
+ return mContext.getString(R.string.user_aspect_ratio_half_screen);
+ case PackageManager.USER_MIN_ASPECT_RATIO_DISPLAY_SIZE:
+ return mContext.getString(R.string.user_aspect_ratio_device_size);
+ case PackageManager.USER_MIN_ASPECT_RATIO_4_3:
+ return mContext.getString(R.string.user_aspect_ratio_4_3);
+ case PackageManager.USER_MIN_ASPECT_RATIO_16_9:
+ return mContext.getString(R.string.user_aspect_ratio_16_9);
+ case PackageManager.USER_MIN_ASPECT_RATIO_3_2:
+ return mContext.getString(R.string.user_aspect_ratio_3_2);
+ default:
+ return mContext.getString(R.string.user_aspect_ratio_app_default);
+ }
+ }
+
+ @Nullable
+ private static Boolean readComponentProperty(PackageManager pm, String packageName,
+ String propertyName) {
+ try {
+ return pm.getProperty(propertyName, packageName).getBoolean();
+ } catch (PackageManager.NameNotFoundException e) {
+ // No such property name
+ }
+ return null;
+ }
+}
diff --git a/src/com/android/settings/applications/manageapplications/ManageApplications.java b/src/com/android/settings/applications/manageapplications/ManageApplications.java
index 548ca55..d734a27 100644
--- a/src/com/android/settings/applications/manageapplications/ManageApplications.java
+++ b/src/com/android/settings/applications/manageapplications/ManageApplications.java
@@ -269,6 +269,7 @@
public static final int LIST_TYPE_CLONED_APPS = 17;
public static final int LIST_TYPE_NFC_TAG_APPS = 18;
public static final int LIST_TYPE_TURN_SCREEN_ON = 19;
+ public static final int LIST_TYPE_USER_ASPECT_RATIO_APPS = 20;
// List types that should show instant apps.
public static final Set<Integer> LIST_TYPES_WITH_INSTANT = new ArraySet<>(Arrays.asList(
diff --git a/src/com/android/settings/applications/manageapplications/ManageApplicationsUtil.kt b/src/com/android/settings/applications/manageapplications/ManageApplicationsUtil.kt
index 78a4a6b..8313686 100644
--- a/src/com/android/settings/applications/manageapplications/ManageApplicationsUtil.kt
+++ b/src/com/android/settings/applications/manageapplications/ManageApplicationsUtil.kt
@@ -20,6 +20,7 @@
import android.util.FeatureFlagUtils
import com.android.settings.Settings.AlarmsAndRemindersActivity
import com.android.settings.Settings.AppBatteryUsageActivity
+import com.android.settings.Settings.UserAspectRatioAppListActivity
import com.android.settings.Settings.ChangeNfcTagAppsActivity
import com.android.settings.Settings.ChangeWifiStateActivity
import com.android.settings.Settings.ClonedAppsListActivity
@@ -40,6 +41,7 @@
import com.android.settings.applications.manageapplications.ManageApplications.LIST_MANAGE_EXTERNAL_STORAGE
import com.android.settings.applications.manageapplications.ManageApplications.LIST_TYPE_ALARMS_AND_REMINDERS
import com.android.settings.applications.manageapplications.ManageApplications.LIST_TYPE_APPS_LOCALE
+import com.android.settings.applications.manageapplications.ManageApplications.LIST_TYPE_USER_ASPECT_RATIO_APPS
import com.android.settings.applications.manageapplications.ManageApplications.LIST_TYPE_BATTERY_OPTIMIZATION
import com.android.settings.applications.manageapplications.ManageApplications.LIST_TYPE_CLONED_APPS
import com.android.settings.applications.manageapplications.ManageApplications.LIST_TYPE_GAMES
@@ -57,12 +59,14 @@
import com.android.settings.applications.manageapplications.ManageApplications.LIST_TYPE_WIFI_ACCESS
import com.android.settings.applications.manageapplications.ManageApplications.LIST_TYPE_WRITE_SETTINGS
import com.android.settings.spa.app.AllAppListPageProvider
+import com.android.settings.spa.app.appcompat.UserAspectRatioAppsPageProvider
import com.android.settings.spa.app.specialaccess.AlarmsAndRemindersAppListProvider
import com.android.settings.spa.app.specialaccess.AllFilesAccessAppListProvider
import com.android.settings.spa.app.specialaccess.DisplayOverOtherAppsAppListProvider
import com.android.settings.spa.app.specialaccess.InstallUnknownAppsListProvider
import com.android.settings.spa.app.specialaccess.MediaManagementAppsAppListProvider
import com.android.settings.spa.app.specialaccess.ModifySystemSettingsAppListProvider
+import com.android.settings.spa.app.specialaccess.NfcTagAppsSettingsProvider
import com.android.settings.spa.app.specialaccess.WifiControlAppListProvider
import com.android.settings.spa.notification.AppListNotificationsPageProvider
import com.android.settings.spa.system.AppLanguagesPageProvider
@@ -91,6 +95,7 @@
ClonedAppsListActivity::class to LIST_TYPE_CLONED_APPS,
ChangeNfcTagAppsActivity::class to LIST_TYPE_NFC_TAG_APPS,
TurnScreenOnSettingsActivity::class to LIST_TYPE_TURN_SCREEN_ON,
+ UserAspectRatioAppListActivity::class to LIST_TYPE_USER_ASPECT_RATIO_APPS,
)
@JvmField
@@ -112,6 +117,8 @@
LIST_TYPE_NOTIFICATION -> AppListNotificationsPageProvider.name
LIST_TYPE_APPS_LOCALE -> AppLanguagesPageProvider.name
LIST_TYPE_MAIN -> AllAppListPageProvider.name
+ LIST_TYPE_NFC_TAG_APPS -> NfcTagAppsSettingsProvider.getAppListRoute()
+ LIST_TYPE_USER_ASPECT_RATIO_APPS -> UserAspectRatioAppsPageProvider.name
else -> null
}
}
diff --git a/src/com/android/settings/applications/specialaccess/DataSaverController.java b/src/com/android/settings/applications/specialaccess/DataSaverController.java
deleted file mode 100644
index d1fd202..0000000
--- a/src/com/android/settings/applications/specialaccess/DataSaverController.java
+++ /dev/null
@@ -1,36 +0,0 @@
-/*
- * Copyright (C) 2017 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.settings.applications.specialaccess;
-
-import android.content.Context;
-
-import com.android.settings.R;
-import com.android.settings.core.BasePreferenceController;
-
-public class DataSaverController extends BasePreferenceController {
-
- public DataSaverController(Context context, String key) {
- super(context, key);
- }
-
- @AvailabilityStatus
- public int getAvailabilityStatus() {
- return mContext.getResources().getBoolean(R.bool.config_show_data_saver)
- ? AVAILABLE : UNSUPPORTED_ON_DEVICE;
- }
-}
diff --git a/src/com/android/settings/applications/specialaccess/DataSaverController.kt b/src/com/android/settings/applications/specialaccess/DataSaverController.kt
new file mode 100644
index 0000000..baed0aa
--- /dev/null
+++ b/src/com/android/settings/applications/specialaccess/DataSaverController.kt
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2017 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.settings.applications.specialaccess
+
+import android.content.Context
+import android.net.NetworkPolicyManager
+import android.os.UserHandle
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
+import androidx.preference.Preference
+import androidx.preference.PreferenceScreen
+import com.android.settings.R
+import com.android.settings.core.BasePreferenceController
+import com.android.settingslib.spa.framework.util.formatString
+import com.android.settingslib.spaprivileged.model.app.AppListRepository
+import com.android.settingslib.spaprivileged.model.app.AppListRepositoryImpl
+import com.google.common.annotations.VisibleForTesting
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.async
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+
+class DataSaverController(context: Context, key: String) : BasePreferenceController(context, key) {
+
+ private lateinit var preference: Preference
+
+ @AvailabilityStatus
+ override fun getAvailabilityStatus(): Int = when {
+ mContext.resources.getBoolean(R.bool.config_show_data_saver) -> AVAILABLE
+ else -> UNSUPPORTED_ON_DEVICE
+ }
+
+ override fun displayPreference(screen: PreferenceScreen) {
+ super.displayPreference(screen)
+ preference = screen.findPreference(preferenceKey)!!
+ }
+
+ override fun onViewCreated(viewLifecycleOwner: LifecycleOwner) {
+ viewLifecycleOwner.lifecycleScope.launch {
+ viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
+ preference.summary = getUnrestrictedSummary(mContext)
+ }
+ }
+ }
+
+ companion object {
+ @VisibleForTesting
+ suspend fun getUnrestrictedSummary(
+ context: Context,
+ appListRepository: AppListRepository =
+ AppListRepositoryImpl(context.applicationContext),
+ ) = context.formatString(
+ R.string.data_saver_unrestricted_summary,
+ "count" to getAllowCount(context.applicationContext, appListRepository),
+ )
+
+ private suspend fun getAllowCount(context: Context, appListRepository: AppListRepository) =
+ withContext(Dispatchers.IO) {
+ coroutineScope {
+ val appsDeferred = async {
+ appListRepository.loadAndFilterApps(
+ userId = UserHandle.myUserId(),
+ isSystemApp = false,
+ )
+ }
+ val uidsAllowed = NetworkPolicyManager.from(context)
+ .getUidsWithPolicy(NetworkPolicyManager.POLICY_ALLOW_METERED_BACKGROUND)
+ appsDeferred.await().count { app -> app.uid in uidsAllowed }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/settings/biometrics/BiometricEnrollBase.java b/src/com/android/settings/biometrics/BiometricEnrollBase.java
index 2f852f0..6e11079 100644
--- a/src/com/android/settings/biometrics/BiometricEnrollBase.java
+++ b/src/com/android/settings/biometrics/BiometricEnrollBase.java
@@ -133,6 +133,7 @@
protected long mChallenge;
protected boolean mFromSettingsSummary;
protected FooterBarMixin mFooterBarMixin;
+ protected boolean mShouldSetFooterBarBackground = true;
@Nullable
protected ScreenSizeFoldProvider mScreenSizeFoldProvider;
@Nullable
@@ -191,12 +192,14 @@
super.onPostCreate(savedInstanceState);
initViews();
- @SuppressLint("VisibleForTests")
- final LinearLayout buttonContainer = mFooterBarMixin != null
- ? mFooterBarMixin.getButtonContainer()
- : null;
- if (buttonContainer != null) {
- buttonContainer.setBackgroundColor(getBackgroundColor());
+ if (mShouldSetFooterBarBackground) {
+ @SuppressLint("VisibleForTests")
+ final LinearLayout buttonContainer = mFooterBarMixin != null
+ ? mFooterBarMixin.getButtonContainer()
+ : null;
+ if (buttonContainer != null) {
+ buttonContainer.setBackgroundColor(getBackgroundColor());
+ }
}
}
@@ -331,7 +334,7 @@
}
@ColorInt
- private int getBackgroundColor() {
+ public int getBackgroundColor() {
final ColorStateList stateList = Utils.getColorAttr(this, android.R.attr.windowBackground);
return stateList != null ? stateList.getDefaultColor() : Color.TRANSPARENT;
}
diff --git a/src/com/android/settings/biometrics/BiometricEnrollIntroduction.java b/src/com/android/settings/biometrics/BiometricEnrollIntroduction.java
index 2a350f4..46f534d 100644
--- a/src/com/android/settings/biometrics/BiometricEnrollIntroduction.java
+++ b/src/com/android/settings/biometrics/BiometricEnrollIntroduction.java
@@ -236,6 +236,9 @@
protected void onResume() {
super.onResume();
+ //reset mNextClick to make sure introduction page would be closed correctly
+ mNextClicked = false;
+
final int errorMsg = checkMaxEnrolled();
if (errorMsg == 0) {
mErrorText.setText(null);
diff --git a/src/com/android/settings/biometrics/BiometricEnrollSidecar.java b/src/com/android/settings/biometrics/BiometricEnrollSidecar.java
index 97d46a4..369fa4b 100644
--- a/src/com/android/settings/biometrics/BiometricEnrollSidecar.java
+++ b/src/com/android/settings/biometrics/BiometricEnrollSidecar.java
@@ -48,11 +48,16 @@
/**
* Called when a pointer down event has occurred.
*/
- default void onPointerDown(int sensorId) { }
+ default void onUdfpsPointerDown(int sensorId) { }
/**
* Called when a pointer up event has occurred.
*/
- default void onPointerUp(int sensorId) { }
+ default void onUdfpsPointerUp(int sensorId) { }
+
+ /**
+ * Called when udfps overlay is shown.
+ */
+ default void onUdfpsOverlayShown() { }
}
private int mEnrollmentSteps = -1;
@@ -126,29 +131,36 @@
}
}
- private class QueuedPointerDown extends QueuedEvent {
+ private class QueuedUdfpsPointerDown extends QueuedEvent {
private final int sensorId;
- public QueuedPointerDown(int sensorId) {
+ QueuedUdfpsPointerDown(int sensorId) {
this.sensorId = sensorId;
}
@Override
public void send(Listener listener) {
- listener.onPointerDown(sensorId);
+ listener.onUdfpsPointerDown(sensorId);
}
}
- private class QueuedPointerUp extends QueuedEvent {
+ private class QueuedUdfpsPointerUp extends QueuedEvent {
private final int sensorId;
- public QueuedPointerUp(int sensorId) {
+ QueuedUdfpsPointerUp(int sensorId) {
this.sensorId = sensorId;
}
@Override
public void send(Listener listener) {
- listener.onPointerUp(sensorId);
+ listener.onUdfpsPointerUp(sensorId);
+ }
+ }
+
+ private class QueuedUdfpsOverlayShown extends QueuedEvent {
+ @Override
+ public void send(Listener listener) {
+ listener.onUdfpsOverlayShown();
}
}
@@ -249,19 +261,27 @@
}
}
- protected void onPointerDown(int sensorId) {
+ protected void onUdfpsPointerDown(int sensorId) {
if (mListener != null) {
- mListener.onPointerDown(sensorId);
+ mListener.onUdfpsPointerDown(sensorId);
} else {
- mQueuedEvents.add(new QueuedPointerDown(sensorId));
+ mQueuedEvents.add(new QueuedUdfpsPointerDown(sensorId));
}
}
- protected void onPointerUp(int sensorId) {
+ protected void onUdfpsPointerUp(int sensorId) {
if (mListener != null) {
- mListener.onPointerUp(sensorId);
+ mListener.onUdfpsPointerUp(sensorId);
} else {
- mQueuedEvents.add(new QueuedPointerUp(sensorId));
+ mQueuedEvents.add(new QueuedUdfpsPointerUp(sensorId));
+ }
+ }
+
+ protected void onUdfpsOverlayShown() {
+ if (mListener != null) {
+ mListener.onUdfpsOverlayShown();
+ } else {
+ mQueuedEvents.add(new QueuedUdfpsOverlayShown());
}
}
diff --git a/src/com/android/settings/biometrics/BiometricUtils.java b/src/com/android/settings/biometrics/BiometricUtils.java
index 3356dfa..4e1a2f3 100644
--- a/src/com/android/settings/biometrics/BiometricUtils.java
+++ b/src/com/android/settings/biometrics/BiometricUtils.java
@@ -527,17 +527,18 @@
// Assume the flow is "Screen Lock" + "Face" + "Fingerprint"
ssb.append(bidi.unicodeWrap(screenLock));
+ if (hasFingerprint) {
+ ssb.append(bidi.unicodeWrap(SEPARATOR));
+ ssb.append(bidi.unicodeWrap(
+ capitalize(context.getString(R.string.security_settings_fingerprint))));
+ }
+
if (isFaceSupported) {
ssb.append(bidi.unicodeWrap(SEPARATOR));
ssb.append(bidi.unicodeWrap(
capitalize(context.getString(R.string.keywords_face_settings))));
}
- if (hasFingerprint) {
- ssb.append(bidi.unicodeWrap(SEPARATOR));
- ssb.append(bidi.unicodeWrap(
- capitalize(context.getString(R.string.security_settings_fingerprint))));
- }
return ssb.toString();
}
diff --git a/src/com/android/settings/biometrics/combination/BiometricsSettingsBase.java b/src/com/android/settings/biometrics/combination/BiometricsSettingsBase.java
index 487e254..69ae9a7 100644
--- a/src/com/android/settings/biometrics/combination/BiometricsSettingsBase.java
+++ b/src/com/android/settings/biometrics/combination/BiometricsSettingsBase.java
@@ -21,6 +21,7 @@
import static com.android.settings.password.ChooseLockPattern.RESULT_FINISHED;
+import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.hardware.biometrics.SensorProperties;
@@ -179,6 +180,12 @@
}
mFaceManager.generateChallenge(mUserId, (sensorId, userId, challenge) -> {
+ final Activity activity = getActivity();
+ if (activity == null || activity.isFinishing()) {
+ Log.e(getLogTag(), "Stop during generating face unlock challenge"
+ + " because activity is null or finishing");
+ return;
+ }
try {
final byte[] token = requestGatekeeperHat(context, mGkPwHandle, mUserId,
challenge);
@@ -215,6 +222,12 @@
}
mFingerprintManager.generateChallenge(mUserId, (sensorId, userId, challenge) -> {
+ final Activity activity = getActivity();
+ if (activity == null || activity.isFinishing()) {
+ Log.e(getLogTag(), "Stop during generating fingerprint challenge"
+ + " because activity is null or finishing");
+ return;
+ }
try {
final byte[] token = requestGatekeeperHat(context, mGkPwHandle, mUserId,
challenge);
diff --git a/src/com/android/settings/biometrics/face/FaceEnrollIntroduction.java b/src/com/android/settings/biometrics/face/FaceEnrollIntroduction.java
index bff998a..bea0c33 100644
--- a/src/com/android/settings/biometrics/face/FaceEnrollIntroduction.java
+++ b/src/com/android/settings/biometrics/face/FaceEnrollIntroduction.java
@@ -120,6 +120,8 @@
protected void onCreate(Bundle savedInstanceState) {
mFaceManager = getFaceManager();
+ super.onCreate(savedInstanceState);
+
if (savedInstanceState == null
&& !WizardManagerHelper.isAnySetupWizard(getIntent())
&& !getIntent().getBooleanExtra(EXTRA_FROM_SETTINGS_SUMMARY, false)
@@ -130,8 +132,6 @@
finish();
}
- super.onCreate(savedInstanceState);
-
// Wait super::onCreated() then return because SuperNotCalledExceptio will be thrown
// if we don't wait for it.
if (isFinishing()) {
diff --git a/src/com/android/settings/biometrics/face/FaceSettings.java b/src/com/android/settings/biometrics/face/FaceSettings.java
index 979faa2..2e94404 100644
--- a/src/com/android/settings/biometrics/face/FaceSettings.java
+++ b/src/com/android/settings/biometrics/face/FaceSettings.java
@@ -208,6 +208,10 @@
mRemoveButton = findPreference(FaceSettingsRemoveButtonPreferenceController.KEY);
mEnrollButton = findPreference(FaceSettingsEnrollButtonPreferenceController.KEY);
+ final boolean hasEnrolled = mFaceManager.hasEnrolledTemplates(mUserId);
+ mEnrollButton.setVisible(!hasEnrolled);
+ mRemoveButton.setVisible(hasEnrolled);
+
// There is no better way to do this :/
for (AbstractPreferenceController controller : mControllers) {
if (controller instanceof FaceSettingsPreferenceController) {
diff --git a/src/com/android/settings/biometrics/face/FaceSettingsRemoveButtonPreferenceController.java b/src/com/android/settings/biometrics/face/FaceSettingsRemoveButtonPreferenceController.java
index 7db5958..4b2e336 100644
--- a/src/com/android/settings/biometrics/face/FaceSettingsRemoveButtonPreferenceController.java
+++ b/src/com/android/settings/biometrics/face/FaceSettingsRemoveButtonPreferenceController.java
@@ -21,6 +21,7 @@
import android.app.settings.SettingsEnums;
import android.content.Context;
import android.content.DialogInterface;
+import android.content.pm.PackageManager;
import android.hardware.face.Face;
import android.hardware.face.FaceManager;
import android.os.Bundle;
@@ -29,6 +30,7 @@
import android.widget.Button;
import android.widget.Toast;
+import androidx.annotation.VisibleForTesting;
import androidx.preference.Preference;
import com.android.settings.R;
@@ -56,10 +58,18 @@
static final String KEY = "security_settings_face_delete_faces_container";
public static class ConfirmRemoveDialog extends InstrumentedDialogFragment {
-
- private boolean mIsConvenience;
+ private static final String KEY_IS_CONVENIENCE = "is_convenience";
private DialogInterface.OnClickListener mOnClickListener;
+ /** Returns the new instance of the class */
+ public static ConfirmRemoveDialog newInstance(boolean isConvenience) {
+ final ConfirmRemoveDialog dialog = new ConfirmRemoveDialog();
+ final Bundle args = new Bundle();
+ args.putBoolean(KEY_IS_CONVENIENCE, isConvenience);
+ dialog.setArguments(args);
+ return dialog;
+ }
+
@Override
public int getMetricsCategory() {
return SettingsEnums.DIALOG_FACE_REMOVE;
@@ -67,12 +77,26 @@
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
+ boolean isConvenience = getArguments().getBoolean(KEY_IS_CONVENIENCE);
+
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
+ final PackageManager pm = getContext().getPackageManager();
+ final boolean hasFingerprint = pm.hasSystemFeature(PackageManager.FEATURE_FINGERPRINT);
+ final int dialogMessageRes;
+
+ if (hasFingerprint) {
+ dialogMessageRes = isConvenience
+ ? R.string.security_settings_face_remove_dialog_details_fingerprint_conv
+ : R.string.security_settings_face_remove_dialog_details_fingerprint;
+ } else {
+ dialogMessageRes = isConvenience
+ ? R.string.security_settings_face_settings_remove_dialog_details_convenience
+ : R.string.security_settings_face_settings_remove_dialog_details;
+ }
+
builder.setTitle(R.string.security_settings_face_settings_remove_dialog_title)
- .setMessage(mIsConvenience
- ? R.string.security_settings_face_settings_remove_dialog_details_convenience
- : R.string.security_settings_face_settings_remove_dialog_details)
+ .setMessage(dialogMessageRes)
.setPositiveButton(R.string.delete, mOnClickListener)
.setNegativeButton(R.string.cancel, mOnClickListener);
AlertDialog dialog = builder.create();
@@ -80,10 +104,6 @@
return dialog;
}
- public void setIsConvenience(boolean isConvenience) {
- mIsConvenience = isConvenience;
- }
-
public void setOnClickListener(DialogInterface.OnClickListener listener) {
mOnClickListener = listener;
}
@@ -98,7 +118,8 @@
private Listener mListener;
private SettingsActivity mActivity;
private int mUserId;
- private boolean mRemoving;
+ @VisibleForTesting
+ boolean mRemoving;
private final MetricsFeatureProvider mMetricsFeatureProvider;
private final Context mContext;
@@ -129,7 +150,7 @@
}
};
- private final DialogInterface.OnClickListener mOnClickListener
+ private final DialogInterface.OnClickListener mOnConfirmDialogClickListener
= new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
@@ -183,6 +204,16 @@
mButton.setOnClickListener(this);
+ // If there is already a ConfirmRemoveDialog showing, reset the listener since the
+ // controller has been recreated.
+ ConfirmRemoveDialog removeDialog =
+ (ConfirmRemoveDialog) mActivity.getSupportFragmentManager()
+ .findFragmentByTag(ConfirmRemoveDialog.class.getName());
+ if (removeDialog != null) {
+ mRemoving = true;
+ removeDialog.setOnClickListener(mOnConfirmDialogClickListener);
+ }
+
if (!FaceSettings.isFaceHardwareDetected(mContext)) {
mButton.setEnabled(false);
} else {
@@ -205,10 +236,11 @@
if (v == mButton) {
mMetricsFeatureProvider.logClickedPreference(mPreference, getMetricsCategory());
mRemoving = true;
- ConfirmRemoveDialog dialog = new ConfirmRemoveDialog();
- dialog.setOnClickListener(mOnClickListener);
- dialog.setIsConvenience(BiometricUtils.isConvenience(mFaceManager));
- dialog.show(mActivity.getSupportFragmentManager(), ConfirmRemoveDialog.class.getName());
+ ConfirmRemoveDialog confirmRemoveDialog =
+ ConfirmRemoveDialog.newInstance(BiometricUtils.isConvenience(mFaceManager));
+ confirmRemoveDialog.setOnClickListener(mOnConfirmDialogClickListener);
+ confirmRemoveDialog.show(mActivity.getSupportFragmentManager(),
+ ConfirmRemoveDialog.class.getName());
}
}
diff --git a/src/com/android/settings/biometrics/fingerprint/FingerprintAuthenticateSidecar.java b/src/com/android/settings/biometrics/fingerprint/FingerprintAuthenticateSidecar.java
index 4264056..f3c8aba 100644
--- a/src/com/android/settings/biometrics/fingerprint/FingerprintAuthenticateSidecar.java
+++ b/src/com/android/settings/biometrics/fingerprint/FingerprintAuthenticateSidecar.java
@@ -21,6 +21,7 @@
import android.hardware.fingerprint.FingerprintManager.AuthenticationResult;
import android.os.CancellationSignal;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.settings.core.InstrumentedFragment;
/**
@@ -80,7 +81,6 @@
@Override
public void onAuthenticationError(int errMsgId, CharSequence errString) {
- mCancellationSignal = null;
if (mListener != null) {
mListener.onAuthenticationError(errMsgId, errString);
} else {
@@ -108,10 +108,12 @@
}
public void stopAuthentication() {
- if (mCancellationSignal != null && !mCancellationSignal.isCanceled()) {
+ if (mCancellationSignal != null) {
+ // This will automatically check if the cancel has been sent and if so
+ // it won't send it again.
mCancellationSignal.cancel();
+ mCancellationSignal = null;
}
- mCancellationSignal = null;
}
public void setListener(Listener listener) {
@@ -129,4 +131,9 @@
}
mListener = listener;
}
+
+ @VisibleForTesting
+ boolean isCancelled() {
+ return mCancellationSignal == null || mCancellationSignal.isCanceled();
+ }
}
\ No newline at end of file
diff --git a/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollEnrolling.java b/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollEnrolling.java
index 7e76405..a62bd67 100644
--- a/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollEnrolling.java
+++ b/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollEnrolling.java
@@ -32,10 +32,8 @@
import android.content.res.ColorStateList;
import android.content.res.Configuration;
import android.content.res.Resources;
-import android.graphics.Point;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffColorFilter;
-import android.graphics.Rect;
import android.graphics.drawable.Animatable2;
import android.graphics.drawable.AnimatedVectorDrawable;
import android.graphics.drawable.Drawable;
@@ -48,22 +46,16 @@
import android.os.VibrationEffect;
import android.os.Vibrator;
import android.text.TextUtils;
-import android.util.FeatureFlagUtils;
import android.util.Log;
-import android.view.DisplayInfo;
import android.view.MotionEvent;
import android.view.OrientationEventListener;
import android.view.Surface;
import android.view.View;
-import android.view.ViewGroup;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityManager;
import android.view.animation.AccelerateDecelerateInterpolator;
import android.view.animation.AnimationUtils;
import android.view.animation.Interpolator;
-import android.widget.FrameLayout;
-import android.widget.ImageView;
-import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.RelativeLayout;
import android.widget.TextView;
@@ -79,25 +71,20 @@
import com.android.settings.biometrics.BiometricsEnrollEnrolling;
import com.android.settings.core.instrumentation.InstrumentedDialogFragment;
import com.android.settingslib.display.DisplayDensityUtils;
-import com.android.settingslib.udfps.UdfpsOverlayParams;
-import com.android.settingslib.udfps.UdfpsUtils;
import com.airbnb.lottie.LottieAnimationView;
import com.airbnb.lottie.LottieCompositionFactory;
import com.airbnb.lottie.LottieProperty;
import com.airbnb.lottie.model.KeyPath;
-import com.google.android.setupcompat.template.FooterActionButton;
import com.google.android.setupcompat.template.FooterBarMixin;
import com.google.android.setupcompat.template.FooterButton;
import com.google.android.setupcompat.util.WizardManagerHelper;
-import com.google.android.setupdesign.GlifLayout;
import com.google.android.setupdesign.template.DescriptionMixin;
import com.google.android.setupdesign.template.HeaderMixin;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.List;
-import java.util.Locale;
/**
* Activity which handles the actual enrolling for fingerprint.
@@ -176,8 +163,6 @@
@VisibleForTesting
@Nullable
UdfpsEnrollHelper mUdfpsEnrollHelper;
- // TODO(b/260617060): Do not hard-code mScaleFactor, referring to AuthController.
- private float mScaleFactor = 1.0f;
private ObjectAnimator mProgressAnim;
private TextView mErrorText;
private Interpolator mFastOutSlowInInterpolator;
@@ -206,7 +191,7 @@
private boolean mHaveShownSfpsLeftEdgeLottie;
private boolean mHaveShownSfpsRightEdgeLottie;
private boolean mShouldShowLottie;
- private UdfpsUtils mUdfpsUtils;
+
private ObjectAnimator mHelpAnimation;
private OrientationEventListener mOrientationEventListener;
@@ -251,82 +236,17 @@
mAccessibilityManager = getSystemService(AccessibilityManager.class);
mIsAccessibilityEnabled = mAccessibilityManager.isEnabled();
- mUdfpsUtils = new UdfpsUtils();
- final boolean isLayoutRtl = (TextUtils.getLayoutDirectionFromLocale(
- Locale.getDefault()) == View.LAYOUT_DIRECTION_RTL);
listenOrientationEvent();
if (mCanAssumeUdfps) {
- int rotation = getApplicationContext().getDisplay().getRotation();
- final GlifLayout layout = (GlifLayout) getLayoutInflater().inflate(
- R.layout.udfps_enroll_enrolling, null, false);
- final UdfpsEnrollView udfpsEnrollView = layout.findViewById(R.id.udfps_animation_view);
- updateUdfpsEnrollView(udfpsEnrollView, props.get(0));
- switch (rotation) {
- case Surface.ROTATION_90:
- final LinearLayout layoutContainer = layout.findViewById(
- R.id.layout_container);
- final LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(
- LinearLayout.LayoutParams.MATCH_PARENT,
- LinearLayout.LayoutParams.MATCH_PARENT);
+ final UdfpsEnrollEnrollingView layout =
+ (UdfpsEnrollEnrollingView) getLayoutInflater().inflate(
+ R.layout.udfps_enroll_enrolling, null, false);
+ setUdfpsEnrollHelper();
+ layout.initView(props.get(0), mUdfpsEnrollHelper, mAccessibilityManager);
- lp.setMarginEnd((int) getResources().getDimension(
- R.dimen.rotation_90_enroll_margin_end));
- layoutContainer.setPaddingRelative((int) getResources().getDimension(
- R.dimen.rotation_90_enroll_padding_start), 0, isLayoutRtl
- ? 0 : (int) getResources().getDimension(
- R.dimen.rotation_90_enroll_padding_end), 0);
- layoutContainer.setLayoutParams(lp);
-
- setOnHoverListener(true, layout, udfpsEnrollView);
- setContentView(layout, lp);
- break;
-
- case Surface.ROTATION_0:
- case Surface.ROTATION_180:
- // In the portrait mode, layout_container's height is 0, so it's
- // always shown at the bottom of the screen.
- final FrameLayout portraitLayoutContainer = layout.findViewById(
- R.id.layout_container);
-
- // In the portrait mode, the title and lottie animation view may
- // overlap when title needs three lines, so adding some paddings
- // between them, and adjusting the fp progress view here accordingly.
- final int layoutLottieAnimationPadding = (int) getResources()
- .getDimension(R.dimen.udfps_lottie_padding_top);
- portraitLayoutContainer.setPadding(0,
- layoutLottieAnimationPadding, 0, 0);
- final ImageView progressView = udfpsEnrollView.findViewById(
- R.id.udfps_enroll_animation_fp_progress_view);
- progressView.setPadding(0, -(layoutLottieAnimationPadding),
- 0, layoutLottieAnimationPadding);
- final ImageView fingerprintView = udfpsEnrollView.findViewById(
- R.id.udfps_enroll_animation_fp_view);
- fingerprintView.setPadding(0, -layoutLottieAnimationPadding,
- 0, layoutLottieAnimationPadding);
-
- // TODO(b/260970216) Instead of hiding the description text view, we should
- // make the header view scrollable if the text is too long.
- // If description text view has overlap with udfps progress view, hide it.
- View view = layout.getDescriptionTextView();
- layout.getViewTreeObserver().addOnDrawListener(() -> {
- if (view.getVisibility() == View.VISIBLE
- && hasOverlap(view, udfpsEnrollView)) {
- view.setVisibility(View.GONE);
- }
- });
-
- setOnHoverListener(false, layout, udfpsEnrollView);
- setContentView(layout);
- break;
-
- case Surface.ROTATION_270:
- default:
- setOnHoverListener(true, layout, udfpsEnrollView);
- setContentView(layout);
- break;
- }
+ setContentView(layout);
setDescriptionText(R.string.security_settings_udfps_enroll_start_message);
} else if (mCanAssumeSfps) {
setContentView(R.layout.sfps_enroll_enrolling);
@@ -366,22 +286,11 @@
.build()
);
- if (FeatureFlagUtils.isEnabled(getApplicationContext(),
- FeatureFlagUtils.SETTINGS_SHOW_UDFPS_ENROLL_IN_SETTINGS)) {
- // Remove the space view and make the width of footer button container WRAP_CONTENT
- // to avoid hiding the udfps view progress bar bottom.
- final LinearLayout buttonContainer = mFooterBarMixin.getButtonContainer();
- View spaceView = null;
- for (int i = 0; i < buttonContainer.getChildCount(); i++) {
- if (!(buttonContainer.getChildAt(i) instanceof FooterActionButton)) {
- spaceView = buttonContainer.getChildAt(i);
- break;
- }
- }
- if (spaceView != null) {
- spaceView.setVisibility(View.GONE);
- buttonContainer.getLayoutParams().width = ViewGroup.LayoutParams.WRAP_CONTENT;
- }
+ // If it's udfps, set the background color only for secondary button if necessary.
+ if (mCanAssumeUdfps) {
+ mShouldSetFooterBarBackground = false;
+ ((UdfpsEnrollEnrollingView) getLayout()).setSecondaryButtonBackground(
+ getBackgroundColor());
}
final LayerDrawable fingerprintDrawable = mProgressBar != null
@@ -919,19 +828,26 @@
}
@Override
- public void onPointerDown(int sensorId) {
+ public void onUdfpsPointerDown(int sensorId) {
if (mUdfpsEnrollHelper != null) {
mUdfpsEnrollHelper.onPointerDown(sensorId);
}
}
@Override
- public void onPointerUp(int sensorId) {
+ public void onUdfpsPointerUp(int sensorId) {
if (mUdfpsEnrollHelper != null) {
mUdfpsEnrollHelper.onPointerUp(sensorId);
}
}
+ @Override
+ public void onUdfpsOverlayShown() {
+ if (mCanAssumeUdfps) {
+ findViewById(R.id.udfps_animation_view).setVisibility(View.VISIBLE);
+ }
+ }
+
private void updateProgress(boolean animate) {
if (mSidecar == null || !mSidecar.isEnrolling()) {
Log.d(TAG, "Enrollment not started yet");
@@ -1185,9 +1101,9 @@
}
}
- @SuppressWarnings("MissingSuperCall") // TODO: Fix me
@Override
public void onConfigurationChanged(@NonNull Configuration newConfig) {
+ super.onConfigurationChanged(newConfig);
maybeHideSfpsText(newConfig);
switch(newConfig.orientation) {
case Configuration.ORIENTATION_LANDSCAPE: {
@@ -1224,30 +1140,7 @@
}
}
- private UdfpsEnrollView updateUdfpsEnrollView(UdfpsEnrollView udfpsEnrollView,
- FingerprintSensorPropertiesInternal udfpsProps) {
- DisplayInfo displayInfo = new DisplayInfo();
- getDisplay().getDisplayInfo(displayInfo);
- mScaleFactor = mUdfpsUtils.getScaleFactor(displayInfo);
- Rect udfpsBounds = udfpsProps.getLocation().getRect();
- udfpsBounds.scale(mScaleFactor);
-
- final Rect overlayBounds = new Rect(
- 0, /* left */
- displayInfo.getNaturalHeight() / 2, /* top */
- displayInfo.getNaturalWidth(), /* right */
- displayInfo.getNaturalHeight() /* botom */);
-
- UdfpsOverlayParams params = new UdfpsOverlayParams(
- udfpsBounds,
- overlayBounds,
- displayInfo.getNaturalWidth(),
- displayInfo.getNaturalHeight(),
- mScaleFactor,
- displayInfo.rotation);
-
- udfpsEnrollView.setOverlayParams(params);
-
+ private void setUdfpsEnrollHelper() {
mUdfpsEnrollHelper = (UdfpsEnrollHelper) getSupportFragmentManager().findFragmentByTag(
FingerprintEnrollEnrolling.TAG_UDFPS_HELPER);
if (mUdfpsEnrollHelper == null) {
@@ -1257,57 +1150,6 @@
.add(mUdfpsEnrollHelper, FingerprintEnrollEnrolling.TAG_UDFPS_HELPER)
.commitAllowingStateLoss();
}
- udfpsEnrollView.setEnrollHelper(mUdfpsEnrollHelper);
-
- return udfpsEnrollView;
- }
-
- private void setOnHoverListener(boolean isLandscape, GlifLayout enrollLayout,
- UdfpsEnrollView udfpsEnrollView) {
- if (!mIsAccessibilityEnabled) return;
-
- final Context context = getApplicationContext();
- final View.OnHoverListener onHoverListener = (v, event) -> {
- // Map the touch to portrait mode if the device is in
- // landscape mode.
- final Point scaledTouch =
- mUdfpsUtils.getTouchInNativeCoordinates(event.getPointerId(0),
- event, udfpsEnrollView.getOverlayParams());
-
- if (mUdfpsUtils.isWithinSensorArea(event.getPointerId(0), event,
- udfpsEnrollView.getOverlayParams())) {
- return false;
- }
-
- final String theStr = mUdfpsUtils.onTouchOutsideOfSensorArea(
- mAccessibilityManager.isTouchExplorationEnabled(), context,
- scaledTouch.x, scaledTouch.y, udfpsEnrollView.getOverlayParams());
- if (theStr != null) {
- v.announceForAccessibility(theStr);
- }
- return false;
- };
-
- enrollLayout.findManagedViewById(isLandscape ? R.id.sud_landscape_content_area
- : R.id.sud_layout_content).setOnHoverListener(onHoverListener);
- }
-
-
- @VisibleForTesting boolean hasOverlap(View view1, View view2) {
- int[] firstPosition = new int[2];
- int[] secondPosition = new int[2];
-
- view1.getLocationOnScreen(firstPosition);
- view2.getLocationOnScreen(secondPosition);
-
- // Rect constructor parameters: left, top, right, bottom
- Rect rectView1 = new Rect(firstPosition[0], firstPosition[1],
- firstPosition[0] + view1.getMeasuredWidth(),
- firstPosition[1] + view1.getMeasuredHeight());
- Rect rectView2 = new Rect(secondPosition[0], secondPosition[1],
- secondPosition[0] + view2.getMeasuredWidth(),
- secondPosition[1] + view2.getMeasuredHeight());
- return rectView1.intersect(rectView2);
}
public static class IconTouchDialog extends InstrumentedDialogFragment {
diff --git a/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollSidecar.java b/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollSidecar.java
index 5d04cd6..493302b 100644
--- a/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollSidecar.java
+++ b/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollSidecar.java
@@ -124,13 +124,18 @@
}
@Override
- public void onPointerDown(int sensorId) {
- FingerprintEnrollSidecar.super.onPointerDown(sensorId);
+ public void onUdfpsPointerDown(int sensorId) {
+ FingerprintEnrollSidecar.super.onUdfpsPointerDown(sensorId);
}
@Override
- public void onPointerUp(int sensorId) {
- FingerprintEnrollSidecar.super.onPointerUp(sensorId);
+ public void onUdfpsPointerUp(int sensorId) {
+ FingerprintEnrollSidecar.super.onUdfpsPointerUp(sensorId);
+ }
+
+ @Override
+ public void onUdfpsOverlayShown() {
+ FingerprintEnrollSidecar.super.onUdfpsOverlayShown();
}
};
diff --git a/src/com/android/settings/biometrics/fingerprint/FingerprintSettings.java b/src/com/android/settings/biometrics/fingerprint/FingerprintSettings.java
index fb3319c..505fe1c 100644
--- a/src/com/android/settings/biometrics/fingerprint/FingerprintSettings.java
+++ b/src/com/android/settings/biometrics/fingerprint/FingerprintSettings.java
@@ -169,7 +169,8 @@
private static final String KEY_LAUNCHED_CONFIRM = "launched_confirm";
private static final String KEY_HAS_FIRST_ENROLLED = "has_first_enrolled";
private static final String KEY_IS_ENROLLING = "is_enrolled";
- private static final String KEY_REQUIRE_SCREEN_ON_TO_AUTH =
+ @VisibleForTesting
+ static final String KEY_REQUIRE_SCREEN_ON_TO_AUTH =
"security_settings_require_screen_on_to_auth";
private static final String KEY_FINGERPRINTS_ENROLLED_CATEGORY =
"security_settings_fingerprints_enrolled";
@@ -479,10 +480,8 @@
R.string.security_settings_fingerprint_enroll_introduction_v3_message,
DeviceHelper.getDeviceName(getActivity()));
column.mLearnMoreClickListener = learnMoreClickListener;
- if (isSfps()) {
- column.mLearnMoreOverrideText = getText(
- R.string.security_settings_fingerprint_settings_footer_learn_more);
- }
+ column.mLearnMoreOverrideText = getText(
+ R.string.security_settings_fingerprint_settings_footer_learn_more);
mFooterColumns.add(column);
}
}
@@ -536,10 +535,6 @@
private void addFingerprintPreferences(PreferenceGroup root) {
final String fpPrefKey = addFingerprintItemPreferences(root);
- if (isSfps()) {
- scrollToPreference(fpPrefKey);
- addFingerprintUnlockCategory();
- }
for (AbstractPreferenceController controller : mControllers) {
if (controller instanceof FingerprintSettingsPreferenceController) {
((FingerprintSettingsPreferenceController) controller).setUserId(mUserId);
@@ -547,6 +542,14 @@
((FingerprintUnlockCategoryController) controller).setUserId(mUserId);
}
}
+
+ // This needs to be after setting ids, otherwise
+ // |mRequireScreenOnToAuthPreferenceController.isChecked| is always checking the primary
+ // user instead of the user with |mUserId|.
+ if (isSfps()) {
+ scrollToPreference(fpPrefKey);
+ addFingerprintUnlockCategory();
+ }
createFooterPreference(root);
}
diff --git a/src/com/android/settings/biometrics/fingerprint/FingerprintUpdater.java b/src/com/android/settings/biometrics/fingerprint/FingerprintUpdater.java
index 36325a7..306b1a3 100644
--- a/src/com/android/settings/biometrics/fingerprint/FingerprintUpdater.java
+++ b/src/com/android/settings/biometrics/fingerprint/FingerprintUpdater.java
@@ -98,13 +98,18 @@
}
@Override
- public void onPointerDown(int sensorId) {
- mCallback.onPointerDown(sensorId);
+ public void onUdfpsPointerDown(int sensorId) {
+ mCallback.onUdfpsPointerDown(sensorId);
}
@Override
- public void onPointerUp(int sensorId) {
- mCallback.onPointerUp(sensorId);
+ public void onUdfpsPointerUp(int sensorId) {
+ mCallback.onUdfpsPointerUp(sensorId);
+ }
+
+ @Override
+ public void onUdfpsOverlayShown() {
+ mCallback.onUdfpsOverlayShown();
}
}
diff --git a/src/com/android/settings/biometrics/fingerprint/UdfpsEnrollEnrollingView.java b/src/com/android/settings/biometrics/fingerprint/UdfpsEnrollEnrollingView.java
new file mode 100644
index 0000000..df2f2f7
--- /dev/null
+++ b/src/com/android/settings/biometrics/fingerprint/UdfpsEnrollEnrollingView.java
@@ -0,0 +1,236 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.biometrics.fingerprint;
+
+import android.content.Context;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.hardware.fingerprint.FingerprintSensorPropertiesInternal;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.view.DisplayInfo;
+import android.view.Gravity;
+import android.view.Surface;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.accessibility.AccessibilityManager;
+import android.widget.Button;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+
+import androidx.annotation.ColorInt;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.settings.R;
+import com.android.settingslib.udfps.UdfpsOverlayParams;
+import com.android.settingslib.udfps.UdfpsUtils;
+
+import com.google.android.setupcompat.template.FooterBarMixin;
+import com.google.android.setupdesign.GlifLayout;
+import com.google.android.setupdesign.view.BottomScrollView;
+
+import java.util.Locale;
+
+/**
+ * View for udfps enrolling.
+ */
+public class UdfpsEnrollEnrollingView extends GlifLayout {
+ private final UdfpsUtils mUdfpsUtils;
+ private final Context mContext;
+ // We don't need to listen to onConfigurationChanged() for mRotation here because
+ // FingerprintEnrollEnrolling is always recreated once the configuration is changed.
+ private final int mRotation;
+ private final boolean mIsLandscape;
+ private final boolean mShouldUseReverseLandscape;
+ private UdfpsEnrollView mUdfpsEnrollView;
+ private View mHeaderView;
+ private AccessibilityManager mAccessibilityManager;
+
+
+ public UdfpsEnrollEnrollingView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ mContext = context;
+ mRotation = mContext.getDisplay().getRotation();
+ mIsLandscape = mRotation == Surface.ROTATION_90 || mRotation == Surface.ROTATION_270;
+ final boolean isLayoutRtl = (TextUtils.getLayoutDirectionFromLocale(Locale.getDefault())
+ == View.LAYOUT_DIRECTION_RTL);
+ mShouldUseReverseLandscape = (mRotation == Surface.ROTATION_90 && isLayoutRtl)
+ || (mRotation == Surface.ROTATION_270 && !isLayoutRtl);
+
+ mUdfpsUtils = new UdfpsUtils();
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ mHeaderView = findViewById(R.id.sud_landscape_header_area);
+ mUdfpsEnrollView = findViewById(R.id.udfps_animation_view);
+ }
+
+ void initView(FingerprintSensorPropertiesInternal udfpsProps,
+ UdfpsEnrollHelper udfpsEnrollHelper,
+ AccessibilityManager accessibilityManager) {
+ mAccessibilityManager = accessibilityManager;
+ initUdfpsEnrollView(udfpsProps, udfpsEnrollHelper);
+
+ if (!mIsLandscape) {
+ adjustPortraitPaddings();
+ } else if (mShouldUseReverseLandscape) {
+ swapHeaderAndContent();
+ }
+ setOnHoverListener();
+ }
+
+ void setSecondaryButtonBackground(@ColorInt int color) {
+ // Set the button background only when the button is not under udfps overlay to avoid UI
+ // overlap.
+ if (!mIsLandscape || mShouldUseReverseLandscape) {
+ return;
+ }
+ final Button secondaryButtonView =
+ getMixin(FooterBarMixin.class).getSecondaryButtonView();
+ secondaryButtonView.setBackgroundColor(color);
+ if (mRotation == Surface.ROTATION_90) {
+ secondaryButtonView.setGravity(Gravity.START);
+ } else {
+ secondaryButtonView.setGravity(Gravity.END);
+ }
+ mHeaderView.post(() -> {
+ secondaryButtonView.setLayoutParams(
+ new LinearLayout.LayoutParams(mHeaderView.getMeasuredWidth(),
+ ViewGroup.LayoutParams.WRAP_CONTENT));
+ });
+ }
+
+ private void initUdfpsEnrollView(FingerprintSensorPropertiesInternal udfpsProps,
+ UdfpsEnrollHelper udfpsEnrollHelper) {
+ DisplayInfo displayInfo = new DisplayInfo();
+ mContext.getDisplay().getDisplayInfo(displayInfo);
+
+ final float scaleFactor = mUdfpsUtils.getScaleFactor(displayInfo);
+ Rect udfpsBounds = udfpsProps.getLocation().getRect();
+ udfpsBounds.scale(scaleFactor);
+
+ final Rect overlayBounds = new Rect(
+ 0, /* left */
+ displayInfo.getNaturalHeight() / 2, /* top */
+ displayInfo.getNaturalWidth(), /* right */
+ displayInfo.getNaturalHeight() /* botom */);
+
+ UdfpsOverlayParams params = new UdfpsOverlayParams(
+ udfpsBounds,
+ overlayBounds,
+ displayInfo.getNaturalWidth(),
+ displayInfo.getNaturalHeight(),
+ scaleFactor,
+ displayInfo.rotation);
+
+ mUdfpsEnrollView.setOverlayParams(params);
+ mUdfpsEnrollView.setEnrollHelper(udfpsEnrollHelper);
+ }
+
+ private void adjustPortraitPaddings() {
+ // In the portrait mode, layout_container's height is 0, so it's
+ // always shown at the bottom of the screen.
+ final FrameLayout portraitLayoutContainer = findViewById(R.id.layout_container);
+
+ // In the portrait mode, the title and lottie animation view may
+ // overlap when title needs three lines, so adding some paddings
+ // between them, and adjusting the fp progress view here accordingly.
+ final int layoutLottieAnimationPadding = (int) getResources()
+ .getDimension(R.dimen.udfps_lottie_padding_top);
+ portraitLayoutContainer.setPadding(0,
+ layoutLottieAnimationPadding, 0, 0);
+ final ImageView progressView = mUdfpsEnrollView.findViewById(
+ R.id.udfps_enroll_animation_fp_progress_view);
+ progressView.setPadding(0, -(layoutLottieAnimationPadding),
+ 0, layoutLottieAnimationPadding);
+ final ImageView fingerprintView = mUdfpsEnrollView.findViewById(
+ R.id.udfps_enroll_animation_fp_view);
+ fingerprintView.setPadding(0, -layoutLottieAnimationPadding,
+ 0, layoutLottieAnimationPadding);
+
+ // TODO(b/260970216) Instead of hiding the description text view, we should
+ // make the header view scrollable if the text is too long.
+ // If description text view has overlap with udfps progress view, hide it.
+ final View descView = getDescriptionTextView();
+ getViewTreeObserver().addOnDrawListener(() -> {
+ if (descView.getVisibility() == View.VISIBLE
+ && hasOverlap(descView, mUdfpsEnrollView)) {
+ descView.setVisibility(View.GONE);
+ }
+ });
+ }
+
+ private void setOnHoverListener() {
+ if (!mAccessibilityManager.isEnabled()) return;
+
+ final View.OnHoverListener onHoverListener = (v, event) -> {
+ // Map the touch to portrait mode if the device is in
+ // landscape mode.
+ final Point scaledTouch =
+ mUdfpsUtils.getTouchInNativeCoordinates(event.getPointerId(0),
+ event, mUdfpsEnrollView.getOverlayParams());
+
+ if (mUdfpsUtils.isWithinSensorArea(event.getPointerId(0), event,
+ mUdfpsEnrollView.getOverlayParams())) {
+ return false;
+ }
+
+ final String theStr = mUdfpsUtils.onTouchOutsideOfSensorArea(
+ mAccessibilityManager.isTouchExplorationEnabled(), mContext,
+ scaledTouch.x, scaledTouch.y, mUdfpsEnrollView.getOverlayParams());
+ if (theStr != null) {
+ v.announceForAccessibility(theStr);
+ }
+ return false;
+ };
+
+ findManagedViewById(mIsLandscape ? R.id.sud_landscape_content_area
+ : R.id.sud_layout_content).setOnHoverListener(onHoverListener);
+ }
+
+ private void swapHeaderAndContent() {
+ // Reverse header and body
+ ViewGroup parentView = (ViewGroup) mHeaderView.getParent();
+ parentView.removeView(mHeaderView);
+ parentView.addView(mHeaderView);
+
+ // Hide scroll indicators
+ BottomScrollView headerScrollView = mHeaderView.findViewById(R.id.sud_header_scroll_view);
+ headerScrollView.setScrollIndicators(0);
+ }
+
+ @VisibleForTesting
+ boolean hasOverlap(View view1, View view2) {
+ int[] firstPosition = new int[2];
+ int[] secondPosition = new int[2];
+
+ view1.getLocationOnScreen(firstPosition);
+ view2.getLocationOnScreen(secondPosition);
+
+ // Rect constructor parameters: left, top, right, bottom
+ Rect rectView1 = new Rect(firstPosition[0], firstPosition[1],
+ firstPosition[0] + view1.getMeasuredWidth(),
+ firstPosition[1] + view1.getMeasuredHeight());
+ Rect rectView2 = new Rect(secondPosition[0], secondPosition[1],
+ secondPosition[0] + view2.getMeasuredWidth(),
+ secondPosition[1] + view2.getMeasuredHeight());
+ return rectView1.intersect(rectView2);
+ }
+}
diff --git a/src/com/android/settings/biometrics/fingerprint/UdfpsEnrollHelper.java b/src/com/android/settings/biometrics/fingerprint/UdfpsEnrollHelper.java
index 70fdbf0..0bfb87d 100644
--- a/src/com/android/settings/biometrics/fingerprint/UdfpsEnrollHelper.java
+++ b/src/com/android/settings/biometrics/fingerprint/UdfpsEnrollHelper.java
@@ -76,6 +76,8 @@
private int mCenterTouchCount = 0;
+ private int mPace = 1;
+
@Nullable
UdfpsEnrollHelper.Listener mListener;
@@ -157,6 +159,9 @@
}
}
+ if (mRemainingSteps > remaining) {
+ mPace = mRemainingSteps - remaining;
+ }
mRemainingSteps = remaining;
if (mListener != null && mTotalSteps != -1) {
@@ -258,7 +263,7 @@
return false;
}
- return mRemainingSteps <= 2 && mRemainingSteps >= 0;
+ return mRemainingSteps <= mPace && mRemainingSteps >= 0;
}
private int getStageThresholdSteps(int totalSteps, int stageIndex) {
diff --git a/src/com/android/settings/biometrics/fingerprint/UdfpsEnrollProgressBarDrawable.java b/src/com/android/settings/biometrics/fingerprint/UdfpsEnrollProgressBarDrawable.java
index aa3f770..75251cf 100644
--- a/src/com/android/settings/biometrics/fingerprint/UdfpsEnrollProgressBarDrawable.java
+++ b/src/com/android/settings/biometrics/fingerprint/UdfpsEnrollProgressBarDrawable.java
@@ -202,6 +202,7 @@
return;
}
+ mShowingHelp = showingHelp;
if (mShowingHelp) {
if (mVibrator != null && mIsAccessibilityEnabled) {
mVibrator.vibrate(Process.myUid(), mContext.getOpPackageName(),
@@ -228,7 +229,6 @@
}
}
- mShowingHelp = showingHelp;
mRemainingSteps = remainingSteps;
mTotalSteps = totalSteps;
diff --git a/src/com/android/settings/biometrics/fingerprint2/domain/interactor/FingerprintManagerInteractor.kt b/src/com/android/settings/biometrics/fingerprint2/domain/interactor/FingerprintManagerInteractor.kt
new file mode 100644
index 0000000..2fbdedf
--- /dev/null
+++ b/src/com/android/settings/biometrics/fingerprint2/domain/interactor/FingerprintManagerInteractor.kt
@@ -0,0 +1,207 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.biometrics.fingerprint2.domain.interactor
+
+import android.content.Context
+import android.content.Intent
+import android.hardware.fingerprint.FingerprintManager
+import android.hardware.fingerprint.FingerprintManager.GenerateChallengeCallback
+import android.hardware.fingerprint.FingerprintManager.RemovalCallback
+import android.hardware.fingerprint.FingerprintSensorPropertiesInternal
+import android.os.CancellationSignal
+import android.util.Log
+import com.android.settings.biometrics.GatekeeperPasswordProvider
+import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FingerprintAuthAttemptViewModel
+import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FingerprintViewModel
+import com.android.settings.password.ChooseLockSettingsHelper
+import kotlin.coroutines.resume
+import kotlin.coroutines.suspendCoroutine
+import kotlinx.coroutines.CancellableContinuation
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flow
+import kotlinx.coroutines.suspendCancellableCoroutine
+import kotlinx.coroutines.withContext
+
+private const val TAG = "FingerprintManagerInteractor"
+
+/** Encapsulates business logic related to managing fingerprints. */
+interface FingerprintManagerInteractor {
+ /** Returns the list of current fingerprints. */
+ val enrolledFingerprints: Flow<List<FingerprintViewModel>>
+
+ /** Returns the max enrollable fingerprints, note during SUW this might be 1 */
+ val maxEnrollableFingerprints: Flow<Int>
+
+ /** Runs [FingerprintManager.authenticate] */
+ suspend fun authenticate(): FingerprintAuthAttemptViewModel
+
+ /**
+ * Generates a challenge with the provided [gateKeeperPasswordHandle] and on success returns a
+ * challenge and challenge token. This info can be used for secure operations such as
+ * [FingerprintManager.enroll]
+ *
+ * @param gateKeeperPasswordHandle GateKeeper password handle generated by a Confirm
+ * @return A [Pair] of the challenge and challenge token
+ */
+ suspend fun generateChallenge(gateKeeperPasswordHandle: Long): Pair<Long, ByteArray>
+
+ /** Returns true if a user can enroll a fingerprint false otherwise. */
+ fun canEnrollFingerprints(numFingerprints: Int): Flow<Boolean>
+
+ /**
+ * Removes the given fingerprint, returning true if it was successfully removed and false
+ * otherwise
+ */
+ suspend fun removeFingerprint(fp: FingerprintViewModel): Boolean
+
+ /** Renames the given fingerprint if one exists */
+ suspend fun renameFingerprint(fp: FingerprintViewModel, newName: String)
+
+ /** Indicates if the device has side fingerprint */
+ suspend fun hasSideFps(): Boolean
+
+ /** Indicates if the press to auth feature has been enabled */
+ suspend fun pressToAuthEnabled(): Boolean
+
+ /** Retrieves the sensor properties of a device */
+ suspend fun sensorPropertiesInternal(): List<FingerprintSensorPropertiesInternal>
+}
+
+class FingerprintManagerInteractorImpl(
+ applicationContext: Context,
+ private val backgroundDispatcher: CoroutineDispatcher,
+ private val fingerprintManager: FingerprintManager,
+ private val gatekeeperPasswordProvider: GatekeeperPasswordProvider,
+ private val pressToAuthProvider: () -> Boolean,
+) : FingerprintManagerInteractor {
+
+ private val maxFingerprints =
+ applicationContext.resources.getInteger(
+ com.android.internal.R.integer.config_fingerprintMaxTemplatesPerUser
+ )
+ private val applicationContext = applicationContext.applicationContext
+
+ override suspend fun generateChallenge(gateKeeperPasswordHandle: Long): Pair<Long, ByteArray> =
+ suspendCoroutine {
+ val callback = GenerateChallengeCallback { _, userId, challenge ->
+ val intent = Intent()
+ intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_GK_PW_HANDLE, gateKeeperPasswordHandle)
+ val challengeToken =
+ gatekeeperPasswordProvider.requestGatekeeperHat(intent, challenge, userId)
+
+ gatekeeperPasswordProvider.removeGatekeeperPasswordHandle(intent, false)
+ val p = Pair(challenge, challengeToken)
+ it.resume(p)
+ }
+ fingerprintManager.generateChallenge(applicationContext.userId, callback)
+ }
+
+ override val enrolledFingerprints: Flow<List<FingerprintViewModel>> = flow {
+ emit(
+ fingerprintManager
+ .getEnrolledFingerprints(applicationContext.userId)
+ .map { (FingerprintViewModel(it.name.toString(), it.biometricId, it.deviceId)) }
+ .toList()
+ )
+ }
+
+ override fun canEnrollFingerprints(numFingerprints: Int): Flow<Boolean> = flow {
+ emit(numFingerprints < maxFingerprints)
+ }
+
+ override val maxEnrollableFingerprints = flow { emit(maxFingerprints) }
+
+ override suspend fun removeFingerprint(fp: FingerprintViewModel): Boolean = suspendCoroutine {
+ val callback =
+ object : RemovalCallback() {
+ override fun onRemovalError(
+ fp: android.hardware.fingerprint.Fingerprint,
+ errMsgId: Int,
+ errString: CharSequence
+ ) {
+ it.resume(false)
+ }
+
+ override fun onRemovalSucceeded(
+ fp: android.hardware.fingerprint.Fingerprint?,
+ remaining: Int
+ ) {
+ it.resume(true)
+ }
+ }
+ fingerprintManager.remove(
+ android.hardware.fingerprint.Fingerprint(fp.name, fp.fingerId, fp.deviceId),
+ applicationContext.userId,
+ callback
+ )
+ }
+
+ override suspend fun renameFingerprint(fp: FingerprintViewModel, newName: String) {
+ withContext(backgroundDispatcher) {
+ fingerprintManager.rename(fp.fingerId, applicationContext.userId, newName)
+ }
+ }
+
+ override suspend fun hasSideFps(): Boolean = suspendCancellableCoroutine {
+ it.resume(fingerprintManager.isPowerbuttonFps)
+ }
+
+ override suspend fun pressToAuthEnabled(): Boolean = suspendCancellableCoroutine {
+ it.resume(pressToAuthProvider())
+ }
+
+ override suspend fun sensorPropertiesInternal(): List<FingerprintSensorPropertiesInternal> =
+ suspendCancellableCoroutine {
+ it.resume(fingerprintManager.sensorPropertiesInternal)
+ }
+
+ override suspend fun authenticate(): FingerprintAuthAttemptViewModel =
+ suspendCancellableCoroutine { c: CancellableContinuation<FingerprintAuthAttemptViewModel> ->
+ val authenticationCallback =
+ object : FingerprintManager.AuthenticationCallback() {
+
+ override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
+ super.onAuthenticationError(errorCode, errString)
+ if (c.isCompleted) {
+ Log.d(TAG, "framework sent down onAuthError after finish")
+ return
+ }
+ c.resume(FingerprintAuthAttemptViewModel.Error(errorCode, errString.toString()))
+ }
+
+ override fun onAuthenticationSucceeded(result: FingerprintManager.AuthenticationResult) {
+ super.onAuthenticationSucceeded(result)
+ if (c.isCompleted) {
+ Log.d(TAG, "framework sent down onAuthError after finish")
+ return
+ }
+ c.resume(FingerprintAuthAttemptViewModel.Success(result.fingerprint?.biometricId ?: -1))
+ }
+ }
+
+ val cancellationSignal = CancellationSignal()
+ c.invokeOnCancellation { cancellationSignal.cancel() }
+ fingerprintManager.authenticate(
+ null,
+ cancellationSignal,
+ authenticationCallback,
+ null,
+ applicationContext.userId
+ )
+ }
+}
diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/binder/FingerprintSettingsViewBinder.kt b/src/com/android/settings/biometrics/fingerprint2/ui/binder/FingerprintSettingsViewBinder.kt
new file mode 100644
index 0000000..d9f3e43
--- /dev/null
+++ b/src/com/android/settings/biometrics/fingerprint2/ui/binder/FingerprintSettingsViewBinder.kt
@@ -0,0 +1,177 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.biometrics.fingerprint2.ui.binder
+
+import android.hardware.fingerprint.FingerprintManager
+import android.util.Log
+import androidx.lifecycle.LifecycleCoroutineScope
+import com.android.settings.biometrics.fingerprint2.ui.binder.FingerprintSettingsViewBinder.FingerprintView
+import com.android.settings.biometrics.fingerprint2.ui.viewmodel.EnrollAdditionalFingerprint
+import com.android.settings.biometrics.fingerprint2.ui.viewmodel.EnrollFirstFingerprint
+import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FingerprintAuthAttemptViewModel
+import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FingerprintSettingsNavigationViewModel
+import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FingerprintSettingsViewModel
+import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FingerprintStateViewModel
+import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FingerprintViewModel
+import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FinishSettings
+import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FinishSettingsWithResult
+import com.android.settings.biometrics.fingerprint2.ui.viewmodel.LaunchConfirmDeviceCredential
+import com.android.settings.biometrics.fingerprint2.ui.viewmodel.LaunchedActivity
+import com.android.settings.biometrics.fingerprint2.ui.viewmodel.PreferenceViewModel
+import com.android.settings.biometrics.fingerprint2.ui.viewmodel.ShowSettings
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.launch
+
+private const val TAG = "FingerprintSettingsViewBinder"
+
+/** Binds a [FingerprintSettingsViewModel] to a [FingerprintView] */
+object FingerprintSettingsViewBinder {
+
+ interface FingerprintView {
+ /**
+ * Helper function to launch fingerprint enrollment(This should be the default behavior when a
+ * user enters their PIN/PATTERN/PASS and no fingerprints are enrolled).
+ */
+ fun launchFullFingerprintEnrollment(
+ userId: Int,
+ gateKeeperPasswordHandle: Long?,
+ challenge: Long?,
+ challengeToken: ByteArray?
+ )
+
+ /** Helper to launch an add fingerprint request */
+ fun launchAddFingerprint(userId: Int, challengeToken: ByteArray?)
+ /**
+ * Helper function that will try and launch confirm lock, if that fails we will prompt user to
+ * choose a PIN/PATTERN/PASS.
+ */
+ fun launchConfirmOrChooseLock(userId: Int)
+
+ /** Used to indicate that FingerprintSettings is finished. */
+ fun finish()
+
+ /** Indicates what result should be set for the returning callee */
+ fun setResultExternal(resultCode: Int)
+ /** Indicates the settings UI should be shown */
+ fun showSettings(state: FingerprintStateViewModel)
+ /** Indicates that a user has been locked out */
+ fun userLockout(authAttemptViewModel: FingerprintAuthAttemptViewModel.Error)
+ /** Indicates a fingerprint preference should be highlighted */
+ suspend fun highlightPref(fingerId: Int)
+ /** Indicates a user should be prompted to delete a fingerprint */
+ suspend fun askUserToDeleteDialog(fingerprintViewModel: FingerprintViewModel): Boolean
+ /** Indicates a user should be asked to renae ma dialog */
+ suspend fun askUserToRenameDialog(
+ fingerprintViewModel: FingerprintViewModel
+ ): Pair<FingerprintViewModel, String>?
+ }
+
+ fun bind(
+ view: FingerprintView,
+ viewModel: FingerprintSettingsViewModel,
+ navigationViewModel: FingerprintSettingsNavigationViewModel,
+ lifecycleScope: LifecycleCoroutineScope,
+ ) {
+
+ /** Result listener for launching enrollments **after** a user has reached the settings page. */
+
+ // Settings display flow
+ lifecycleScope.launch {
+ viewModel.fingerprintState.filterNotNull().collect { view.showSettings(it) }
+ }
+
+ // Dialog flow
+ lifecycleScope.launch {
+ viewModel.isShowingDialog.collectLatest {
+ if (it == null) {
+ return@collectLatest
+ }
+ when (it) {
+ is PreferenceViewModel.RenameDialog -> {
+ val willRename = view.askUserToRenameDialog(it.fingerprintViewModel)
+ if (willRename != null) {
+ Log.d(TAG, "renaming fingerprint $it")
+ viewModel.renameFingerprint(willRename.first, willRename.second)
+ }
+ viewModel.onRenameDialogFinished()
+ }
+ is PreferenceViewModel.DeleteDialog -> {
+ if (view.askUserToDeleteDialog(it.fingerprintViewModel)) {
+ Log.d(TAG, "deleting fingerprint $it")
+ viewModel.deleteFingerprint(it.fingerprintViewModel)
+ }
+ viewModel.onDeleteDialogFinished()
+ }
+ }
+ }
+ }
+
+ // Auth flow
+ lifecycleScope.launch {
+ viewModel.authFlow.filterNotNull().collect {
+ when (it) {
+ is FingerprintAuthAttemptViewModel.Success -> {
+ view.highlightPref(it.fingerId)
+ }
+ is FingerprintAuthAttemptViewModel.Error -> {
+ if (it.error == FingerprintManager.FINGERPRINT_ERROR_LOCKOUT) {
+ view.userLockout(it)
+ }
+ }
+ }
+ }
+ }
+
+ // Launch this on Dispatchers.Default and not main.
+ // Otherwise it takes too long for state transitions such as PIN/PATTERN/PASS
+ // to enrollment, which makes gives the user a janky experience.
+ lifecycleScope.launch(Dispatchers.Default) {
+ var settingsShowingJob: Job? = null
+ navigationViewModel.nextStep.filterNotNull().collect { nextStep ->
+ settingsShowingJob?.cancel()
+ settingsShowingJob = null
+ Log.d(TAG, "next step = $nextStep")
+ when (nextStep) {
+ is EnrollFirstFingerprint ->
+ view.launchFullFingerprintEnrollment(
+ nextStep.userId,
+ nextStep.gateKeeperPasswordHandle,
+ nextStep.challenge,
+ nextStep.challengeToken
+ )
+ is EnrollAdditionalFingerprint ->
+ view.launchAddFingerprint(nextStep.userId, nextStep.challengeToken)
+ is LaunchConfirmDeviceCredential -> view.launchConfirmOrChooseLock(nextStep.userId)
+ is FinishSettings -> {
+ Log.d(TAG, "Finishing due to ${nextStep.reason}")
+ view.finish()
+ }
+ is FinishSettingsWithResult -> {
+ Log.d(TAG, "Finishing with result ${nextStep.result} due to ${nextStep.reason}")
+ view.setResultExternal(nextStep.result)
+ view.finish()
+ }
+ is ShowSettings -> Log.d(TAG, "Showing settings")
+ is LaunchedActivity -> Log.d(TAG, "Launched activity, awaiting result")
+ }
+ }
+ }
+ }
+}
diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/fragment/FingerprintDeletionDialog.kt b/src/com/android/settings/biometrics/fingerprint2/ui/fragment/FingerprintDeletionDialog.kt
new file mode 100644
index 0000000..42e2047
--- /dev/null
+++ b/src/com/android/settings/biometrics/fingerprint2/ui/fragment/FingerprintDeletionDialog.kt
@@ -0,0 +1,119 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.biometrics.fingerprint2.ui.fragment
+
+import android.app.Dialog
+import android.app.admin.DevicePolicyManager
+import android.app.admin.DevicePolicyResources.Strings.Settings.WORK_PROFILE_FINGERPRINT_LAST_DELETE_MESSAGE
+import android.app.admin.DevicePolicyResources.UNDEFINED
+import android.app.settings.SettingsEnums
+import android.content.DialogInterface
+import android.os.Bundle
+import android.os.UserManager
+import androidx.appcompat.app.AlertDialog
+import com.android.settings.R
+import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FingerprintViewModel
+import com.android.settings.core.instrumentation.InstrumentedDialogFragment
+import kotlin.coroutines.resume
+import kotlinx.coroutines.suspendCancellableCoroutine
+
+private const val KEY_IS_LAST_FINGERPRINT = "IS_LAST_FINGERPRINT"
+
+class FingerprintDeletionDialog : InstrumentedDialogFragment() {
+ private lateinit var fingerprintViewModel: FingerprintViewModel
+ private var isLastFingerprint: Boolean = false
+ private lateinit var alertDialog: AlertDialog
+ lateinit var onClickListener: DialogInterface.OnClickListener
+ lateinit var onNegativeClickListener: DialogInterface.OnClickListener
+ lateinit var onCancelListener: DialogInterface.OnCancelListener
+
+ override fun getMetricsCategory(): Int {
+ return SettingsEnums.DIALOG_FINGERPINT_EDIT
+ }
+
+ override fun onCancel(dialog: DialogInterface) {
+ onCancelListener.onCancel(dialog)
+ }
+
+ override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+ val fp = requireArguments().get(KEY_FINGERPRINT) as android.hardware.fingerprint.Fingerprint
+ fingerprintViewModel = FingerprintViewModel(fp.name.toString(), fp.biometricId, fp.deviceId)
+ isLastFingerprint = requireArguments().getBoolean(KEY_IS_LAST_FINGERPRINT)
+ val title = getString(R.string.fingerprint_delete_title, fingerprintViewModel.name)
+ var message = getString(R.string.fingerprint_v2_delete_message, fingerprintViewModel.name)
+ val context = requireContext()
+
+ if (isLastFingerprint) {
+ val isProfileChallengeUser = UserManager.get(context).isManagedProfile(context.userId)
+ val messageId =
+ if (isProfileChallengeUser) {
+ WORK_PROFILE_FINGERPRINT_LAST_DELETE_MESSAGE
+ } else {
+ UNDEFINED
+ }
+ val defaultMessageId =
+ if (isProfileChallengeUser) {
+ R.string.fingerprint_last_delete_message_profile_challenge
+ } else {
+ R.string.fingerprint_last_delete_message
+ }
+ val devicePolicyManager = requireContext().getSystemService(DevicePolicyManager::class.java)
+ message =
+ devicePolicyManager?.resources?.getString(messageId) {
+ message + "\n\n" + context.getString(defaultMessageId)
+ }
+ ?: ""
+ }
+
+ alertDialog =
+ AlertDialog.Builder(requireActivity())
+ .setTitle(title)
+ .setMessage(message)
+ .setPositiveButton(
+ R.string.security_settings_fingerprint_enroll_dialog_delete,
+ onClickListener
+ )
+ .setNegativeButton(R.string.cancel, onNegativeClickListener)
+ .create()
+ return alertDialog
+ }
+
+ companion object {
+ private const val KEY_FINGERPRINT = "fingerprint"
+ suspend fun showInstance(
+ fp: FingerprintViewModel,
+ lastFingerprint: Boolean,
+ target: FingerprintSettingsV2Fragment,
+ ) = suspendCancellableCoroutine { continuation ->
+ val dialog = FingerprintDeletionDialog()
+ dialog.onClickListener = DialogInterface.OnClickListener { _, _ -> continuation.resume(true) }
+ dialog.onNegativeClickListener =
+ DialogInterface.OnClickListener { _, _ -> continuation.resume(false) }
+ dialog.onCancelListener = DialogInterface.OnCancelListener { continuation.resume(false) }
+
+ continuation.invokeOnCancellation { dialog.dismiss() }
+ val bundle = Bundle()
+ bundle.putObject(
+ KEY_FINGERPRINT,
+ android.hardware.fingerprint.Fingerprint(fp.name, fp.fingerId, fp.deviceId)
+ )
+ bundle.putBoolean(KEY_IS_LAST_FINGERPRINT, lastFingerprint)
+ dialog.arguments = bundle
+ dialog.show(target.parentFragmentManager, FingerprintDeletionDialog::class.java.toString())
+ }
+ }
+}
diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/fragment/FingerprintSettingsPreference.kt b/src/com/android/settings/biometrics/fingerprint2/ui/fragment/FingerprintSettingsPreference.kt
new file mode 100644
index 0000000..e12785d
--- /dev/null
+++ b/src/com/android/settings/biometrics/fingerprint2/ui/fragment/FingerprintSettingsPreference.kt
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.biometrics.fingerprint2.ui.fragment
+
+import android.content.Context
+import android.util.Log
+import android.view.View
+import androidx.lifecycle.lifecycleScope
+import androidx.preference.PreferenceViewHolder
+import com.android.settings.R
+import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FingerprintViewModel
+import com.android.settingslib.widget.TwoTargetPreference
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+
+private const val TAG = "FingerprintSettingsPreference"
+
+class FingerprintSettingsPreference(
+ context: Context,
+ val fingerprintViewModel: FingerprintViewModel,
+ val fragment: FingerprintSettingsV2Fragment,
+ val isLastFingerprint: Boolean
+) : TwoTargetPreference(context) {
+ private lateinit var myView: View
+
+ init {
+ key = "FINGERPRINT_" + fingerprintViewModel.fingerId
+ Log.d(TAG, "FingerprintPreference $this with frag $fragment $key")
+ title = fingerprintViewModel.name
+ isPersistent = false
+ setIcon(R.drawable.ic_fingerprint_24dp)
+ setOnPreferenceClickListener {
+ fragment.lifecycleScope.launch { fragment.onPrefClicked(fingerprintViewModel) }
+ true
+ }
+ }
+
+ override fun onBindViewHolder(view: PreferenceViewHolder) {
+ super.onBindViewHolder(view)
+ myView = view.itemView
+ view.itemView.findViewById<View>(R.id.delete_button)?.setOnClickListener {
+ fragment.lifecycleScope.launch { fragment.onDeletePrefClicked(fingerprintViewModel) }
+ }
+ }
+
+ /** Highlights this dialog. */
+ suspend fun highlight() {
+ fragment.activity?.getDrawable(R.drawable.preference_highlight)?.let { highlight ->
+ val centerX: Float = myView.width / 2.0f
+ val centerY: Float = myView.height / 2.0f
+ highlight.setHotspot(centerX, centerY)
+ myView.background = highlight
+ myView.isPressed = true
+ myView.isPressed = false
+ delay(300)
+ myView.background = null
+ }
+ }
+
+ override fun getSecondTargetResId(): Int {
+ return R.layout.preference_widget_delete
+ }
+
+ suspend fun askUserToDeleteDialog(): Boolean {
+ return FingerprintDeletionDialog.showInstance(fingerprintViewModel, isLastFingerprint, fragment)
+ }
+
+ suspend fun askUserToRenameDialog(): Pair<FingerprintViewModel, String>? {
+ return FingerprintSettingsRenameDialog.showInstance(fingerprintViewModel, fragment)
+ }
+}
diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/fragment/FingerprintSettingsRenameDialog.kt b/src/com/android/settings/biometrics/fingerprint2/ui/fragment/FingerprintSettingsRenameDialog.kt
new file mode 100644
index 0000000..9542ed8
--- /dev/null
+++ b/src/com/android/settings/biometrics/fingerprint2/ui/fragment/FingerprintSettingsRenameDialog.kt
@@ -0,0 +1,145 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.biometrics.fingerprint2.ui.fragment
+
+import android.app.Dialog
+import android.app.settings.SettingsEnums
+import android.content.DialogInterface
+import android.os.Bundle
+import android.text.InputFilter
+import android.text.Spanned
+import android.text.TextUtils
+import android.util.Log
+import android.widget.ImeAwareEditText
+import androidx.appcompat.app.AlertDialog
+import com.android.settings.R
+import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FingerprintViewModel
+import com.android.settings.core.instrumentation.InstrumentedDialogFragment
+import kotlin.coroutines.resume
+import kotlinx.coroutines.suspendCancellableCoroutine
+
+private const val TAG = "FingerprintSettingsRenameDialog"
+
+class FingerprintSettingsRenameDialog : InstrumentedDialogFragment() {
+ lateinit var onClickListener: DialogInterface.OnClickListener
+ lateinit var onCancelListener: DialogInterface.OnCancelListener
+
+ override fun onCancel(dialog: DialogInterface) {
+ Log.d(TAG, "onCancel $dialog")
+ onCancelListener.onCancel(dialog)
+ }
+
+ override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+ Log.d(TAG, "onCreateDialog $this")
+ val fp = requireArguments().get(KEY_FINGERPRINT) as android.hardware.fingerprint.Fingerprint
+ val fingerprintViewModel = FingerprintViewModel(fp.name.toString(), fp.biometricId, fp.deviceId)
+
+ val context = requireContext()
+ val alertDialog =
+ AlertDialog.Builder(context)
+ .setView(R.layout.fingerprint_rename_dialog)
+ .setPositiveButton(R.string.security_settings_fingerprint_enroll_dialog_ok, onClickListener)
+ .create()
+ alertDialog.setOnShowListener {
+ (dialog?.findViewById(R.id.fingerprint_rename_field) as ImeAwareEditText?)?.apply {
+ val name = fingerprintViewModel.name
+ setText(name)
+ filters = this@FingerprintSettingsRenameDialog.getFilters()
+ selectAll()
+ requestFocus()
+ scheduleShowSoftInput()
+ }
+ }
+
+ return alertDialog
+ }
+
+ private fun getFilters(): Array<InputFilter> {
+ val filter: InputFilter =
+ object : InputFilter {
+
+ override fun filter(
+ source: CharSequence,
+ start: Int,
+ end: Int,
+ dest: Spanned?,
+ dstart: Int,
+ dend: Int
+ ): CharSequence? {
+ for (index in start until end) {
+ val c = source[index]
+ // KXMLSerializer does not allow these characters,
+ // see KXmlSerializer.java:162.
+ if (c.code < 0x20) {
+ return ""
+ }
+ }
+ return null
+ }
+ }
+ return arrayOf(filter)
+ }
+
+ override fun getMetricsCategory(): Int {
+ return SettingsEnums.DIALOG_FINGERPINT_EDIT
+ }
+
+ companion object {
+ private const val KEY_FINGERPRINT = "fingerprint"
+
+ suspend fun showInstance(fp: FingerprintViewModel, target: FingerprintSettingsV2Fragment) =
+ suspendCancellableCoroutine { continuation ->
+ val dialog = FingerprintSettingsRenameDialog()
+ val onClick =
+ DialogInterface.OnClickListener { _, _ ->
+ val dialogTextField = dialog.requireDialog()
+ .requireViewById(R.id.fingerprint_rename_field) as ImeAwareEditText
+ val newName = dialogTextField.text.toString()
+ if (!TextUtils.equals(newName, fp.name)) {
+ Log.d(TAG, "rename $fp.name to $newName for $dialog")
+ continuation.resume(Pair(fp, newName))
+ } else {
+ continuation.resume(null)
+ }
+ }
+
+ dialog.onClickListener = onClick
+ dialog.onCancelListener =
+ DialogInterface.OnCancelListener {
+ Log.d(TAG, "onCancelListener clicked $dialog")
+ continuation.resume(null)
+ }
+
+ continuation.invokeOnCancellation {
+ Log.d(TAG, "invokeOnCancellation $dialog")
+ dialog.dismiss()
+ }
+
+ val bundle = Bundle()
+ bundle.putObject(
+ KEY_FINGERPRINT,
+ android.hardware.fingerprint.Fingerprint(fp.name, fp.fingerId, fp.deviceId)
+ )
+ dialog.arguments = bundle
+ Log.d(TAG, "showing dialog $dialog")
+ dialog.show(
+ target.parentFragmentManager,
+ FingerprintSettingsRenameDialog::class.java.toString()
+ )
+ }
+ }
+}
diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/fragment/FingerprintSettingsV2Fragment.kt b/src/com/android/settings/biometrics/fingerprint2/ui/fragment/FingerprintSettingsV2Fragment.kt
new file mode 100644
index 0000000..b82f7c1
--- /dev/null
+++ b/src/com/android/settings/biometrics/fingerprint2/ui/fragment/FingerprintSettingsV2Fragment.kt
@@ -0,0 +1,581 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.biometrics.fingerprint2.ui.fragment
+
+import android.app.Activity
+import android.app.admin.DevicePolicyManager
+import android.app.admin.DevicePolicyResources.Strings.Settings.FINGERPRINT_UNLOCK_DISABLED_EXPLANATION
+import android.app.settings.SettingsEnums
+import android.content.Context.FINGERPRINT_SERVICE
+import android.content.Intent
+import android.hardware.fingerprint.FingerprintManager
+import android.os.Bundle
+import android.provider.Settings.Secure
+import android.text.TextUtils
+import android.util.FeatureFlagUtils
+import android.util.Log
+import android.view.View
+import android.widget.Toast
+import androidx.activity.result.ActivityResultLauncher
+import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
+import androidx.lifecycle.ViewModelProvider
+import androidx.lifecycle.lifecycleScope
+import androidx.preference.Preference
+import androidx.preference.PreferenceCategory
+import com.android.internal.widget.LockPatternUtils
+import com.android.settings.R
+import com.android.settings.Utils.SETTINGS_PACKAGE_NAME
+import com.android.settings.biometrics.BiometricEnrollBase
+import com.android.settings.biometrics.BiometricEnrollBase.CONFIRM_REQUEST
+import com.android.settings.biometrics.BiometricEnrollBase.EXTRA_FROM_SETTINGS_SUMMARY
+import com.android.settings.biometrics.BiometricEnrollBase.RESULT_FINISHED
+import com.android.settings.biometrics.GatekeeperPasswordProvider
+import com.android.settings.biometrics.fingerprint.FingerprintEnrollEnrolling
+import com.android.settings.biometrics.fingerprint.FingerprintEnrollIntroductionInternal
+import com.android.settings.biometrics.fingerprint2.domain.interactor.FingerprintManagerInteractorImpl
+import com.android.settings.biometrics.fingerprint2.ui.binder.FingerprintSettingsViewBinder
+import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FingerprintAuthAttemptViewModel
+import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FingerprintSettingsNavigationViewModel
+import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FingerprintSettingsViewModel
+import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FingerprintStateViewModel
+import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FingerprintViewModel
+import com.android.settings.core.SettingsBaseActivity
+import com.android.settings.core.instrumentation.InstrumentedDialogFragment
+import com.android.settings.dashboard.DashboardFragment
+import com.android.settings.password.ChooseLockGeneric
+import com.android.settings.password.ChooseLockSettingsHelper
+import com.android.settings.password.ChooseLockSettingsHelper.EXTRA_KEY_GK_PW_HANDLE
+import com.android.settingslib.HelpUtils
+import com.android.settingslib.RestrictedLockUtils
+import com.android.settingslib.RestrictedLockUtilsInternal
+import com.android.settingslib.transition.SettingsTransitionHelper
+import com.android.settingslib.widget.FooterPreference
+import com.google.android.setupdesign.util.DeviceHelper
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+
+private const val TAG = "FingerprintSettingsV2Fragment"
+private const val KEY_FINGERPRINTS_ENROLLED_CATEGORY = "security_settings_fingerprints_enrolled"
+private const val KEY_FINGERPRINT_SIDE_FPS_CATEGORY =
+ "security_settings_fingerprint_unlock_category"
+private const val KEY_FINGERPRINT_ADD = "key_fingerprint_add"
+private const val KEY_FINGERPRINT_SIDE_FPS_SCREEN_ON_TO_AUTH =
+ "security_settings_require_screen_on_to_auth"
+private const val KEY_FINGERPRINT_FOOTER = "security_settings_fingerprint_footer"
+
+/**
+ * A class responsible for showing FingerprintSettings. Typical activity Flows are
+ * 1. Settings > FingerprintSettings > PIN/PATTERN/PASS -> FingerprintSettings
+ * 2. FingerprintSettings -> FingerprintEnrollment fow
+ *
+ * This page typically allows for
+ * 1. Fingerprint deletion
+ * 2. Fingerprint enrollment
+ * 3. Renaming a fingerprint
+ * 4. Enabling/Disabling a feature
+ */
+class FingerprintSettingsV2Fragment :
+ DashboardFragment(), FingerprintSettingsViewBinder.FingerprintView {
+ private lateinit var settingsViewModel: FingerprintSettingsViewModel
+ private lateinit var navigationViewModel: FingerprintSettingsNavigationViewModel
+
+ /** Result listener for ChooseLock activity flow. */
+ private val confirmDeviceResultListener =
+ registerForActivityResult(StartActivityForResult()) { result ->
+ val resultCode = result.resultCode
+ val data = result.data
+ onConfirmDevice(resultCode, data)
+ }
+
+ /** Result listener for launching enrollments **after** a user has reached the settings page. */
+ private val launchAdditionalFingerprintListener: ActivityResultLauncher<Intent> =
+ registerForActivityResult(StartActivityForResult()) { result ->
+ lifecycleScope.launch {
+ val resultCode = result.resultCode
+ Log.d(TAG, "onEnrollAdditionalFingerprint($resultCode)")
+
+ if (resultCode == BiometricEnrollBase.RESULT_TIMEOUT) {
+ navigationViewModel.onEnrollAdditionalFailure()
+ } else {
+ navigationViewModel.onEnrollSuccess()
+ }
+ }
+ }
+
+ /** Initial listener for the first enrollment request */
+ private val launchFirstEnrollmentListener: ActivityResultLauncher<Intent> =
+ registerForActivityResult(StartActivityForResult()) { result ->
+ lifecycleScope.launch {
+ val resultCode = result.resultCode
+ val data = result.data
+
+ Log.d(TAG, "onEnrollFirstFingerprint($resultCode, $data)")
+ if (resultCode != RESULT_FINISHED || data == null) {
+ if (resultCode == BiometricEnrollBase.RESULT_TIMEOUT) {
+ navigationViewModel.onEnrollFirstFailure(
+ "Received RESULT_TIMEOUT when enrolling",
+ resultCode
+ )
+ } else {
+ navigationViewModel.onEnrollFirstFailure(
+ "Incorrect resultCode or data was null",
+ resultCode
+ )
+ }
+ } else {
+ val token = data.getByteArrayExtra(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN)
+ val challenge = data.getExtra(BiometricEnrollBase.EXTRA_KEY_CHALLENGE) as Long?
+ navigationViewModel.onEnrollFirst(token, challenge)
+ }
+ }
+ }
+
+ override fun userLockout(authAttemptViewModel: FingerprintAuthAttemptViewModel.Error) {
+ Toast.makeText(activity, authAttemptViewModel.message, Toast.LENGTH_SHORT).show()
+ }
+
+ override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
+ // This is needed to support ChooseLockSettingBuilder...show(). All other activity
+ // calls should use the registerForActivity method call.
+ super.onActivityResult(requestCode, resultCode, data)
+ onConfirmDevice(resultCode, data)
+ }
+
+ override fun onCreate(icicle: Bundle?) {
+ super.onCreate(icicle)
+
+ if (icicle != null) {
+ Log.d(TAG, "onCreateWithSavedState")
+ } else {
+ Log.d(TAG, "onCreate()")
+ }
+
+ if (
+ !FeatureFlagUtils.isEnabled(
+ context,
+ FeatureFlagUtils.SETTINGS_BIOMETRICS2_FINGERPRINT_SETTINGS
+ )
+ ) {
+ Log.d(TAG, "Finishing due to feature not being enabled")
+ finish()
+ return
+ }
+
+ val context = requireContext()
+ val userId = context.userId
+
+ preferenceScreen.isVisible = false
+
+ val fingerprintManager = context.getSystemService(FINGERPRINT_SERVICE) as FingerprintManager
+
+ val backgroundDispatcher = Dispatchers.IO
+ val activity = requireActivity()
+ val userHandle = activity.user.identifier
+
+ val interactor =
+ FingerprintManagerInteractorImpl(
+ context.applicationContext,
+ backgroundDispatcher,
+ fingerprintManager,
+ GatekeeperPasswordProvider(LockPatternUtils(context.applicationContext))
+ ) {
+ var toReturn: Int =
+ Secure.getIntForUser(
+ context.contentResolver,
+ Secure.SFPS_PERFORMANT_AUTH_ENABLED,
+ -1,
+ userHandle,
+ )
+ if (toReturn == -1) {
+ toReturn =
+ if (
+ context.resources.getBoolean(com.android.internal.R.bool.config_performantAuthDefault)
+ ) {
+ 1
+ } else {
+ 0
+ }
+ Secure.putIntForUser(
+ context.contentResolver,
+ Secure.SFPS_PERFORMANT_AUTH_ENABLED,
+ toReturn,
+ userHandle
+ )
+ }
+
+ toReturn == 1
+ }
+
+ val token = intent.getByteArrayExtra(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN)
+ val challenge = intent.getLongExtra(BiometricEnrollBase.EXTRA_KEY_CHALLENGE, -1L)
+
+ navigationViewModel =
+ ViewModelProvider(
+ this,
+ FingerprintSettingsNavigationViewModel.FingerprintSettingsNavigationModelFactory(
+ userId,
+ interactor,
+ backgroundDispatcher,
+ token,
+ challenge
+ )
+ )[FingerprintSettingsNavigationViewModel::class.java]
+
+ settingsViewModel =
+ ViewModelProvider(
+ this,
+ FingerprintSettingsViewModel.FingerprintSettingsViewModelFactory(
+ userId,
+ interactor,
+ backgroundDispatcher,
+ navigationViewModel,
+ )
+ )[FingerprintSettingsViewModel::class.java]
+
+ FingerprintSettingsViewBinder.bind(
+ this,
+ settingsViewModel,
+ navigationViewModel,
+ lifecycleScope,
+ )
+ }
+
+ override fun getMetricsCategory(): Int {
+ return SettingsEnums.FINGERPRINT
+ }
+
+ override fun getPreferenceScreenResId(): Int {
+ return R.xml.security_settings_fingerprint_limbo
+ }
+
+ override fun getLogTag(): String {
+ return TAG
+ }
+
+ override fun onStop() {
+ super.onStop()
+ navigationViewModel.maybeFinishActivity(requireActivity().isChangingConfigurations)
+ }
+
+ override fun onPause() {
+ super.onPause()
+ settingsViewModel.shouldAuthenticate(false)
+ val transaction = parentFragmentManager.beginTransaction()
+ for (frag in parentFragmentManager.fragments) {
+ if (frag is InstrumentedDialogFragment) {
+ Log.d(TAG, "removing dialog settings fragment $frag")
+ frag.dismiss()
+ transaction.remove(frag)
+ }
+ }
+ transaction.commit()
+ }
+
+ override fun onResume() {
+ super.onResume()
+ settingsViewModel.shouldAuthenticate(true)
+ }
+
+ /** Used to indicate that preference has been clicked */
+ fun onPrefClicked(fingerprintViewModel: FingerprintViewModel) {
+ Log.d(TAG, "onPrefClicked(${fingerprintViewModel})")
+ settingsViewModel.onPrefClicked(fingerprintViewModel)
+ }
+
+ /** Used to indicate that a delete pref has been clicked */
+ fun onDeletePrefClicked(fingerprintViewModel: FingerprintViewModel) {
+ Log.d(TAG, "onDeletePrefClicked(${fingerprintViewModel})")
+ settingsViewModel.onDeleteClicked(fingerprintViewModel)
+ }
+
+ override fun showSettings(state: FingerprintStateViewModel) {
+ val category =
+ this@FingerprintSettingsV2Fragment.findPreference(KEY_FINGERPRINTS_ENROLLED_CATEGORY)
+ as PreferenceCategory?
+
+ category?.removeAll()
+
+ state.fingerprintViewModels.forEach { fingerprint ->
+ category?.addPreference(
+ FingerprintSettingsPreference(
+ requireContext(),
+ fingerprint,
+ this@FingerprintSettingsV2Fragment,
+ state.fingerprintViewModels.size == 1,
+ )
+ )
+ }
+ category?.isVisible = true
+
+ createFingerprintsFooterPreference(state.canEnroll, state.maxFingerprints)
+ preferenceScreen.isVisible = true
+
+ val sideFpsPref =
+ this@FingerprintSettingsV2Fragment.findPreference(KEY_FINGERPRINT_SIDE_FPS_CATEGORY)
+ as PreferenceCategory?
+ sideFpsPref?.isVisible = false
+
+ if (state.hasSideFps) {
+ sideFpsPref?.isVisible = state.fingerprintViewModels.isNotEmpty()
+ val otherPref =
+ this@FingerprintSettingsV2Fragment.findPreference(
+ KEY_FINGERPRINT_SIDE_FPS_SCREEN_ON_TO_AUTH
+ ) as Preference?
+ otherPref?.isVisible = state.fingerprintViewModels.isNotEmpty()
+ }
+ addFooter(state.hasSideFps)
+ }
+ private fun addFooter(hasSideFps: Boolean) {
+ val footer =
+ this@FingerprintSettingsV2Fragment.findPreference(KEY_FINGERPRINT_FOOTER)
+ as PreferenceCategory?
+ val admin =
+ RestrictedLockUtilsInternal.checkIfKeyguardFeaturesDisabled(
+ activity,
+ DevicePolicyManager.KEYGUARD_DISABLE_FINGERPRINT,
+ requireActivity().userId
+ )
+ val activity = requireActivity()
+ val helpIntent =
+ HelpUtils.getHelpIntent(activity, getString(helpResource), activity::class.java.name)
+ val learnMoreClickListener =
+ View.OnClickListener { v: View? -> activity.startActivityForResult(helpIntent, 0) }
+
+ class FooterColumn {
+ var title: CharSequence? = null
+ var learnMoreOverrideText: CharSequence? = null
+ var learnMoreOnClickListener: View.OnClickListener? = null
+ }
+
+ var footerColumns = mutableListOf<FooterColumn>()
+ if (admin != null) {
+ val devicePolicyManager = getSystemService(DevicePolicyManager::class.java)
+ val column1 = FooterColumn()
+ column1.title =
+ devicePolicyManager.resources.getString(FINGERPRINT_UNLOCK_DISABLED_EXPLANATION) {
+ getString(R.string.security_fingerprint_disclaimer_lockscreen_disabled_1)
+ }
+
+ column1.learnMoreOnClickListener =
+ View.OnClickListener { _ ->
+ RestrictedLockUtils.sendShowAdminSupportDetailsIntent(activity, admin)
+ }
+ column1.learnMoreOverrideText = getText(R.string.admin_support_more_info)
+ footerColumns.add(column1)
+ val column2 = FooterColumn()
+ column2.title = getText(R.string.security_fingerprint_disclaimer_lockscreen_disabled_2)
+ if (hasSideFps) {
+ column2.learnMoreOverrideText =
+ getText(R.string.security_settings_fingerprint_settings_footer_learn_more)
+ }
+ column2.learnMoreOnClickListener = learnMoreClickListener
+ footerColumns.add(column2)
+ } else {
+ val column = FooterColumn()
+ column.title =
+ getString(
+ R.string.security_settings_fingerprint_enroll_introduction_v3_message,
+ DeviceHelper.getDeviceName(requireActivity())
+ )
+ column.learnMoreOnClickListener = learnMoreClickListener
+ if (hasSideFps) {
+ column.learnMoreOverrideText =
+ getText(R.string.security_settings_fingerprint_settings_footer_learn_more)
+ }
+ footerColumns.add(column)
+ }
+
+ footer?.removeAll()
+ for (i in 0 until footerColumns.size) {
+ val column = footerColumns[i]
+ val footerPrefToAdd: FooterPreference =
+ FooterPreference.Builder(requireContext()).setTitle(column.title).build()
+ if (i > 0) {
+ footerPrefToAdd.setIconVisibility(View.GONE)
+ }
+ if (column.learnMoreOnClickListener != null) {
+ footerPrefToAdd.setLearnMoreAction(column.learnMoreOnClickListener)
+ if (!TextUtils.isEmpty(column.learnMoreOverrideText)) {
+ footerPrefToAdd.setLearnMoreText(column.learnMoreOverrideText)
+ }
+ }
+ footer?.addPreference(footerPrefToAdd)
+ }
+ }
+
+ override suspend fun askUserToDeleteDialog(fingerprintViewModel: FingerprintViewModel): Boolean {
+ Log.d(TAG, "showing delete dialog for (${fingerprintViewModel})")
+
+ try {
+ val willDelete =
+ fingerprintPreferences()
+ .first { it?.fingerprintViewModel == fingerprintViewModel }
+ ?.askUserToDeleteDialog()
+ ?: false
+ if (willDelete) {
+ mMetricsFeatureProvider.action(
+ context,
+ SettingsEnums.ACTION_FINGERPRINT_DELETE,
+ fingerprintViewModel.fingerId
+ )
+ }
+ return willDelete
+ } catch (exception: Exception) {
+ Log.d(TAG, "askUserToDeleteDialog exception $exception")
+ return false
+ }
+ }
+
+ override suspend fun askUserToRenameDialog(
+ fingerprintViewModel: FingerprintViewModel
+ ): Pair<FingerprintViewModel, String>? {
+ Log.d(TAG, "showing rename dialog for (${fingerprintViewModel})")
+ try {
+ val toReturn =
+ fingerprintPreferences()
+ .first { it?.fingerprintViewModel == fingerprintViewModel }
+ ?.askUserToRenameDialog()
+ if (toReturn != null) {
+ mMetricsFeatureProvider.action(
+ context,
+ SettingsEnums.ACTION_FINGERPRINT_RENAME,
+ toReturn.first.fingerId
+ )
+ }
+ return toReturn
+ } catch (exception: Exception) {
+ Log.d(TAG, "askUserToRenameDialog exception $exception")
+ return null
+ }
+ }
+
+ override suspend fun highlightPref(fingerId: Int) {
+ fingerprintPreferences()
+ .first { pref -> pref?.fingerprintViewModel?.fingerId == fingerId }
+ ?.highlight()
+ }
+
+ override fun launchConfirmOrChooseLock(userId: Int) {
+ lifecycleScope.launch(Dispatchers.Default) {
+ navigationViewModel.setStepToLaunched()
+ val intent = Intent()
+ val builder =
+ ChooseLockSettingsHelper.Builder(requireActivity(), this@FingerprintSettingsV2Fragment)
+ val launched =
+ builder
+ .setRequestCode(CONFIRM_REQUEST)
+ .setTitle(getString(R.string.security_settings_fingerprint_preference_title))
+ .setRequestGatekeeperPasswordHandle(true)
+ .setUserId(userId)
+ .setForegroundOnly(true)
+ .setReturnCredentials(true)
+ .show()
+ if (!launched) {
+ intent.setClassName(SETTINGS_PACKAGE_NAME, ChooseLockGeneric::class.java.name)
+ intent.putExtra(ChooseLockGeneric.ChooseLockGenericFragment.HIDE_INSECURE_OPTIONS, true)
+ intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_REQUEST_GK_PW_HANDLE, true)
+ intent.putExtra(Intent.EXTRA_USER_ID, userId)
+ confirmDeviceResultListener.launch(intent)
+ }
+ }
+ }
+
+ override fun launchFullFingerprintEnrollment(
+ userId: Int,
+ gateKeeperPasswordHandle: Long?,
+ challenge: Long?,
+ challengeToken: ByteArray?,
+ ) {
+ navigationViewModel.setStepToLaunched()
+ Log.d(TAG, "launchFullFingerprintEnrollment")
+ val intent = Intent()
+ intent.setClassName(
+ SETTINGS_PACKAGE_NAME,
+ FingerprintEnrollIntroductionInternal::class.java.name
+ )
+ intent.putExtra(EXTRA_FROM_SETTINGS_SUMMARY, true)
+ intent.putExtra(
+ SettingsBaseActivity.EXTRA_PAGE_TRANSITION_TYPE,
+ SettingsTransitionHelper.TransitionType.TRANSITION_SLIDE
+ )
+
+ intent.putExtra(Intent.EXTRA_USER_ID, userId)
+
+ if (gateKeeperPasswordHandle != null) {
+ intent.putExtra(EXTRA_KEY_GK_PW_HANDLE, gateKeeperPasswordHandle)
+ } else {
+ intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN, challengeToken)
+ intent.putExtra(BiometricEnrollBase.EXTRA_KEY_CHALLENGE, challenge)
+ }
+ launchFirstEnrollmentListener.launch(intent)
+ }
+
+ override fun setResultExternal(resultCode: Int) {
+ setResult(resultCode)
+ }
+
+ override fun launchAddFingerprint(userId: Int, challengeToken: ByteArray?) {
+ navigationViewModel.setStepToLaunched()
+ val intent = Intent()
+ intent.setClassName(
+ SETTINGS_PACKAGE_NAME,
+ FingerprintEnrollEnrolling::class.qualifiedName.toString()
+ )
+ intent.putExtra(Intent.EXTRA_USER_ID, userId)
+ intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN, challengeToken)
+ launchAdditionalFingerprintListener.launch(intent)
+ }
+
+ private fun onConfirmDevice(resultCode: Int, data: Intent?) {
+ val wasSuccessful = resultCode == RESULT_FINISHED || resultCode == Activity.RESULT_OK
+ val gateKeeperPasswordHandle = data?.getExtra(EXTRA_KEY_GK_PW_HANDLE) as Long?
+ lifecycleScope.launch {
+ navigationViewModel.onConfirmDevice(wasSuccessful, gateKeeperPasswordHandle)
+ }
+ }
+
+ private fun createFingerprintsFooterPreference(canEnroll: Boolean, maxFingerprints: Int) {
+ val pref = this@FingerprintSettingsV2Fragment.findPreference<Preference>(KEY_FINGERPRINT_ADD)
+ val maxSummary = context?.getString(R.string.fingerprint_add_max, maxFingerprints) ?: ""
+ pref?.summary = maxSummary
+ pref?.isEnabled = canEnroll
+ pref?.setOnPreferenceClickListener {
+ navigationViewModel.onAddFingerprintClicked()
+ true
+ }
+ pref?.isVisible = true
+ }
+
+ private fun fingerprintPreferences(): List<FingerprintSettingsPreference?> {
+ val category =
+ this@FingerprintSettingsV2Fragment.findPreference(KEY_FINGERPRINTS_ENROLLED_CATEGORY)
+ as PreferenceCategory?
+
+ return category?.let { cat ->
+ cat.childrenToList().map { it as FingerprintSettingsPreference? }
+ }
+ ?: emptyList()
+ }
+
+ private fun PreferenceCategory.childrenToList(): List<Preference> {
+ val mutable: MutableList<Preference> = mutableListOf()
+ for (i in 0 until this.preferenceCount) {
+ mutable.add(this.getPreference(i))
+ }
+ return mutable.toList()
+ }
+}
diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/viewmodel/FingerprintSettingsNavigationViewModel.kt b/src/com/android/settings/biometrics/fingerprint2/ui/viewmodel/FingerprintSettingsNavigationViewModel.kt
new file mode 100644
index 0000000..a3a5d3c
--- /dev/null
+++ b/src/com/android/settings/biometrics/fingerprint2/ui/viewmodel/FingerprintSettingsNavigationViewModel.kt
@@ -0,0 +1,189 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.biometrics.fingerprint2.ui.viewmodel
+
+import android.hardware.fingerprint.FingerprintManager
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.ViewModelProvider
+import androidx.lifecycle.viewModelScope
+import com.android.settings.biometrics.BiometricEnrollBase
+import com.android.settings.biometrics.fingerprint2.domain.interactor.FingerprintManagerInteractor
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.launch
+
+/** A Viewmodel that represents the navigation of the FingerprintSettings activity. */
+class FingerprintSettingsNavigationViewModel(
+ private val userId: Int,
+ private val fingerprintManagerInteractor: FingerprintManagerInteractor,
+ private val backgroundDispatcher: CoroutineDispatcher,
+ tokenInit: ByteArray?,
+ challengeInit: Long?,
+) : ViewModel() {
+
+ private var token = tokenInit
+ private var challenge = challengeInit
+
+ private val _nextStep: MutableStateFlow<NextStepViewModel?> = MutableStateFlow(null)
+ /** This flow represents the high level state for the FingerprintSettingsV2Fragment. */
+ val nextStep: StateFlow<NextStepViewModel?> = _nextStep.asStateFlow()
+
+ init {
+ if (challengeInit == null || tokenInit == null) {
+ _nextStep.update { LaunchConfirmDeviceCredential(userId) }
+ } else {
+ viewModelScope.launch { showSettingsHelper() }
+ }
+ }
+
+ /** Used to indicate that FingerprintSettings is complete. */
+ fun finish() {
+ _nextStep.update { null }
+ }
+
+ /** Used to finish settings in certain cases. */
+ fun maybeFinishActivity(changingConfig: Boolean) {
+ val isConfirmingOrEnrolling =
+ _nextStep.value is LaunchConfirmDeviceCredential ||
+ _nextStep.value is EnrollAdditionalFingerprint ||
+ _nextStep.value is EnrollFirstFingerprint ||
+ _nextStep.value is LaunchedActivity
+ if (!isConfirmingOrEnrolling && !changingConfig)
+ _nextStep.update {
+ FinishSettingsWithResult(BiometricEnrollBase.RESULT_TIMEOUT, "onStop finishing settings")
+ }
+ }
+
+ /** Used to indicate that we have launched another activity and we should await its result. */
+ fun setStepToLaunched() {
+ _nextStep.update { LaunchedActivity }
+ }
+
+ /** Indicates a successful enroll has occurred */
+ fun onEnrollSuccess() {
+ showSettingsHelper()
+ }
+
+ /** Add fingerprint clicked */
+ fun onAddFingerprintClicked() {
+ _nextStep.update { EnrollAdditionalFingerprint(userId, token) }
+ }
+
+ /** Enrolling of an additional fingerprint failed */
+ fun onEnrollAdditionalFailure() {
+ launchFinishSettings("Failed to enroll additional fingerprint")
+ }
+
+ /** The first fingerprint enrollment failed */
+ fun onEnrollFirstFailure(reason: String) {
+ launchFinishSettings(reason)
+ }
+
+ /** The first fingerprint enrollment failed with a result code */
+ fun onEnrollFirstFailure(reason: String, resultCode: Int) {
+ launchFinishSettings(reason, resultCode)
+ }
+
+ /** Notifies that a users first enrollment succeeded. */
+ fun onEnrollFirst(theToken: ByteArray?, theChallenge: Long?) {
+ if (theToken == null) {
+ launchFinishSettings("Error, empty token")
+ return
+ }
+ if (theChallenge == null) {
+ launchFinishSettings("Error, empty keyChallenge")
+ return
+ }
+ token = theToken!!
+ challenge = theChallenge!!
+
+ showSettingsHelper()
+ }
+
+ /**
+ * Indicates to the view model that a confirm device credential action has been completed with a
+ * [theGateKeeperPasswordHandle] which will be used for [FingerprintManager] operations such as
+ * [FingerprintManager.enroll].
+ */
+ suspend fun onConfirmDevice(wasSuccessful: Boolean, theGateKeeperPasswordHandle: Long?) {
+ if (!wasSuccessful) {
+ launchFinishSettings("ConfirmDeviceCredential was unsuccessful")
+ return
+ }
+ if (theGateKeeperPasswordHandle == null) {
+ launchFinishSettings("ConfirmDeviceCredential gatekeeper password was null")
+ return
+ }
+
+ launchEnrollNextStep(theGateKeeperPasswordHandle)
+ }
+
+ private fun showSettingsHelper() {
+ _nextStep.update { ShowSettings }
+ }
+
+ private suspend fun launchEnrollNextStep(gateKeeperPasswordHandle: Long?) {
+ fingerprintManagerInteractor.enrolledFingerprints.collect {
+ if (it.isEmpty()) {
+ _nextStep.update { EnrollFirstFingerprint(userId, gateKeeperPasswordHandle, null, null) }
+ } else {
+ viewModelScope.launch(backgroundDispatcher) {
+ val challengePair =
+ fingerprintManagerInteractor.generateChallenge(gateKeeperPasswordHandle!!)
+ challenge = challengePair.first
+ token = challengePair.second
+
+ showSettingsHelper()
+ }
+ }
+ }
+ }
+
+ private fun launchFinishSettings(reason: String) {
+ _nextStep.update { FinishSettings(reason) }
+ }
+
+ private fun launchFinishSettings(reason: String, errorCode: Int) {
+ _nextStep.update { FinishSettingsWithResult(errorCode, reason) }
+ }
+ class FingerprintSettingsNavigationModelFactory(
+ private val userId: Int,
+ private val interactor: FingerprintManagerInteractor,
+ private val backgroundDispatcher: CoroutineDispatcher,
+ private val token: ByteArray?,
+ private val challenge: Long?,
+ ) : ViewModelProvider.Factory {
+
+ @Suppress("UNCHECKED_CAST")
+ override fun <T : ViewModel> create(
+ modelClass: Class<T>,
+ ): T {
+
+ return FingerprintSettingsNavigationViewModel(
+ userId,
+ interactor,
+ backgroundDispatcher,
+ token,
+ challenge,
+ )
+ as T
+ }
+ }
+}
diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/viewmodel/FingerprintSettingsViewModel.kt b/src/com/android/settings/biometrics/fingerprint2/ui/viewmodel/FingerprintSettingsViewModel.kt
new file mode 100644
index 0000000..554f336
--- /dev/null
+++ b/src/com/android/settings/biometrics/fingerprint2/ui/viewmodel/FingerprintSettingsViewModel.kt
@@ -0,0 +1,324 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.biometrics.fingerprint2.ui.viewmodel
+
+import android.hardware.fingerprint.FingerprintManager
+import android.hardware.fingerprint.FingerprintSensorProperties.TYPE_UDFPS_OPTICAL
+import android.hardware.fingerprint.FingerprintSensorProperties.TYPE_UDFPS_ULTRASONIC
+import android.hardware.fingerprint.FingerprintSensorPropertiesInternal
+import android.util.Log
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.ViewModelProvider
+import androidx.lifecycle.viewModelScope
+import com.android.settings.biometrics.fingerprint2.domain.interactor.FingerprintManagerInteractor
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.combineTransform
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.last
+import kotlinx.coroutines.flow.sample
+import kotlinx.coroutines.flow.transformLatest
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.launch
+
+private const val TAG = "FingerprintSettingsViewModel"
+private const val DEBUG = false
+
+/** Models the UI state for fingerprint settings. */
+class FingerprintSettingsViewModel(
+ private val userId: Int,
+ private val fingerprintManagerInteractor: FingerprintManagerInteractor,
+ private val backgroundDispatcher: CoroutineDispatcher,
+ private val navigationViewModel: FingerprintSettingsNavigationViewModel,
+) : ViewModel() {
+
+ private val _consumerShouldAuthenticate: MutableStateFlow<Boolean> = MutableStateFlow(false)
+
+ private val fingerprintSensorPropertiesInternal:
+ MutableStateFlow<List<FingerprintSensorPropertiesInternal>?> =
+ MutableStateFlow(null)
+
+ private val _isShowingDialog: MutableStateFlow<PreferenceViewModel?> = MutableStateFlow(null)
+ val isShowingDialog =
+ _isShowingDialog.combine(navigationViewModel.nextStep) { dialogFlow, nextStep ->
+ if (nextStep is ShowSettings) {
+ return@combine dialogFlow
+ } else {
+ return@combine null
+ }
+ }
+
+ init {
+ viewModelScope.launch {
+ fingerprintSensorPropertiesInternal.update {
+ fingerprintManagerInteractor.sensorPropertiesInternal()
+ }
+ }
+
+ viewModelScope.launch {
+ navigationViewModel.nextStep.filterNotNull().collect {
+ _isShowingDialog.update { null }
+ if (it is ShowSettings) {
+ // reset state
+ updateSettingsData()
+ }
+ }
+ }
+ }
+
+ private val _fingerprintStateViewModel: MutableStateFlow<FingerprintStateViewModel?> =
+ MutableStateFlow(null)
+ val fingerprintState: Flow<FingerprintStateViewModel?> =
+ _fingerprintStateViewModel.combineTransform(navigationViewModel.nextStep) {
+ settingsShowingViewModel,
+ currStep ->
+ if (currStep != null && currStep is ShowSettings) {
+ emit(settingsShowingViewModel)
+ }
+ }
+
+ private val _isLockedOut: MutableStateFlow<FingerprintAuthAttemptViewModel.Error?> =
+ MutableStateFlow(null)
+
+ private val _authSucceeded: MutableSharedFlow<FingerprintAuthAttemptViewModel.Success?> =
+ MutableSharedFlow()
+
+ private val attemptsSoFar: MutableStateFlow<Int> = MutableStateFlow(0)
+
+ /**
+ * This is a very tricky flow. The current fingerprint manager APIs are not robust, and a proper
+ * implementation would take quite a lot of code to implement, it might be easier to rewrite
+ * FingerprintManager.
+ *
+ * The hack to note is the sample(400), if we call authentications in too close of proximity
+ * without waiting for a response, the fingerprint manager will send us the results of the
+ * previous attempt.
+ */
+ private val canAuthenticate: Flow<Boolean> =
+ combine(
+ _isShowingDialog,
+ navigationViewModel.nextStep,
+ _consumerShouldAuthenticate,
+ _fingerprintStateViewModel,
+ _isLockedOut,
+ attemptsSoFar,
+ fingerprintSensorPropertiesInternal
+ ) { dialogShowing, step, resume, fingerprints, isLockedOut, attempts, sensorProps ->
+ if (DEBUG) {
+ Log.d(
+ TAG,
+ "canAuthenticate(isShowingDialog=${dialogShowing != null}," +
+ "nextStep=${step}," +
+ "resumed=${resume}," +
+ "fingerprints=${fingerprints}," +
+ "lockedOut=${isLockedOut}," +
+ "attempts=${attempts}," +
+ "sensorProps=${sensorProps}"
+ )
+ }
+ if (sensorProps.isNullOrEmpty()) {
+ return@combine false
+ }
+ val sensorType = sensorProps[0].sensorType
+ if (listOf(TYPE_UDFPS_OPTICAL, TYPE_UDFPS_ULTRASONIC).contains(sensorType)) {
+ return@combine false
+ }
+
+ if (step != null && step is ShowSettings) {
+ if (fingerprints?.fingerprintViewModels?.isNotEmpty() == true) {
+ return@combine dialogShowing == null && isLockedOut == null && resume && attempts < 15
+ }
+ }
+ false
+ }
+ .sample(400)
+ .distinctUntilChanged()
+
+ /** Represents a consistent stream of authentication attempts. */
+ val authFlow: Flow<FingerprintAuthAttemptViewModel> =
+ canAuthenticate
+ .transformLatest {
+ try {
+ Log.d(TAG, "canAuthenticate $it")
+ while (it && navigationViewModel.nextStep.value is ShowSettings) {
+ Log.d(TAG, "canAuthenticate authing")
+ attemptingAuth()
+ when (val authAttempt = fingerprintManagerInteractor.authenticate()) {
+ is FingerprintAuthAttemptViewModel.Success -> {
+ onAuthSuccess(authAttempt)
+ emit(authAttempt)
+ }
+ is FingerprintAuthAttemptViewModel.Error -> {
+ if (authAttempt.error == FingerprintManager.FINGERPRINT_ERROR_LOCKOUT) {
+ lockout(authAttempt)
+ emit(authAttempt)
+ return@transformLatest
+ }
+ }
+ }
+ }
+ } catch (exception: Exception) {
+ Log.d(TAG, "shouldAuthenticate exception $exception")
+ }
+ }
+ .flowOn(backgroundDispatcher)
+
+ /** The rename dialog has finished */
+ fun onRenameDialogFinished() {
+ _isShowingDialog.update { null }
+ }
+
+ /** The delete dialog has finished */
+ fun onDeleteDialogFinished() {
+ _isShowingDialog.update { null }
+ }
+
+ override fun toString(): String {
+ return "userId: $userId\n" + "fingerprintState: ${_fingerprintStateViewModel.value}\n"
+ }
+
+ /** The fingerprint delete button has been clicked. */
+ fun onDeleteClicked(fingerprintViewModel: FingerprintViewModel) {
+ viewModelScope.launch {
+ if (_isShowingDialog.value == null || navigationViewModel.nextStep.value != ShowSettings) {
+ _isShowingDialog.tryEmit(PreferenceViewModel.DeleteDialog(fingerprintViewModel))
+ } else {
+ Log.d(TAG, "Ignoring onDeleteClicked due to dialog showing ${_isShowingDialog.value}")
+ }
+ }
+ }
+
+ /** The rename fingerprint dialog has been clicked. */
+ fun onPrefClicked(fingerprintViewModel: FingerprintViewModel) {
+ viewModelScope.launch {
+ if (_isShowingDialog.value == null || navigationViewModel.nextStep.value != ShowSettings) {
+ _isShowingDialog.tryEmit(PreferenceViewModel.RenameDialog(fingerprintViewModel))
+ } else {
+ Log.d(TAG, "Ignoring onPrefClicked due to dialog showing ${_isShowingDialog.value}")
+ }
+ }
+ }
+
+ /** A request to delete a fingerprint */
+ fun deleteFingerprint(fp: FingerprintViewModel) {
+ viewModelScope.launch(backgroundDispatcher) {
+ if (fingerprintManagerInteractor.removeFingerprint(fp)) {
+ updateSettingsData()
+ }
+ }
+ }
+
+ /** A request to rename a fingerprint */
+ fun renameFingerprint(fp: FingerprintViewModel, newName: String) {
+ viewModelScope.launch {
+ fingerprintManagerInteractor.renameFingerprint(fp, newName)
+ updateSettingsData()
+ }
+ }
+
+ private fun attemptingAuth() {
+ attemptsSoFar.update { it + 1 }
+ }
+
+ private suspend fun onAuthSuccess(success: FingerprintAuthAttemptViewModel.Success) {
+ _authSucceeded.emit(success)
+ attemptsSoFar.update { 0 }
+ }
+
+ private fun lockout(attemptViewModel: FingerprintAuthAttemptViewModel.Error) {
+ _isLockedOut.update { attemptViewModel }
+ }
+
+ /**
+ * This function is sort of a hack, it's used whenever we want to check for fingerprint state
+ * updates.
+ */
+ private suspend fun updateSettingsData() {
+ Log.d(TAG, "update settings data called")
+ val fingerprints = fingerprintManagerInteractor.enrolledFingerprints.last()
+ val canEnrollFingerprint =
+ fingerprintManagerInteractor.canEnrollFingerprints(fingerprints.size).last()
+ val maxFingerprints = fingerprintManagerInteractor.maxEnrollableFingerprints.last()
+ val hasSideFps = fingerprintManagerInteractor.hasSideFps()
+ val pressToAuthEnabled = fingerprintManagerInteractor.pressToAuthEnabled()
+ _fingerprintStateViewModel.update {
+ FingerprintStateViewModel(
+ fingerprints,
+ canEnrollFingerprint,
+ maxFingerprints,
+ hasSideFps,
+ pressToAuthEnabled
+ )
+ }
+ }
+
+ /** Used to indicate whether the consumer of the view model is ready for authentication. */
+ fun shouldAuthenticate(authenticate: Boolean) {
+ _consumerShouldAuthenticate.update { authenticate }
+ }
+
+ class FingerprintSettingsViewModelFactory(
+ private val userId: Int,
+ private val interactor: FingerprintManagerInteractor,
+ private val backgroundDispatcher: CoroutineDispatcher,
+ private val navigationViewModel: FingerprintSettingsNavigationViewModel,
+ ) : ViewModelProvider.Factory {
+
+ @Suppress("UNCHECKED_CAST")
+ override fun <T : ViewModel> create(
+ modelClass: Class<T>,
+ ): T {
+
+ return FingerprintSettingsViewModel(
+ userId,
+ interactor,
+ backgroundDispatcher,
+ navigationViewModel,
+ )
+ as T
+ }
+ }
+}
+
+private inline fun <T1, T2, T3, T4, T5, T6, T7, R> combine(
+ flow: Flow<T1>,
+ flow2: Flow<T2>,
+ flow3: Flow<T3>,
+ flow4: Flow<T4>,
+ flow5: Flow<T5>,
+ flow6: Flow<T6>,
+ flow7: Flow<T7>,
+ crossinline transform: suspend (T1, T2, T3, T4, T5, T6, T7) -> R
+): Flow<R> {
+ return combine(flow, flow2, flow3, flow4, flow5, flow6, flow7) { args: Array<*> ->
+ @Suppress("UNCHECKED_CAST")
+ transform(
+ args[0] as T1,
+ args[1] as T2,
+ args[2] as T3,
+ args[3] as T4,
+ args[4] as T5,
+ args[5] as T6,
+ args[6] as T7,
+ )
+ }
+}
diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/viewmodel/FingerprintViewModel.kt b/src/com/android/settings/biometrics/fingerprint2/ui/viewmodel/FingerprintViewModel.kt
new file mode 100644
index 0000000..1df0e34
--- /dev/null
+++ b/src/com/android/settings/biometrics/fingerprint2/ui/viewmodel/FingerprintViewModel.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.biometrics.fingerprint2.ui.viewmodel
+
+/** Represents the fingerprint data nad the relevant state. */
+data class FingerprintStateViewModel(
+ val fingerprintViewModels: List<FingerprintViewModel>,
+ val canEnroll: Boolean,
+ val maxFingerprints: Int,
+ val hasSideFps: Boolean,
+ val pressToAuth: Boolean,
+)
+
+data class FingerprintViewModel(
+ val name: String,
+ val fingerId: Int,
+ val deviceId: Long,
+)
+
+sealed class FingerprintAuthAttemptViewModel {
+ data class Success(
+ val fingerId: Int,
+ ) : FingerprintAuthAttemptViewModel()
+
+ data class Error(
+ val error: Int,
+ val message: String,
+ ) : FingerprintAuthAttemptViewModel()
+}
diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/viewmodel/NextStepViewModel.kt b/src/com/android/settings/biometrics/fingerprint2/ui/viewmodel/NextStepViewModel.kt
new file mode 100644
index 0000000..f9dbbff
--- /dev/null
+++ b/src/com/android/settings/biometrics/fingerprint2/ui/viewmodel/NextStepViewModel.kt
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.biometrics.fingerprint2.ui.viewmodel
+
+/**
+ * A class to represent a high level step for FingerprintSettings. This is typically to perform an
+ * action like launching an activity.
+ */
+sealed class NextStepViewModel
+
+data class EnrollFirstFingerprint(
+ val userId: Int,
+ val gateKeeperPasswordHandle: Long?,
+ val challenge: Long?,
+ val challengeToken: ByteArray?,
+) : NextStepViewModel()
+
+data class EnrollAdditionalFingerprint(
+ val userId: Int,
+ val challengeToken: ByteArray?,
+) : NextStepViewModel()
+
+data class FinishSettings(val reason: String) : NextStepViewModel()
+
+data class FinishSettingsWithResult(val result: Int, val reason: String) : NextStepViewModel()
+
+object ShowSettings : NextStepViewModel()
+
+object LaunchedActivity : NextStepViewModel()
+
+data class LaunchConfirmDeviceCredential(val userId: Int) : NextStepViewModel()
diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/viewmodel/PreferenceViewModel.kt b/src/com/android/settings/biometrics/fingerprint2/ui/viewmodel/PreferenceViewModel.kt
new file mode 100644
index 0000000..05764a2
--- /dev/null
+++ b/src/com/android/settings/biometrics/fingerprint2/ui/viewmodel/PreferenceViewModel.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.biometrics.fingerprint2.ui.viewmodel
+
+/** Classed use to represent a Dialogs state. */
+sealed class PreferenceViewModel {
+ data class RenameDialog(
+ val fingerprintViewModel: FingerprintViewModel,
+ ) : PreferenceViewModel()
+
+ data class DeleteDialog(
+ val fingerprintViewModel: FingerprintViewModel,
+ ) : PreferenceViewModel()
+}
diff --git a/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollProgressViewModel.java b/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollProgressViewModel.java
index d77d9d3..7074288 100644
--- a/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollProgressViewModel.java
+++ b/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollProgressViewModel.java
@@ -103,12 +103,12 @@
}
@Override
- public void onPointerDown(int sensorId) {
+ public void onUdfpsPointerDown(int sensorId) {
mPointerDownLiveData.postValue(sensorId);
}
@Override
- public void onPointerUp(int sensorId) {
+ public void onUdfpsPointerUp(int sensorId) {
mPointerUpLiveData.postValue(sensorId);
}
};
diff --git a/src/com/android/settings/bluetooth/BlockingPrefWithSliceController.java b/src/com/android/settings/bluetooth/BlockingPrefWithSliceController.java
index 93a2747..0690186 100644
--- a/src/com/android/settings/bluetooth/BlockingPrefWithSliceController.java
+++ b/src/com/android/settings/bluetooth/BlockingPrefWithSliceController.java
@@ -59,7 +59,7 @@
* until {@link Slice} is fully loaded.
*/
public class BlockingPrefWithSliceController extends BasePreferenceController implements
- LifecycleObserver, OnStart, OnStop, Observer<Slice>, BasePreferenceController.UiBlocker{
+ LifecycleObserver, OnStart, OnStop, Observer<Slice>, BasePreferenceController.UiBlocker {
private static final String TAG = "BlockingPrefWithSliceController";
private static final String PREFIX_KEY = "slice_preference_item_";
@@ -225,7 +225,8 @@
} else {
expectedActivityIntent = intentFromSliceAction;
}
- if (expectedActivityIntent != null) {
+ if (expectedActivityIntent != null && expectedActivityIntent.resolveActivity(
+ mContext.getPackageManager()) != null) {
Log.d(TAG, "setIntent: ActivityIntent" + expectedActivityIntent);
// Since UI needs to support the Settings' 2 panel feature, the intent can't use the
// FLAG_ACTIVITY_NEW_TASK. The above intent may have the FLAG_ACTIVITY_NEW_TASK
@@ -234,6 +235,7 @@
preference.setIntent(expectedActivityIntent);
} else {
Log.d(TAG, "setIntent: Intent is null");
+ preference.setSelectable(false);
}
}
diff --git a/src/com/android/settings/bluetooth/BluetoothDetailsAudioDeviceTypeController.java b/src/com/android/settings/bluetooth/BluetoothDetailsAudioDeviceTypeController.java
new file mode 100644
index 0000000..9571767
--- /dev/null
+++ b/src/com/android/settings/bluetooth/BluetoothDetailsAudioDeviceTypeController.java
@@ -0,0 +1,184 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.bluetooth;
+
+import static android.bluetooth.BluetoothDevice.DEVICE_TYPE_LE;
+import static android.media.AudioManager.AUDIO_DEVICE_CATEGORY_CARKIT;
+import static android.media.AudioManager.AUDIO_DEVICE_CATEGORY_HEADPHONES;
+import static android.media.AudioManager.AUDIO_DEVICE_CATEGORY_HEARING_AID;
+import static android.media.AudioManager.AUDIO_DEVICE_CATEGORY_OTHER;
+import static android.media.AudioManager.AUDIO_DEVICE_CATEGORY_SPEAKER;
+import static android.media.AudioManager.AUDIO_DEVICE_CATEGORY_UNKNOWN;
+
+import android.content.Context;
+import android.media.AudioManager;
+import android.media.AudioManager.AudioDeviceCategory;
+import android.util.Log;
+
+import androidx.annotation.VisibleForTesting;
+import androidx.preference.ListPreference;
+import androidx.preference.Preference;
+import androidx.preference.PreferenceCategory;
+import androidx.preference.PreferenceFragmentCompat;
+import androidx.preference.PreferenceScreen;
+
+import com.android.settings.R;
+import com.android.settingslib.bluetooth.A2dpProfile;
+import com.android.settingslib.bluetooth.CachedBluetoothDevice;
+import com.android.settingslib.bluetooth.LeAudioProfile;
+import com.android.settingslib.bluetooth.LocalBluetoothManager;
+import com.android.settingslib.bluetooth.LocalBluetoothProfileManager;
+import com.android.settingslib.core.lifecycle.Lifecycle;
+
+/**
+ * Controller responsible for the bluetooth audio device type selection
+ */
+public class BluetoothDetailsAudioDeviceTypeController extends BluetoothDetailsController
+ implements Preference.OnPreferenceChangeListener {
+ private static final String TAG = "BluetoothDetailsAudioDeviceTypeController";
+
+ private static final boolean DEBUG = false;
+
+ private static final String KEY_BT_AUDIO_DEVICE_TYPE_GROUP =
+ "bluetooth_audio_device_type_group";
+ private static final String KEY_BT_AUDIO_DEVICE_TYPE = "bluetooth_audio_device_type";
+
+ private final AudioManager mAudioManager;
+
+ private ListPreference mAudioDeviceTypePreference;
+
+ private final LocalBluetoothProfileManager mProfileManager;
+
+ @VisibleForTesting
+ PreferenceCategory mProfilesContainer;
+
+ public BluetoothDetailsAudioDeviceTypeController(
+ Context context,
+ PreferenceFragmentCompat fragment,
+ LocalBluetoothManager manager,
+ CachedBluetoothDevice device,
+ Lifecycle lifecycle) {
+ super(context, fragment, device, lifecycle);
+ mAudioManager = context.getSystemService(AudioManager.class);
+ mProfileManager = manager.getProfileManager();
+ }
+
+ @Override
+ public boolean isAvailable() {
+ // Available only for A2DP and BLE devices.
+ A2dpProfile a2dpProfile = mProfileManager.getA2dpProfile();
+ boolean a2dpProfileEnabled = false;
+ if (a2dpProfile != null) {
+ a2dpProfileEnabled = a2dpProfile.isEnabled(mCachedDevice.getDevice());
+ }
+
+ LeAudioProfile leAudioProfile = mProfileManager.getLeAudioProfile();
+ boolean leAudioProfileEnabled = false;
+ if (leAudioProfile != null) {
+ leAudioProfileEnabled = leAudioProfile.isEnabled(mCachedDevice.getDevice());
+ }
+
+ return a2dpProfileEnabled || leAudioProfileEnabled;
+ }
+
+ @Override
+ public boolean onPreferenceChange(Preference preference, Object newValue) {
+ if (preference instanceof ListPreference) {
+ final ListPreference pref = (ListPreference) preference;
+ final String key = pref.getKey();
+ if (key.equals(KEY_BT_AUDIO_DEVICE_TYPE)) {
+ if (newValue instanceof String) {
+ final String value = (String) newValue;
+ final int index = pref.findIndexOfValue(value);
+ if (index >= 0) {
+ pref.setSummary(pref.getEntries()[index]);
+ mAudioManager.setBluetoothAudioDeviceCategory(mCachedDevice.getAddress(),
+ mCachedDevice.getDevice().getType() == DEVICE_TYPE_LE,
+ Integer.parseInt(value));
+ mCachedDevice.onAudioDeviceCategoryChanged();
+ }
+ }
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ @Override
+ public String getPreferenceKey() {
+ return KEY_BT_AUDIO_DEVICE_TYPE_GROUP;
+ }
+
+ @Override
+ protected void init(PreferenceScreen screen) {
+ mProfilesContainer = screen.findPreference(getPreferenceKey());
+ refresh();
+ }
+
+ @Override
+ protected void refresh() {
+ mAudioDeviceTypePreference = mProfilesContainer.findPreference(
+ KEY_BT_AUDIO_DEVICE_TYPE);
+ if (mAudioDeviceTypePreference == null) {
+ createAudioDeviceTypePreference(mProfilesContainer.getContext());
+ mProfilesContainer.addPreference(mAudioDeviceTypePreference);
+ }
+ }
+
+ @VisibleForTesting
+ void createAudioDeviceTypePreference(Context context) {
+ mAudioDeviceTypePreference = new ListPreference(context);
+ mAudioDeviceTypePreference.setKey(KEY_BT_AUDIO_DEVICE_TYPE);
+ mAudioDeviceTypePreference.setTitle(
+ mContext.getString(R.string.bluetooth_details_audio_device_types_title));
+ mAudioDeviceTypePreference.setEntries(new CharSequence[]{
+ mContext.getString(R.string.bluetooth_details_audio_device_type_unknown),
+ mContext.getString(R.string.bluetooth_details_audio_device_type_speaker),
+ mContext.getString(R.string.bluetooth_details_audio_device_type_headphones),
+ mContext.getString(R.string.bluetooth_details_audio_device_type_carkit),
+ mContext.getString(R.string.bluetooth_details_audio_device_type_hearing_aid),
+ mContext.getString(R.string.bluetooth_details_audio_device_type_other),
+ });
+ mAudioDeviceTypePreference.setEntryValues(new CharSequence[]{
+ Integer.toString(AUDIO_DEVICE_CATEGORY_UNKNOWN),
+ Integer.toString(AUDIO_DEVICE_CATEGORY_SPEAKER),
+ Integer.toString(AUDIO_DEVICE_CATEGORY_HEADPHONES),
+ Integer.toString(AUDIO_DEVICE_CATEGORY_CARKIT),
+ Integer.toString(AUDIO_DEVICE_CATEGORY_HEARING_AID),
+ Integer.toString(AUDIO_DEVICE_CATEGORY_OTHER),
+ });
+
+ @AudioDeviceCategory final int deviceCategory =
+ mAudioManager.getBluetoothAudioDeviceCategory(mCachedDevice.getAddress(),
+ mCachedDevice.getDevice().getType() == DEVICE_TYPE_LE);
+ if (DEBUG) {
+ Log.v(TAG, "getBluetoothAudioDeviceCategory() device: "
+ + mCachedDevice.getDevice().getAnonymizedAddress()
+ + ", has audio device category: " + deviceCategory);
+ }
+ mAudioDeviceTypePreference.setValue(Integer.toString(deviceCategory));
+
+ mAudioDeviceTypePreference.setSummary(mAudioDeviceTypePreference.getEntry());
+ mAudioDeviceTypePreference.setOnPreferenceChangeListener(this);
+ }
+
+ @VisibleForTesting
+ ListPreference getAudioDeviceTypePreference() {
+ return mAudioDeviceTypePreference;
+ }
+}
diff --git a/src/com/android/settings/bluetooth/BluetoothDetailsDataSyncController.java b/src/com/android/settings/bluetooth/BluetoothDetailsDataSyncController.java
new file mode 100644
index 0000000..0d74f3c
--- /dev/null
+++ b/src/com/android/settings/bluetooth/BluetoothDetailsDataSyncController.java
@@ -0,0 +1,146 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.bluetooth;
+
+import android.companion.AssociationInfo;
+import android.companion.CompanionDeviceManager;
+import android.companion.datatransfer.PermissionSyncRequest;
+import android.content.Context;
+import android.provider.Settings;
+
+import androidx.preference.Preference;
+import androidx.preference.PreferenceCategory;
+import androidx.preference.PreferenceFragmentCompat;
+import androidx.preference.PreferenceScreen;
+import androidx.preference.SwitchPreference;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.settings.R;
+import com.android.settingslib.bluetooth.CachedBluetoothDevice;
+import com.android.settingslib.core.lifecycle.Lifecycle;
+
+import com.google.common.base.Objects;
+
+import java.util.Comparator;
+
+/**
+ * The controller of the CDM data sync in the bluetooth detail settings.
+ */
+public class BluetoothDetailsDataSyncController extends BluetoothDetailsController
+ implements Preference.OnPreferenceClickListener {
+
+ private static final int DUMMY_ASSOCIATION_ID = -1;
+ private static final String TAG = "BTDataSyncController";
+ private static final String KEY_DATA_SYNC_GROUP = "data_sync_group";
+ private static final String KEY_PERM_SYNC = "perm_sync";
+
+ @VisibleForTesting
+ PreferenceCategory mPreferenceCategory;
+ @VisibleForTesting
+ int mAssociationId = DUMMY_ASSOCIATION_ID;
+
+ private CachedBluetoothDevice mCachedDevice;
+ private CompanionDeviceManager mCompanionDeviceManager;
+
+ public BluetoothDetailsDataSyncController(Context context,
+ PreferenceFragmentCompat fragment,
+ CachedBluetoothDevice device,
+ Lifecycle lifecycle) {
+ super(context, fragment, device, lifecycle);
+ mCachedDevice = device;
+ mCompanionDeviceManager = context.getSystemService(CompanionDeviceManager.class);
+
+ mCompanionDeviceManager.getAllAssociations().stream().filter(
+ a -> a.getDeviceMacAddress() != null).filter(
+ a -> Objects.equal(mCachedDevice.getAddress(),
+ a.getDeviceMacAddress().toString().toUpperCase())).max(
+ Comparator.comparingLong(AssociationInfo::getTimeApprovedMs)).ifPresent(
+ a -> mAssociationId = a.getId());
+ }
+
+ @Override
+ public boolean isAvailable() {
+ if (mAssociationId == DUMMY_ASSOCIATION_ID) {
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ public boolean onPreferenceClick(Preference preference) {
+ SwitchPreference switchPreference = (SwitchPreference) preference;
+ String key = switchPreference.getKey();
+ if (key.equals(KEY_PERM_SYNC)) {
+ if (switchPreference.isChecked()) {
+ mCompanionDeviceManager.enablePermissionsSync(mAssociationId);
+ } else {
+ mCompanionDeviceManager.disablePermissionsSync(mAssociationId);
+ }
+ }
+ return true;
+ }
+
+ @Override
+ public String getPreferenceKey() {
+ return KEY_DATA_SYNC_GROUP;
+ }
+
+ @Override
+ protected void init(PreferenceScreen screen) {
+ mPreferenceCategory = screen.findPreference(getPreferenceKey());
+ refresh();
+ }
+
+ @Override
+ protected void refresh() {
+ SwitchPreference permSyncPref = mPreferenceCategory.findPreference(KEY_PERM_SYNC);
+ if (permSyncPref == null) {
+ permSyncPref = createPermSyncPreference(mPreferenceCategory.getContext());
+ mPreferenceCategory.addPreference(permSyncPref);
+ }
+
+ if (mAssociationId == DUMMY_ASSOCIATION_ID) {
+ permSyncPref.setVisible(false);
+ return;
+ }
+
+ boolean visible = false;
+ boolean checked = false;
+ PermissionSyncRequest request = mCompanionDeviceManager.getPermissionSyncRequest(
+ mAssociationId);
+ if (request != null) {
+ visible = true;
+ if (request.isUserConsented()) {
+ checked = true;
+ }
+ }
+ permSyncPref.setVisible(visible);
+ permSyncPref.setChecked(checked);
+ }
+
+ @VisibleForTesting
+ SwitchPreference createPermSyncPreference(Context context) {
+ SwitchPreference pref = new SwitchPreference(context);
+ pref.setKey(KEY_PERM_SYNC);
+ pref.setTitle(context.getString(R.string.bluetooth_details_permissions_sync_title));
+ pref.setSummary(context.getString(R.string.bluetooth_details_permissions_sync_summary,
+ mCachedDevice.getName(),
+ Settings.Global.getString(context.getContentResolver(), "device_name")));
+ pref.setOnPreferenceClickListener(this);
+ return pref;
+ }
+}
diff --git a/src/com/android/settings/bluetooth/BluetoothDetailsProfilesController.java b/src/com/android/settings/bluetooth/BluetoothDetailsProfilesController.java
index 724947c..e76b92e 100644
--- a/src/com/android/settings/bluetooth/BluetoothDetailsProfilesController.java
+++ b/src/com/android/settings/bluetooth/BluetoothDetailsProfilesController.java
@@ -69,6 +69,9 @@
private static final String ENABLE_DUAL_MODE_AUDIO =
"persist.bluetooth.enable_dual_mode_audio";
private static final String CONFIG_LE_AUDIO_ENABLED_BY_DEFAULT = "le_audio_enabled_by_default";
+ private static final boolean LE_AUDIO_TOGGLE_VISIBLE_DEFAULT_VALUE = true;
+ private static final String LE_AUDIO_TOGGLE_VISIBLE_PROPERTY =
+ "persist.bluetooth.leaudio.toggle_visible";
private LocalBluetoothManager mManager;
private LocalBluetoothProfileManager mProfileManager;
@@ -88,7 +91,7 @@
mManager = manager;
mProfileManager = mManager.getProfileManager();
mCachedDevice = device;
- mAllOfCachedDevices = Utils.getAllOfCachedBluetoothDevices(mContext, mCachedDevice);
+ mAllOfCachedDevices = Utils.getAllOfCachedBluetoothDevices(mManager, mCachedDevice);
lifecycle.addObserver(this);
}
@@ -96,12 +99,6 @@
protected void init(PreferenceScreen screen) {
mProfilesContainer = (PreferenceCategory)screen.findPreference(getPreferenceKey());
mProfilesContainer.setLayoutResource(R.layout.preference_bluetooth_profile_category);
- mIsLeContactSharingEnabled = DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_SETTINGS_UI,
- SettingsUIDeviceConfig.BT_LE_AUDIO_CONTACT_SHARING_ENABLED, true);
- mIsLeAudioToggleEnabled = DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_SETTINGS_UI,
- SettingsUIDeviceConfig.BT_LE_AUDIO_DEVICE_DETAIL_ENABLED, false)
- || DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_BLUETOOTH,
- CONFIG_LE_AUDIO_ENABLED_BY_DEFAULT, false);
// Call refresh here even though it will get called later in onResume, to avoid the
// list of switches appearing to "pop" into the page.
refresh();
@@ -151,8 +148,8 @@
profilePref.setEnabled(!mCachedDevice.isBusy());
}
- if (profile instanceof LeAudioProfile && !mIsLeAudioToggleEnabled) {
- profilePref.setVisible(false);
+ if (profile instanceof LeAudioProfile) {
+ profilePref.setVisible(mIsLeAudioToggleEnabled);
}
if (profile instanceof MapProfile) {
@@ -329,11 +326,16 @@
return;
}
+ LocalBluetoothProfile asha = mProfileManager.getHearingAidProfile();
+
for (CachedBluetoothDevice leAudioDevice : mProfileDeviceMap.get(profile.toString())) {
Log.d(TAG,
"device:" + leAudioDevice.getDevice().getAnonymizedAddress()
+ "disable LE profile");
profile.setEnabled(leAudioDevice.getDevice(), false);
+ if (asha != null) {
+ asha.setEnabled(leAudioDevice.getDevice(), true);
+ }
}
if (!SystemProperties.getBoolean(ENABLE_DUAL_MODE_AUDIO, false)) {
@@ -359,12 +361,16 @@
disableProfileBeforeUserEnablesLeAudio(mProfileManager.getA2dpProfile());
disableProfileBeforeUserEnablesLeAudio(mProfileManager.getHeadsetProfile());
}
+ LocalBluetoothProfile asha = mProfileManager.getHearingAidProfile();
for (CachedBluetoothDevice leAudioDevice : mProfileDeviceMap.get(profile.toString())) {
Log.d(TAG,
"device:" + leAudioDevice.getDevice().getAnonymizedAddress()
+ "enable LE profile");
profile.setEnabled(leAudioDevice.getDevice(), true);
+ if (asha != null) {
+ asha.setEnabled(leAudioDevice.getDevice(), false);
+ }
}
}
@@ -381,6 +387,12 @@
+ profile.toString() + " profile is disabled. Do nothing.");
}
}
+ } else {
+ if (profile == null) {
+ Log.w(TAG, "profile is null");
+ } else {
+ Log.w(TAG, profile.toString() + " is not in " + mProfileDeviceMap);
+ }
}
}
@@ -397,6 +409,12 @@
+ profile.toString() + " profile is enabled. Do nothing.");
}
}
+ } else {
+ if (profile == null) {
+ Log.w(TAG, "profile is null");
+ } else {
+ Log.w(TAG, profile.toString() + " is not in " + mProfileDeviceMap);
+ }
}
}
@@ -437,6 +455,7 @@
@Override
public void onResume() {
+ updateLeAudioConfig();
for (CachedBluetoothDevice item : mAllOfCachedDevices) {
item.registerCallback(this);
}
@@ -444,12 +463,25 @@
refresh();
}
+ private void updateLeAudioConfig() {
+ mIsLeContactSharingEnabled = DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_SETTINGS_UI,
+ SettingsUIDeviceConfig.BT_LE_AUDIO_CONTACT_SHARING_ENABLED, true);
+ boolean isLeAudioToggleVisible = SystemProperties.getBoolean(
+ LE_AUDIO_TOGGLE_VISIBLE_PROPERTY, LE_AUDIO_TOGGLE_VISIBLE_DEFAULT_VALUE);
+ boolean isLeEnabledByDefault = DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_BLUETOOTH,
+ CONFIG_LE_AUDIO_ENABLED_BY_DEFAULT, false);
+ mIsLeAudioToggleEnabled = isLeAudioToggleVisible || isLeEnabledByDefault;
+ Log.d(TAG, "BT_LE_AUDIO_CONTACT_SHARING_ENABLED:" + mIsLeContactSharingEnabled
+ + ", LE_AUDIO_TOGGLE_VISIBLE_PROPERTY:" + isLeAudioToggleVisible
+ + ", CONFIG_LE_AUDIO_ENABLED_BY_DEFAULT:" + isLeEnabledByDefault);
+ }
+
@Override
public void onDeviceAttributesChanged() {
for (CachedBluetoothDevice item : mAllOfCachedDevices) {
item.unregisterCallback(this);
}
- mAllOfCachedDevices = Utils.getAllOfCachedBluetoothDevices(mContext, mCachedDevice);
+ mAllOfCachedDevices = Utils.getAllOfCachedBluetoothDevices(mManager, mCachedDevice);
for (CachedBluetoothDevice item : mAllOfCachedDevices) {
item.registerCallback(this);
}
diff --git a/src/com/android/settings/bluetooth/BluetoothDetailsSpatialAudioController.java b/src/com/android/settings/bluetooth/BluetoothDetailsSpatialAudioController.java
index a1e133e..c431cee 100644
--- a/src/com/android/settings/bluetooth/BluetoothDetailsSpatialAudioController.java
+++ b/src/com/android/settings/bluetooth/BluetoothDetailsSpatialAudioController.java
@@ -16,6 +16,8 @@
package com.android.settings.bluetooth;
+import static android.media.Spatializer.SPATIALIZER_IMMERSIVE_LEVEL_NONE;
+
import android.content.Context;
import android.media.AudioDeviceAttributes;
import android.media.AudioDeviceInfo;
@@ -51,9 +53,7 @@
@VisibleForTesting
PreferenceCategory mProfilesContainer;
@VisibleForTesting
- AudioDeviceAttributes mAudioDevice;
-
- private boolean mIsAvailable;
+ AudioDeviceAttributes mAudioDevice = null;
public BluetoothDetailsSpatialAudioController(
Context context,
@@ -63,13 +63,11 @@
super(context, fragment, device, lifecycle);
AudioManager audioManager = context.getSystemService(AudioManager.class);
mSpatializer = audioManager.getSpatializer();
- getAvailableDevice();
-
}
@Override
public boolean isAvailable() {
- return mIsAvailable;
+ return mSpatializer.getImmersiveAudioLevel() != SPATIALIZER_IMMERSIVE_LEVEL_NONE;
}
@Override
@@ -77,15 +75,11 @@
SwitchPreference switchPreference = (SwitchPreference) preference;
String key = switchPreference.getKey();
if (TextUtils.equals(key, KEY_SPATIAL_AUDIO)) {
- if (switchPreference.isChecked()) {
- mSpatializer.addCompatibleAudioDevice(mAudioDevice);
- } else {
- mSpatializer.removeCompatibleAudioDevice(mAudioDevice);
- }
- refresh();
+ updateSpatializerEnabled(switchPreference.isChecked());
+ refreshSpatialAudioEnabled(switchPreference);
return true;
} else if (TextUtils.equals(key, KEY_HEAD_TRACKING)) {
- mSpatializer.setHeadTrackerEnabled(switchPreference.isChecked(), mAudioDevice);
+ updateSpatializerHeadTracking(switchPreference.isChecked());
return true;
} else {
Log.w(TAG, "invalid key name.");
@@ -93,6 +87,26 @@
}
}
+ private void updateSpatializerEnabled(boolean enabled) {
+ if (mAudioDevice == null) {
+ Log.w(TAG, "cannot update spatializer enabled for null audio device.");
+ return;
+ }
+ if (enabled) {
+ mSpatializer.addCompatibleAudioDevice(mAudioDevice);
+ } else {
+ mSpatializer.removeCompatibleAudioDevice(mAudioDevice);
+ }
+ }
+
+ private void updateSpatializerHeadTracking(boolean enabled) {
+ if (mAudioDevice == null) {
+ Log.w(TAG, "cannot update spatializer head tracking for null audio device.");
+ return;
+ }
+ mSpatializer.setHeadTrackerEnabled(enabled, mAudioDevice);
+ }
+
@Override
public String getPreferenceKey() {
return KEY_SPATIAL_AUDIO_GROUP;
@@ -106,12 +120,31 @@
@Override
protected void refresh() {
- SwitchPreference spatialAudioPref = mProfilesContainer.findPreference(KEY_SPATIAL_AUDIO);
- if (spatialAudioPref == null) {
- spatialAudioPref = createSpatialAudioPreference(mProfilesContainer.getContext());
- mProfilesContainer.addPreference(spatialAudioPref);
+ if (mAudioDevice == null) {
+ getAvailableDevice();
}
+ SwitchPreference spatialAudioPref = mProfilesContainer.findPreference(KEY_SPATIAL_AUDIO);
+ if (spatialAudioPref == null && mAudioDevice != null) {
+ spatialAudioPref = createSpatialAudioPreference(mProfilesContainer.getContext());
+ mProfilesContainer.addPreference(spatialAudioPref);
+ } else if (mAudioDevice == null || !mSpatializer.isAvailableForDevice(mAudioDevice)) {
+ if (spatialAudioPref != null) {
+ mProfilesContainer.removePreference(spatialAudioPref);
+ }
+ final SwitchPreference headTrackingPref =
+ mProfilesContainer.findPreference(KEY_HEAD_TRACKING);
+ if (headTrackingPref != null) {
+ mProfilesContainer.removePreference(headTrackingPref);
+ }
+ mAudioDevice = null;
+ return;
+ }
+
+ refreshSpatialAudioEnabled(spatialAudioPref);
+ }
+
+ private void refreshSpatialAudioEnabled(SwitchPreference spatialAudioPref) {
boolean isSpatialAudioOn = mSpatializer.getCompatibleAudioDevices().contains(mAudioDevice);
Log.d(TAG, "refresh() isSpatialAudioOn : " + isSpatialAudioOn);
spatialAudioPref.setChecked(isSpatialAudioOn);
@@ -121,9 +154,13 @@
headTrackingPref = createHeadTrackingPreference(mProfilesContainer.getContext());
mProfilesContainer.addPreference(headTrackingPref);
}
+ refreshHeadTracking(spatialAudioPref, headTrackingPref);
+ }
+ private void refreshHeadTracking(SwitchPreference spatialAudioPref,
+ SwitchPreference headTrackingPref) {
boolean isHeadTrackingAvailable =
- isSpatialAudioOn && mSpatializer.hasHeadTracker(mAudioDevice);
+ spatialAudioPref.isChecked() && mSpatializer.hasHeadTracker(mAudioDevice);
Log.d(TAG, "refresh() has head tracker : " + mSpatializer.hasHeadTracker(mAudioDevice));
headTrackingPref.setVisible(isHeadTrackingAvailable);
if (isHeadTrackingAvailable) {
@@ -173,7 +210,6 @@
AudioDeviceInfo.TYPE_HEARING_AID,
mCachedDevice.getAddress());
- mIsAvailable = true;
if (mSpatializer.isAvailableForDevice(bleHeadsetDevice)) {
mAudioDevice = bleHeadsetDevice;
} else if (mSpatializer.isAvailableForDevice(bleSpeakerDevice)) {
@@ -182,20 +218,20 @@
mAudioDevice = bleBroadcastDevice;
} else if (mSpatializer.isAvailableForDevice(a2dpDevice)) {
mAudioDevice = a2dpDevice;
- } else {
- mIsAvailable = mSpatializer.isAvailableForDevice(hearingAidDevice);
+ } else if (mSpatializer.isAvailableForDevice(hearingAidDevice)) {
mAudioDevice = hearingAidDevice;
+ } else {
+ mAudioDevice = null;
}
Log.d(TAG, "getAvailableDevice() device : "
+ mCachedDevice.getDevice().getAnonymizedAddress()
- + ", type : " + mAudioDevice.getType()
- + ", is available : " + mIsAvailable);
+ + ", is available : " + (mAudioDevice != null)
+ + ", type : " + (mAudioDevice == null ? "no type" : mAudioDevice.getType()));
}
@VisibleForTesting
void setAvailableDevice(AudioDeviceAttributes audioDevice) {
mAudioDevice = audioDevice;
- mIsAvailable = mSpatializer.isAvailableForDevice(audioDevice);
}
}
diff --git a/src/com/android/settings/bluetooth/BluetoothDeviceDetailsFragment.java b/src/com/android/settings/bluetooth/BluetoothDeviceDetailsFragment.java
index 99f3e31..ae022aa 100644
--- a/src/com/android/settings/bluetooth/BluetoothDeviceDetailsFragment.java
+++ b/src/com/android/settings/bluetooth/BluetoothDeviceDetailsFragment.java
@@ -300,6 +300,8 @@
lifecycle));
controllers.add(new BluetoothDetailsCompanionAppsController(context, this,
mCachedDevice, lifecycle));
+ controllers.add(new BluetoothDetailsAudioDeviceTypeController(context, this, mManager,
+ mCachedDevice, lifecycle));
controllers.add(new BluetoothDetailsSpatialAudioController(context, this, mCachedDevice,
lifecycle));
controllers.add(new BluetoothDetailsProfilesController(context, this, mManager,
@@ -314,6 +316,8 @@
lifecycle));
controllers.add(new BluetoothDetailsHearingDeviceControlsController(context, this,
mCachedDevice, lifecycle));
+ controllers.add(new BluetoothDetailsDataSyncController(context, this,
+ mCachedDevice, lifecycle));
}
return controllers;
}
diff --git a/src/com/android/settings/bluetooth/BluetoothDevicePairingDetailBase.java b/src/com/android/settings/bluetooth/BluetoothDevicePairingDetailBase.java
index 7ee61ee..f2bc6fc 100644
--- a/src/com/android/settings/bluetooth/BluetoothDevicePairingDetailBase.java
+++ b/src/com/android/settings/bluetooth/BluetoothDevicePairingDetailBase.java
@@ -128,7 +128,7 @@
if (device != null && mSelectedList.contains(device)) {
setResult(RESULT_OK);
finish();
- } else if (mDevicePreferenceMap.containsKey(cachedDevice)) {
+ } else {
onDeviceDeleted(cachedDevice);
}
}
@@ -175,8 +175,6 @@
public void updateContent(int bluetoothState) {
switch (bluetoothState) {
case BluetoothAdapter.STATE_ON:
- mDevicePreferenceMap.clear();
- clearPreferenceGroupCache();
mBluetoothAdapter.enable();
enableScanning();
break;
@@ -187,14 +185,6 @@
}
}
- /**
- * Clears all cached preferences in {@code preferenceGroup}.
- */
- private void clearPreferenceGroupCache() {
- cacheRemoveAllPrefs(mAvailableDevicesCategory);
- removeCachedPrefs(mAvailableDevicesCategory);
- }
-
@VisibleForTesting
void showBluetoothTurnedOnToast() {
Toast.makeText(getContext(), R.string.connected_device_bluetooth_turned_on_toast,
diff --git a/src/com/android/settings/bluetooth/BluetoothDevicePreference.java b/src/com/android/settings/bluetooth/BluetoothDevicePreference.java
index 5256f3d..039080b 100644
--- a/src/com/android/settings/bluetooth/BluetoothDevicePreference.java
+++ b/src/com/android/settings/bluetooth/BluetoothDevicePreference.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2008 The Android Open Source Project
+ * Copyright (C) 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -35,6 +35,8 @@
import android.widget.ImageView;
import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.appcompat.app.AlertDialog;
import androidx.preference.Preference;
@@ -52,6 +54,7 @@
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.RejectedExecutionException;
+import java.util.concurrent.atomic.AtomicInteger;
/**
* BluetoothDevicePreference is the preference type used to display each remote
@@ -79,7 +82,9 @@
@VisibleForTesting
BluetoothAdapter mBluetoothAdapter;
private final boolean mShowDevicesWithoutNames;
- private final long mCurrentTime;
+ @NonNull
+ private static final AtomicInteger sNextId = new AtomicInteger();
+ private final int mId;
private final int mType;
private AlertDialog mDisconnectDialog;
@@ -127,8 +132,9 @@
mCachedDevice = cachedDevice;
mCallback = new BluetoothDevicePreferenceCallback();
- mCurrentTime = System.currentTimeMillis();
+ mId = sNextId.getAndIncrement();
mType = type;
+ setVisible(false);
onPreferenceAttributesChanged();
}
@@ -229,35 +235,41 @@
@SuppressWarnings("FutureReturnValueIgnored")
void onPreferenceAttributesChanged() {
- Pair<Drawable, String> pair = mCachedDevice.getDrawableWithDescription();
- setIcon(pair.first);
- contentDescription = pair.second;
-
- /*
- * The preference framework takes care of making sure the value has
- * changed before proceeding. It will also call notifyChanged() if
- * any preference info has changed from the previous value.
- */
- setTitle(mCachedDevice.getName());
try {
ThreadUtils.postOnBackgroundThread(() -> {
+ @Nullable String name = mCachedDevice.getName();
// Null check is done at the framework
- ThreadUtils.postOnMainThread(() -> setSummary(getConnectionSummary()));
+ @Nullable String connectionSummary = getConnectionSummary();
+ @NonNull Pair<Drawable, String> pair = mCachedDevice.getDrawableWithDescription();
+ boolean isBusy = mCachedDevice.isBusy();
+ // Device is only visible in the UI if it has a valid name besides MAC address or
+ // when user allows showing devices without user-friendly name in developer settings
+ boolean isVisible =
+ mShowDevicesWithoutNames || mCachedDevice.hasHumanReadableName();
+
+ ThreadUtils.postOnMainThread(() -> {
+ /*
+ * The preference framework takes care of making sure the value has
+ * changed before proceeding. It will also call notifyChanged() if
+ * any preference info has changed from the previous value.
+ */
+ setTitle(name);
+ setSummary(connectionSummary);
+ setIcon(pair.first);
+ contentDescription = pair.second;
+ // Used to gray out the item
+ setEnabled(!isBusy);
+ setVisible(isVisible);
+
+ // This could affect ordering, so notify that
+ if (mNeedNotifyHierarchyChanged) {
+ notifyHierarchyChanged();
+ }
+ });
});
} catch (RejectedExecutionException e) {
Log.w(TAG, "Handler thread unavailable, skipping getConnectionSummary!");
}
- // Used to gray out the item
- setEnabled(!mCachedDevice.isBusy());
-
- // Device is only visible in the UI if it has a valid name besides MAC address or when user
- // allows showing devices without user-friendly name in developer settings
- setVisible(mShowDevicesWithoutNames || mCachedDevice.hasHumanReadableName());
-
- // This could affect ordering, so notify that
- if (mNeedNotifyHierarchyChanged) {
- notifyHierarchyChanged();
- }
}
@Override
@@ -311,7 +323,7 @@
return mCachedDevice
.compareTo(((BluetoothDevicePreference) another).mCachedDevice);
case SortType.TYPE_FIFO:
- return mCurrentTime > ((BluetoothDevicePreference) another).mCurrentTime ? 1 : -1;
+ return mId > ((BluetoothDevicePreference) another).mId ? 1 : -1;
default:
return super.compareTo(another);
}
diff --git a/src/com/android/settings/bluetooth/BluetoothFindBroadcastsFragment.java b/src/com/android/settings/bluetooth/BluetoothFindBroadcastsFragment.java
index d435639..cffd68c 100644
--- a/src/com/android/settings/bluetooth/BluetoothFindBroadcastsFragment.java
+++ b/src/com/android/settings/bluetooth/BluetoothFindBroadcastsFragment.java
@@ -131,6 +131,10 @@
Log.w(TAG, "onSourceAdded: mSelectedPreference == null!");
return;
}
+ if (mLeBroadcastAssistant != null
+ && mLeBroadcastAssistant.isSearchInProgress()) {
+ mLeBroadcastAssistant.stopSearchingForSources();
+ }
getActivity().runOnUiThread(() -> updateListCategoryFromBroadcastMetadata(
mSelectedPreference.getBluetoothLeBroadcastMetadata(), true));
}
@@ -238,6 +242,9 @@
public void onStop() {
super.onStop();
if (mLeBroadcastAssistant != null) {
+ if (mLeBroadcastAssistant.isSearchInProgress()) {
+ mLeBroadcastAssistant.stopSearchingForSources();
+ }
mLeBroadcastAssistant.unregisterServiceCallBack(mBroadcastAssistantCallback);
}
}
diff --git a/src/com/android/settings/bluetooth/BluetoothPairingDetail.java b/src/com/android/settings/bluetooth/BluetoothPairingDetail.java
index a78bf27..234d6d2 100644
--- a/src/com/android/settings/bluetooth/BluetoothPairingDetail.java
+++ b/src/com/android/settings/bluetooth/BluetoothPairingDetail.java
@@ -101,10 +101,8 @@
if (bluetoothState == BluetoothAdapter.STATE_ON) {
if (mInitialScanStarted) {
// Don't show bonded devices when screen turned back on
- setFilter(BluetoothDeviceFilter.UNBONDED_DEVICE_FILTER);
- addCachedDevices();
+ addCachedDevices(BluetoothDeviceFilter.UNBONDED_DEVICE_FILTER);
}
- setFilter(BluetoothDeviceFilter.ALL_FILTER);
updateFooterPreference(mFooterPreference);
mAlwaysDiscoverable.start();
}
diff --git a/src/com/android/settings/bluetooth/DeviceListPreferenceFragment.java b/src/com/android/settings/bluetooth/DeviceListPreferenceFragment.java
deleted file mode 100644
index a4a9891..0000000
--- a/src/com/android/settings/bluetooth/DeviceListPreferenceFragment.java
+++ /dev/null
@@ -1,351 +0,0 @@
-/*
- * Copyright (C) 2011 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.settings.bluetooth;
-
-import android.bluetooth.BluetoothAdapter;
-import android.bluetooth.BluetoothDevice;
-import android.bluetooth.le.BluetoothLeScanner;
-import android.bluetooth.le.ScanCallback;
-import android.bluetooth.le.ScanFilter;
-import android.bluetooth.le.ScanResult;
-import android.bluetooth.le.ScanSettings;
-import android.os.Bundle;
-import android.os.SystemProperties;
-import android.text.BidiFormatter;
-import android.util.Log;
-
-import androidx.annotation.VisibleForTesting;
-import androidx.preference.Preference;
-import androidx.preference.PreferenceCategory;
-import androidx.preference.PreferenceGroup;
-
-import com.android.settings.R;
-import com.android.settings.dashboard.RestrictedDashboardFragment;
-import com.android.settingslib.bluetooth.BluetoothCallback;
-import com.android.settingslib.bluetooth.BluetoothDeviceFilter;
-import com.android.settingslib.bluetooth.CachedBluetoothDevice;
-import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager;
-import com.android.settingslib.bluetooth.LocalBluetoothManager;
-
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.HashMap;
-import java.util.List;
-
-/**
- * Parent class for settings fragments that contain a list of Bluetooth
- * devices.
- *
- * @see DevicePickerFragment
- */
-// TODO: Refactor this fragment
-public abstract class DeviceListPreferenceFragment extends
- RestrictedDashboardFragment implements BluetoothCallback {
-
- private static final String TAG = "DeviceListPreferenceFragment";
-
- private static final String KEY_BT_SCAN = "bt_scan";
-
- // Copied from BluetoothDeviceNoNamePreferenceController.java
- private static final String BLUETOOTH_SHOW_DEVICES_WITHOUT_NAMES_PROPERTY =
- "persist.bluetooth.showdeviceswithoutnames";
-
- private BluetoothDeviceFilter.Filter mFilter;
- private List<ScanFilter> mLeScanFilters;
- private ScanCallback mScanCallback;
-
- @VisibleForTesting
- protected boolean mScanEnabled;
-
- protected BluetoothDevice mSelectedDevice;
-
- protected BluetoothAdapter mBluetoothAdapter;
- protected LocalBluetoothManager mLocalManager;
- protected CachedBluetoothDeviceManager mCachedDeviceManager;
-
- @VisibleForTesting
- protected PreferenceGroup mDeviceListGroup;
-
- protected final HashMap<CachedBluetoothDevice, BluetoothDevicePreference> mDevicePreferenceMap =
- new HashMap<>();
- protected final List<BluetoothDevice> mSelectedList = new ArrayList<>();
-
- protected boolean mShowDevicesWithoutNames;
-
- public DeviceListPreferenceFragment(String restrictedKey) {
- super(restrictedKey);
- mFilter = BluetoothDeviceFilter.ALL_FILTER;
- }
-
- protected final void setFilter(BluetoothDeviceFilter.Filter filter) {
- mFilter = filter;
- }
-
- protected final void setFilter(int filterType) {
- mFilter = BluetoothDeviceFilter.getFilter(filterType);
- }
-
- /**
- * Sets the bluetooth device scanning filter with {@link ScanFilter}s. It will change to start
- * {@link BluetoothLeScanner} which will scan BLE device only.
- *
- * @param leScanFilters list of settings to filter scan result
- */
- protected void setFilter(List<ScanFilter> leScanFilters) {
- mFilter = null;
- mLeScanFilters = leScanFilters;
- }
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
-
- mLocalManager = Utils.getLocalBtManager(getActivity());
- if (mLocalManager == null) {
- Log.e(TAG, "Bluetooth is not supported on this device");
- return;
- }
- mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
- mCachedDeviceManager = mLocalManager.getCachedDeviceManager();
- mShowDevicesWithoutNames = SystemProperties.getBoolean(
- BLUETOOTH_SHOW_DEVICES_WITHOUT_NAMES_PROPERTY, false);
-
- initPreferencesFromPreferenceScreen();
-
- mDeviceListGroup = (PreferenceCategory) findPreference(getDeviceListKey());
- }
-
- /** find and update preference that already existed in preference screen */
- protected abstract void initPreferencesFromPreferenceScreen();
-
- @Override
- public void onStart() {
- super.onStart();
- if (mLocalManager == null || isUiRestricted()) return;
-
- mLocalManager.setForegroundActivity(getActivity());
- mLocalManager.getEventManager().registerCallback(this);
- }
-
- @Override
- public void onStop() {
- super.onStop();
- if (mLocalManager == null || isUiRestricted()) {
- return;
- }
-
- removeAllDevices();
- mLocalManager.setForegroundActivity(null);
- mLocalManager.getEventManager().unregisterCallback(this);
- }
-
- void removeAllDevices() {
- mDevicePreferenceMap.clear();
- mDeviceListGroup.removeAll();
- }
-
- void addCachedDevices() {
- Collection<CachedBluetoothDevice> cachedDevices =
- mCachedDeviceManager.getCachedDevicesCopy();
- for (CachedBluetoothDevice cachedDevice : cachedDevices) {
- onDeviceAdded(cachedDevice);
- }
- }
-
- @Override
- public boolean onPreferenceTreeClick(Preference preference) {
- if (KEY_BT_SCAN.equals(preference.getKey())) {
- startScanning();
- return true;
- }
-
- if (preference instanceof BluetoothDevicePreference) {
- BluetoothDevicePreference btPreference = (BluetoothDevicePreference) preference;
- CachedBluetoothDevice device = btPreference.getCachedDevice();
- mSelectedDevice = device.getDevice();
- mSelectedList.add(mSelectedDevice);
- onDevicePreferenceClick(btPreference);
- return true;
- }
-
- return super.onPreferenceTreeClick(preference);
- }
-
- protected void onDevicePreferenceClick(BluetoothDevicePreference btPreference) {
- btPreference.onClicked();
- }
-
- @Override
- public void onDeviceAdded(CachedBluetoothDevice cachedDevice) {
- if (mDevicePreferenceMap.get(cachedDevice) != null) {
- return;
- }
-
- // Prevent updates while the list shows one of the state messages
- if (mBluetoothAdapter.getState() != BluetoothAdapter.STATE_ON) {
- return;
- }
-
- if (mFilter != null && mFilter.matches(cachedDevice.getDevice())) {
- createDevicePreference(cachedDevice);
- }
- }
-
- void createDevicePreference(CachedBluetoothDevice cachedDevice) {
- if (mDeviceListGroup == null) {
- Log.w(TAG, "Trying to create a device preference before the list group/category "
- + "exists!");
- return;
- }
-
- String key = cachedDevice.getDevice().getAddress();
- BluetoothDevicePreference preference = (BluetoothDevicePreference) getCachedPreference(key);
-
- if (preference == null) {
- preference = new BluetoothDevicePreference(getPrefContext(), cachedDevice,
- mShowDevicesWithoutNames, BluetoothDevicePreference.SortType.TYPE_FIFO);
- preference.setKey(key);
- //Set hideSecondTarget is true if it's bonded device.
- preference.hideSecondTarget(true);
- mDeviceListGroup.addPreference(preference);
- }
-
- initDevicePreference(preference);
- mDevicePreferenceMap.put(cachedDevice, preference);
- }
-
- protected void initDevicePreference(BluetoothDevicePreference preference) {
- // Does nothing by default
- }
-
- @VisibleForTesting
- void updateFooterPreference(Preference myDevicePreference) {
- final BidiFormatter bidiFormatter = BidiFormatter.getInstance();
-
- myDevicePreference.setTitle(getString(
- R.string.bluetooth_footer_mac_message,
- bidiFormatter.unicodeWrap(mBluetoothAdapter.getAddress())));
- }
-
- @Override
- public void onDeviceDeleted(CachedBluetoothDevice cachedDevice) {
- BluetoothDevicePreference preference = mDevicePreferenceMap.remove(cachedDevice);
- if (preference != null) {
- mDeviceListGroup.removePreference(preference);
- }
- }
-
- @VisibleForTesting
- protected void enableScanning() {
- // BluetoothAdapter already handles repeated scan requests
- if (!mScanEnabled) {
- startScanning();
- mScanEnabled = true;
- }
- }
-
- @VisibleForTesting
- protected void disableScanning() {
- if (mScanEnabled) {
- stopScanning();
- mScanEnabled = false;
- }
- }
-
- @Override
- public void onScanningStateChanged(boolean started) {
- if (!started && mScanEnabled) {
- startScanning();
- }
- }
-
- /**
- * Return the key of the {@link PreferenceGroup} that contains the bluetooth devices
- */
- public abstract String getDeviceListKey();
-
- public boolean shouldShowDevicesWithoutNames() {
- return mShowDevicesWithoutNames;
- }
-
- @VisibleForTesting
- void startScanning() {
- if (mFilter != null) {
- startClassicScanning();
- } else if (mLeScanFilters != null) {
- startLeScanning();
- }
-
- }
-
- @VisibleForTesting
- void stopScanning() {
- if (mFilter != null) {
- stopClassicScanning();
- } else if (mLeScanFilters != null) {
- stopLeScanning();
- }
- }
-
- private void startClassicScanning() {
- if (!mBluetoothAdapter.isDiscovering()) {
- mBluetoothAdapter.startDiscovery();
- }
- }
-
- private void stopClassicScanning() {
- if (mBluetoothAdapter.isDiscovering()) {
- mBluetoothAdapter.cancelDiscovery();
- }
- }
-
- private void startLeScanning() {
- final BluetoothLeScanner scanner = mBluetoothAdapter.getBluetoothLeScanner();
- final ScanSettings settings = new ScanSettings.Builder()
- .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
- .build();
- mScanCallback = new ScanCallback() {
- @Override
- public void onScanResult(int callbackType, ScanResult result) {
- final BluetoothDevice device = result.getDevice();
- CachedBluetoothDevice cachedDevice = mCachedDeviceManager.findDevice(device);
- if (cachedDevice == null) {
- cachedDevice = mCachedDeviceManager.addDevice(device);
- }
- // Only add device preference when it's not found in the map and there's no other
- // state message showing in the list
- if (mDevicePreferenceMap.get(cachedDevice) == null
- && mBluetoothAdapter.getState() == BluetoothAdapter.STATE_ON) {
- createDevicePreference(cachedDevice);
- }
- }
-
- @Override
- public void onScanFailed(int errorCode) {
- Log.w(TAG, "BLE Scan failed with error code " + errorCode);
- }
- };
- scanner.startScan(mLeScanFilters, settings, mScanCallback);
- }
-
- private void stopLeScanning() {
- final BluetoothLeScanner scanner = mBluetoothAdapter.getBluetoothLeScanner();
- if (scanner != null) {
- scanner.stopScan(mScanCallback);
- }
- }
-}
diff --git a/src/com/android/settings/bluetooth/DeviceListPreferenceFragment.kt b/src/com/android/settings/bluetooth/DeviceListPreferenceFragment.kt
new file mode 100644
index 0000000..f18ae46
--- /dev/null
+++ b/src/com/android/settings/bluetooth/DeviceListPreferenceFragment.kt
@@ -0,0 +1,356 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.settings.bluetooth
+
+import android.bluetooth.BluetoothAdapter
+import android.bluetooth.BluetoothDevice
+import android.bluetooth.le.BluetoothLeScanner
+import android.bluetooth.le.ScanCallback
+import android.bluetooth.le.ScanFilter
+import android.bluetooth.le.ScanResult
+import android.bluetooth.le.ScanSettings
+import android.os.Bundle
+import android.os.SystemProperties
+import android.text.BidiFormatter
+import android.util.Log
+import android.view.View
+import androidx.annotation.VisibleForTesting
+import androidx.lifecycle.lifecycleScope
+import androidx.preference.Preference
+import androidx.preference.PreferenceCategory
+import androidx.preference.PreferenceGroup
+import com.android.settings.R
+import com.android.settings.dashboard.RestrictedDashboardFragment
+import com.android.settingslib.bluetooth.BluetoothCallback
+import com.android.settingslib.bluetooth.BluetoothDeviceFilter
+import com.android.settingslib.bluetooth.CachedBluetoothDevice
+import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager
+import com.android.settingslib.bluetooth.LocalBluetoothManager
+import java.util.concurrent.ConcurrentHashMap
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+
+/**
+ * Parent class for settings fragments that contain a list of Bluetooth devices.
+ *
+ * @see DevicePickerFragment
+ *
+ * TODO: Refactor this fragment
+ */
+abstract class DeviceListPreferenceFragment(restrictedKey: String?) :
+ RestrictedDashboardFragment(restrictedKey), BluetoothCallback {
+
+ private var filter: BluetoothDeviceFilter.Filter? = BluetoothDeviceFilter.ALL_FILTER
+ private var leScanFilters: List<ScanFilter>? = null
+
+ @JvmField
+ @VisibleForTesting
+ var mScanEnabled = false
+
+ @JvmField
+ var mSelectedDevice: BluetoothDevice? = null
+
+ @JvmField
+ var mBluetoothAdapter: BluetoothAdapter? = null
+
+ @JvmField
+ var mLocalManager: LocalBluetoothManager? = null
+
+ @JvmField
+ var mCachedDeviceManager: CachedBluetoothDeviceManager? = null
+
+ @JvmField
+ @VisibleForTesting
+ var mDeviceListGroup: PreferenceGroup? = null
+
+ @VisibleForTesting
+ val devicePreferenceMap =
+ ConcurrentHashMap<CachedBluetoothDevice, BluetoothDevicePreference>()
+
+ @JvmField
+ val mSelectedList: MutableList<BluetoothDevice> = ArrayList()
+
+ @VisibleForTesting
+ var lifecycleScope: CoroutineScope? = null
+
+ private var showDevicesWithoutNames = false
+
+ protected fun setFilter(filterType: Int) {
+ filter = BluetoothDeviceFilter.getFilter(filterType)
+ }
+
+ /**
+ * Sets the bluetooth device scanning filter with [ScanFilter]s. It will change to start
+ * [BluetoothLeScanner] which will scan BLE device only.
+ *
+ * @param leScanFilters list of settings to filter scan result
+ */
+ fun setFilter(leScanFilters: List<ScanFilter>?) {
+ filter = null
+ this.leScanFilters = leScanFilters
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ mLocalManager = Utils.getLocalBtManager(activity)
+ if (mLocalManager == null) {
+ Log.e(TAG, "Bluetooth is not supported on this device")
+ return
+ }
+ mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter()
+ mCachedDeviceManager = mLocalManager!!.cachedDeviceManager
+ showDevicesWithoutNames = SystemProperties.getBoolean(
+ BLUETOOTH_SHOW_DEVICES_WITHOUT_NAMES_PROPERTY, false
+ )
+ initPreferencesFromPreferenceScreen()
+ mDeviceListGroup = findPreference<Preference>(deviceListKey) as PreferenceCategory
+ }
+
+ /** find and update preference that already existed in preference screen */
+ protected abstract fun initPreferencesFromPreferenceScreen()
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ lifecycleScope = viewLifecycleOwner.lifecycleScope
+ }
+
+ override fun onStart() {
+ super.onStart()
+ if (mLocalManager == null || isUiRestricted) return
+ mLocalManager!!.foregroundActivity = activity
+ mLocalManager!!.eventManager.registerCallback(this)
+ }
+
+ override fun onStop() {
+ super.onStop()
+ if (mLocalManager == null || isUiRestricted) {
+ return
+ }
+ removeAllDevices()
+ mLocalManager!!.foregroundActivity = null
+ mLocalManager!!.eventManager.unregisterCallback(this)
+ }
+
+ fun removeAllDevices() {
+ devicePreferenceMap.clear()
+ mDeviceListGroup!!.removeAll()
+ }
+
+ @JvmOverloads
+ fun addCachedDevices(filterForCachedDevices: BluetoothDeviceFilter.Filter? = null) {
+ lifecycleScope?.launch {
+ withContext(Dispatchers.Default) {
+ mCachedDeviceManager!!.cachedDevicesCopy
+ .filter {
+ filterForCachedDevices == null || filterForCachedDevices.matches(it.device)
+ }
+ .forEach(::onDeviceAdded)
+ }
+ }
+ }
+
+ override fun onPreferenceTreeClick(preference: Preference): Boolean {
+ if (KEY_BT_SCAN == preference.key) {
+ startScanning()
+ return true
+ }
+ if (preference is BluetoothDevicePreference) {
+ val device = preference.cachedDevice.device
+ mSelectedDevice = device
+ mSelectedList.add(device)
+ onDevicePreferenceClick(preference)
+ return true
+ }
+ return super.onPreferenceTreeClick(preference)
+ }
+
+ protected open fun onDevicePreferenceClick(btPreference: BluetoothDevicePreference) {
+ btPreference.onClicked()
+ }
+
+ override fun onDeviceAdded(cachedDevice: CachedBluetoothDevice) {
+ lifecycleScope?.launch {
+ addDevice(cachedDevice)
+ }
+ }
+
+ private suspend fun addDevice(cachedDevice: CachedBluetoothDevice) =
+ withContext(Dispatchers.Default) {
+ // TODO(b/289189853): Replace checking if `filter` is null or not to decide which type
+ // of Bluetooth scanning method will be used
+ val filterMatched = filter == null || filter!!.matches(cachedDevice.device) == true
+ // Prevent updates while the list shows one of the state messages
+ if (mBluetoothAdapter!!.state == BluetoothAdapter.STATE_ON && filterMatched) {
+ createDevicePreference(cachedDevice)
+ }
+ }
+
+ private suspend fun createDevicePreference(cachedDevice: CachedBluetoothDevice) {
+ if (mDeviceListGroup == null) {
+ Log.w(
+ TAG,
+ "Trying to create a device preference before the list group/category exists!",
+ )
+ return
+ }
+ // Only add device preference when it's not found in the map and there's no other state
+ // message showing in the list
+ val preference = devicePreferenceMap.computeIfAbsent(cachedDevice) {
+ BluetoothDevicePreference(
+ prefContext,
+ cachedDevice,
+ showDevicesWithoutNames,
+ BluetoothDevicePreference.SortType.TYPE_FIFO,
+ ).apply {
+ key = cachedDevice.device.address
+ //Set hideSecondTarget is true if it's bonded device.
+ hideSecondTarget(true)
+ }
+ }
+ withContext(Dispatchers.Main) {
+ mDeviceListGroup!!.addPreference(preference)
+ initDevicePreference(preference)
+ }
+ }
+
+ protected open fun initDevicePreference(preference: BluetoothDevicePreference?) {
+ // Does nothing by default
+ }
+
+ @VisibleForTesting
+ fun updateFooterPreference(myDevicePreference: Preference) {
+ val bidiFormatter = BidiFormatter.getInstance()
+ myDevicePreference.title = getString(
+ R.string.bluetooth_footer_mac_message,
+ bidiFormatter.unicodeWrap(mBluetoothAdapter!!.address)
+ )
+ }
+
+ override fun onDeviceDeleted(cachedDevice: CachedBluetoothDevice) {
+ devicePreferenceMap.remove(cachedDevice)?.let {
+ mDeviceListGroup!!.removePreference(it)
+ }
+ }
+
+ @VisibleForTesting
+ open fun enableScanning() {
+ // BluetoothAdapter already handles repeated scan requests
+ if (!mScanEnabled) {
+ startScanning()
+ mScanEnabled = true
+ }
+ }
+
+ @VisibleForTesting
+ fun disableScanning() {
+ if (mScanEnabled) {
+ stopScanning()
+ mScanEnabled = false
+ }
+ }
+
+ override fun onScanningStateChanged(started: Boolean) {
+ if (!started && mScanEnabled) {
+ startScanning()
+ }
+ }
+
+ /**
+ * Return the key of the [PreferenceGroup] that contains the bluetooth devices
+ */
+ abstract val deviceListKey: String
+
+ @VisibleForTesting
+ open fun startScanning() {
+ if (filter != null) {
+ startClassicScanning()
+ } else if (leScanFilters != null) {
+ startLeScanning()
+ }
+ }
+
+ @VisibleForTesting
+ open fun stopScanning() {
+ if (filter != null) {
+ stopClassicScanning()
+ } else if (leScanFilters != null) {
+ stopLeScanning()
+ }
+ }
+
+ private fun startClassicScanning() {
+ if (!mBluetoothAdapter!!.isDiscovering) {
+ mBluetoothAdapter!!.startDiscovery()
+ }
+ }
+
+ private fun stopClassicScanning() {
+ if (mBluetoothAdapter!!.isDiscovering) {
+ mBluetoothAdapter!!.cancelDiscovery()
+ }
+ }
+
+ private val leScanCallback = object : ScanCallback() {
+ override fun onScanResult(callbackType: Int, result: ScanResult) {
+ handleLeScanResult(result)
+ }
+
+ override fun onBatchScanResults(results: MutableList<ScanResult>?) {
+ for (result in results.orEmpty()) {
+ handleLeScanResult(result)
+ }
+ }
+
+ override fun onScanFailed(errorCode: Int) {
+ Log.w(TAG, "BLE Scan failed with error code $errorCode")
+ }
+ }
+
+ private fun startLeScanning() {
+ val scanner = mBluetoothAdapter!!.bluetoothLeScanner
+ val settings = ScanSettings.Builder()
+ .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
+ .build()
+ scanner.startScan(leScanFilters, settings, leScanCallback)
+ }
+
+ private fun stopLeScanning() {
+ val scanner = mBluetoothAdapter!!.bluetoothLeScanner
+ scanner?.stopScan(leScanCallback)
+ }
+
+ private fun handleLeScanResult(result: ScanResult) {
+ lifecycleScope?.launch {
+ withContext(Dispatchers.Default) {
+ val device = result.device
+ val cachedDevice = mCachedDeviceManager!!.findDevice(device)
+ ?: mCachedDeviceManager!!.addDevice(device, leScanFilters)
+ addDevice(cachedDevice)
+ }
+ }
+ }
+
+ companion object {
+ private const val TAG = "DeviceListPreferenceFragment"
+ private const val KEY_BT_SCAN = "bt_scan"
+
+ // Copied from BluetoothDeviceNoNamePreferenceController.java
+ private const val BLUETOOTH_SHOW_DEVICES_WITHOUT_NAMES_PROPERTY =
+ "persist.bluetooth.showdeviceswithoutnames"
+ }
+}
diff --git a/src/com/android/settings/bluetooth/ForgetDeviceDialogFragment.java b/src/com/android/settings/bluetooth/ForgetDeviceDialogFragment.java
index 1da8672..60d63c6 100644
--- a/src/com/android/settings/bluetooth/ForgetDeviceDialogFragment.java
+++ b/src/com/android/settings/bluetooth/ForgetDeviceDialogFragment.java
@@ -23,6 +23,7 @@
import android.content.Context;
import android.content.DialogInterface;
import android.os.Bundle;
+import android.util.Log;
import androidx.annotation.VisibleForTesting;
import androidx.appcompat.app.AlertDialog;
@@ -63,6 +64,13 @@
@Override
public Dialog onCreateDialog(Bundle inState) {
+ Context context = getContext();
+ mDevice = getDevice(context);
+ if (mDevice == null) {
+ Log.e(TAG, "onCreateDialog: Device is null.");
+ return null;
+ }
+
DialogInterface.OnClickListener onConfirm = (dialog, which) -> {
mDevice.unpair();
Activity activity = getActivity();
@@ -70,9 +78,6 @@
activity.finish();
}
};
- Context context = getContext();
- mDevice = getDevice(context);
-
AlertDialog dialog = new AlertDialog.Builder(context)
.setPositiveButton(R.string.bluetooth_unpair_dialog_forget_confirm_button,
onConfirm)
diff --git a/src/com/android/settings/bluetooth/LeAudioBluetoothDetailsHeaderController.java b/src/com/android/settings/bluetooth/LeAudioBluetoothDetailsHeaderController.java
index e30bbfb..f72494f 100644
--- a/src/com/android/settings/bluetooth/LeAudioBluetoothDetailsHeaderController.java
+++ b/src/com/android/settings/bluetooth/LeAudioBluetoothDetailsHeaderController.java
@@ -88,6 +88,7 @@
@VisibleForTesting
LayoutPreference mLayoutPreference;
+ LocalBluetoothManager mManager;
private CachedBluetoothDevice mCachedDevice;
private List<CachedBluetoothDevice> mAllOfCachedDevices;
@VisibleForTesting
@@ -152,8 +153,9 @@
public void init(CachedBluetoothDevice cachedBluetoothDevice,
LocalBluetoothManager bluetoothManager) {
mCachedDevice = cachedBluetoothDevice;
+ mManager = bluetoothManager;
mProfileManager = bluetoothManager.getProfileManager();
- mAllOfCachedDevices = Utils.getAllOfCachedBluetoothDevices(mContext, mCachedDevice);
+ mAllOfCachedDevices = Utils.getAllOfCachedBluetoothDevices(mManager, mCachedDevice);
}
@VisibleForTesting
@@ -300,7 +302,7 @@
for (CachedBluetoothDevice item : mAllOfCachedDevices) {
item.unregisterCallback(this);
}
- mAllOfCachedDevices = Utils.getAllOfCachedBluetoothDevices(mContext, mCachedDevice);
+ mAllOfCachedDevices = Utils.getAllOfCachedBluetoothDevices(mManager, mCachedDevice);
for (CachedBluetoothDevice item : mAllOfCachedDevices) {
item.registerCallback(this);
}
diff --git a/src/com/android/settings/bluetooth/QrCodeScanModeActivity.java b/src/com/android/settings/bluetooth/QrCodeScanModeActivity.java
index 92786c9..a0b249d 100644
--- a/src/com/android/settings/bluetooth/QrCodeScanModeActivity.java
+++ b/src/com/android/settings/bluetooth/QrCodeScanModeActivity.java
@@ -98,7 +98,7 @@
BluetoothBroadcastUtils.TAG_FRAGMENT_QR_CODE_SCANNER);
if (fragment == null) {
- fragment = new QrCodeScanModeFragment(mIsGroupOp, mSink);
+ fragment = new QrCodeScanModeFragment();
} else {
if (fragment.isVisible()) {
return;
diff --git a/src/com/android/settings/bluetooth/QrCodeScanModeFragment.java b/src/com/android/settings/bluetooth/QrCodeScanModeFragment.java
index f89dac6..80aedd7 100644
--- a/src/com/android/settings/bluetooth/QrCodeScanModeFragment.java
+++ b/src/com/android/settings/bluetooth/QrCodeScanModeFragment.java
@@ -18,7 +18,6 @@
import android.app.Activity;
import android.app.settings.SettingsEnums;
-import android.bluetooth.BluetoothDevice;
import android.content.Context;
import android.content.Intent;
import android.graphics.Matrix;
@@ -71,9 +70,7 @@
public static final String KEY_BROADCAST_METADATA = "key_broadcast_metadata";
- private boolean mIsGroupOp;
private int mCornerRadius;
- private BluetoothDevice mSink;
private String mBroadcastMetadata;
private Context mContext;
private QrCamera mCamera;
@@ -81,11 +78,6 @@
private TextView mSummary;
private TextView mErrorMessage;
- public QrCodeScanModeFragment(boolean isGroupOp, BluetoothDevice sink) {
- mIsGroupOp = isGroupOp;
- mSink = sink;
- }
-
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
diff --git a/src/com/android/settings/bluetooth/Utils.java b/src/com/android/settings/bluetooth/Utils.java
index 79a2de0..f1d6b20 100644
--- a/src/com/android/settings/bluetooth/Utils.java
+++ b/src/com/android/settings/bluetooth/Utils.java
@@ -235,7 +235,8 @@
* @param cachedBluetoothDevice The main cachedBluetoothDevice.
* @return all cachedBluetoothDevices with the same groupId.
*/
- public static List<CachedBluetoothDevice> getAllOfCachedBluetoothDevices(Context context,
+ public static List<CachedBluetoothDevice> getAllOfCachedBluetoothDevices(
+ LocalBluetoothManager localBtMgr,
CachedBluetoothDevice cachedBluetoothDevice) {
List<CachedBluetoothDevice> cachedBluetoothDevices = new ArrayList<>();
if (cachedBluetoothDevice == null) {
@@ -248,7 +249,6 @@
return cachedBluetoothDevices;
}
- final LocalBluetoothManager localBtMgr = Utils.getLocalBtManager(context);
if (localBtMgr == null) {
Log.e(TAG, "getAllOfCachedBluetoothDevices: no LocalBluetoothManager");
return cachedBluetoothDevices;
diff --git a/src/com/android/settings/connecteddevice/stylus/StylusDevicesController.java b/src/com/android/settings/connecteddevice/stylus/StylusDevicesController.java
index c93a1c6..985c8b7 100644
--- a/src/com/android/settings/connecteddevice/stylus/StylusDevicesController.java
+++ b/src/com/android/settings/connecteddevice/stylus/StylusDevicesController.java
@@ -16,12 +16,17 @@
package com.android.settings.connecteddevice.stylus;
+import android.app.Dialog;
import android.app.role.RoleManager;
import android.bluetooth.BluetoothDevice;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
+import android.content.pm.UserInfo;
+import android.os.Process;
+import android.os.UserHandle;
+import android.os.UserManager;
import android.provider.Settings;
import android.provider.Settings.Secure;
import android.text.TextUtils;
@@ -38,6 +43,9 @@
import androidx.preference.SwitchPreference;
import com.android.settings.R;
+import com.android.settings.dashboard.profileselector.ProfileSelectDialog;
+import com.android.settings.dashboard.profileselector.UserAdapter;
+import com.android.settingslib.PrimarySwitchPreference;
import com.android.settingslib.bluetooth.BluetoothUtils;
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
import com.android.settingslib.core.AbstractPreferenceController;
@@ -45,13 +53,15 @@
import com.android.settingslib.core.lifecycle.LifecycleObserver;
import com.android.settingslib.core.lifecycle.events.OnResume;
+import java.util.ArrayList;
import java.util.List;
/**
* This class adds stylus preferences.
*/
public class StylusDevicesController extends AbstractPreferenceController implements
- Preference.OnPreferenceClickListener, LifecycleObserver, OnResume {
+ Preference.OnPreferenceClickListener, Preference.OnPreferenceChangeListener,
+ LifecycleObserver, OnResume {
@VisibleForTesting
static final String KEY_STYLUS = "device_stylus";
@@ -73,6 +83,9 @@
@VisibleForTesting
PreferenceCategory mPreferencesContainer;
+ @VisibleForTesting
+ Dialog mDialog;
+
public StylusDevicesController(Context context, InputDevice inputDevice,
CachedBluetoothDevice cachedBluetoothDevice, Lifecycle lifecycle) {
super(context);
@@ -100,8 +113,8 @@
pref.setOnPreferenceClickListener(this);
pref.setEnabled(true);
- List<String> roleHolders = rm.getRoleHoldersAsUser(RoleManager.ROLE_NOTES,
- mContext.getUser());
+ UserHandle user = getDefaultNoteTaskProfile();
+ List<String> roleHolders = rm.getRoleHoldersAsUser(RoleManager.ROLE_NOTES, user);
if (roleHolders.isEmpty()) {
pref.setSummary(R.string.default_app_none);
return pref;
@@ -113,19 +126,29 @@
try {
ApplicationInfo ai = pm.getApplicationInfo(packageName,
PackageManager.ApplicationInfoFlags.of(0));
- appName = ai == null ? packageName : pm.getApplicationLabel(ai).toString();
+ appName = ai == null ? "" : pm.getApplicationLabel(ai).toString();
} catch (PackageManager.NameNotFoundException e) {
Log.e(TAG, "Notes role package not found.");
}
- pref.setSummary(appName);
+
+ if (mContext.getSystemService(UserManager.class).isManagedProfile(user.getIdentifier())) {
+ pref.setSummary(
+ mContext.getString(R.string.stylus_default_notes_summary_work, appName));
+ } else {
+ pref.setSummary(appName);
+ }
return pref;
}
- private SwitchPreference createOrUpdateHandwritingPreference(SwitchPreference preference) {
- SwitchPreference pref = preference == null ? new SwitchPreference(mContext) : preference;
+ private PrimarySwitchPreference createOrUpdateHandwritingPreference(
+ PrimarySwitchPreference preference) {
+ PrimarySwitchPreference pref = preference == null ? new PrimarySwitchPreference(mContext)
+ : preference;
pref.setKey(KEY_HANDWRITING);
pref.setTitle(mContext.getString(R.string.stylus_textfield_handwriting));
pref.setIcon(R.drawable.ic_text_fields_alt);
+ // Using a two-target preference, clicking will send an intent and change will toggle.
+ pref.setOnPreferenceChangeListener(this);
pref.setOnPreferenceClickListener(this);
pref.setChecked(Settings.Secure.getInt(mContext.getContentResolver(),
Settings.Secure.STYLUS_HANDWRITING_ENABLED,
@@ -148,30 +171,28 @@
@Override
public boolean onPreferenceClick(Preference preference) {
String key = preference.getKey();
-
switch (key) {
case KEY_DEFAULT_NOTES:
PackageManager pm = mContext.getPackageManager();
String packageName = pm.getPermissionControllerPackageName();
Intent intent = new Intent(Intent.ACTION_MANAGE_DEFAULT_APP).setPackage(
packageName).putExtra(Intent.EXTRA_ROLE_NAME, RoleManager.ROLE_NOTES);
- mContext.startActivity(intent);
+
+ List<UserHandle> users = getUserAndManagedProfiles();
+ if (users.size() <= 1) {
+ mContext.startActivity(intent);
+ } else {
+ createAndShowProfileSelectDialog(intent, users);
+ }
break;
case KEY_HANDWRITING:
- Settings.Secure.putInt(mContext.getContentResolver(),
- Settings.Secure.STYLUS_HANDWRITING_ENABLED,
- ((SwitchPreference) preference).isChecked() ? 1 : 0);
-
- if (((SwitchPreference) preference).isChecked()) {
- InputMethodManager imm = mContext.getSystemService(InputMethodManager.class);
- InputMethodInfo inputMethod = imm.getCurrentInputMethodInfo();
- if (inputMethod == null) break;
-
- Intent handwritingIntent =
- inputMethod.createStylusHandwritingSettingsActivityIntent();
- if (handwritingIntent != null) {
- mContext.startActivity(handwritingIntent);
- }
+ InputMethodManager imm = mContext.getSystemService(InputMethodManager.class);
+ InputMethodInfo inputMethod = imm.getCurrentInputMethodInfo();
+ if (inputMethod == null) break;
+ Intent handwritingIntent =
+ inputMethod.createStylusHandwritingSettingsActivityIntent();
+ if (handwritingIntent != null) {
+ mContext.startActivity(handwritingIntent);
}
break;
case KEY_IGNORE_BUTTON:
@@ -184,6 +205,19 @@
}
@Override
+ public boolean onPreferenceChange(Preference preference, Object newValue) {
+ String key = preference.getKey();
+ switch (key) {
+ case KEY_HANDWRITING:
+ Settings.Secure.putInt(mContext.getContentResolver(),
+ Settings.Secure.STYLUS_HANDWRITING_ENABLED,
+ (boolean) newValue ? 1 : 0);
+ break;
+ }
+ return true;
+ }
+
+ @Override
public final void displayPreference(PreferenceScreen screen) {
mPreferencesContainer = (PreferenceCategory) screen.findPreference(getPreferenceKey());
super.displayPreference(screen);
@@ -210,7 +244,7 @@
mPreferencesContainer.addPreference(notesPref);
}
- SwitchPreference currHandwritingPref = mPreferencesContainer.findPreference(
+ PrimarySwitchPreference currHandwritingPref = mPreferencesContainer.findPreference(
KEY_HANDWRITING);
Preference handwritingPref = createOrUpdateHandwritingPreference(currHandwritingPref);
if (currHandwritingPref == null) {
@@ -229,6 +263,56 @@
return inputMethod != null && inputMethod.supportsStylusHandwriting();
}
+ private List<UserHandle> getUserAndManagedProfiles() {
+ UserManager um = mContext.getSystemService(UserManager.class);
+ final List<UserHandle> userManagedProfiles = new ArrayList<>();
+ // Add the current user, then add all the associated managed profiles.
+ final UserHandle currentUser = Process.myUserHandle();
+ userManagedProfiles.add(currentUser);
+
+ final List<UserInfo> userInfos = um.getUsers();
+ for (UserInfo info : userInfos) {
+ int userId = info.id;
+ if (um.isManagedProfile(userId)
+ && um.getProfileParent(userId).id == currentUser.getIdentifier()) {
+ userManagedProfiles.add(UserHandle.of(userId));
+ }
+ }
+ return userManagedProfiles;
+ }
+
+ private UserHandle getDefaultNoteTaskProfile() {
+ final int userId = Secure.getInt(
+ mContext.getContentResolver(),
+ Secure.DEFAULT_NOTE_TASK_PROFILE,
+ UserHandle.myUserId());
+ return UserHandle.of(userId);
+ }
+
+ @VisibleForTesting
+ UserAdapter.OnClickListener createProfileDialogClickCallback(
+ Intent intent, List<UserHandle> users) {
+ // TODO(b/281659827): improve UX flow for when activity is cancelled
+ return (int position) -> {
+ intent.putExtra(Intent.EXTRA_USER, users.get(position));
+
+ Secure.putInt(mContext.getContentResolver(),
+ Secure.DEFAULT_NOTE_TASK_PROFILE,
+ users.get(position).getIdentifier());
+ mContext.startActivity(intent);
+
+ mDialog.dismiss();
+ };
+ }
+
+ private void createAndShowProfileSelectDialog(Intent intent, List<UserHandle> users) {
+ mDialog = ProfileSelectDialog.createDialog(
+ mContext,
+ users,
+ createProfileDialogClickCallback(intent, users));
+ mDialog.show();
+ }
+
/**
* Identifies whether a device is a stylus using the associated {@link InputDevice} or
* {@link CachedBluetoothDevice}.
@@ -255,5 +339,4 @@
return false;
}
-
}
diff --git a/src/com/android/settings/connecteddevice/stylus/StylusFeatureProvider.java b/src/com/android/settings/connecteddevice/stylus/StylusFeatureProvider.java
new file mode 100644
index 0000000..7ca35d8
--- /dev/null
+++ b/src/com/android/settings/connecteddevice/stylus/StylusFeatureProvider.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.connecteddevice.stylus;
+
+import android.content.Context;
+import android.hardware.usb.UsbDevice;
+
+import androidx.preference.Preference;
+
+import java.util.List;
+
+import javax.annotation.Nullable;
+
+/** FeatureProvider for USB settings */
+public interface StylusFeatureProvider {
+
+ /**
+ * Returns whether the current attached USB device allows firmware updates.
+ *
+ * @param usbDevice The USB device to check
+ */
+ boolean isUsbFirmwareUpdateEnabled(UsbDevice usbDevice);
+
+ /**
+ * Returns a list of preferences for the connected USB device if exists. If not, returns
+ * null. If an update is not available but firmware update feature is enabled for the device,
+ * the list will contain only the preference showing the current firmware version.
+ *
+ * @param context The context
+ * @param usbDevice The USB device for which to generate preferences.
+ */
+ @Nullable
+ List<Preference> getUsbFirmwareUpdatePreferences(Context context, UsbDevice usbDevice);
+}
diff --git a/src/com/android/settings/connecteddevice/stylus/StylusFeatureProviderImpl.java b/src/com/android/settings/connecteddevice/stylus/StylusFeatureProviderImpl.java
new file mode 100644
index 0000000..be5ae40
--- /dev/null
+++ b/src/com/android/settings/connecteddevice/stylus/StylusFeatureProviderImpl.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.connecteddevice.stylus;
+
+import android.content.Context;
+import android.hardware.usb.UsbDevice;
+
+import androidx.preference.Preference;
+
+import java.util.List;
+
+/** Default implementation for StylusFeatureProvider */
+public class StylusFeatureProviderImpl implements StylusFeatureProvider {
+ @Override
+ public boolean isUsbFirmwareUpdateEnabled(UsbDevice usbDevice) {
+ return false;
+ }
+
+ @Override
+ public List<Preference> getUsbFirmwareUpdatePreferences(Context context, UsbDevice usbDevice) {
+ return null;
+ }
+}
diff --git a/src/com/android/settings/connecteddevice/stylus/StylusUsbFirmwareController.java b/src/com/android/settings/connecteddevice/stylus/StylusUsbFirmwareController.java
new file mode 100644
index 0000000..9c567a4
--- /dev/null
+++ b/src/com/android/settings/connecteddevice/stylus/StylusUsbFirmwareController.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.connecteddevice.stylus;
+
+import android.content.Context;
+import android.hardware.usb.UsbDevice;
+import android.hardware.usb.UsbManager;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+import androidx.preference.Preference;
+import androidx.preference.PreferenceCategory;
+import androidx.preference.PreferenceScreen;
+
+import com.android.settings.core.BasePreferenceController;
+import com.android.settings.overlay.FeatureFactory;
+import com.android.settingslib.core.lifecycle.LifecycleObserver;
+import com.android.settingslib.core.lifecycle.events.OnStart;
+import com.android.settingslib.core.lifecycle.events.OnStop;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/** Preference controller for stylus firmware updates via USB */
+public class StylusUsbFirmwareController extends BasePreferenceController
+ implements LifecycleObserver, OnStart, OnStop {
+ private static final String TAG = StylusUsbFirmwareController.class.getSimpleName();
+ @Nullable
+ private UsbDevice mStylusUsbDevice;
+ private final UsbStylusBroadcastReceiver mUsbStylusBroadcastReceiver;
+
+ private PreferenceScreen mPreferenceScreen;
+ private PreferenceCategory mPreference;
+
+ @VisibleForTesting
+ UsbStylusBroadcastReceiver.UsbStylusConnectionListener mUsbConnectionListener =
+ (stylusUsbDevice, attached) -> {
+ refresh();
+ };
+
+ public StylusUsbFirmwareController(Context context, String key) {
+ super(context, key);
+ mUsbStylusBroadcastReceiver = new UsbStylusBroadcastReceiver(context,
+ mUsbConnectionListener);
+ }
+
+ @Override
+ public void displayPreference(PreferenceScreen screen) {
+ mPreferenceScreen = screen;
+ refresh();
+ super.displayPreference(screen);
+ }
+
+ @Override
+ public int getAvailabilityStatus() {
+ // always available, preferences will be added or
+ // removed according to the connected usb device
+ return AVAILABLE;
+ }
+
+ private void refresh() {
+ if (mPreferenceScreen == null) return;
+
+ UsbDevice device = getStylusUsbDevice();
+ if (device == mStylusUsbDevice) {
+ return;
+ }
+ mStylusUsbDevice = device;
+ mPreference = mPreferenceScreen.findPreference(getPreferenceKey());
+ if (mPreference != null) {
+ mPreferenceScreen.removePreference(mPreference);
+ }
+ if (hasUsbStylusFirmwareUpdateFeature(mStylusUsbDevice)) {
+ StylusFeatureProvider featureProvider = FeatureFactory.getFactory(
+ mContext).getStylusFeatureProvider();
+ List<Preference> preferences =
+ featureProvider.getUsbFirmwareUpdatePreferences(mContext, mStylusUsbDevice);
+
+ if (preferences != null) {
+ mPreference = new PreferenceCategory(mContext);
+ mPreference.setKey(getPreferenceKey());
+ mPreferenceScreen.addPreference(mPreference);
+
+ for (Preference preference : preferences) {
+ mPreference.addPreference(preference);
+ }
+ }
+ }
+ }
+
+ @Override
+ public void onStart() {
+ mUsbStylusBroadcastReceiver.register();
+ }
+
+ @Override
+ public void onStop() {
+ mUsbStylusBroadcastReceiver.unregister();
+ }
+
+ private UsbDevice getStylusUsbDevice() {
+ UsbManager usbManager = mContext.getSystemService(UsbManager.class);
+
+ if (usbManager == null) {
+ return null;
+ }
+
+ List<UsbDevice> devices = new ArrayList<>(usbManager.getDeviceList().values());
+ if (devices.isEmpty()) {
+ return null;
+ }
+
+ UsbDevice usbDevice = devices.get(0);
+ if (hasUsbStylusFirmwareUpdateFeature(usbDevice)) {
+ return usbDevice;
+ }
+ return null;
+ }
+
+ static boolean hasUsbStylusFirmwareUpdateFeature(UsbDevice usbDevice) {
+ if (usbDevice == null) return false;
+
+ StylusFeatureProvider featureProvider = FeatureFactory.getFactory(
+ FeatureFactory.getAppContext()).getStylusFeatureProvider();
+
+ return featureProvider.isUsbFirmwareUpdateEnabled(usbDevice);
+ }
+}
diff --git a/src/com/android/settings/connecteddevice/stylus/StylusUsiDetailsFragment.java b/src/com/android/settings/connecteddevice/stylus/StylusUsiDetailsFragment.java
index 5e68a53..ea9781e 100644
--- a/src/com/android/settings/connecteddevice/stylus/StylusUsiDetailsFragment.java
+++ b/src/com/android/settings/connecteddevice/stylus/StylusUsiDetailsFragment.java
@@ -54,7 +54,6 @@
}
}
-
@Override
public int getMetricsCategory() {
return SettingsEnums.USI_DEVICE_DETAILS;
diff --git a/src/com/android/settings/connecteddevice/stylus/UsbStylusBroadcastReceiver.java b/src/com/android/settings/connecteddevice/stylus/UsbStylusBroadcastReceiver.java
new file mode 100644
index 0000000..41d88d2
--- /dev/null
+++ b/src/com/android/settings/connecteddevice/stylus/UsbStylusBroadcastReceiver.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.connecteddevice.stylus;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.hardware.usb.UsbDevice;
+import android.hardware.usb.UsbManager;
+
+/** Broadcast receiver for styluses connected via USB */
+public class UsbStylusBroadcastReceiver extends BroadcastReceiver {
+ private Context mContext;
+ private UsbStylusConnectionListener mUsbConnectionListener;
+ private boolean mListeningToUsbEvents;
+
+ public UsbStylusBroadcastReceiver(Context context,
+ UsbStylusConnectionListener usbConnectionListener) {
+ mContext = context;
+ mUsbConnectionListener = usbConnectionListener;
+ }
+
+ /** Registers the receiver. */
+ public void register() {
+ if (!mListeningToUsbEvents) {
+ final IntentFilter intentFilter = new IntentFilter();
+ intentFilter.addAction(UsbManager.ACTION_USB_DEVICE_ATTACHED);
+ intentFilter.addAction(UsbManager.ACTION_USB_DEVICE_DETACHED);
+ intentFilter.addAction(UsbManager.ACTION_USB_STATE);
+ final Intent intent = mContext.registerReceiver(this, intentFilter);
+ if (intent != null) {
+ onReceive(mContext, intent);
+ }
+ mListeningToUsbEvents = true;
+ }
+ }
+
+ /** Unregisters the receiver. */
+ public void unregister() {
+ if (mListeningToUsbEvents) {
+ mContext.unregisterReceiver(this);
+ mListeningToUsbEvents = false;
+ }
+ }
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ UsbDevice usbDevice = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE, UsbDevice.class);
+ if (StylusUsbFirmwareController.hasUsbStylusFirmwareUpdateFeature(usbDevice)) {
+ mUsbConnectionListener.onUsbStylusConnectionChanged(usbDevice,
+ intent.getAction().equals(UsbManager.ACTION_USB_DEVICE_ATTACHED));
+ }
+ }
+
+ /**
+ * Interface definition for a callback to be invoked when stylus usb connection is changed.
+ */
+ interface UsbStylusConnectionListener {
+ void onUsbStylusConnectionChanged(UsbDevice device, boolean connected);
+ }
+}
diff --git a/src/com/android/settings/core/SettingsUIDeviceConfig.java b/src/com/android/settings/core/SettingsUIDeviceConfig.java
index 404b0b4..94074df 100644
--- a/src/com/android/settings/core/SettingsUIDeviceConfig.java
+++ b/src/com/android/settings/core/SettingsUIDeviceConfig.java
@@ -42,9 +42,4 @@
* {@code true} whether or not event_log for generic actions is enabled. Default is true.
*/
public static final String GENERIC_EVENT_LOGGING_ENABLED = "event_logging_enabled";
- /**
- * {@code true} whether to show LE Audio toggle in device detail page. Default is false.
- */
- public static final String BT_LE_AUDIO_DEVICE_DETAIL_ENABLED =
- "bt_le_audio_device_detail_enabled";
}
diff --git a/src/com/android/settings/core/gateway/SettingsGateway.java b/src/com/android/settings/core/gateway/SettingsGateway.java
index ff4b47f..7c8a3cc 100644
--- a/src/com/android/settings/core/gateway/SettingsGateway.java
+++ b/src/com/android/settings/core/gateway/SettingsGateway.java
@@ -42,6 +42,7 @@
import com.android.settings.applications.ProcessStatsSummary;
import com.android.settings.applications.ProcessStatsUi;
import com.android.settings.applications.UsageAccessDetails;
+import com.android.settings.applications.appcompat.UserAspectRatioDetails;
import com.android.settings.applications.appinfo.AlarmsAndRemindersDetails;
import com.android.settings.applications.appinfo.AppInfoDashboardFragment;
import com.android.settings.applications.appinfo.AppLocaleDetails;
@@ -72,6 +73,7 @@
import com.android.settings.biometrics.combination.CombinedBiometricSettings;
import com.android.settings.biometrics.face.FaceSettings;
import com.android.settings.biometrics.fingerprint.FingerprintSettings;
+import com.android.settings.biometrics.fingerprint2.ui.fragment.FingerprintSettingsV2Fragment;
import com.android.settings.bluetooth.BluetoothBroadcastDialog;
import com.android.settings.bluetooth.BluetoothDeviceDetailsFragment;
import com.android.settings.bluetooth.BluetoothFindBroadcastsFragment;
@@ -94,6 +96,7 @@
import com.android.settings.deviceinfo.PublicVolumeSettings;
import com.android.settings.deviceinfo.StorageDashboardFragment;
import com.android.settings.deviceinfo.aboutphone.MyDeviceInfoFragment;
+import com.android.settings.deviceinfo.batteryinfo.BatteryInfoFragment;
import com.android.settings.deviceinfo.firmwareversion.FirmwareVersionSettings;
import com.android.settings.deviceinfo.legal.ModuleLicensesDashboard;
import com.android.settings.display.AutoBrightnessSettings;
@@ -263,6 +266,7 @@
AssistGestureSettings.class.getName(),
FaceSettings.class.getName(),
FingerprintSettings.FingerprintSettingsFragment.class.getName(),
+ FingerprintSettingsV2Fragment.class.getName(),
CombinedBiometricSettings.class.getName(),
CombinedBiometricProfileSettings.class.getName(),
SwipeToNotificationSettings.class.getName(),
@@ -369,7 +373,9 @@
NfcAndPaymentFragment.class.getName(),
ColorAndMotionFragment.class.getName(),
LongBackgroundTasksDetails.class.getName(),
- RegionalPreferencesEntriesFragment.class.getName()
+ RegionalPreferencesEntriesFragment.class.getName(),
+ BatteryInfoFragment.class.getName(),
+ UserAspectRatioDetails.class.getName()
};
public static final String[] SETTINGS_FOR_RESTRICTED = {
diff --git a/src/com/android/settings/dashboard/DashboardFeatureProviderImpl.java b/src/com/android/settings/dashboard/DashboardFeatureProviderImpl.java
index 4dc8f1a..578493a 100644
--- a/src/com/android/settings/dashboard/DashboardFeatureProviderImpl.java
+++ b/src/com/android/settings/dashboard/DashboardFeatureProviderImpl.java
@@ -33,6 +33,7 @@
import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_TITLE;
import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_TITLE_URI;
+import android.app.PendingIntent;
import android.app.settings.SettingsEnums;
import android.content.ComponentName;
import android.content.Context;
@@ -75,6 +76,8 @@
import com.android.settingslib.utils.ThreadUtils;
import com.android.settingslib.widget.AdaptiveIcon;
+import com.google.common.collect.Iterables;
+
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@@ -152,7 +155,14 @@
}
bindIcon(pref, tile, forceRoundedIcon);
- if (tile instanceof ActivityTile) {
+ if (tile.hasPendingIntent()) {
+ // Pending intent cannot be launched within the settings app panel, and will thus always
+ // be executed directly.
+ pref.setOnPreferenceClickListener(preference -> {
+ launchPendingIntentOrSelectProfile(activity, tile, fragment.getMetricsCategory());
+ return true;
+ });
+ } else if (tile instanceof ActivityTile) {
final int sourceMetricsCategory = fragment.getMetricsCategory();
final Bundle metadata = tile.getMetaData();
String clsName = null;
@@ -441,6 +451,33 @@
preference.setIcon(iconDrawable);
}
+ private void launchPendingIntentOrSelectProfile(FragmentActivity activity, Tile tile,
+ int sourceMetricCategory) {
+ ProfileSelectDialog.updatePendingIntentsIfNeeded(mContext, tile);
+
+ if (tile.pendingIntentMap.isEmpty()) {
+ Log.w(TAG, "Cannot resolve pendingIntent, skipping. " + tile.getIntent());
+ return;
+ }
+
+ mMetricsFeatureProvider.logSettingsTileClick(tile.getKey(mContext), sourceMetricCategory);
+
+ // Launch the pending intent directly if there's only one available.
+ if (tile.pendingIntentMap.size() == 1) {
+ PendingIntent pendingIntent = Iterables.getOnlyElement(tile.pendingIntentMap.values());
+ try {
+ pendingIntent.send();
+ } catch (PendingIntent.CanceledException e) {
+ Log.w(TAG, "Failed executing pendingIntent. " + pendingIntent.getIntent(), e);
+ }
+ return;
+ }
+
+ ProfileSelectDialog.show(activity.getSupportFragmentManager(), tile,
+ sourceMetricCategory, /* onShowListener= */ null,
+ /* onDismissListener= */ null, /* onCancelListener= */ null);
+ }
+
private void launchIntentOrSelectProfile(FragmentActivity activity, Tile tile, Intent intent,
int sourceMetricCategory, TopLevelHighlightMixin highlightMixin,
boolean isDuplicateClick) {
diff --git a/src/com/android/settings/dashboard/DashboardFragment.java b/src/com/android/settings/dashboard/DashboardFragment.java
index 6076a25..d4acfa1 100644
--- a/src/com/android/settings/dashboard/DashboardFragment.java
+++ b/src/com/android/settings/dashboard/DashboardFragment.java
@@ -25,12 +25,16 @@
import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.Log;
+import android.view.View;
import androidx.annotation.CallSuper;
+import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.lifecycle.LifecycleObserver;
+import androidx.lifecycle.LifecycleOwner;
import androidx.preference.Preference;
+import androidx.preference.PreferenceCategory;
import androidx.preference.PreferenceGroup;
import androidx.preference.PreferenceManager;
import androidx.preference.PreferenceScreen;
@@ -47,7 +51,6 @@
import com.android.settingslib.core.AbstractPreferenceController;
import com.android.settingslib.core.lifecycle.Lifecycle;
import com.android.settingslib.drawer.DashboardCategory;
-import com.android.settingslib.drawer.ProviderTile;
import com.android.settingslib.drawer.Tile;
import com.android.settingslib.search.Indexable;
@@ -55,6 +58,7 @@
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
+import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@@ -169,6 +173,15 @@
}
@Override
+ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ LifecycleOwner viewLifecycleOwner = getViewLifecycleOwner();
+ for (AbstractPreferenceController controller : mControllers) {
+ controller.onViewCreated(viewLifecycleOwner);
+ }
+ }
+
+ @Override
public void onCategoriesChanged(Set<String> categories) {
final String categoryKey = getCategoryKey();
final DashboardCategory dashboardCategory =
@@ -504,6 +517,10 @@
// Install dashboard tiles and collect pending observers.
final boolean forceRoundedIcons = shouldForceRoundedIcon();
final List<DynamicDataObserver> pendingObservers = new ArrayList<>();
+
+ // Move group tiles to the beginning of the list to ensure they are created before the
+ // other tiles.
+ tiles.sort(Comparator.comparingInt(tile -> tile.getType() == Tile.Type.GROUP ? 0 : 1));
for (Tile tile : tiles) {
final String key = mDashboardFeatureProvider.getDashboardKeyForTile(tile);
if (TextUtils.isEmpty(key)) {
@@ -526,7 +543,14 @@
observers = mDashboardFeatureProvider.bindPreferenceToTileAndGetObservers(
getActivity(), this, forceRoundedIcons, pref, tile, key,
mPlaceholderPreferenceController.getOrder());
- screen.addPreference(pref);
+ if (tile.hasGroupKey() && mDashboardTilePrefKeys.containsKey(tile.getGroupKey())) {
+ final Preference group = screen.findPreference(tile.getGroupKey());
+ if (group instanceof PreferenceCategory) {
+ ((PreferenceCategory) group).addPreference(pref);
+ }
+ } else {
+ screen.addPreference(pref);
+ }
registerDynamicDataObservers(observers);
mDashboardTilePrefKeys.put(key, observers);
}
@@ -569,11 +593,28 @@
}
protected Preference createPreference(Tile tile) {
- return tile instanceof ProviderTile
- ? new SwitchPreference(getPrefContext())
- : tile.hasSwitch()
- ? new PrimarySwitchPreference(getPrefContext())
- : new Preference(getPrefContext());
+ switch (tile.getType()) {
+ case EXTERNAL_ACTION:
+ Preference externalActionPreference = new Preference(getPrefContext());
+ externalActionPreference
+ .setWidgetLayoutResource(R.layout.preference_external_action_icon);
+ return externalActionPreference;
+ case SWITCH:
+ return new SwitchPreference(getPrefContext());
+ case SWITCH_WITH_ACTION:
+ return new PrimarySwitchPreference(getPrefContext());
+ case GROUP:
+ mMetricsFeatureProvider.action(
+ mMetricsFeatureProvider.getAttribution(getActivity()),
+ SettingsEnums.ACTION_SETTINGS_GROUP_TILE_ADDED_TO_SCREEN,
+ getMetricsCategory(),
+ tile.getKey(getContext()),
+ /* value= */ 0);
+ return new PreferenceCategory((getPrefContext()));
+ case ACTION:
+ default:
+ return new Preference(getPrefContext());
+ }
}
@VisibleForTesting
diff --git a/src/com/android/settings/dashboard/profileselector/ProfileSelectDialog.java b/src/com/android/settings/dashboard/profileselector/ProfileSelectDialog.java
index ef6ad83..58a51cb 100644
--- a/src/com/android/settings/dashboard/profileselector/ProfileSelectDialog.java
+++ b/src/com/android/settings/dashboard/profileselector/ProfileSelectDialog.java
@@ -17,6 +17,7 @@
package com.android.settings.dashboard.profileselector;
import android.app.Dialog;
+import android.app.PendingIntent;
import android.content.Context;
import android.content.DialogInterface;
import android.content.DialogInterface.OnCancelListener;
@@ -127,13 +128,25 @@
@Override
public void onClick(int position) {
final UserHandle user = mSelectedTile.userHandle.get(position);
- // Show menu on top level items.
- final Intent intent = new Intent(mSelectedTile.getIntent());
- FeatureFactory.getFactory(getContext()).getMetricsFeatureProvider()
- .logStartedIntentWithProfile(intent, mSourceMetricCategory,
- position == 1 /* isWorkProfile */);
- intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
- getActivity().startActivityAsUser(intent, user);
+ if (!mSelectedTile.hasPendingIntent()) {
+ final Intent intent = new Intent(mSelectedTile.getIntent());
+ FeatureFactory.getFactory(getContext()).getMetricsFeatureProvider()
+ .logStartedIntentWithProfile(intent, mSourceMetricCategory,
+ position == 1 /* isWorkProfile */);
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
+ getActivity().startActivityAsUser(intent, user);
+ } else {
+ PendingIntent pendingIntent = mSelectedTile.pendingIntentMap.get(user);
+ FeatureFactory.getFactory(getContext()).getMetricsFeatureProvider()
+ .logSettingsTileClickWithProfile(mSelectedTile.getKey(getContext()),
+ mSourceMetricCategory,
+ position == 1 /* isWorkProfile */);
+ try {
+ pendingIntent.send();
+ } catch (PendingIntent.CanceledException e) {
+ Log.w(TAG, "Failed executing pendingIntent. " + pendingIntent.getIntent(), e);
+ }
+ }
dismiss();
}
@@ -178,4 +191,36 @@
}
}
}
+
+ /**
+ * Checks the userHandle and pendingIntentMap in the provided tile, and remove the invalid
+ * entries if any.
+ */
+ public static void updatePendingIntentsIfNeeded(Context context, Tile tile) {
+ if (tile.userHandle == null || tile.userHandle.size() <= 1
+ || tile.pendingIntentMap.size() <= 1) {
+ return;
+ }
+ for (UserHandle userHandle : List.copyOf(tile.userHandle)) {
+ if (!tile.pendingIntentMap.containsKey(userHandle)) {
+ if (DEBUG) {
+ Log.d(TAG, "Delete the user without pending intent: "
+ + userHandle.getIdentifier());
+ }
+ tile.userHandle.remove(userHandle);
+ }
+ }
+
+ final UserManager userManager = UserManager.get(context);
+ for (UserHandle userHandle : List.copyOf(tile.pendingIntentMap.keySet())) {
+ UserInfo userInfo = userManager.getUserInfo(userHandle.getIdentifier());
+ if (userInfo == null || userInfo.isCloneProfile()) {
+ if (DEBUG) {
+ Log.d(TAG, "Delete the user: " + userHandle.getIdentifier());
+ }
+ tile.userHandle.remove(userHandle);
+ tile.pendingIntentMap.remove(userHandle);
+ }
+ }
+ }
}
diff --git a/src/com/android/settings/datausage/BillingCycleSettings.java b/src/com/android/settings/datausage/BillingCycleSettings.java
index 3047d73..c3ddb2e 100644
--- a/src/com/android/settings/datausage/BillingCycleSettings.java
+++ b/src/com/android/settings/datausage/BillingCycleSettings.java
@@ -22,8 +22,6 @@
import android.content.Context;
import android.content.DialogInterface;
import android.content.res.Resources;
-import android.icu.text.MeasureFormat;
-import android.icu.util.MeasureUnit;
import android.net.NetworkPolicy;
import android.net.NetworkTemplate;
import android.os.Bundle;
@@ -322,14 +320,10 @@
final boolean isLimit = getArguments().getBoolean(EXTRA_LIMIT);
final long bytes = isLimit ? editor.getPolicyLimitBytes(template)
: editor.getPolicyWarningBytes(template);
- final long limitDisabled = isLimit ? LIMIT_DISABLED : WARNING_DISABLED;
- final MeasureFormat formatter = MeasureFormat.getInstance(
- getContext().getResources().getConfiguration().locale,
- MeasureFormat.FormatWidth.SHORT);
final String[] unitNames = new String[] {
- formatter.getUnitDisplayName(MeasureUnit.MEGABYTE),
- formatter.getUnitDisplayName(MeasureUnit.GIGABYTE)
+ DataUsageFormatter.INSTANCE.getBytesDisplayUnit(getResources(), MIB_IN_BYTES),
+ DataUsageFormatter.INSTANCE.getBytesDisplayUnit(getResources(), GIB_IN_BYTES),
};
final ArrayAdapter<String> adapter = new ArrayAdapter<String>(
getContext(), android.R.layout.simple_spinner_item, unitNames);
diff --git a/src/com/android/settings/datausage/DataSaverBackend.java b/src/com/android/settings/datausage/DataSaverBackend.java
index e47ecbd..6a39234 100644
--- a/src/com/android/settings/datausage/DataSaverBackend.java
+++ b/src/com/android/settings/datausage/DataSaverBackend.java
@@ -196,8 +196,10 @@
public interface Listener {
void onDataSaverChanged(boolean isDataSaving);
- void onAllowlistStatusChanged(int uid, boolean isAllowlisted);
+ /** This is called when allow list status is changed. */
+ default void onAllowlistStatusChanged(int uid, boolean isAllowlisted) {}
- void onDenylistStatusChanged(int uid, boolean isDenylisted);
+ /** This is called when deny list status is changed. */
+ default void onDenylistStatusChanged(int uid, boolean isDenylisted) {}
}
}
diff --git a/src/com/android/settings/datausage/DataSaverSummary.kt b/src/com/android/settings/datausage/DataSaverSummary.kt
index 1d9cbb7..0828d36 100644
--- a/src/com/android/settings/datausage/DataSaverSummary.kt
+++ b/src/com/android/settings/datausage/DataSaverSummary.kt
@@ -15,33 +15,22 @@
*/
package com.android.settings.datausage
-import android.app.Application
import android.app.settings.SettingsEnums
import android.content.Context
import android.os.Bundle
import android.telephony.SubscriptionManager
import android.widget.Switch
-import androidx.lifecycle.lifecycleScope
-import androidx.preference.Preference
import com.android.settings.R
import com.android.settings.SettingsActivity
-import com.android.settings.SettingsPreferenceFragment
-import com.android.settings.applications.AppStateBaseBridge
-import com.android.settings.datausage.AppStateDataUsageBridge.DataUsageState
+import com.android.settings.dashboard.DashboardFragment
import com.android.settings.search.BaseSearchIndexProvider
import com.android.settings.widget.SettingsMainSwitchBar
-import com.android.settingslib.applications.ApplicationsState
import com.android.settingslib.search.SearchIndexable
-import com.android.settingslib.spa.framework.util.formatString
-import kotlinx.coroutines.launch
@SearchIndexable
-class DataSaverSummary : SettingsPreferenceFragment() {
+class DataSaverSummary : DashboardFragment() {
private lateinit var switchBar: SettingsMainSwitchBar
private lateinit var dataSaverBackend: DataSaverBackend
- private lateinit var unrestrictedAccess: Preference
- private var dataUsageBridge: AppStateDataUsageBridge? = null
- private var session: ApplicationsState.Session? = null
// Flag used to avoid infinite loop due if user switch it on/off too quick.
private var switching = false
@@ -54,8 +43,6 @@
return
}
- addPreferencesFromResource(R.xml.data_saver)
- unrestrictedAccess = findPreference(KEY_UNRESTRICTED_ACCESS)!!
dataSaverBackend = DataSaverBackend(requireContext())
}
@@ -72,27 +59,12 @@
override fun onResume() {
super.onResume()
- dataSaverBackend.refreshAllowlist()
- dataSaverBackend.refreshDenylist()
dataSaverBackend.addListener(dataSaverBackendListener)
- dataUsageBridge?.resume(/* forceLoadAllApps= */ true)
- ?: viewLifecycleOwner.lifecycleScope.launch {
- val applicationsState = ApplicationsState.getInstance(
- requireContext().applicationContext as Application
- )
- dataUsageBridge = AppStateDataUsageBridge(
- applicationsState, dataUsageBridgeCallbacks, dataSaverBackend
- )
- session =
- applicationsState.newSession(applicationsStateCallbacks, settingsLifecycle)
- dataUsageBridge?.resume(/* forceLoadAllApps= */ true)
- }
}
override fun onPause() {
super.onPause()
dataSaverBackend.remListener(dataSaverBackendListener)
- dataUsageBridge?.pause()
}
private fun onSwitchChanged(isChecked: Boolean) {
@@ -104,9 +76,10 @@
}
}
+ override fun getPreferenceScreenResId() = R.xml.data_saver
override fun getMetricsCategory() = SettingsEnums.DATA_SAVER_SUMMARY
-
override fun getHelpResource() = R.string.help_url_data_saver
+ override fun getLogTag() = TAG
private val dataSaverBackendListener = object : DataSaverBackend.Listener {
override fun onDataSaverChanged(isDataSaving: Boolean) {
@@ -115,51 +88,10 @@
switching = false
}
}
-
- override fun onAllowlistStatusChanged(uid: Int, isAllowlisted: Boolean) {}
-
- override fun onDenylistStatusChanged(uid: Int, isDenylisted: Boolean) {}
- }
-
- private val dataUsageBridgeCallbacks = AppStateBaseBridge.Callback {
- updateUnrestrictedAccessSummary()
- }
-
- private val applicationsStateCallbacks = object : ApplicationsState.Callbacks {
- override fun onRunningStateChanged(running: Boolean) {}
-
- override fun onPackageListChanged() {}
-
- override fun onRebuildComplete(apps: ArrayList<ApplicationsState.AppEntry>?) {}
-
- override fun onPackageIconChanged() {}
-
- override fun onPackageSizeChanged(packageName: String?) {}
-
- override fun onAllSizesComputed() {
- updateUnrestrictedAccessSummary()
- }
-
- override fun onLauncherInfoChanged() {
- updateUnrestrictedAccessSummary()
- }
-
- override fun onLoadEntriesCompleted() {}
- }
-
- private fun updateUnrestrictedAccessSummary() {
- if (!isAdded || isFinishingOrDestroyed) return
- val allApps = session?.allApps ?: return
- val count = allApps.count {
- ApplicationsState.FILTER_DOWNLOADED_AND_LAUNCHER.filterApp(it) &&
- (it.extraInfo as? DataUsageState)?.isDataSaverAllowlisted == true
- }
- unrestrictedAccess.summary =
- resources.formatString(R.string.data_saver_unrestricted_summary, "count" to count)
}
companion object {
- private const val KEY_UNRESTRICTED_ACCESS = "unrestricted_access"
+ private const val TAG = "DataSaverSummary"
private fun Context.isDataSaverVisible(): Boolean =
resources.getBoolean(R.bool.config_show_data_saver)
diff --git a/src/com/android/settings/datausage/DataUsageFormatter.kt b/src/com/android/settings/datausage/DataUsageFormatter.kt
new file mode 100644
index 0000000..16a9ae8
--- /dev/null
+++ b/src/com/android/settings/datausage/DataUsageFormatter.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.datausage
+
+import android.content.res.Resources
+import android.text.format.Formatter
+
+object DataUsageFormatter {
+
+ /**
+ * Gets the display unit of the given bytes.
+ *
+ * Similar to MeasureFormat.getUnitDisplayName(), but with the expected result for the bytes in
+ * Settings, and align with other places in Settings.
+ */
+ fun Resources.getBytesDisplayUnit(bytes: Long): String =
+ Formatter.formatBytes(this, bytes, Formatter.FLAG_IEC_UNITS).units
+}
\ No newline at end of file
diff --git a/src/com/android/settings/development/BluetoothLeAudioDeviceDetailsPreferenceController.java b/src/com/android/settings/development/BluetoothLeAudioDeviceDetailsPreferenceController.java
index a54c594..980bdaa 100644
--- a/src/com/android/settings/development/BluetoothLeAudioDeviceDetailsPreferenceController.java
+++ b/src/com/android/settings/development/BluetoothLeAudioDeviceDetailsPreferenceController.java
@@ -20,6 +20,7 @@
import android.bluetooth.BluetoothManager;
import android.bluetooth.BluetoothStatusCodes;
import android.content.Context;
+import android.os.SystemProperties;
import android.provider.DeviceConfig;
import androidx.annotation.VisibleForTesting;
@@ -27,7 +28,6 @@
import androidx.preference.SwitchPreference;
import com.android.settings.core.PreferenceControllerMixin;
-import com.android.settings.core.SettingsUIDeviceConfig;
import com.android.settingslib.development.DeveloperOptionsPreferenceController;
/**
@@ -40,8 +40,12 @@
private static final String PREFERENCE_KEY = "bluetooth_show_leaudio_device_details";
private static final String CONFIG_LE_AUDIO_ENABLED_BY_DEFAULT = "le_audio_enabled_by_default";
+ private static final boolean LE_AUDIO_TOGGLE_VISIBLE_DEFAULT_VALUE = true;
static int sLeAudioSupportedStateCache = BluetoothStatusCodes.ERROR_UNKNOWN;
+ static final String LE_AUDIO_TOGGLE_VISIBLE_PROPERTY =
+ "persist.bluetooth.leaudio.toggle_visible";
+
@VisibleForTesting
BluetoothAdapter mBluetoothAdapter;
@@ -72,10 +76,7 @@
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
final boolean isEnabled = (Boolean) newValue;
- DeviceConfig.setProperty(
- DeviceConfig.NAMESPACE_SETTINGS_UI,
- SettingsUIDeviceConfig.BT_LE_AUDIO_DEVICE_DETAIL_ENABLED,
- isEnabled ? "true" : "false", false);
+ SystemProperties.set(LE_AUDIO_TOGGLE_VISIBLE_PROPERTY, Boolean.toString(isEnabled));
return true;
}
@@ -85,23 +86,13 @@
return;
}
- final boolean leAudioDeviceDetailEnabled = DeviceConfig.getBoolean(
- DeviceConfig.NAMESPACE_SETTINGS_UI,
- SettingsUIDeviceConfig.BT_LE_AUDIO_DEVICE_DETAIL_ENABLED, false);
+ final boolean isLeAudioToggleVisible = SystemProperties.getBoolean(
+ LE_AUDIO_TOGGLE_VISIBLE_PROPERTY, LE_AUDIO_TOGGLE_VISIBLE_DEFAULT_VALUE);
final boolean leAudioEnabledByDefault = DeviceConfig.getBoolean(
DeviceConfig.NAMESPACE_BLUETOOTH, CONFIG_LE_AUDIO_ENABLED_BY_DEFAULT, false);
mPreference.setEnabled(!leAudioEnabledByDefault);
- ((SwitchPreference) mPreference).setChecked(leAudioDeviceDetailEnabled
+ ((SwitchPreference) mPreference).setChecked(isLeAudioToggleVisible
|| leAudioEnabledByDefault);
}
-
- @Override
- protected void onDeveloperOptionsSwitchDisabled() {
- super.onDeveloperOptionsSwitchDisabled();
- // Reset the toggle to null when the developer option is disabled
- DeviceConfig.setProperty(
- DeviceConfig.NAMESPACE_SETTINGS_UI,
- SettingsUIDeviceConfig.BT_LE_AUDIO_DEVICE_DETAIL_ENABLED, "null", false);
- }
}
diff --git a/src/com/android/settings/development/DevelopmentOptionsActivityRequestCodes.java b/src/com/android/settings/development/DevelopmentOptionsActivityRequestCodes.java
index 0d91fdd..b7b2759 100644
--- a/src/com/android/settings/development/DevelopmentOptionsActivityRequestCodes.java
+++ b/src/com/android/settings/development/DevelopmentOptionsActivityRequestCodes.java
@@ -25,12 +25,4 @@
int REQUEST_CODE_DEBUG_APP = 1;
int REQUEST_MOCK_LOCATION_APP = 2;
-
- int REQUEST_CODE_ANGLE_ALL_USE_ANGLE = 3;
-
- int REQUEST_CODE_ANGLE_DRIVER_PKGS = 4;
-
- int REQUEST_CODE_ANGLE_DRIVER_VALUES = 5;
-
- int REQUEST_COMPAT_CHANGE_APP = 6;
}
diff --git a/src/com/android/settings/development/DevelopmentSettingsDashboardFragment.java b/src/com/android/settings/development/DevelopmentSettingsDashboardFragment.java
index f7be1aa..047b219 100644
--- a/src/com/android/settings/development/DevelopmentSettingsDashboardFragment.java
+++ b/src/com/android/settings/development/DevelopmentSettingsDashboardFragment.java
@@ -675,6 +675,7 @@
controllers.add(new NfcVerboseVendorLogPreferenceController(context, fragment));
controllers.add(new ShowTapsPreferenceController(context));
controllers.add(new PointerLocationPreferenceController(context));
+ controllers.add(new ShowKeyPressesPreferenceController(context));
controllers.add(new ShowSurfaceUpdatesPreferenceController(context));
controllers.add(new ShowLayoutBoundsPreferenceController(context));
controllers.add(new ShowRefreshRatePreferenceController(context));
diff --git a/src/com/android/settings/development/EnableVerboseVendorLoggingPreferenceController.java b/src/com/android/settings/development/EnableVerboseVendorLoggingPreferenceController.java
index 051cede..f13143d 100644
--- a/src/com/android/settings/development/EnableVerboseVendorLoggingPreferenceController.java
+++ b/src/com/android/settings/development/EnableVerboseVendorLoggingPreferenceController.java
@@ -29,6 +29,7 @@
import com.android.settings.core.PreferenceControllerMixin;
import com.android.settingslib.development.DeveloperOptionsPreferenceController;
+import com.android.settingslib.utils.ThreadUtils;
import java.util.NoSuchElementException;
@@ -66,23 +67,34 @@
return isIDumpstateDeviceAidlServiceAvailable() || isIDumpstateDeviceV1_1ServiceAvailable();
}
+ @SuppressWarnings("FutureReturnValueIgnored")
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
final boolean isEnabled = (Boolean) newValue;
- setVerboseLoggingEnabled(isEnabled);
+ // IDumpstateDevice IPC may be blocking when system is extremely heavily-loaded.
+ // Post to background thread to avoid ANR. Ignore the returned Future.
+ ThreadUtils.postOnBackgroundThread(() ->
+ setVerboseLoggingEnabled(isEnabled));
return true;
}
+ @SuppressWarnings("FutureReturnValueIgnored")
@Override
public void updateState(Preference preference) {
- final boolean enabled = getVerboseLoggingEnabled();
- ((SwitchPreference) mPreference).setChecked(enabled);
+ ThreadUtils.postOnBackgroundThread(() -> {
+ final boolean enabled = getVerboseLoggingEnabled();
+ ThreadUtils.getUiThreadHandler().post(() ->
+ ((SwitchPreference) mPreference).setChecked(enabled));
+ }
+ );
}
+ @SuppressWarnings("FutureReturnValueIgnored")
@Override
protected void onDeveloperOptionsSwitchDisabled() {
super.onDeveloperOptionsSwitchDisabled();
- setVerboseLoggingEnabled(false);
+ ThreadUtils.postOnBackgroundThread(() ->
+ setVerboseLoggingEnabled(false));
((SwitchPreference) mPreference).setChecked(false);
}
diff --git a/src/com/android/settings/development/ShowKeyPressesPreferenceController.java b/src/com/android/settings/development/ShowKeyPressesPreferenceController.java
new file mode 100644
index 0000000..247f59a
--- /dev/null
+++ b/src/com/android/settings/development/ShowKeyPressesPreferenceController.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.development;
+
+import android.content.Context;
+import android.provider.Settings;
+
+import androidx.annotation.VisibleForTesting;
+import androidx.preference.Preference;
+import androidx.preference.SwitchPreference;
+
+import com.android.settings.core.PreferenceControllerMixin;
+import com.android.settingslib.development.DeveloperOptionsPreferenceController;
+
+/** PreferenceController that controls the "Show key presses" developer option. */
+public class ShowKeyPressesPreferenceController extends
+ DeveloperOptionsPreferenceController implements
+ Preference.OnPreferenceChangeListener, PreferenceControllerMixin {
+
+ private static final String SHOW_KEY_PRESSES_KEY = "show_key_presses";
+
+ @VisibleForTesting
+ static final int SETTING_VALUE_ON = 1;
+ @VisibleForTesting
+ static final int SETTING_VALUE_OFF = 0;
+
+ public ShowKeyPressesPreferenceController(Context context) {
+ super(context);
+ }
+
+ @Override
+ public String getPreferenceKey() {
+ return SHOW_KEY_PRESSES_KEY;
+ }
+
+ @Override
+ public boolean onPreferenceChange(Preference preference, Object newValue) {
+ final boolean isEnabled = (Boolean) newValue;
+ Settings.System.putInt(mContext.getContentResolver(),
+ Settings.System.SHOW_KEY_PRESSES, isEnabled ? SETTING_VALUE_ON : SETTING_VALUE_OFF);
+ return true;
+ }
+
+ @Override
+ public void updateState(Preference preference) {
+ int showKeyPresses = Settings.System.getInt(mContext.getContentResolver(),
+ Settings.System.SHOW_KEY_PRESSES, SETTING_VALUE_OFF);
+ ((SwitchPreference) mPreference).setChecked(showKeyPresses != SETTING_VALUE_OFF);
+ }
+
+ @Override
+ protected void onDeveloperOptionsSwitchDisabled() {
+ super.onDeveloperOptionsSwitchDisabled();
+ Settings.System.putInt(mContext.getContentResolver(), Settings.System.SHOW_KEY_PRESSES,
+ SETTING_VALUE_OFF);
+ ((SwitchPreference) mPreference).setChecked(false);
+ }
+}
diff --git a/src/com/android/settings/development/compat/PlatformCompatDashboard.java b/src/com/android/settings/development/compat/PlatformCompatDashboard.java
index f8cbf21..3f0ffc7 100644
--- a/src/com/android/settings/development/compat/PlatformCompatDashboard.java
+++ b/src/com/android/settings/development/compat/PlatformCompatDashboard.java
@@ -17,21 +17,16 @@
package com.android.settings.development.compat;
import static com.android.internal.compat.OverrideAllowedState.ALLOWED;
-import static com.android.settings.development.DevelopmentOptionsActivityRequestCodes.REQUEST_COMPAT_CHANGE_APP;
-import android.app.Activity;
-import android.app.AlertDialog;
import android.app.settings.SettingsEnums;
import android.compat.Compatibility.ChangeConfig;
import android.content.Context;
-import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.os.RemoteException;
import android.os.ServiceManager;
-import android.text.TextUtils;
import android.util.ArraySet;
import androidx.annotation.VisibleForTesting;
@@ -40,35 +35,28 @@
import androidx.preference.PreferenceCategory;
import androidx.preference.SwitchPreference;
-import com.android.internal.compat.AndroidBuildClassifier;
import com.android.internal.compat.CompatibilityChangeConfig;
import com.android.internal.compat.CompatibilityChangeInfo;
import com.android.internal.compat.IPlatformCompat;
import com.android.settings.R;
import com.android.settings.dashboard.DashboardFragment;
-import com.android.settings.development.AppPicker;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
-
/**
* Dashboard for Platform Compat preferences.
*/
public class PlatformCompatDashboard extends DashboardFragment {
private static final String TAG = "PlatformCompatDashboard";
- private static final String COMPAT_APP = "compat_app";
+ public static final String COMPAT_APP = "compat_app";
private IPlatformCompat mPlatformCompat;
private CompatibilityChangeInfo[] mChanges;
- private AndroidBuildClassifier mAndroidBuildClassifier = new AndroidBuildClassifier();
-
- private boolean mShouldStartAppPickerOnResume = true;
-
@VisibleForTesting
String mSelectedApp;
@@ -108,32 +96,6 @@
} catch (RemoteException e) {
throw new RuntimeException("Could not list changes!", e);
}
- if (icicle != null) {
- mShouldStartAppPickerOnResume = false;
- mSelectedApp = icicle.getString(COMPAT_APP);
- }
- }
-
- @Override
- public void onActivityResult(int requestCode, int resultCode, Intent data) {
- if (requestCode == REQUEST_COMPAT_CHANGE_APP) {
- mShouldStartAppPickerOnResume = false;
- switch (resultCode) {
- case Activity.RESULT_OK:
- mSelectedApp = data.getAction();
- break;
- case Activity.RESULT_CANCELED:
- if (TextUtils.isEmpty(mSelectedApp)) {
- finish();
- }
- break;
- case AppPicker.RESULT_NO_MATCHING_APPS:
- mSelectedApp = null;
- break;
- }
- return;
- }
- super.onActivityResult(requestCode, resultCode, data);
}
@Override
@@ -142,33 +104,18 @@
if (isFinishingOrDestroyed()) {
return;
}
- if (!mShouldStartAppPickerOnResume) {
- if (TextUtils.isEmpty(mSelectedApp)) {
- new AlertDialog.Builder(getContext())
- .setTitle(R.string.platform_compat_dialog_title_no_apps)
- .setMessage(R.string.platform_compat_dialog_text_no_apps)
- .setPositiveButton(R.string.okay, (dialog, which) -> finish())
- .setOnDismissListener(dialog -> finish())
- .setCancelable(false)
- .show();
- return;
- }
- try {
- final ApplicationInfo applicationInfo = getApplicationInfo();
- addPreferences(applicationInfo);
- return;
- } catch (PackageManager.NameNotFoundException e) {
- mShouldStartAppPickerOnResume = true;
- mSelectedApp = null;
- }
+ Bundle arguments = getArguments();
+ if (arguments == null) {
+ finish();
+ return;
}
- startAppPicker();
- }
-
- @Override
- public void onSaveInstanceState(Bundle outState) {
- super.onSaveInstanceState(outState);
- outState.putString(COMPAT_APP, mSelectedApp);
+ mSelectedApp = arguments.getString(COMPAT_APP);
+ try {
+ final ApplicationInfo applicationInfo = getApplicationInfo();
+ addPreferences(applicationInfo);
+ } catch (PackageManager.NameNotFoundException ignored) {
+ finish();
+ }
}
private void addPreferences(ApplicationInfo applicationInfo) {
@@ -266,12 +213,6 @@
appPreference.setIcon(icon);
appPreference.setSummary(getString(R.string.platform_compat_selected_app_summary,
mSelectedApp, applicationInfo.targetSdkVersion));
- appPreference.setKey(mSelectedApp);
- appPreference.setOnPreferenceClickListener(
- preference -> {
- startAppPicker();
- return true;
- });
return appPreference;
}
@@ -294,17 +235,6 @@
}
}
- private void startAppPicker() {
- final Intent intent = new Intent(getContext(), AppPicker.class)
- .putExtra(AppPicker.EXTRA_INCLUDE_NOTHING, false);
- // If build is neither userdebug nor eng, only include debuggable apps
- final boolean debuggableBuild = mAndroidBuildClassifier.isDebuggableBuild();
- if (!debuggableBuild) {
- intent.putExtra(AppPicker.EXTRA_DEBUGGABLE, true /* value */);
- }
- startActivityForResult(intent, REQUEST_COMPAT_CHANGE_APP);
- }
-
private class CompatChangePreferenceChangeListener implements OnPreferenceChangeListener {
private final long changeId;
diff --git a/src/com/android/settings/deviceinfo/TopLevelStoragePreferenceController.java b/src/com/android/settings/deviceinfo/TopLevelStoragePreferenceController.java
index e682783..ccae7e9 100644
--- a/src/com/android/settings/deviceinfo/TopLevelStoragePreferenceController.java
+++ b/src/com/android/settings/deviceinfo/TopLevelStoragePreferenceController.java
@@ -74,10 +74,12 @@
return ThreadUtils.postOnBackgroundThread(() -> {
final PrivateStorageInfo info = PrivateStorageInfo.getPrivateStorageInfo(
getStorageManagerVolumeProvider());
- storageCacheHelper.cacheUsedSize(info.totalBytes - info.freeBytes);
+
+ long usedBytes = info.totalBytes - info.freeBytes;
+ storageCacheHelper.cacheUsedSize(usedBytes);
ThreadUtils.postOnMainThread(() -> {
preference.setSummary(
- getSummary(info.totalBytes - info.freeBytes, info.totalBytes));
+ getSummary(usedBytes, info.totalBytes));
});
});
}
diff --git a/src/com/android/settings/deviceinfo/batteryinfo/BatteryCycleCountPreferenceController.java b/src/com/android/settings/deviceinfo/batteryinfo/BatteryCycleCountPreferenceController.java
new file mode 100644
index 0000000..b022fcf
--- /dev/null
+++ b/src/com/android/settings/deviceinfo/batteryinfo/BatteryCycleCountPreferenceController.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.deviceinfo.batteryinfo;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.BatteryManager;
+
+import com.android.settings.R;
+import com.android.settings.core.BasePreferenceController;
+import com.android.settingslib.fuelgauge.BatteryUtils;
+
+/**
+ * A controller that manages the information about battery cycle count.
+ */
+public class BatteryCycleCountPreferenceController extends BasePreferenceController {
+
+ public BatteryCycleCountPreferenceController(Context context,
+ String preferenceKey) {
+ super(context, preferenceKey);
+ }
+
+ @Override
+ public int getAvailabilityStatus() {
+ return AVAILABLE;
+ }
+
+ @Override
+ public CharSequence getSummary() {
+ final Intent batteryIntent = BatteryUtils.getBatteryIntent(mContext);
+ final int cycleCount = batteryIntent.getIntExtra(BatteryManager.EXTRA_CYCLE_COUNT, -1);
+
+ return cycleCount == -1
+ ? mContext.getText(R.string.battery_cycle_count_not_available)
+ : Integer.toString(cycleCount);
+ }
+}
diff --git a/src/com/android/settings/deviceinfo/batteryinfo/BatteryFirstUseDatePreferenceController.java b/src/com/android/settings/deviceinfo/batteryinfo/BatteryFirstUseDatePreferenceController.java
new file mode 100644
index 0000000..6c7a743
--- /dev/null
+++ b/src/com/android/settings/deviceinfo/batteryinfo/BatteryFirstUseDatePreferenceController.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.deviceinfo.batteryinfo;
+
+import android.content.Context;
+import android.os.BatteryManager;
+
+import com.android.settings.core.BasePreferenceController;
+import com.android.settings.fuelgauge.BatterySettingsFeatureProvider;
+import com.android.settings.fuelgauge.BatteryUtils;
+import com.android.settings.overlay.FeatureFactory;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * A controller that manages the information about battery first use date.
+ */
+public class BatteryFirstUseDatePreferenceController extends BasePreferenceController {
+
+ private final BatterySettingsFeatureProvider mBatterySettingsFeatureProvider;
+ private final BatteryManager mBatteryManager;
+
+ private long mFirstUseDateInMs;
+
+ public BatteryFirstUseDatePreferenceController(Context context, String preferenceKey) {
+ super(context, preferenceKey);
+ mBatterySettingsFeatureProvider = FeatureFactory.getFactory(
+ context).getBatterySettingsFeatureProvider();
+ mBatteryManager = mContext.getSystemService(BatteryManager.class);
+ }
+
+ @Override
+ public int getAvailabilityStatus() {
+ return mBatterySettingsFeatureProvider.isFirstUseDateAvailable(mContext, getFirstUseDate())
+ ? AVAILABLE : CONDITIONALLY_UNAVAILABLE;
+ }
+
+ @Override
+ public CharSequence getSummary() {
+ return isAvailable()
+ ? BatteryUtils.getBatteryInfoFormattedDate(mFirstUseDateInMs)
+ : null;
+ }
+
+ private long getFirstUseDate() {
+ if (mFirstUseDateInMs == 0L) {
+ final long firstUseDateInSec = mBatteryManager.getLongProperty(
+ BatteryManager.BATTERY_PROPERTY_FIRST_USAGE_DATE);
+ mFirstUseDateInMs = TimeUnit.MILLISECONDS.convert(firstUseDateInSec, TimeUnit.SECONDS);
+ }
+ return mFirstUseDateInMs;
+ }
+}
diff --git a/src/com/android/settings/deviceinfo/batteryinfo/BatteryInfoFragment.java b/src/com/android/settings/deviceinfo/batteryinfo/BatteryInfoFragment.java
new file mode 100644
index 0000000..1731212
--- /dev/null
+++ b/src/com/android/settings/deviceinfo/batteryinfo/BatteryInfoFragment.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.deviceinfo.batteryinfo;
+
+import android.app.settings.SettingsEnums;
+
+import com.android.settings.R;
+import com.android.settings.dashboard.DashboardFragment;
+import com.android.settings.search.BaseSearchIndexProvider;
+import com.android.settingslib.search.SearchIndexable;
+
+/**
+ * A fragment that shows battery hardware information.
+ */
+@SearchIndexable
+public class BatteryInfoFragment extends DashboardFragment {
+
+ public static final String TAG = "BatteryInfo";
+
+ @Override
+ public int getMetricsCategory() {
+ return SettingsEnums.SETTINGS_BATTERY_INFORMATION;
+ }
+
+ @Override
+ protected int getPreferenceScreenResId() {
+ return R.xml.battery_info;
+ }
+
+ @Override
+ protected String getLogTag() {
+ return TAG;
+ }
+
+ public static final BaseSearchIndexProvider SEARCH_INDEX_DATA_PROVIDER =
+ new BaseSearchIndexProvider(R.xml.battery_info);
+}
diff --git a/src/com/android/settings/deviceinfo/batteryinfo/BatteryManufactureDatePreferenceController.java b/src/com/android/settings/deviceinfo/batteryinfo/BatteryManufactureDatePreferenceController.java
new file mode 100644
index 0000000..ff54c77
--- /dev/null
+++ b/src/com/android/settings/deviceinfo/batteryinfo/BatteryManufactureDatePreferenceController.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.deviceinfo.batteryinfo;
+
+import android.content.Context;
+import android.os.BatteryManager;
+
+import com.android.settings.core.BasePreferenceController;
+import com.android.settings.fuelgauge.BatterySettingsFeatureProvider;
+import com.android.settings.fuelgauge.BatteryUtils;
+import com.android.settings.overlay.FeatureFactory;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * A controller that manages the information about battery manufacture date.
+ */
+public class BatteryManufactureDatePreferenceController extends BasePreferenceController {
+
+ private final BatterySettingsFeatureProvider mBatterySettingsFeatureProvider;
+ private final BatteryManager mBatteryManager;
+
+ private long mManufactureDateInMs;
+
+ public BatteryManufactureDatePreferenceController(Context context, String preferenceKey) {
+ super(context, preferenceKey);
+ mBatterySettingsFeatureProvider = FeatureFactory.getFactory(
+ context).getBatterySettingsFeatureProvider();
+ mBatteryManager = mContext.getSystemService(BatteryManager.class);
+ }
+
+ @Override
+ public int getAvailabilityStatus() {
+ return mBatterySettingsFeatureProvider.isManufactureDateAvailable(mContext,
+ getManufactureDate())
+ ? AVAILABLE : CONDITIONALLY_UNAVAILABLE;
+ }
+
+ @Override
+ public CharSequence getSummary() {
+ return isAvailable()
+ ? BatteryUtils.getBatteryInfoFormattedDate(mManufactureDateInMs)
+ : null;
+ }
+
+ private long getManufactureDate() {
+ if (mManufactureDateInMs == 0L) {
+ final long manufactureDateInSec = mBatteryManager.getLongProperty(
+ BatteryManager.BATTERY_PROPERTY_MANUFACTURING_DATE);
+ mManufactureDateInMs = TimeUnit.MILLISECONDS.convert(manufactureDateInSec,
+ TimeUnit.SECONDS);
+ }
+ return mManufactureDateInMs;
+ }
+}
diff --git a/src/com/android/settings/deviceinfo/regulatory/RegulatoryInfo.kt b/src/com/android/settings/deviceinfo/regulatory/RegulatoryInfo.kt
new file mode 100644
index 0000000..e26e061
--- /dev/null
+++ b/src/com/android/settings/deviceinfo/regulatory/RegulatoryInfo.kt
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.deviceinfo.regulatory
+
+import android.content.Context
+import android.content.res.Resources
+import android.graphics.drawable.Drawable
+import android.os.SystemProperties
+import androidx.annotation.DrawableRes
+import androidx.annotation.VisibleForTesting
+import com.android.settings.R
+
+
+
+/** To load Regulatory Info from device. */
+object RegulatoryInfo {
+ private const val REGULATORY_INFO_RESOURCE = "regulatory_info"
+
+ @VisibleForTesting
+ const val KEY_COO = "ro.boot.hardware.coo"
+
+ @VisibleForTesting
+ const val KEY_SKU = "ro.boot.hardware.sku"
+
+ /** Gets the regulatory drawable. */
+ fun Context.getRegulatoryInfo(): Drawable? {
+ val sku = getSku()
+ if (sku.isNotBlank()) {
+ // When hardware coo property exists, use regulatory_info_<sku>_<coo> resource if valid.
+ val coo = getCoo()
+ if (coo.isNotBlank()) {
+ getRegulatoryInfo("${REGULATORY_INFO_RESOURCE}_${sku}_$coo")?.let { return it }
+ }
+ // Use regulatory_info_<sku> resource if valid.
+ getRegulatoryInfo("${REGULATORY_INFO_RESOURCE}_$sku")?.let { return it }
+ }
+ return getRegulatoryInfo(REGULATORY_INFO_RESOURCE)
+ }
+
+ private fun getCoo(): String = SystemProperties.get(KEY_COO).lowercase()
+
+ private fun getSku(): String = SystemProperties.get(KEY_SKU).lowercase()
+
+ private fun Context.getRegulatoryInfo(fileName: String): Drawable? {
+ val overlayPackageName =
+ resources.getString(R.string.config_regulatory_info_overlay_package_name)
+ .ifBlank { packageName }
+ val resources = packageManager.getResourcesForApplication(overlayPackageName)
+ val id = resources.getIdentifier(fileName, "drawable", overlayPackageName)
+ return if (id > 0) resources.getRegulatoryInfo(id) else null
+ }
+
+ private fun Resources.getRegulatoryInfo(@DrawableRes resId: Int): Drawable? = try {
+ getDrawable(resId, null).takeIf {
+ // Ignore the placeholder image
+ it.intrinsicWidth > 10 && it.intrinsicHeight > 10
+ }
+ } catch (_: Resources.NotFoundException) {
+ null
+ }
+}
diff --git a/src/com/android/settings/display/FoldLockBehaviorPreferenceController.java b/src/com/android/settings/display/FoldLockBehaviorPreferenceController.java
new file mode 100644
index 0000000..88e78e8
--- /dev/null
+++ b/src/com/android/settings/display/FoldLockBehaviorPreferenceController.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.display;
+
+import static android.provider.Settings.System.FOLD_LOCK_BEHAVIOR;
+
+import static com.android.settings.display.FoldLockBehaviorSettings.SETTING_VALUES;
+import static com.android.settings.display.FoldLockBehaviorSettings.SETTING_VALUE_SELECTIVE_STAY_AWAKE;
+import static com.android.settings.display.FoldLockBehaviorSettings.SETTING_VALUE_SLEEP_ON_FOLD;
+import static com.android.settings.display.FoldLockBehaviorSettings.SETTING_VALUE_STAY_AWAKE_ON_FOLD;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.os.UserHandle;
+import android.provider.Settings;
+
+import androidx.preference.Preference;
+
+import com.android.settings.R;
+import com.android.settings.core.BasePreferenceController;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * A preference controller for the @link android.provider.Settings.System#FOLD_LOCK_BEHAVIOR
+ * setting.
+ *
+ * This preference controller allows users to control whether or not the device
+ * stays awake when it is folded.
+ */
+public class FoldLockBehaviorPreferenceController extends BasePreferenceController {
+
+ private final Resources mResources;
+
+ private static Map<String, String> KEY_TO_TEXT = new HashMap<>();
+
+ public FoldLockBehaviorPreferenceController(Context context, String key) {
+ this(context, key, context.getResources());
+ }
+
+ public FoldLockBehaviorPreferenceController(Context context, String key, Resources resources) {
+ super(context, key);
+ mResources = resources;
+ KEY_TO_TEXT.put(SETTING_VALUE_STAY_AWAKE_ON_FOLD,
+ resourceToString(R.string.stay_awake_on_fold_title));
+ KEY_TO_TEXT.put(SETTING_VALUE_SELECTIVE_STAY_AWAKE,
+ resourceToString(R.string.selective_stay_awake_title));
+ KEY_TO_TEXT.put(SETTING_VALUE_SLEEP_ON_FOLD,
+ resourceToString(R.string.sleep_on_fold_title));
+ }
+
+ @Override
+ public int getAvailabilityStatus() {
+ return mResources.getBoolean(com.android.internal.R.bool.config_fold_lock_behavior)
+ ? AVAILABLE : UNSUPPORTED_ON_DEVICE;
+ }
+
+ @Override
+ public void updateState(Preference preference) {
+ String summary = KEY_TO_TEXT.get(getFoldSettingValue());
+ preference.setSummary(summary);
+ }
+
+ private String getFoldSettingValue() {
+ String foldSettingValue = Settings.System.getStringForUser(mContext.getContentResolver(),
+ FOLD_LOCK_BEHAVIOR, UserHandle.USER_CURRENT);
+ return (foldSettingValue != null && SETTING_VALUES.contains(foldSettingValue))
+ ? foldSettingValue : SETTING_VALUE_SELECTIVE_STAY_AWAKE;
+ }
+
+ @Override
+ public int getSliceHighlightMenuRes() {
+ return R.string.menu_key_display;
+ }
+
+ private String resourceToString(int resource) {
+ return mContext.getText(resource).toString();
+ }
+
+}
diff --git a/src/com/android/settings/display/FoldLockBehaviorSettings.java b/src/com/android/settings/display/FoldLockBehaviorSettings.java
new file mode 100644
index 0000000..beda52e
--- /dev/null
+++ b/src/com/android/settings/display/FoldLockBehaviorSettings.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.display;
+
+import static android.provider.Settings.System.FOLD_LOCK_BEHAVIOR;
+
+import android.app.settings.SettingsEnums;
+import android.content.Context;
+import android.os.UserHandle;
+import android.provider.Settings;
+import android.util.Log;
+
+import com.android.settings.R;
+import com.android.settings.support.actionbar.HelpResourceProvider;
+import com.android.settings.utils.CandidateInfoExtra;
+import com.android.settings.widget.RadioButtonPickerFragment;
+import com.android.settingslib.widget.CandidateInfo;
+import com.android.settingslib.widget.SelectorWithWidgetPreference;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Fragment that is used to control fold setting.
+ *
+ * Keep the setting values in this class in sync with the values in
+ * {@link com.android.server.utils.FoldSettingProvider}
+ */
+public class FoldLockBehaviorSettings extends RadioButtonPickerFragment implements
+ HelpResourceProvider {
+
+ public static final String SETTING_VALUE_STAY_AWAKE_ON_FOLD = "stay_awake_on_fold_key";
+ public static final String SETTING_VALUE_SELECTIVE_STAY_AWAKE = "selective_stay_awake_key";
+ public static final String SETTING_VALUE_SLEEP_ON_FOLD = "sleep_on_fold_key";
+ private static final String SETTING_VALUE_DEFAULT = SETTING_VALUE_SELECTIVE_STAY_AWAKE;
+ public static final String TAG = "FoldLockBehaviorSetting";
+ public static final HashSet<String> SETTING_VALUES = new HashSet<>(
+ Set.of(SETTING_VALUE_STAY_AWAKE_ON_FOLD, SETTING_VALUE_SELECTIVE_STAY_AWAKE,
+ SETTING_VALUE_SLEEP_ON_FOLD));
+
+ private Context mContext;
+
+ @Override
+ public void onAttach(Context context) {
+ super.onAttach(context);
+ mContext = context;
+ }
+
+ @Override
+ protected List<? extends CandidateInfo> getCandidates() {
+ List<CandidateInfoExtra> candidates = new ArrayList<>();
+ candidates.add(new CandidateInfoExtra(
+ resourceToString(R.string.stay_awake_on_fold_title),
+ resourceToString(R.string.stay_awake_on_fold_summary),
+ SETTING_VALUE_STAY_AWAKE_ON_FOLD, /* enabled */ true));
+ candidates.add(new CandidateInfoExtra(
+ resourceToString(R.string.selective_stay_awake_title),
+ resourceToString(R.string.selective_stay_awake_summary),
+ SETTING_VALUE_SELECTIVE_STAY_AWAKE, /* enabled */ true));
+ candidates.add(new CandidateInfoExtra(
+ resourceToString(R.string.sleep_on_fold_title),
+ resourceToString(R.string.sleep_on_fold_summary),
+ SETTING_VALUE_SLEEP_ON_FOLD, /* enabled */ true));
+ return candidates;
+ }
+
+ @Override
+ public void bindPreferenceExtra(SelectorWithWidgetPreference pref,
+ String key, CandidateInfo info, String defaultKey, String systemDefaultKey) {
+ if (!(info instanceof CandidateInfoExtra)) {
+ return;
+ }
+
+ pref.setSummary(((CandidateInfoExtra) info).loadSummary());
+ }
+
+ @Override
+ protected String getDefaultKey() {
+ String foldSettingValue = getCurrentFoldSettingValue();
+ foldSettingValue = (foldSettingValue != null) ? foldSettingValue : SETTING_VALUE_DEFAULT;
+ if (!SETTING_VALUES.contains(foldSettingValue)) {
+ Log.e(TAG,
+ "getDefaultKey: Invalid setting value, returning default setting value");
+ foldSettingValue = SETTING_VALUE_DEFAULT;
+ }
+
+ return foldSettingValue;
+ }
+
+ @Override
+ protected boolean setDefaultKey(String key) {
+ if (!SETTING_VALUES.contains(key)) {
+ Log.e(TAG, "setDefaultKey: Can not set invalid key: " + key);
+ key = SETTING_VALUE_SELECTIVE_STAY_AWAKE;
+ }
+ setCurrentFoldSettingValue(key);
+ return true;
+ }
+
+ @Override
+ public int getMetricsCategory() {
+ return SettingsEnums.FOLD_LOCK_BEHAVIOR;
+ }
+
+ @Override
+ protected int getPreferenceScreenResId() {
+ return R.xml.fold_lock_behavior_settings;
+ }
+
+ private String getCurrentFoldSettingValue() {
+ return Settings.System.getStringForUser(mContext.getContentResolver(),
+ FOLD_LOCK_BEHAVIOR,
+ UserHandle.USER_CURRENT);
+ }
+
+ private void setCurrentFoldSettingValue(String key) {
+ Settings.System.putStringForUser(mContext.getContentResolver(),
+ FOLD_LOCK_BEHAVIOR,
+ key,
+ UserHandle.USER_CURRENT);
+ }
+
+ private String resourceToString(int resource) {
+ return mContext.getText(resource).toString();
+ }
+}
diff --git a/src/com/android/settings/display/ScreenResolutionFragment.java b/src/com/android/settings/display/ScreenResolutionFragment.java
index de7d25f..daf1793 100644
--- a/src/com/android/settings/display/ScreenResolutionFragment.java
+++ b/src/com/android/settings/display/ScreenResolutionFragment.java
@@ -369,6 +369,12 @@
private void restoreDensity() {
final DisplayDensityUtils density = new DisplayDensityUtils(mContext);
+ /* If current density is the same as a default density of other resolutions,
+ * then mCurrentIndex may be out of boundary.
+ */
+ if (density.getDefaultDisplayDensityValues().length <= mCurrentIndex) {
+ mCurrentIndex = density.getCurrentIndexForDefaultDisplay();
+ }
if (density.getDefaultDisplayDensityValues()[mCurrentIndex]
!= density.getDefaultDensityForDefaultDisplay()) {
density.setForcedDisplayDensity(mCurrentIndex);
diff --git a/src/com/android/settings/dream/WhenToDreamPicker.java b/src/com/android/settings/dream/WhenToDreamPicker.java
index 13cdadf..3052d20 100644
--- a/src/com/android/settings/dream/WhenToDreamPicker.java
+++ b/src/com/android/settings/dream/WhenToDreamPicker.java
@@ -50,7 +50,7 @@
@Override
public int getMetricsCategory() {
- return SettingsEnums.DREAM;
+ return SettingsEnums.SETTINGS_WHEN_TO_DREAM;
}
@Override
diff --git a/src/com/android/settings/fuelgauge/AdvancedPowerUsageDetail.java b/src/com/android/settings/fuelgauge/AdvancedPowerUsageDetail.java
index 79e0194..d38dede 100644
--- a/src/com/android/settings/fuelgauge/AdvancedPowerUsageDetail.java
+++ b/src/com/android/settings/fuelgauge/AdvancedPowerUsageDetail.java
@@ -16,6 +16,8 @@
package com.android.settings.fuelgauge;
+import static com.android.settings.fuelgauge.batteryusage.ConvertUtils.isUserConsumer;
+
import android.app.Activity;
import android.app.ActivityManager;
import android.app.backup.BackupManager;
@@ -41,7 +43,6 @@
import com.android.settings.fuelgauge.BatteryOptimizeHistoricalLogEntry.Action;
import com.android.settings.fuelgauge.batteryusage.BatteryDiffEntry;
import com.android.settings.fuelgauge.batteryusage.BatteryEntry;
-import com.android.settings.fuelgauge.batteryusage.BatteryHistEntry;
import com.android.settings.overlay.FeatureFactory;
import com.android.settings.widget.EntityHeaderController;
import com.android.settingslib.HelpUtils;
@@ -149,14 +150,13 @@
Context context, int sourceMetricsCategory,
BatteryDiffEntry diffEntry, String usagePercent, String slotInformation,
boolean showTimeInformation) {
- final BatteryHistEntry histEntry = diffEntry.mBatteryHistEntry;
final LaunchBatteryDetailPageArgs launchArgs = new LaunchBatteryDetailPageArgs();
// configure the launch argument.
launchArgs.mUsagePercent = usagePercent;
launchArgs.mPackageName = diffEntry.getPackageName();
launchArgs.mAppLabel = diffEntry.getAppLabel();
launchArgs.mSlotInformation = slotInformation;
- launchArgs.mUid = (int) histEntry.mUid;
+ launchArgs.mUid = (int) diffEntry.mUid;
launchArgs.mIconId = diffEntry.getAppIconId();
launchArgs.mConsumedPower = (int) diffEntry.mConsumePower;
if (showTimeInformation) {
@@ -164,7 +164,7 @@
launchArgs.mBackgroundTimeMs = diffEntry.mBackgroundUsageTimeInMs;
launchArgs.mScreenOnTimeMs = diffEntry.mScreenOnTimeInMs;
}
- launchArgs.mIsUserEntry = histEntry.isUserEntry();
+ launchArgs.mIsUserEntry = isUserConsumer(diffEntry.mConsumerType);
startBatteryDetailPage(context, sourceMetricsCategory, launchArgs);
}
@@ -289,12 +289,14 @@
mLogStringBuilder.append(", onPause mode = ").append(selectedPreference);
logMetricCategory(selectedPreference);
- BatteryHistoricalLogUtil.writeLog(
- getContext().getApplicationContext(),
- Action.LEAVE,
- BatteryHistoricalLogUtil.getPackageNameWithUserId(
- mBatteryOptimizeUtils.getPackageName(), UserHandle.myUserId()),
- mLogStringBuilder.toString());
+ mExecutor.execute(() -> {
+ BatteryOptimizeLogUtils.writeLog(
+ getContext().getApplicationContext(),
+ Action.LEAVE,
+ BatteryOptimizeLogUtils.getPackageNameWithUserId(
+ mBatteryOptimizeUtils.getPackageName(), UserHandle.myUserId()),
+ mLogStringBuilder.toString());
+ });
Log.d(TAG, "Leave with mode: " + selectedPreference);
}
diff --git a/src/com/android/settings/fuelgauge/BatteryBackupHelper.java b/src/com/android/settings/fuelgauge/BatteryBackupHelper.java
index 66ffc90..50f1b90 100644
--- a/src/com/android/settings/fuelgauge/BatteryBackupHelper.java
+++ b/src/com/android/settings/fuelgauge/BatteryBackupHelper.java
@@ -199,7 +199,7 @@
info.packageName + DELIMITER_MODE + optimizationMode;
builder.append(packageOptimizeMode + DELIMITER);
Log.d(TAG, "backupOptimizationMode: " + packageOptimizeMode);
- BatteryHistoricalLogUtil.writeLog(
+ BatteryOptimizeLogUtils.writeLog(
sharedPreferences, Action.BACKUP, info.packageName,
/* actionDescription */ "mode: " + optimizationMode);
backupCount++;
@@ -275,7 +275,7 @@
/** Dump the app optimization mode backup history data. */
public static void dumpHistoricalData(Context context, PrintWriter writer) {
- BatteryHistoricalLogUtil.printBatteryOptimizeHistoricalLog(
+ BatteryOptimizeLogUtils.printBatteryOptimizeHistoricalLog(
getSharedPreferences(context), writer);
}
diff --git a/src/com/android/settings/fuelgauge/BatteryHistoricalLogUtil.java b/src/com/android/settings/fuelgauge/BatteryOptimizeLogUtils.java
similarity index 89%
rename from src/com/android/settings/fuelgauge/BatteryHistoricalLogUtil.java
rename to src/com/android/settings/fuelgauge/BatteryOptimizeLogUtils.java
index f82b703..d093d35 100644
--- a/src/com/android/settings/fuelgauge/BatteryHistoricalLogUtil.java
+++ b/src/com/android/settings/fuelgauge/BatteryOptimizeLogUtils.java
@@ -20,23 +20,25 @@
import android.content.SharedPreferences;
import android.util.Base64;
+import androidx.annotation.VisibleForTesting;
+
import com.android.settings.fuelgauge.BatteryOptimizeHistoricalLogEntry.Action;
import com.android.settings.fuelgauge.batteryusage.ConvertUtils;
-import com.google.common.annotations.VisibleForTesting;
-
import java.io.PrintWriter;
import java.util.List;
/** Writes and reads a historical log of battery related state change events. */
-public final class BatteryHistoricalLogUtil {
+public final class BatteryOptimizeLogUtils {
+ private static final String TAG = "BatteryOptimizeLogUtils";
private static final String BATTERY_OPTIMIZE_FILE_NAME = "battery_optimize_historical_logs";
private static final String LOGS_KEY = "battery_optimize_logs_key";
- private static final String TAG = "BatteryHistoricalLogUtil";
@VisibleForTesting
static final int MAX_ENTRIES = 40;
+ private BatteryOptimizeLogUtils() {}
+
/** Writes a log entry for battery optimization mode. */
static void writeLog(
Context context, Action action, String packageName, String actionDescription) {
@@ -67,7 +69,7 @@
newLogBuilder.addLogEntry(logEntry);
String loggingContent =
- Base64.encodeToString(newLogBuilder.build().toByteArray(), Base64.DEFAULT);
+ Base64.encodeToString(newLogBuilder.build().toByteArray(), Base64.DEFAULT);
sharedPreferences
.edit()
.putString(LOGS_KEY, loggingContent)
@@ -94,7 +96,7 @@
if (logEntryList.isEmpty()) {
writer.println("\tnothing to dump");
} else {
- writer.println("0:UNKNOWN 1:RESTRICTED 2:UNRESTRICTED 3:OPTIMIZED");
+ writer.println("0:UNKNOWN 1:RESTRICTED 2:UNRESTRICTED 3:OPTIMIZED");
logEntryList.forEach(entry -> writer.println(toString(entry)));
}
}
@@ -113,6 +115,7 @@
@VisibleForTesting
static SharedPreferences getSharedPreferences(Context context) {
- return context.getSharedPreferences(BATTERY_OPTIMIZE_FILE_NAME, Context.MODE_PRIVATE);
+ return context.getApplicationContext()
+ .getSharedPreferences(BATTERY_OPTIMIZE_FILE_NAME, Context.MODE_PRIVATE);
}
}
diff --git a/src/com/android/settings/fuelgauge/BatteryOptimizeUtils.java b/src/com/android/settings/fuelgauge/BatteryOptimizeUtils.java
index 589e1fd..124840e 100644
--- a/src/com/android/settings/fuelgauge/BatteryOptimizeUtils.java
+++ b/src/com/android/settings/fuelgauge/BatteryOptimizeUtils.java
@@ -245,7 +245,7 @@
Context context, int appStandbyMode, boolean allowListed, int uid, String packageName,
BatteryUtils batteryUtils, PowerAllowlistBackend powerAllowlistBackend,
Action action) {
- final String packageNameKey = BatteryHistoricalLogUtil
+ final String packageNameKey = BatteryOptimizeLogUtils
.getPackageNameWithUserId(packageName, UserHandle.myUserId());
try {
batteryUtils.setForceAppStandby(uid, packageName, appStandbyMode);
@@ -259,7 +259,7 @@
appStandbyMode = -1;
Log.e(TAG, "set OPTIMIZATION MODE failed for " + packageName, e);
}
- BatteryHistoricalLogUtil.writeLog(
+ BatteryOptimizeLogUtils.writeLog(
context,
action,
packageNameKey,
diff --git a/src/com/android/settings/fuelgauge/BatterySettingsFeatureProvider.java b/src/com/android/settings/fuelgauge/BatterySettingsFeatureProvider.java
index f6efb24..260fde0 100644
--- a/src/com/android/settings/fuelgauge/BatterySettingsFeatureProvider.java
+++ b/src/com/android/settings/fuelgauge/BatterySettingsFeatureProvider.java
@@ -16,9 +16,14 @@
package com.android.settings.fuelgauge;
-import android.content.ComponentName;
+import android.content.Context;
/** Feature provider for battery settings usage. */
public interface BatterySettingsFeatureProvider {
+ /** Returns true if manufacture date should be shown */
+ boolean isManufactureDateAvailable(Context context, long manufactureDateMs);
+
+ /** Returns true if first use date should be shown */
+ boolean isFirstUseDateAvailable(Context context, long firstUseDateMs);
}
diff --git a/src/com/android/settings/fuelgauge/BatterySettingsFeatureProviderImpl.java b/src/com/android/settings/fuelgauge/BatterySettingsFeatureProviderImpl.java
index 39fe118..6b456b7 100644
--- a/src/com/android/settings/fuelgauge/BatterySettingsFeatureProviderImpl.java
+++ b/src/com/android/settings/fuelgauge/BatterySettingsFeatureProviderImpl.java
@@ -21,9 +21,13 @@
/** Feature provider implementation for battery settings usage. */
public class BatterySettingsFeatureProviderImpl implements BatterySettingsFeatureProvider {
- protected Context mContext;
+ @Override
+ public boolean isManufactureDateAvailable(Context context, long manufactureDateMs) {
+ return false;
+ }
- public BatterySettingsFeatureProviderImpl(Context context) {
- mContext = context.getApplicationContext();
+ @Override
+ public boolean isFirstUseDateAvailable(Context context, long firstUseDateMs) {
+ return false;
}
}
diff --git a/src/com/android/settings/fuelgauge/BatterySettingsMigrateChecker.java b/src/com/android/settings/fuelgauge/BatterySettingsMigrateChecker.java
index 4b9e6ef..8697e43 100644
--- a/src/com/android/settings/fuelgauge/BatterySettingsMigrateChecker.java
+++ b/src/com/android/settings/fuelgauge/BatterySettingsMigrateChecker.java
@@ -16,8 +16,8 @@
package com.android.settings.fuelgauge;
-import android.content.ContentResolver;
import android.content.BroadcastReceiver;
+import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.provider.Settings;
@@ -25,8 +25,6 @@
import androidx.annotation.VisibleForTesting;
-import com.android.settings.R;
-import com.android.settings.fuelgauge.BatteryOptimizeHistoricalLogEntry;
import com.android.settings.fuelgauge.batterysaver.BatterySaverScheduleRadioButtonsController;
import com.android.settingslib.fuelgauge.BatterySaverUtils;
@@ -41,6 +39,7 @@
@Override
public void onReceive(Context context, Intent intent) {
+ Log.d(TAG, "onReceive: " + intent + " owner: " + BatteryBackupHelper.isOwner());
if (intent != null
&& Intent.ACTION_BOOT_COMPLETED.equals(intent.getAction())
&& BatteryBackupHelper.isOwner()) {
diff --git a/src/com/android/settings/fuelgauge/BatteryUtils.java b/src/com/android/settings/fuelgauge/BatteryUtils.java
index 12760b1..1f7e3ec 100644
--- a/src/com/android/settings/fuelgauge/BatteryUtils.java
+++ b/src/com/android/settings/fuelgauge/BatteryUtils.java
@@ -64,8 +64,10 @@
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
-import java.time.Duration;
import java.time.Instant;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+import java.time.format.FormatStyle;
import java.util.List;
/**
@@ -353,7 +355,7 @@
@SuppressWarnings("unchecked")
public static <T extends MessageLite> T parseProtoFromString(
String serializedProto, T protoClass) {
- if (serializedProto.isEmpty()) {
+ if (serializedProto == null || serializedProto.isEmpty()) {
return (T) protoClass.getDefaultInstanceForType();
}
try {
@@ -451,12 +453,10 @@
@VisibleForTesting
Estimate getEnhancedEstimate() {
- Estimate estimate = null;
- // Get enhanced prediction if available
- if (Duration.between(Estimate.getLastCacheUpdateTime(mContext), Instant.now())
- .compareTo(Duration.ofSeconds(10)) < 0) {
- estimate = Estimate.getCachedEstimateIfAvailable(mContext);
- } else if (mPowerUsageFeatureProvider != null &&
+ // Align the same logic in the BatteryControllerImpl.updateEstimate()
+ Estimate estimate = Estimate.getCachedEstimateIfAvailable(mContext);
+ if (estimate == null &&
+ mPowerUsageFeatureProvider != null &&
mPowerUsageFeatureProvider.isEnhancedBatteryPredictionEnabled(mContext)) {
estimate = mPowerUsageFeatureProvider.getEnhancedBatteryPrediction(mContext);
if (estimate != null) {
@@ -673,6 +673,14 @@
}
return summary.toString();
}
+ /** Format the date of battery related info */
+ public static CharSequence getBatteryInfoFormattedDate(long dateInMs) {
+ final Instant instant = Instant.ofEpochMilli(dateInMs);
+ final String localDate = instant.atZone(ZoneId.systemDefault()).toLocalDate().format(
+ DateTimeFormatter.ofLocalizedDate(FormatStyle.LONG));
+
+ return localDate;
+ }
/** Builds the battery usage time information for one timestamp. */
private static String buildBatteryUsageTimeInfo(final Context context, long timeInMs,
diff --git a/src/com/android/settings/fuelgauge/PowerUsageFeatureProvider.java b/src/com/android/settings/fuelgauge/PowerUsageFeatureProvider.java
index 0b0e243..4253ca6 100644
--- a/src/com/android/settings/fuelgauge/PowerUsageFeatureProvider.java
+++ b/src/com/android/settings/fuelgauge/PowerUsageFeatureProvider.java
@@ -18,9 +18,11 @@
import android.content.Context;
import android.content.Intent;
+import android.os.Bundle;
import android.util.ArrayMap;
import android.util.SparseIntArray;
+import com.android.settings.fuelgauge.batteryusage.PowerAnomalyEventList;
import com.android.settingslib.fuelgauge.Estimate;
import java.util.List;
@@ -37,6 +39,11 @@
boolean isBatteryUsageEnabled();
/**
+ * Check whether the battery tips card is enabled in the battery usage page
+ */
+ boolean isBatteryTipsEnabled();
+
+ /**
* Returns a threshold (in milliseconds) for the minimal screen on time in battery usage list
*/
double getBatteryUsageListScreenOnTimeThresholdInMs();
@@ -129,6 +136,16 @@
boolean delayHourlyJobWhenBooting();
/**
+ * Insert settings configuration data for anomaly detection
+ */
+ void insertSettingsData(Context context, double displayDrain);
+
+ /**
+ * Returns {@link Bundle} for settings anomaly detection result
+ */
+ PowerAnomalyEventList detectSettingsAnomaly(Context context, double displayDrain);
+
+ /**
* Gets an intent for one time bypass charge limited to resume charging.
*/
Intent getResumeChargeIntent(boolean isDockDefender);
diff --git a/src/com/android/settings/fuelgauge/PowerUsageFeatureProviderImpl.java b/src/com/android/settings/fuelgauge/PowerUsageFeatureProviderImpl.java
index 1d0ba18..5931e206 100644
--- a/src/com/android/settings/fuelgauge/PowerUsageFeatureProviderImpl.java
+++ b/src/com/android/settings/fuelgauge/PowerUsageFeatureProviderImpl.java
@@ -27,6 +27,7 @@
import android.util.SparseIntArray;
import com.android.internal.util.ArrayUtils;
+import com.android.settings.fuelgauge.batteryusage.PowerAnomalyEventList;
import com.android.settingslib.fuelgauge.Estimate;
import java.util.ArrayList;
@@ -75,6 +76,11 @@
}
@Override
+ public boolean isBatteryTipsEnabled() {
+ return false;
+ }
+
+ @Override
public double getBatteryUsageListScreenOnTimeThresholdInMs() {
return 0;
}
@@ -161,6 +167,14 @@
}
@Override
+ public void insertSettingsData(Context context, double displayDrain) {}
+
+ @Override
+ public PowerAnomalyEventList detectSettingsAnomaly(Context context, double displayDrain) {
+ return null;
+ }
+
+ @Override
public Set<Integer> getOthersSystemComponentSet() {
return new ArraySet<>();
}
diff --git a/src/com/android/settings/fuelgauge/TopLevelBatteryPreferenceController.java b/src/com/android/settings/fuelgauge/TopLevelBatteryPreferenceController.java
index 254cf04..e08f4ba 100644
--- a/src/com/android/settings/fuelgauge/TopLevelBatteryPreferenceController.java
+++ b/src/com/android/settings/fuelgauge/TopLevelBatteryPreferenceController.java
@@ -18,6 +18,7 @@
import android.content.ComponentName;
import android.content.Context;
+import android.os.BatteryManager;
import android.util.Log;
import androidx.annotation.VisibleForTesting;
@@ -139,7 +140,10 @@
if (Utils.containsIncompatibleChargers(mContext, TAG)) {
return mContext.getString(R.string.battery_info_status_not_charging);
}
- if (!info.discharging && info.chargeLabel != null) {
+ if (info.batteryStatus == BatteryManager.BATTERY_STATUS_NOT_CHARGING) {
+ // Present status only if no remaining time or status anomalous
+ return info.statusLabel;
+ } else if (!info.discharging && info.chargeLabel != null) {
return info.chargeLabel;
} else if (info.remainingLabel == null) {
return info.batteryPercentString;
diff --git a/src/com/android/settings/fuelgauge/batterytip/detectors/LowBatteryDetector.java b/src/com/android/settings/fuelgauge/batterytip/detectors/LowBatteryDetector.java
index ed8cc62..9e970d2 100644
--- a/src/com/android/settings/fuelgauge/batterytip/detectors/LowBatteryDetector.java
+++ b/src/com/android/settings/fuelgauge/batterytip/detectors/LowBatteryDetector.java
@@ -23,8 +23,6 @@
import com.android.settings.fuelgauge.batterytip.tips.BatteryTip;
import com.android.settings.fuelgauge.batterytip.tips.LowBatteryTip;
-import java.util.concurrent.TimeUnit;
-
/**
* Detect whether the battery is too low
*/
@@ -46,9 +44,7 @@
@Override
public BatteryTip detect() {
- final boolean lowBattery = mBatteryInfo.batteryLevel <= mWarningLevel
- || (mBatteryInfo.discharging && mBatteryInfo.remainingTimeUs != 0
- && mBatteryInfo.remainingTimeUs < TimeUnit.HOURS.toMicros(mPolicy.lowBatteryHour));
+ final boolean lowBattery = mBatteryInfo.batteryLevel <= mWarningLevel;
final boolean lowBatteryEnabled = mPolicy.lowBatteryEnabled && !mIsPowerSaveMode;
final boolean dischargingLowBatteryState =
mPolicy.testLowBatteryTip || (mBatteryInfo.discharging && lowBattery);
diff --git a/src/com/android/settings/fuelgauge/batterytip/tips/BatteryTip.java b/src/com/android/settings/fuelgauge/batterytip/tips/BatteryTip.java
index 8aabc37..fdafca6 100644
--- a/src/com/android/settings/fuelgauge/batterytip/tips/BatteryTip.java
+++ b/src/com/android/settings/fuelgauge/batterytip/tips/BatteryTip.java
@@ -20,9 +20,8 @@
import android.os.Parcel;
import android.os.Parcelable;
import android.util.SparseIntArray;
-import android.view.View;
-import androidx.annotation.IdRes;
+import androidx.annotation.DrawableRes;
import androidx.annotation.IntDef;
import androidx.annotation.VisibleForTesting;
import androidx.preference.Preference;
@@ -134,7 +133,8 @@
public abstract CharSequence getSummary(Context context);
- @IdRes
+ /** Gets the drawable resource id for the icon. */
+ @DrawableRes
public abstract int getIconId();
/**
@@ -162,21 +162,12 @@
preference.setTitle(getTitle(context));
preference.setSummary(getSummary(context));
preference.setIcon(getIconId());
- @IdRes int iconTintColorId = getIconTintColorId();
- if (iconTintColorId != View.NO_ID) {
- preference.getIcon().setTint(context.getColor(iconTintColorId));
- }
final CardPreference cardPreference = castToCardPreferenceSafely(preference);
if (cardPreference != null) {
cardPreference.resetLayoutState();
}
}
- /** Returns the color resid for tinting {@link #getIconId()} or {@link View#NO_ID} if none. */
- public @IdRes int getIconTintColorId() {
- return View.NO_ID;
- }
-
public boolean shouldShowDialog() {
return mShowDialog;
}
diff --git a/src/com/android/settings/fuelgauge/batterytip/tips/IncompatibleChargerTip.java b/src/com/android/settings/fuelgauge/batterytip/tips/IncompatibleChargerTip.java
index 1c5616f..48cfb7a 100644
--- a/src/com/android/settings/fuelgauge/batterytip/tips/IncompatibleChargerTip.java
+++ b/src/com/android/settings/fuelgauge/batterytip/tips/IncompatibleChargerTip.java
@@ -52,7 +52,7 @@
@Override
public int getIconId() {
- return R.drawable.ic_battery_alert_theme;
+ return R.drawable.ic_battery_charger;
}
@Override
diff --git a/src/com/android/settings/fuelgauge/batteryusage/AnomalyAppItemPreference.java b/src/com/android/settings/fuelgauge/batteryusage/AnomalyAppItemPreference.java
new file mode 100644
index 0000000..2f139ec
--- /dev/null
+++ b/src/com/android/settings/fuelgauge/batteryusage/AnomalyAppItemPreference.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.fuelgauge.batteryusage;
+
+import android.content.Context;
+import android.text.TextUtils;
+import android.view.View;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import androidx.preference.PreferenceViewHolder;
+
+import com.android.settings.R;
+
+class AnomalyAppItemPreference extends PowerGaugePreference {
+
+ private static final String TAG = "AnomalyAppItemPreference";
+
+ private CharSequence mAnomalyHintText;
+
+ AnomalyAppItemPreference(Context context) {
+ super(context, /* attrs */ null);
+ setLayoutResource(R.layout.anomaly_app_item_preference);
+ }
+
+ void setAnomalyHint(CharSequence anomalyHintText) {
+ if (!TextUtils.equals(mAnomalyHintText, anomalyHintText)) {
+ mAnomalyHintText = anomalyHintText;
+ notifyChanged();
+ }
+ }
+
+ @Override
+ public void onBindViewHolder(PreferenceViewHolder viewHolder) {
+ super.onBindViewHolder(viewHolder);
+ final LinearLayout warningChipView =
+ (LinearLayout) viewHolder.findViewById(R.id.warning_chip);
+
+ if (!TextUtils.isEmpty(mAnomalyHintText)) {
+ ((TextView) warningChipView.findViewById(R.id.warning_info)).setText(mAnomalyHintText);
+ warningChipView.setVisibility(View.VISIBLE);
+ } else {
+ warningChipView.setVisibility(View.GONE);
+ }
+ }
+}
diff --git a/src/com/android/settings/fuelgauge/batteryusage/AnomalyEventWrapper.java b/src/com/android/settings/fuelgauge/batteryusage/AnomalyEventWrapper.java
new file mode 100644
index 0000000..d535490
--- /dev/null
+++ b/src/com/android/settings/fuelgauge/batteryusage/AnomalyEventWrapper.java
@@ -0,0 +1,231 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.fuelgauge.batteryusage;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.text.TextUtils;
+import android.util.Pair;
+
+import com.android.settings.R;
+import com.android.settings.SettingsActivity;
+import com.android.settings.core.SubSettingLauncher;
+
+import java.util.function.Function;
+
+final class AnomalyEventWrapper {
+ private static final String TAG = "AnomalyEventWrapper";
+
+ private final Context mContext;
+ private final PowerAnomalyEvent mPowerAnomalyEvent;
+
+ private final int mCardStyleId;
+ private final int mResourceIndex;
+
+ private SubSettingLauncher mSubSettingLauncher = null;
+ private Pair<Integer, Integer> mHighlightSlotPair = null;
+ private BatteryDiffEntry mRelatedBatteryDiffEntry = null;
+
+ AnomalyEventWrapper(Context context, PowerAnomalyEvent powerAnomalyEvent) {
+ mContext = context;
+ mPowerAnomalyEvent = powerAnomalyEvent;
+ // Set basic battery tips card info
+ mCardStyleId = mPowerAnomalyEvent.getType().getNumber();
+ mResourceIndex = mPowerAnomalyEvent.getKey().getNumber();
+ }
+
+ private <T> T getInfo(Function<WarningBannerInfo, T> warningBannerInfoSupplier,
+ Function<WarningItemInfo, T> warningItemInfoSupplier) {
+ if (warningBannerInfoSupplier != null && mPowerAnomalyEvent.hasWarningBannerInfo()) {
+ return warningBannerInfoSupplier.apply(mPowerAnomalyEvent.getWarningBannerInfo());
+ } else if (warningItemInfoSupplier != null && mPowerAnomalyEvent.hasWarningItemInfo()) {
+ return warningItemInfoSupplier.apply(mPowerAnomalyEvent.getWarningItemInfo());
+ }
+ return null;
+ }
+
+ private int getResourceId(int resourceId, int resourceIndex, String defType) {
+ final String key = getStringFromArrayResource(resourceId, resourceIndex);
+ return TextUtils.isEmpty(key) ? 0
+ : mContext.getResources().getIdentifier(key, defType, mContext.getPackageName());
+ }
+
+ private String getString(Function<WarningBannerInfo, String> warningBannerInfoSupplier,
+ Function<WarningItemInfo, String> warningItemInfoSupplier,
+ int resourceId, int resourceIndex) {
+ final String string = getInfo(warningBannerInfoSupplier, warningItemInfoSupplier);
+ return (!TextUtils.isEmpty(string) || resourceId <= 0) ? string
+ : getStringFromArrayResource(resourceId, resourceIndex);
+ }
+
+ private String getStringFromArrayResource(int resourceId, int resourceIndex) {
+ if (resourceId <= 0 || resourceIndex < 0) {
+ return null;
+ }
+ final String[] stringArray = mContext.getResources().getStringArray(resourceId);
+ return (resourceIndex >= 0 && resourceIndex < stringArray.length)
+ ? stringArray[resourceIndex] : null;
+ }
+
+ void setRelatedBatteryDiffEntry(BatteryDiffEntry batteryDiffEntry) {
+ mRelatedBatteryDiffEntry = batteryDiffEntry;
+ }
+
+ String getEventId() {
+ return mPowerAnomalyEvent.hasEventId() ? mPowerAnomalyEvent.getEventId() : null;
+ }
+
+ int getIconResId() {
+ return getResourceId(R.array.battery_tips_card_icons, mCardStyleId, "drawable");
+ }
+
+ int getColorResId() {
+ return getResourceId(R.array.battery_tips_card_colors, mCardStyleId, "color");
+ }
+
+ String getTitleString() {
+ final String protoTitleString = getInfo(WarningBannerInfo::getTitleString,
+ WarningItemInfo::getTitleString);
+ if (!TextUtils.isEmpty(protoTitleString)) {
+ return protoTitleString;
+ }
+ final int titleFormatResId = getResourceId(R.array.power_anomaly_title_ids,
+ mResourceIndex, "string");
+ if (mPowerAnomalyEvent.hasWarningBannerInfo()) {
+ return mContext.getString(titleFormatResId);
+ } else if (mPowerAnomalyEvent.hasWarningItemInfo() && mRelatedBatteryDiffEntry != null) {
+ final String appLabel = mRelatedBatteryDiffEntry.getAppLabel();
+ return mContext.getString(titleFormatResId, appLabel);
+ }
+ return null;
+ }
+
+ String getMainBtnString() {
+ return getString(WarningBannerInfo::getMainButtonString,
+ WarningItemInfo::getMainButtonString,
+ R.array.power_anomaly_main_btn_strings, mResourceIndex);
+ }
+
+ String getDismissBtnString() {
+ return getString(WarningBannerInfo::getCancelButtonString,
+ WarningItemInfo::getCancelButtonString,
+ R.array.power_anomaly_dismiss_btn_strings, mResourceIndex);
+ }
+
+ String getAnomalyHintString() {
+ return getStringFromArrayResource(R.array.power_anomaly_hint_messages, mResourceIndex);
+ }
+
+ String getDismissRecordKey() {
+ return mPowerAnomalyEvent.getDismissRecordKey();
+ }
+
+ boolean hasAnomalyEntryKey() {
+ return getAnomalyEntryKey() != null;
+ }
+
+ String getAnomalyEntryKey() {
+ return mPowerAnomalyEvent.hasWarningItemInfo()
+ && mPowerAnomalyEvent.getWarningItemInfo().hasItemKey()
+ ? mPowerAnomalyEvent.getWarningItemInfo().getItemKey() : null;
+ }
+
+ boolean hasSubSettingLauncher() {
+ if (mSubSettingLauncher == null) {
+ mSubSettingLauncher = getSubSettingLauncher();
+ }
+ return mSubSettingLauncher != null;
+ }
+
+ SubSettingLauncher getSubSettingLauncher() {
+ if (mSubSettingLauncher != null) {
+ return mSubSettingLauncher;
+ }
+ final String destinationClassName = getInfo(
+ WarningBannerInfo::getMainButtonDestination, null);
+ if (!TextUtils.isEmpty(destinationClassName)) {
+ final Integer sourceMetricsCategory = getInfo(
+ WarningBannerInfo::getMainButtonSourceMetricsCategory, null);
+ final String preferenceHighlightKey = getInfo(
+ WarningBannerInfo::getMainButtonSourceHighlightKey, null);
+ Bundle arguments = Bundle.EMPTY;
+ if (!TextUtils.isEmpty(preferenceHighlightKey)) {
+ arguments = new Bundle(1);
+ arguments.putString(SettingsActivity.EXTRA_FRAGMENT_ARG_KEY,
+ preferenceHighlightKey);
+ }
+ mSubSettingLauncher = new SubSettingLauncher(mContext)
+ .setDestination(destinationClassName)
+ .setSourceMetricsCategory(sourceMetricsCategory)
+ .setArguments(arguments);
+ }
+ return mSubSettingLauncher;
+ }
+
+ boolean hasHighlightSlotPair(BatteryLevelData batteryLevelData) {
+ if (mHighlightSlotPair == null) {
+ mHighlightSlotPair = getHighlightSlotPair(batteryLevelData);
+ }
+ return mHighlightSlotPair != null;
+ }
+
+ Pair<Integer, Integer> getHighlightSlotPair(BatteryLevelData batteryLevelData) {
+ if (mHighlightSlotPair != null) {
+ return mHighlightSlotPair;
+ }
+ if (!mPowerAnomalyEvent.hasWarningItemInfo()) {
+ return null;
+ }
+ final WarningItemInfo warningItemInfo = mPowerAnomalyEvent.getWarningItemInfo();
+ final Long startTimestamp = warningItemInfo.hasStartTimestamp()
+ ? warningItemInfo.getStartTimestamp() : null;
+ final Long endTimestamp = warningItemInfo.hasEndTimestamp()
+ ? warningItemInfo.getEndTimestamp() : null;
+ if (startTimestamp != null && endTimestamp != null) {
+ mHighlightSlotPair = batteryLevelData
+ .getIndexByTimestamps(startTimestamp, endTimestamp);
+ if (mHighlightSlotPair.first == BatteryChartViewModel.SELECTED_INDEX_INVALID
+ || mHighlightSlotPair.second == BatteryChartViewModel.SELECTED_INDEX_INVALID) {
+ // Drop invalid mHighlightSlotPair index
+ mHighlightSlotPair = null;
+ }
+ }
+ return mHighlightSlotPair;
+ }
+
+ boolean updateTipsCardPreference(BatteryTipsCardPreference preference) {
+ final String titleString = getTitleString();
+ if (TextUtils.isEmpty(titleString)) {
+ return false;
+ }
+ preference.setTitle(titleString);
+ preference.setIconResourceId(getIconResId());
+ preference.setMainButtonStrokeColorResourceId(getColorResId());
+ preference.setMainButtonLabel(getMainBtnString());
+ preference.setDismissButtonLabel(getDismissBtnString());
+ return true;
+ }
+
+ boolean launchSubSetting() {
+ if (!hasSubSettingLauncher()) {
+ return false;
+ }
+ // Navigate to sub setting page
+ mSubSettingLauncher.launch();
+ return true;
+ }
+}
diff --git a/src/com/android/settings/fuelgauge/batteryusage/AppUsageDataLoader.java b/src/com/android/settings/fuelgauge/batteryusage/AppUsageDataLoader.java
deleted file mode 100644
index c336fcd..0000000
--- a/src/com/android/settings/fuelgauge/batteryusage/AppUsageDataLoader.java
+++ /dev/null
@@ -1,83 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.settings.fuelgauge.batteryusage;
-
-import android.app.usage.UsageEvents;
-import android.content.Context;
-import android.os.AsyncTask;
-import android.util.Log;
-
-import androidx.annotation.VisibleForTesting;
-
-import java.util.List;
-import java.util.Map;
-import java.util.function.Supplier;
-
-/** Load app usage events data in the background. */
-public final class AppUsageDataLoader {
- private static final String TAG = "AppUsageDataLoader";
-
- // For testing only.
- @VisibleForTesting
- static Supplier<Map<Long, UsageEvents>> sFakeAppUsageEventsSupplier;
- @VisibleForTesting
- static Supplier<List<AppUsageEvent>> sFakeUsageEventsListSupplier;
-
- private AppUsageDataLoader() {}
-
- static void enqueueWork(final Context context) {
- AsyncTask.execute(() -> {
- Log.d(TAG, "loadAppUsageDataSafely() in the AsyncTask");
- loadAppUsageDataSafely(context.getApplicationContext());
- });
- }
-
- @VisibleForTesting
- static void loadAppUsageData(final Context context) {
- final long start = System.currentTimeMillis();
- final Map<Long, UsageEvents> appUsageEvents =
- sFakeAppUsageEventsSupplier != null
- ? sFakeAppUsageEventsSupplier.get()
- : DataProcessor.getAppUsageEvents(context);
- if (appUsageEvents == null) {
- Log.w(TAG, "loadAppUsageData() returns null");
- return;
- }
- final List<AppUsageEvent> appUsageEventList =
- sFakeUsageEventsListSupplier != null
- ? sFakeUsageEventsListSupplier.get()
- : DataProcessor.generateAppUsageEventListFromUsageEvents(
- context, appUsageEvents);
- if (appUsageEventList == null || appUsageEventList.isEmpty()) {
- Log.w(TAG, "loadAppUsageData() returns null or empty content");
- return;
- }
- final long elapsedTime = System.currentTimeMillis() - start;
- Log.d(TAG, String.format("loadAppUsageData() size=%d in %d/ms", appUsageEventList.size(),
- elapsedTime));
- // Uploads the AppUsageEvent data into database.
- DatabaseUtils.sendAppUsageEventData(context, appUsageEventList);
- }
-
- private static void loadAppUsageDataSafely(final Context context) {
- try {
- loadAppUsageData(context);
- } catch (RuntimeException e) {
- Log.e(TAG, "loadAppUsageData:" + e);
- }
- }
-}
diff --git a/src/com/android/settings/fuelgauge/batteryusage/BatteryChartPreferenceController.java b/src/com/android/settings/fuelgauge/batteryusage/BatteryChartPreferenceController.java
index 17d9c8a..844241e 100644
--- a/src/com/android/settings/fuelgauge/batteryusage/BatteryChartPreferenceController.java
+++ b/src/com/android/settings/fuelgauge/batteryusage/BatteryChartPreferenceController.java
@@ -67,40 +67,17 @@
private static final String KEY_DAILY_CHART_INDEX = "daily_chart_index";
private static final String KEY_HOURLY_CHART_INDEX = "hourly_chart_index";
- /**
- * A callback listener for battery usage is updated.
- * This happens when battery usage data is ready or the selected index is changed.
- */
- public interface OnBatteryUsageUpdatedListener {
- /**
- * The callback function for battery usage is updated.
- * @param slotUsageData The battery usage diff data for the selected slot. This is used in
- * the app list.
- * @param slotTimestamp The selected slot timestamp information. This is used in the battery
- * usage breakdown category.
- * @param isAllUsageDataEmpty Whether all the battery usage data is null or empty. This is
- * used when showing the footer.
- */
- void onBatteryUsageUpdated(
- BatteryDiffData slotUsageData, String slotTimestamp, boolean isAllUsageDataEmpty);
- }
-
- /**
- * A callback listener for the device screen on time is updated.
- * This happens when screen on time data is ready or the selected index is changed.
- */
- public interface OnScreenOnTimeUpdatedListener {
- /**
- * The callback function for the device screen on time is updated.
- * @param screenOnTime The selected slot device screen on time.
- * @param slotTimestamp The selected slot timestamp information.
- */
- void onScreenOnTimeUpdated(Long screenOnTime, String slotTimestamp);
+ /** A callback listener for the selected index is updated. */
+ interface OnSelectedIndexUpdatedListener {
+ /** The callback function for the selected index is updated. */
+ void onSelectedIndexUpdated();
}
@VisibleForTesting
Context mPrefContext;
@VisibleForTesting
+ TextView mChartSummaryTextView;
+ @VisibleForTesting
BatteryChartView mDailyChartView;
@VisibleForTesting
BatteryChartView mHourlyChartView;
@@ -109,16 +86,15 @@
@VisibleForTesting
int mHourlyChartIndex = BatteryChartViewModel.SELECTED_INDEX_ALL;
@VisibleForTesting
- Map<Integer, Map<Integer, BatteryDiffData>> mBatteryUsageMap;
+ int mDailyHighlightSlotIndex = BatteryChartViewModel.SELECTED_INDEX_INVALID;
+ @VisibleForTesting
+ int mHourlyHighlightSlotIndex = BatteryChartViewModel.SELECTED_INDEX_INVALID;
private boolean mIs24HourFormat;
- private boolean mHourlyChartVisible = true;
private View mBatteryChartViewGroup;
- private TextView mChartSummaryTextView;
private BatteryChartViewModel mDailyViewModel;
private List<BatteryChartViewModel> mHourlyViewModels;
- private OnBatteryUsageUpdatedListener mOnBatteryUsageUpdatedListener;
- private OnScreenOnTimeUpdatedListener mOnScreenOnTimeUpdatedListener;
+ private OnSelectedIndexUpdatedListener mOnSelectedIndexUpdatedListener;
private final SettingsActivity mActivity;
private final MetricsFeatureProvider mMetricsFeatureProvider;
@@ -201,28 +177,20 @@
return PREFERENCE_KEY;
}
- void setOnBatteryUsageUpdatedListener(OnBatteryUsageUpdatedListener listener) {
- mOnBatteryUsageUpdatedListener = listener;
+ int getDailyChartIndex() {
+ return mDailyChartIndex;
}
- void setOnScreenOnTimeUpdatedListener(OnScreenOnTimeUpdatedListener listener) {
- mOnScreenOnTimeUpdatedListener = listener;
+ int getHourlyChartIndex() {
+ return mHourlyChartIndex;
}
- void setBatteryHistoryMap(
- final Map<Long, Map<String, BatteryHistEntry>> batteryHistoryMap) {
- Log.d(TAG, "setBatteryHistoryMap() " + (batteryHistoryMap == null ? "null"
- : ("size=" + batteryHistoryMap.size())));
- // Ensure the battery chart group is visible for users.
- animateBatteryChartViewGroup();
- final BatteryLevelData batteryLevelData =
- DataProcessManager.getBatteryLevelData(mContext, mHandler, batteryHistoryMap,
- batteryUsageMap -> {
- mBatteryUsageMap = batteryUsageMap;
- logScreenUsageTime();
- refreshUi();
- });
- Log.d(TAG, "getBatteryLevelData: " + batteryLevelData);
+ void setOnSelectedIndexUpdatedListener(OnSelectedIndexUpdatedListener listener) {
+ mOnSelectedIndexUpdatedListener = listener;
+ }
+
+ void onBatteryLevelDataUpdate(final BatteryLevelData batteryLevelData) {
+ Log.d(TAG, "onBatteryLevelDataUpdate: " + batteryLevelData);
mMetricsFeatureProvider.action(
mPrefContext,
SettingsEnums.ACTION_BATTERY_HISTORY_LOADED,
@@ -253,6 +221,43 @@
refreshUi();
}
+ boolean isHighlightSlotFocused() {
+ return (mDailyHighlightSlotIndex != BatteryChartViewModel.SELECTED_INDEX_INVALID
+ && mDailyHighlightSlotIndex == mDailyChartIndex
+ && mHourlyHighlightSlotIndex != BatteryChartViewModel.SELECTED_INDEX_INVALID
+ && mHourlyHighlightSlotIndex == mHourlyChartIndex);
+ }
+
+ void onHighlightSlotIndexUpdate(int dailyHighlightSlotIndex, int hourlyHighlightSlotIndex) {
+ mDailyHighlightSlotIndex = dailyHighlightSlotIndex;
+ mHourlyHighlightSlotIndex = hourlyHighlightSlotIndex;
+ refreshUi();
+ if (mOnSelectedIndexUpdatedListener != null) {
+ mOnSelectedIndexUpdatedListener.onSelectedIndexUpdated();
+ }
+ }
+
+ void selectHighlightSlotIndex() {
+ if (mDailyHighlightSlotIndex == BatteryChartViewModel.SELECTED_INDEX_INVALID
+ || mHourlyHighlightSlotIndex == BatteryChartViewModel.SELECTED_INDEX_INVALID) {
+ return;
+ }
+ if (mDailyHighlightSlotIndex == mDailyChartIndex
+ && mHourlyHighlightSlotIndex == mHourlyChartIndex) {
+ return;
+ }
+ mDailyChartIndex = mDailyHighlightSlotIndex;
+ mHourlyChartIndex = mHourlyHighlightSlotIndex;
+ Log.d(TAG, String.format("onDailyChartSelect:%d, onHourlyChartSelect:%d",
+ mDailyChartIndex, mHourlyChartIndex));
+ refreshUi();
+ mHandler.post(() -> mDailyChartView.announceForAccessibility(
+ getAccessibilityAnnounceMessage()));
+ if (mOnSelectedIndexUpdatedListener != null) {
+ mOnSelectedIndexUpdatedListener.onSelectedIndexUpdated();
+ }
+ }
+
void setBatteryChartView(@NonNull final BatteryChartView dailyChartView,
@NonNull final BatteryChartView hourlyChartView) {
final View parentView = (View) dailyChartView.getParent();
@@ -289,6 +294,9 @@
? SettingsEnums.ACTION_BATTERY_USAGE_DAILY_SHOW_ALL
: SettingsEnums.ACTION_BATTERY_USAGE_DAILY_TIME_SLOT,
mDailyChartIndex);
+ if (mOnSelectedIndexUpdatedListener != null) {
+ mOnSelectedIndexUpdatedListener.onSelectedIndexUpdated();
+ }
});
mHourlyChartView = hourlyChartView;
mHourlyChartView.setOnSelectListener(trapezoidIndex -> {
@@ -310,67 +318,37 @@
? SettingsEnums.ACTION_BATTERY_USAGE_SHOW_ALL
: SettingsEnums.ACTION_BATTERY_USAGE_TIME_SLOT,
mHourlyChartIndex);
+ if (mOnSelectedIndexUpdatedListener != null) {
+ mOnSelectedIndexUpdatedListener.onSelectedIndexUpdated();
+ }
});
refreshUi();
}
- @VisibleForTesting
- boolean refreshUi() {
- if (mDailyChartView == null || mHourlyChartView == null) {
- // Chart views are not initialized.
- return false;
- }
-
- // When mDailyViewModel or mHourlyViewModels is null, there is no battery level data.
- // This is mainly in 2 cases:
- // 1) battery data is within 2 hours
- // 2) no battery data in the latest 7 days (power off >= 7 days)
- final boolean refreshUiResult = mDailyViewModel == null || mHourlyViewModels == null
- ? refreshUiWithNoLevelDataCase()
- : refreshUiWithLevelDataCase();
-
- if (!refreshUiResult) {
- return false;
- }
-
- if (mOnBatteryUsageUpdatedListener != null && mBatteryUsageMap != null
- && mBatteryUsageMap.get(mDailyChartIndex) != null) {
- final BatteryDiffData slotUsageData =
- mBatteryUsageMap.get(mDailyChartIndex).get(mHourlyChartIndex);
- if (slotUsageData != null) {
- mOnScreenOnTimeUpdatedListener.onScreenOnTimeUpdated(
- slotUsageData.getScreenOnTime(),
- getSlotInformation());
- }
- mOnBatteryUsageUpdatedListener.onBatteryUsageUpdated(
- slotUsageData, getSlotInformation(), isBatteryUsageMapNullOrEmpty());
- }
- return true;
+ // Show empty hourly chart view only if there is no valid battery usage data.
+ void showEmptyChart() {
+ setChartSummaryVisible(true);
+ mDailyChartView.setVisibility(View.GONE);
+ mHourlyChartView.setVisibility(View.VISIBLE);
+ mHourlyChartView.setViewModel(null);
}
- private boolean refreshUiWithNoLevelDataCase() {
- setChartSummaryVisible(false);
- if (mBatteryUsageMap == null) {
- // There is no battery level data and battery usage data is not ready, wait for data
- // ready to refresh UI. Show nothing temporarily.
+ @VisibleForTesting
+ void refreshUi() {
+ if (mDailyChartView == null || mHourlyChartView == null) {
+ // Chart views are not initialized.
+ return;
+ }
+
+ if (mDailyViewModel == null || mHourlyViewModels == null) {
+ setChartSummaryVisible(false);
mDailyChartView.setVisibility(View.GONE);
mHourlyChartView.setVisibility(View.GONE);
mDailyChartView.setViewModel(null);
mHourlyChartView.setViewModel(null);
- return false;
- } else if (mBatteryUsageMap
- .get(BatteryChartViewModel.SELECTED_INDEX_ALL)
- .get(BatteryChartViewModel.SELECTED_INDEX_ALL) == null) {
- // There is no battery level data and battery usage data, show an empty hourly chart
- // view.
- mDailyChartView.setVisibility(View.GONE);
- mHourlyChartView.setVisibility(View.VISIBLE);
- mHourlyChartView.setViewModel(null);
+ return;
}
- return true;
- }
- private boolean refreshUiWithLevelDataCase() {
setChartSummaryVisible(true);
// Gets valid battery level data.
if (isBatteryLevelDataInOneDay()) {
@@ -383,6 +361,7 @@
mDailyChartIndex = BatteryChartViewModel.SELECTED_INDEX_ALL;
}
mDailyViewModel.setSelectedIndex(mDailyChartIndex);
+ mDailyViewModel.setHighlightSlotIndex(mDailyHighlightSlotIndex);
mDailyChartView.setViewModel(mDailyViewModel);
}
@@ -397,17 +376,13 @@
mHourlyChartIndex = BatteryChartViewModel.SELECTED_INDEX_ALL;
}
hourlyViewModel.setSelectedIndex(mHourlyChartIndex);
+ hourlyViewModel.setHighlightSlotIndex((mDailyChartIndex == mDailyHighlightSlotIndex)
+ ? mHourlyHighlightSlotIndex
+ : BatteryChartViewModel.SELECTED_INDEX_INVALID);
mHourlyChartView.setViewModel(hourlyViewModel);
}
-
- if (mBatteryUsageMap == null) {
- // Battery usage data is not ready, wait for data ready to refresh UI.
- return false;
- }
- return true;
}
- @VisibleForTesting
String getSlotInformation() {
if (mDailyViewModel == null || mHourlyViewModels == null) {
// No data
@@ -436,7 +411,7 @@
final String slotInformation = getSlotInformation();
return slotInformation == null
? mPrefContext.getString(
- R.string.battery_usage_breakdown_title_since_last_full_charge)
+ R.string.battery_usage_breakdown_title_since_last_full_charge)
: mPrefContext.getString(
R.string.battery_usage_breakdown_title_for_slot, slotInformation);
}
@@ -449,10 +424,10 @@
}
private void animateBatteryHourlyChartView(final boolean visible) {
- if (mHourlyChartView == null || mHourlyChartVisible == visible) {
+ if (mHourlyChartView == null
+ || (mHourlyChartView.getVisibility() == View.VISIBLE) == visible) {
return;
}
- mHourlyChartVisible = visible;
if (visible) {
mHourlyChartView.setVisibility(View.VISIBLE);
@@ -498,44 +473,6 @@
};
}
- private void logScreenUsageTime() {
- if (mBatteryUsageMap == null) {
- return;
- }
- final BatteryDiffData allBatteryDiffData = mBatteryUsageMap.get(
- BatteryChartViewModel.SELECTED_INDEX_ALL).get(
- BatteryChartViewModel.SELECTED_INDEX_ALL);
- if (allBatteryDiffData == null) {
- return;
- }
- mMetricsFeatureProvider.action(
- mPrefContext,
- SettingsEnums.ACTION_BATTERY_USAGE_SCREEN_ON_TIME,
- (int) allBatteryDiffData.getScreenOnTime());
- mMetricsFeatureProvider.action(
- mPrefContext,
- SettingsEnums.ACTION_BATTERY_USAGE_FOREGROUND_USAGE_TIME,
- (int) getTotalForegroundUsageTime());
- }
-
- private long getTotalForegroundUsageTime() {
- if (mBatteryUsageMap == null) {
- return 0;
- }
- final BatteryDiffData totalBatteryUsageDiffData =
- mBatteryUsageMap
- .get(BatteryChartViewModel.SELECTED_INDEX_ALL)
- .get(BatteryChartViewModel.SELECTED_INDEX_ALL);
- if (totalBatteryUsageDiffData == null) {
- return 0;
- }
- long totalValue = 0;
- for (final BatteryDiffEntry entry : totalBatteryUsageDiffData.getAppDiffEntryList()) {
- totalValue += entry.mForegroundUsageTimeInMs;
- }
- return totalValue;
- }
-
private boolean isBatteryLevelDataInOneDay() {
return mHourlyViewModels != null && mHourlyViewModels.size() == 1;
}
@@ -546,19 +483,6 @@
&& mHourlyChartIndex == BatteryChartViewModel.SELECTED_INDEX_ALL;
}
- private boolean isBatteryUsageMapNullOrEmpty() {
- if (mBatteryUsageMap == null) {
- return true;
- }
- BatteryDiffData allBatteryDiffData = mBatteryUsageMap
- .get(BatteryChartViewModel.SELECTED_INDEX_ALL)
- .get(BatteryChartViewModel.SELECTED_INDEX_ALL);
- // If all data is null or empty, each slot must be null or empty.
- return allBatteryDiffData == null
- || (allBatteryDiffData.getAppDiffEntryList().isEmpty()
- && allBatteryDiffData.getSystemDiffEntryList().isEmpty());
- }
-
@VisibleForTesting
static int getTotalHours(final BatteryLevelData batteryLevelData) {
if (batteryLevelData == null) {
@@ -609,10 +533,8 @@
return null;
}
for (BatteryDiffEntry entry : entries) {
- final BatteryHistEntry batteryHistEntry = entry.mBatteryHistEntry;
- if (batteryHistEntry != null
- && batteryHistEntry.mConsumerType == ConvertUtils.CONSUMER_TYPE_UID_BATTERY
- && batteryHistEntry.mUserId == userId
+ if (!entry.isSystemEntry()
+ && entry.mUserId == userId
&& packageName.equals(entry.getPackageName())) {
return entry;
}
diff --git a/src/com/android/settings/fuelgauge/batteryusage/BatteryChartView.java b/src/com/android/settings/fuelgauge/batteryusage/BatteryChartView.java
index 445a5d1..bb468fe 100644
--- a/src/com/android/settings/fuelgauge/batteryusage/BatteryChartView.java
+++ b/src/com/android/settings/fuelgauge/batteryusage/BatteryChartView.java
@@ -17,7 +17,9 @@
import static com.android.settings.Utils.formatPercentage;
import static com.android.settings.fuelgauge.batteryusage.BatteryChartViewModel.AxisLabelPosition.BETWEEN_TRAPEZOIDS;
+import static com.android.settingslib.fuelgauge.BatteryStatus.BATTERY_LEVEL_UNKNOWN;
+import static java.lang.Math.abs;
import static java.lang.Math.round;
import static java.util.Objects.requireNonNull;
@@ -29,6 +31,7 @@
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.util.ArraySet;
import android.util.AttributeSet;
@@ -61,6 +64,7 @@
private static final String TAG = "BatteryChartView";
private static final int DIVIDER_COLOR = Color.parseColor("#CDCCC5");
+ private static final int HORIZONTAL_DIVIDER_COUNT = 5;
/** A callback listener for selected group index is updated. */
public interface OnSelectListener {
@@ -73,6 +77,8 @@
private final Rect[] mPercentageBounds = new Rect[]{new Rect(), new Rect(), new Rect()};
private final List<Rect> mAxisLabelsBounds = new ArrayList<>();
private final Set<Integer> mLabelDrawnIndexes = new ArraySet<>();
+ private final int mLayoutDirection =
+ getContext().getResources().getConfiguration().getLayoutDirection();
private BatteryChartViewModel mViewModel;
private int mHoveredIndex = BatteryChartViewModel.SELECTED_INDEX_INVALID;
@@ -85,6 +91,15 @@
private int mTrapezoidHoverColor;
private int mDefaultTextColor;
private int mTextPadding;
+ private int mTransomIconSize;
+ private int mTransomTop;
+ private int mTransomViewHeight;
+ private int mTransomLineDefaultColor;
+ private int mTransomLineSelectedColor;
+ private float mTransomPadding;
+ private Drawable mTransomIcon;
+ private Paint mTransomLinePaint;
+ private Paint mTransomSelectedSlotPaint;
private Paint mDividerPaint;
private Paint mTrapezoidPaint;
private Paint mTextPaint;
@@ -118,8 +133,9 @@
return;
}
- Log.d(TAG, String.format("setViewModel(): size: %d, selectedIndex: %d.",
- viewModel.size(), viewModel.selectedIndex()));
+ Log.d(TAG, String.format(
+ "setViewModel(): size: %d, selectedIndex: %d, getHighlightSlotIndex: %d",
+ viewModel.size(), viewModel.selectedIndex(), viewModel.getHighlightSlotIndex()));
mViewModel = viewModel;
initializeAxisLabelsBounds();
initializeTrapezoidSlots(viewModel.size() - 1);
@@ -157,8 +173,13 @@
mPercentageBounds[index]);
}
// Updates the indent configurations.
- mIndent.top = mPercentageBounds[0].height();
- mIndent.right = mPercentageBounds[0].width() + mTextPadding;
+ mIndent.top = mPercentageBounds[0].height() + mTransomViewHeight;
+ final int textWidth = mPercentageBounds[0].width() + mTextPadding;
+ if (isRTL()) {
+ mIndent.left = textWidth;
+ } else {
+ mIndent.right = textWidth;
+ }
if (mViewModel != null) {
int maxTop = 0;
@@ -186,6 +207,7 @@
}
drawVerticalDividers(canvas);
drawTrapezoids(canvas);
+ drawTransomLine(canvas);
}
@Override
@@ -330,28 +352,64 @@
resources.getDimensionPixelSize(R.dimen.chartview_trapezoid_radius)));
// Initializes for drawing text information.
mTextPadding = resources.getDimensionPixelSize(R.dimen.chartview_text_padding);
+ // Initializes the padding top for drawing text information.
+ mTransomViewHeight = resources.getDimensionPixelSize(
+ R.dimen.chartview_transom_layout_height);
+ }
+
+ private void initializeTransomPaint() {
+ if (mTransomLinePaint != null && mTransomSelectedSlotPaint != null
+ && mTransomIcon != null) {
+ return;
+ }
+ // Initializes the transom line paint.
+ final Resources resources = getContext().getResources();
+ final int transomLineWidth = resources.getDimensionPixelSize(
+ R.dimen.chartview_transom_width);
+ final int transomRadius = resources.getDimensionPixelSize(R.dimen.chartview_transom_radius);
+ mTransomPadding = transomRadius * .5f;
+ mTransomTop = resources.getDimensionPixelSize(R.dimen.chartview_transom_padding_top);
+ mTransomLineDefaultColor = Utils.getDisabled(mContext, DIVIDER_COLOR);
+ mTransomLineSelectedColor = resources.getColor(
+ R.color.color_battery_anomaly_yellow_selector);
+ final int slotHighlightColor = Utils.getDisabled(mContext, mTransomLineSelectedColor);
+ mTransomIconSize = resources.getDimensionPixelSize(R.dimen.chartview_transom_icon_size);
+ mTransomLinePaint = new Paint();
+ mTransomLinePaint.setAntiAlias(true);
+ mTransomLinePaint.setStyle(Paint.Style.STROKE);
+ mTransomLinePaint.setStrokeWidth(transomLineWidth);
+ mTransomLinePaint.setStrokeCap(Paint.Cap.ROUND);
+ mTransomLinePaint.setPathEffect(new CornerPathEffect(transomRadius));
+ mTransomSelectedSlotPaint = new Paint();
+ mTransomSelectedSlotPaint.setAntiAlias(true);
+ mTransomSelectedSlotPaint.setColor(slotHighlightColor);
+ mTransomSelectedSlotPaint.setStyle(Paint.Style.FILL);
+ // Get the companion icon beside transom line
+ mTransomIcon = getResources().getDrawable(R.drawable.ic_battery_tips_warning_icon);
}
private void drawHorizontalDividers(Canvas canvas) {
- final int width = getWidth() - mIndent.right;
+ final int width = getWidth() - abs(mIndent.width());
final int height = getHeight() - mIndent.top - mIndent.bottom;
- // Draws the top divider line for 100% curve.
- float offsetY = mIndent.top + mDividerWidth * .5f;
+ final float topOffsetY = mIndent.top + mDividerWidth * .5f;
+ final float bottomOffsetY = mIndent.top + (height - mDividerHeight - mDividerWidth * .5f);
+ final float availableSpace = bottomOffsetY - topOffsetY;
+
mDividerPaint.setColor(DIVIDER_COLOR);
- canvas.drawLine(0, offsetY, width, offsetY, mDividerPaint);
- drawPercentage(canvas, /*index=*/ 0, offsetY);
+ final float dividerOffsetUnit =
+ availableSpace / (float) (HORIZONTAL_DIVIDER_COUNT - 1);
- // Draws the center divider line for 50% curve.
- final float availableSpace =
- height - mDividerWidth * 2 - mTrapezoidVOffset - mDividerHeight;
- offsetY = mIndent.top + mDividerWidth + availableSpace * .5f;
- canvas.drawLine(0, offsetY, width, offsetY, mDividerPaint);
- drawPercentage(canvas, /*index=*/ 1, offsetY);
+ // Draws 5 divider lines.
+ for (int index = 0; index < HORIZONTAL_DIVIDER_COUNT; index++) {
+ float offsetY = topOffsetY + dividerOffsetUnit * index;
+ canvas.drawLine(mIndent.left, offsetY,
+ mIndent.left + width, offsetY, mDividerPaint);
- // Draws the bottom divider line for 0% curve.
- offsetY = mIndent.top + (height - mDividerHeight - mDividerWidth * .5f);
- canvas.drawLine(0, offsetY, width, offsetY, mDividerPaint);
- drawPercentage(canvas, /*index=*/ 2, offsetY);
+ // Draws percentage text only for 100% / 50% / 0%
+ if (index % 2 == 0) {
+ drawPercentage(canvas, /*index=*/ (index + 1) / 2, offsetY);
+ }
+ }
}
private void drawPercentage(Canvas canvas, int index, float offsetY) {
@@ -360,14 +418,14 @@
mTextPaint.setColor(mDefaultTextColor);
canvas.drawText(
mPercentages[index],
- getWidth(),
+ isRTL() ? mIndent.left - mTextPadding : getWidth(),
offsetY + mPercentageBounds[index].height() * .5f,
mTextPaint);
}
}
private void drawVerticalDividers(Canvas canvas) {
- final int width = getWidth() - mIndent.right;
+ final int width = getWidth() - abs(mIndent.width());
final int dividerCount = mTrapezoidSlots.length + 1;
final float dividerSpace = dividerCount * mDividerWidth;
final float unitWidth = (width - dividerSpace) / (float) mTrapezoidSlots.length;
@@ -382,7 +440,7 @@
case CENTER_OF_TRAPEZOIDS:
axisLabelDisplayAreas = getAxisLabelDisplayAreas(
/* size= */ mViewModel.size() - 1,
- /* baselineX= */ mDividerWidth + unitWidth * .5f,
+ /* baselineX= */ mIndent.left + mDividerWidth + unitWidth * .5f,
/* offsetX= */ mDividerWidth + unitWidth,
baselineY,
/* shiftFirstAndLast= */ false);
@@ -391,7 +449,7 @@
default:
axisLabelDisplayAreas = getAxisLabelDisplayAreas(
/* size= */ mViewModel.size(),
- /* baselineX= */ mDividerWidth * .5f,
+ /* baselineX= */ mIndent.left + mDividerWidth * .5f,
/* offsetX= */ mDividerWidth + unitWidth,
baselineY,
/* shiftFirstAndLast= */ true);
@@ -400,7 +458,7 @@
drawAxisLabels(canvas, axisLabelDisplayAreas, baselineY);
}
// Draws each vertical dividers.
- float startX = mDividerWidth * .5f;
+ float startX = mDividerWidth * .5f + mIndent.left;
for (int index = 0; index < dividerCount; index++) {
float dividerY = bottomY;
if (mViewModel.axisLabelPosition() == BETWEEN_TRAPEZOIDS
@@ -414,8 +472,9 @@
final float nextX = startX + mDividerWidth + unitWidth;
// Updates the trapezoid slots for drawing.
if (index < mTrapezoidSlots.length) {
- mTrapezoidSlots[index].mLeft = round(startX + trapezoidSlotOffset);
- mTrapezoidSlots[index].mRight = round(nextX - trapezoidSlotOffset);
+ final int trapezoidIndex = isRTL() ? mTrapezoidSlots.length - index - 1 : index;
+ mTrapezoidSlots[trapezoidIndex].mLeft = round(startX + trapezoidSlotOffset);
+ mTrapezoidSlots[trapezoidIndex].mRight = round(nextX - trapezoidSlotOffset);
}
startX = nextX;
}
@@ -507,10 +566,20 @@
return displayAreas[leftIndex].right + mTextPadding * 2.3f > displayAreas[rightIndex].left;
}
+ private boolean isRTL() {
+ return mLayoutDirection == View.LAYOUT_DIRECTION_RTL;
+ }
+
private void drawAxisLabelText(
- Canvas canvas, final int index, final Rect displayArea, final float baselineY) {
+ Canvas canvas, int index, final Rect displayArea, final float baselineY) {
mTextPaint.setColor(mTrapezoidSolidColor);
mTextPaint.setTextAlign(Paint.Align.CENTER);
+ // Reverse the sort of axis labels for RTL
+ if (isRTL()) {
+ index = mViewModel.axisLabelPosition() == BETWEEN_TRAPEZOIDS
+ ? mViewModel.size() - index - 1 // for hourly
+ : mViewModel.size() - index - 2; // for daily
+ }
canvas.drawText(
mViewModel.getText(index),
displayArea.centerX(),
@@ -546,10 +615,16 @@
mHoveredIndex);
mTrapezoidPaint.setColor(isHoverState ? mTrapezoidHoverColor : trapezoidColor);
- final float leftTop = round(
+ float leftTop = round(
trapezoidBottom - requireNonNull(mViewModel.getLevel(index)) * unitHeight);
- final float rightTop = round(trapezoidBottom
+ float rightTop = round(trapezoidBottom
- requireNonNull(mViewModel.getLevel(index + 1)) * unitHeight);
+ // Mirror the shape of the trapezoid for RTL
+ if (isRTL()) {
+ float temp = leftTop;
+ leftTop = rightTop;
+ rightTop = temp;
+ }
trapezoidPath.reset();
trapezoidPath.moveTo(mTrapezoidSlots[index].mLeft, trapezoidBottom);
trapezoidPath.lineTo(mTrapezoidSlots[index].mLeft, leftTop);
@@ -563,6 +638,50 @@
}
}
+ private boolean isHighlightSlotValid() {
+ return mViewModel != null && mViewModel.getHighlightSlotIndex()
+ != BatteryChartViewModel.SELECTED_INDEX_INVALID;
+ }
+
+ private void drawTransomLine(Canvas canvas) {
+ if (!isHighlightSlotValid()) {
+ return;
+ }
+ initializeTransomPaint();
+ // Draw the whole transom line and a warning icon
+ mTransomLinePaint.setColor(mTransomLineDefaultColor);
+ final int width = getWidth() - abs(mIndent.width());
+ final float transomOffset = mTrapezoidHOffset + mDividerWidth * .5f + mTransomPadding;
+ final float trapezoidBottom = getHeight() - mIndent.bottom - mDividerHeight - mDividerWidth
+ - mTrapezoidVOffset;
+ canvas.drawLine(mIndent.left + transomOffset, mTransomTop,
+ mIndent.left + width - transomOffset, mTransomTop,
+ mTransomLinePaint);
+ drawTransomIcon(canvas);
+ // Draw selected segment of transom line and a highlight slot
+ mTransomLinePaint.setColor(mTransomLineSelectedColor);
+ final int index = mViewModel.getHighlightSlotIndex();
+ final float startX = mTrapezoidSlots[index].mLeft;
+ final float endX = mTrapezoidSlots[index].mRight;
+ canvas.drawLine(startX + mTransomPadding, mTransomTop,
+ endX - mTransomPadding, mTransomTop,
+ mTransomLinePaint);
+ canvas.drawRect(startX, mTransomTop, endX, trapezoidBottom,
+ mTransomSelectedSlotPaint);
+ }
+
+ private void drawTransomIcon(Canvas canvas) {
+ if (mTransomIcon == null) {
+ return;
+ }
+ final int left = isRTL()
+ ? mIndent.left - mTextPadding - mTransomIconSize
+ : getWidth() - abs(mIndent.width()) + mTextPadding;
+ mTransomIcon.setBounds(left, mTransomTop - mTransomIconSize / 2,
+ left + mTransomIconSize, mTransomTop + mTransomIconSize / 2);
+ mTransomIcon.draw(canvas);
+ }
+
// Searches the corresponding trapezoid index from x location.
private int getTrapezoidIndex(float x) {
if (mTrapezoidSlots == null) {
@@ -587,8 +706,8 @@
private static boolean isTrapezoidValid(
@NonNull BatteryChartViewModel viewModel, int trapezoidIndex) {
- return viewModel.getLevel(trapezoidIndex) != null
- && viewModel.getLevel(trapezoidIndex + 1) != null;
+ return viewModel.getLevel(trapezoidIndex) != BATTERY_LEVEL_UNKNOWN
+ && viewModel.getLevel(trapezoidIndex + 1) != BATTERY_LEVEL_UNKNOWN;
}
private static boolean isTrapezoidIndexValid(
diff --git a/src/com/android/settings/fuelgauge/batteryusage/BatteryChartViewModel.java b/src/com/android/settings/fuelgauge/batteryusage/BatteryChartViewModel.java
index f58d241..bf8a771 100644
--- a/src/com/android/settings/fuelgauge/batteryusage/BatteryChartViewModel.java
+++ b/src/com/android/settings/fuelgauge/batteryusage/BatteryChartViewModel.java
@@ -55,6 +55,7 @@
private final String[] mFullTexts;
private int mSelectedIndex = SELECTED_INDEX_ALL;
+ private int mHighlightSlotIndex = SELECTED_INDEX_INVALID;
BatteryChartViewModel(@NonNull List<Integer> levels, @NonNull List<Long> timestamps,
@NonNull AxisLabelPosition axisLabelPosition,
@@ -106,6 +107,14 @@
mSelectedIndex = index;
}
+ public int getHighlightSlotIndex() {
+ return mHighlightSlotIndex;
+ }
+
+ public void setHighlightSlotIndex(int index) {
+ mHighlightSlotIndex = index;
+ }
+
@Override
public int hashCode() {
return Objects.hash(mLevels, mTimestamps, mSelectedIndex, mAxisLabelPosition);
diff --git a/src/com/android/settings/fuelgauge/batteryusage/BatteryDiffData.java b/src/com/android/settings/fuelgauge/batteryusage/BatteryDiffData.java
index 47ae568..53861e3 100644
--- a/src/com/android/settings/fuelgauge/batteryusage/BatteryDiffData.java
+++ b/src/com/android/settings/fuelgauge/batteryusage/BatteryDiffData.java
@@ -16,6 +16,8 @@
package com.android.settings.fuelgauge.batteryusage;
+import static com.android.settings.fuelgauge.batteryusage.ConvertUtils.utcToLocalTimeForLogging;
+
import android.content.Context;
import android.os.BatteryConsumer;
@@ -34,6 +36,10 @@
public class BatteryDiffData {
static final double SMALL_PERCENTAGE_THRESHOLD = 1f;
+ private final long mStartTimestamp;
+ private final long mEndTimestamp;
+ private final int mStartBatteryLevel;
+ private final int mEndBatteryLevel;
private final long mScreenOnTime;
private final List<BatteryDiffEntry> mAppEntries;
private final List<BatteryDiffEntry> mSystemEntries;
@@ -41,12 +47,20 @@
/** Constructor for the diff entries. */
public BatteryDiffData(
final Context context,
+ final long startTimestamp,
+ final long endTimestamp,
+ final int startBatteryLevel,
+ final int endBatteryLevel,
final long screenOnTime,
final @NonNull List<BatteryDiffEntry> appDiffEntries,
final @NonNull List<BatteryDiffEntry> systemDiffEntries,
final @NonNull Set<String> systemAppsPackageNames,
final @NonNull Set<Integer> systemAppsUids,
final boolean isAccumulated) {
+ mStartTimestamp = startTimestamp;
+ mEndTimestamp = endTimestamp;
+ mStartBatteryLevel = startBatteryLevel;
+ mEndBatteryLevel = endBatteryLevel;
mScreenOnTime = screenOnTime;
mAppEntries = appDiffEntries;
mSystemEntries = systemDiffEntries;
@@ -63,18 +77,48 @@
processAndSortEntries(mSystemEntries);
}
- public long getScreenOnTime() {
+ long getStartTimestamp() {
+ return mStartTimestamp;
+ }
+
+ long getEndTimestamp() {
+ return mEndTimestamp;
+ }
+
+ int getStartBatteryLevel() {
+ return mStartBatteryLevel;
+ }
+
+ int getEndBatteryLevel() {
+ return mEndBatteryLevel;
+ }
+
+ long getScreenOnTime() {
return mScreenOnTime;
}
- public List<BatteryDiffEntry> getAppDiffEntryList() {
+ List<BatteryDiffEntry> getAppDiffEntryList() {
return mAppEntries;
}
- public List<BatteryDiffEntry> getSystemDiffEntryList() {
+ List<BatteryDiffEntry> getSystemDiffEntryList() {
return mSystemEntries;
}
+ @Override
+ public String toString() {
+ return new StringBuilder("BatteryDiffData{")
+ .append("startTimestamp:" + utcToLocalTimeForLogging(mStartTimestamp))
+ .append("|endTimestamp:" + utcToLocalTimeForLogging(mEndTimestamp))
+ .append("|startLevel:" + mStartBatteryLevel)
+ .append("|endLevel:" + mEndBatteryLevel)
+ .append("|screenOnTime:" + mScreenOnTime)
+ .append("|appEntries.size:" + mAppEntries.size())
+ .append("|systemEntries.size:" + mSystemEntries.size())
+ .append("}")
+ .toString();
+ }
+
/** Removes fake usage data and hidden packages. */
private void purgeBatteryDiffData(final PowerUsageFeatureProvider featureProvider) {
purgeBatteryDiffData(featureProvider, mAppEntries);
@@ -109,7 +153,7 @@
final long screenOnTimeInMs = entry.mScreenOnTimeInMs;
final double comsumePower = entry.mConsumePower;
final String packageName = entry.getPackageName();
- final Integer componentId = entry.mBatteryHistEntry.mDrainType;
+ final Integer componentId = entry.mComponentId;
if ((screenOnTimeInMs < screenOnTimeThresholdInMs
&& comsumePower < consumePowerThreshold)
|| ConvertUtils.FAKE_PACKAGE_NAME.equals(packageName)
@@ -130,14 +174,16 @@
final @NonNull Set<Integer> systemAppsUids,
final @NonNull List<BatteryDiffEntry> appEntries) {
final List<String> systemAppsAllowlist = featureProvider.getSystemAppsAllowlist();
- BatteryDiffEntry.SystemAppsBatteryDiffEntry systemAppsDiffEntry = null;
+ BatteryDiffEntry systemAppsDiffEntry = null;
final Iterator<BatteryDiffEntry> appListIterator = appEntries.iterator();
while (appListIterator.hasNext()) {
final BatteryDiffEntry batteryDiffEntry = appListIterator.next();
if (needsCombineInSystemApp(batteryDiffEntry, systemAppsAllowlist,
systemAppsPackageNames, systemAppsUids)) {
if (systemAppsDiffEntry == null) {
- systemAppsDiffEntry = new BatteryDiffEntry.SystemAppsBatteryDiffEntry(context);
+ systemAppsDiffEntry = new BatteryDiffEntry(context,
+ BatteryDiffEntry.SYSTEM_APPS_KEY, BatteryDiffEntry.SYSTEM_APPS_KEY,
+ ConvertUtils.CONSUMER_TYPE_UID_BATTERY);
}
systemAppsDiffEntry.mConsumePower += batteryDiffEntry.mConsumePower;
systemAppsDiffEntry.mForegroundUsageTimeInMs +=
@@ -159,17 +205,18 @@
final Set<Integer> othersSystemComponentSet = featureProvider.getOthersSystemComponentSet();
final Set<String> othersCustomComponentNameSet =
featureProvider.getOthersCustomComponentNameSet();
- BatteryDiffEntry.OthersBatteryDiffEntry othersDiffEntry = null;
+ BatteryDiffEntry othersDiffEntry = null;
final Iterator<BatteryDiffEntry> systemListIterator = systemEntries.iterator();
while (systemListIterator.hasNext()) {
final BatteryDiffEntry batteryDiffEntry = systemListIterator.next();
- final int componentId = batteryDiffEntry.mBatteryHistEntry.mDrainType;
+ final int componentId = batteryDiffEntry.mComponentId;
if (othersSystemComponentSet.contains(componentId) || (
componentId >= BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID
&& othersCustomComponentNameSet.contains(
batteryDiffEntry.getAppLabel()))) {
if (othersDiffEntry == null) {
- othersDiffEntry = new BatteryDiffEntry.OthersBatteryDiffEntry(context);
+ othersDiffEntry = new BatteryDiffEntry(context, BatteryDiffEntry.OTHERS_KEY,
+ BatteryDiffEntry.OTHERS_KEY, ConvertUtils.CONSUMER_TYPE_SYSTEM_BATTERY);
}
othersDiffEntry.mConsumePower += batteryDiffEntry.mConsumePower;
othersDiffEntry.setTotalConsumePower(
@@ -188,7 +235,7 @@
final @NonNull List<String> systemAppsAllowlist,
final @NonNull Set<String> systemAppsPackageNames,
final @NonNull Set<Integer> systemAppsUids) {
- if (batteryDiffEntry.mBatteryHistEntry.mIsHidden) {
+ if (batteryDiffEntry.mIsHidden) {
return true;
}
@@ -201,7 +248,7 @@
return true;
}
- int uid = (int) batteryDiffEntry.mBatteryHistEntry.mUid;
+ int uid = (int) batteryDiffEntry.mUid;
return systemAppsPackageNames.contains(packageName) || systemAppsUids.contains(uid);
}
diff --git a/src/com/android/settings/fuelgauge/batteryusage/BatteryDiffEntry.java b/src/com/android/settings/fuelgauge/batteryusage/BatteryDiffEntry.java
index 2ed9196..b284ea5 100644
--- a/src/com/android/settings/fuelgauge/batteryusage/BatteryDiffEntry.java
+++ b/src/com/android/settings/fuelgauge/batteryusage/BatteryDiffEntry.java
@@ -15,7 +15,6 @@
*/
package com.android.settings.fuelgauge.batteryusage;
-import android.content.ContentValues;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
@@ -24,6 +23,7 @@
import android.os.UserHandle;
import android.os.UserManager;
import android.util.Log;
+import android.util.Pair;
import androidx.annotation.VisibleForTesting;
@@ -45,12 +45,29 @@
static final Map<String, BatteryEntry.NameAndIcon> sResourceCache = new HashMap<>();
// Whether a specific item is valid to launch restriction page?
@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
- public static final Map<String, Boolean> sValidForRestriction = new HashMap<>();
-
+ static final Map<String, Boolean> sValidForRestriction = new HashMap<>();
/** A comparator for {@link BatteryDiffEntry} based on the sorting key. */
- public static final Comparator<BatteryDiffEntry> COMPARATOR =
+ static final Comparator<BatteryDiffEntry> COMPARATOR =
(a, b) -> Double.compare(b.getSortingKey(), a.getSortingKey());
+ static final String SYSTEM_APPS_KEY = "A|SystemApps";
+ static final String OTHERS_KEY = "S|Others";
+ // key -> (label_id, icon_id)
+ private static final Map<String, Pair<Integer, Integer>> SPECIAL_ENTRY_MAP = Map.of(
+ SYSTEM_APPS_KEY,
+ Pair.create(R.string.battery_usage_system_apps, R.drawable.ic_power_system),
+ OTHERS_KEY,
+ Pair.create(R.string.battery_usage_others,
+ R.drawable.ic_settings_battery_usage_others));
+
+ public long mUid;
+ public long mUserId;
+ public String mKey;
+ public boolean mIsHidden;
+ public int mComponentId;
+ public String mLegacyPackageName;
+ public String mLegacyLabel;
+ public int mConsumerType;
public long mForegroundUsageTimeInMs;
public long mBackgroundUsageTimeInMs;
public long mScreenOnTimeInMs;
@@ -59,8 +76,6 @@
public double mForegroundServiceUsageConsumePower;
public double mBackgroundUsageConsumePower;
public double mCachedUsageConsumePower;
- // A BatteryHistEntry corresponding to this diff usage data.
- public final BatteryHistEntry mBatteryHistEntry;
protected Context mContext;
@@ -83,6 +98,14 @@
public BatteryDiffEntry(
Context context,
+ long uid,
+ long userId,
+ String key,
+ boolean isHidden,
+ int componentId,
+ String legacyPackageName,
+ String legacyLabel,
+ int consumerType,
long foregroundUsageTimeInMs,
long backgroundUsageTimeInMs,
long screenOnTimeInMs,
@@ -90,21 +113,36 @@
double foregroundUsageConsumePower,
double foregroundServiceUsageConsumePower,
double backgroundUsageConsumePower,
- double cachedUsageConsumePower,
- BatteryHistEntry batteryHistEntry) {
+ double cachedUsageConsumePower) {
mContext = context;
+ mUid = uid;
+ mUserId = userId;
+ mKey = key;
+ mIsHidden = isHidden;
+ mComponentId = componentId;
+ mLegacyPackageName = legacyPackageName;
+ mLegacyLabel = legacyLabel;
+ mConsumerType = consumerType;
+ mForegroundUsageTimeInMs = foregroundUsageTimeInMs;
+ mBackgroundUsageTimeInMs = backgroundUsageTimeInMs;
+ mScreenOnTimeInMs = screenOnTimeInMs;
mConsumePower = consumePower;
mForegroundUsageConsumePower = foregroundUsageConsumePower;
mForegroundServiceUsageConsumePower = foregroundServiceUsageConsumePower;
mBackgroundUsageConsumePower = backgroundUsageConsumePower;
mCachedUsageConsumePower = cachedUsageConsumePower;
- mForegroundUsageTimeInMs = foregroundUsageTimeInMs;
- mBackgroundUsageTimeInMs = backgroundUsageTimeInMs;
- mScreenOnTimeInMs = screenOnTimeInMs;
- mBatteryHistEntry = batteryHistEntry;
mUserManager = context.getSystemService(UserManager.class);
}
+ public BatteryDiffEntry(Context context, String key, String legacyLabel, int consumerType) {
+ this(context, /*uid=*/ 0, /*userId=*/ 0, key, /*isHidden=*/ false, /*componentId=*/ -1,
+ /*legacyPackageName=*/ null, legacyLabel, consumerType,
+ /*foregroundUsageTimeInMs=*/ 0, /*backgroundUsageTimeInMs=*/ 0,
+ /*screenOnTimeInMs=*/ 0, /*consumePower=*/ 0, /*foregroundUsageConsumePower=*/ 0,
+ /*foregroundServiceUsageConsumePower=*/ 0, /*backgroundUsageConsumePower=*/ 0,
+ /*cachedUsageConsumePower=*/ 0);
+ }
+
/** Sets the total consumed power in a specific time slot. */
public void setTotalConsumePower(double totalConsumePower) {
mTotalConsumePower = totalConsumePower;
@@ -135,13 +173,22 @@
/** Gets the key for sorting */
public double getSortingKey() {
- return getPercentage() + getAdjustPercentageOffset();
+ return getKey() != null && SPECIAL_ENTRY_MAP.containsKey(getKey())
+ ? -1 : getPercentage() + getAdjustPercentageOffset();
}
/** Clones a new instance. */
public BatteryDiffEntry clone() {
return new BatteryDiffEntry(
this.mContext,
+ this.mUid,
+ this.mUserId,
+ this.mKey,
+ this.mIsHidden,
+ this.mComponentId,
+ this.mLegacyPackageName,
+ this.mLegacyLabel,
+ this.mConsumerType,
this.mForegroundUsageTimeInMs,
this.mBackgroundUsageTimeInMs,
this.mScreenOnTimeInMs,
@@ -149,17 +196,14 @@
this.mForegroundUsageConsumePower,
this.mForegroundServiceUsageConsumePower,
this.mBackgroundUsageConsumePower,
- this.mCachedUsageConsumePower,
- this.mBatteryHistEntry /*same instance*/);
+ this.mCachedUsageConsumePower);
}
/** Gets the app label name for this entry. */
public String getAppLabel() {
loadLabelAndIcon();
- // Returns default applicationn label if we cannot find it.
- return mAppLabel == null || mAppLabel.length() == 0
- ? mBatteryHistEntry.mAppLabel
- : mAppLabel;
+ // Returns default application label if we cannot find it.
+ return mAppLabel == null || mAppLabel.length() == 0 ? mLegacyLabel : mAppLabel;
}
/** Gets the app icon {@link Drawable} for this entry. */
@@ -179,7 +223,7 @@
/** Gets the searching package name for UID battery type. */
public String getPackageName() {
final String packageName = mDefaultPackageName != null
- ? mDefaultPackageName : mBatteryHistEntry.mPackageName;
+ ? mDefaultPackageName : mLegacyPackageName;
if (packageName == null) {
return packageName;
}
@@ -198,10 +242,10 @@
/** Whether the current BatteryDiffEntry is system component or not. */
public boolean isSystemEntry() {
- if (mBatteryHistEntry.mIsHidden) {
+ if (mIsHidden) {
return false;
}
- switch (mBatteryHistEntry.mConsumerType) {
+ switch (mConsumerType) {
case ConvertUtils.CONSUMER_TYPE_USER_BATTERY:
case ConvertUtils.CONSUMER_TYPE_SYSTEM_BATTERY:
return true;
@@ -236,12 +280,22 @@
updateRestrictionFlagState();
sValidForRestriction.put(getKey(), Boolean.valueOf(mValidForRestriction));
+ if (getKey() != null && SPECIAL_ENTRY_MAP.containsKey(getKey())) {
+ Pair<Integer, Integer> pair = SPECIAL_ENTRY_MAP.get(getKey());
+ mAppLabel = mContext.getString(pair.first);
+ mAppIconId = pair.second;
+ mAppIcon = mContext.getDrawable(mAppIconId);
+ sResourceCache.put(
+ getKey(),
+ new BatteryEntry.NameAndIcon(mAppLabel, mAppIcon, mAppIconId));
+ return;
+ }
+
// Loads application icon and label based on consumer type.
- switch (mBatteryHistEntry.mConsumerType) {
+ switch (mConsumerType) {
case ConvertUtils.CONSUMER_TYPE_USER_BATTERY:
final BatteryEntry.NameAndIcon nameAndIconForUser =
- BatteryEntry.getNameAndIconFromUserId(
- mContext, (int) mBatteryHistEntry.mUserId);
+ BatteryEntry.getNameAndIconFromUserId(mContext, (int) mUserId);
if (nameAndIconForUser != null) {
mAppIcon = nameAndIconForUser.mIcon;
mAppLabel = nameAndIconForUser.mName;
@@ -252,8 +306,7 @@
break;
case ConvertUtils.CONSUMER_TYPE_SYSTEM_BATTERY:
final BatteryEntry.NameAndIcon nameAndIconForSystem =
- BatteryEntry.getNameAndIconFromPowerComponent(
- mContext, mBatteryHistEntry.mDrainType);
+ BatteryEntry.getNameAndIconFromPowerComponent(mContext, mComponentId);
if (nameAndIconForSystem != null) {
mAppLabel = nameAndIconForSystem.mName;
if (nameAndIconForSystem.mIconId != 0) {
@@ -283,12 +336,12 @@
}
String getKey() {
- return mBatteryHistEntry.getKey();
+ return mKey;
}
@VisibleForTesting
void updateRestrictionFlagState() {
- if (!mBatteryHistEntry.isAppEntry()) {
+ if (isSystemEntry()) {
mValidForRestriction = false;
return;
}
@@ -348,7 +401,7 @@
return;
}
- final int uid = (int) mBatteryHistEntry.mUid;
+ final int uid = (int) mUid;
final String[] packages = packageManager.getPackagesForUid(uid);
// Loads special defined application label and icon if available.
if (packages == null || packages.length == 0) {
@@ -394,8 +447,7 @@
StringUtil.formatElapsedTime(mContext, (double) mScreenOnTimeInMs,
/*withSeconds=*/ true, /*collapseTimeUnit=*/ false)))
.append(String.format("\n\tpackage:%s|%s uid:%d userId:%d",
- mBatteryHistEntry.mPackageName, getPackageName(),
- mBatteryHistEntry.mUid, mBatteryHistEntry.mUserId));
+ mLegacyPackageName, getPackageName(), mUid, mUserId));
return builder.toString();
}
@@ -406,130 +458,8 @@
}
private Drawable getBadgeIconForUser(Drawable icon) {
- final int userId = UserHandle.getUserId((int) mBatteryHistEntry.mUid);
+ final int userId = UserHandle.getUserId((int) mUid);
return userId == UserHandle.USER_OWNER ? icon :
mUserManager.getBadgedIconForUser(icon, new UserHandle(userId));
}
-
- /** Specific battery diff entry for system apps. */
- static class SystemAppsBatteryDiffEntry extends BatteryDiffEntry {
- SystemAppsBatteryDiffEntry(Context context) {
- super(context,
- /*foregroundUsageTimeInMs=*/ 0,
- /*backgroundUsageTimeInMs=*/ 0,
- /*screenOnTimeInMs=*/ 0,
- /*consumePower=*/ 0,
- /*foregroundUsageConsumePower=*/ 0,
- /*foregroundServiceUsageConsumePower=*/ 0,
- /*backgroundUsageConsumePower=*/ 0,
- /*cachedUsageConsumePower=*/ 0,
- new BatteryHistEntry(new ContentValues()));
- }
-
- @Override
- public String getKey() {
- return "A|SystemApps";
- }
-
- @Override
- public String getAppLabel() {
- return mContext.getString(R.string.battery_usage_system_apps);
- }
-
- @Override
- public Drawable getAppIcon() {
- return mContext.getDrawable(R.drawable.ic_power_system);
- }
-
- @Override
- public boolean validForRestriction() {
- return false;
- }
-
- @Override
- public boolean isSystemEntry() {
- return false;
- }
-
- @Override
- public double getSortingKey() {
- // Always on the bottom of the app list.
- return -1;
- }
-
- @Override
- public BatteryDiffEntry clone() {
- SystemAppsBatteryDiffEntry newEntry = new SystemAppsBatteryDiffEntry(this.mContext);
- newEntry.mForegroundUsageTimeInMs = this.mForegroundUsageTimeInMs;
- newEntry.mBackgroundUsageTimeInMs = this.mBackgroundUsageTimeInMs;
- newEntry.mScreenOnTimeInMs = this.mScreenOnTimeInMs;
- newEntry.mConsumePower = this.mConsumePower;
- newEntry.mForegroundUsageConsumePower = this.mForegroundUsageConsumePower;
- newEntry.mForegroundServiceUsageConsumePower = this.mForegroundServiceUsageConsumePower;
- newEntry.mBackgroundUsageConsumePower = this.mBackgroundUsageConsumePower;
- newEntry.mCachedUsageConsumePower = this.mCachedUsageConsumePower;
- return newEntry;
- }
- }
-
- /** Specific battery diff entry for others. */
- static class OthersBatteryDiffEntry extends BatteryDiffEntry {
- OthersBatteryDiffEntry(Context context) {
- super(context,
- /*foregroundUsageTimeInMs=*/ 0,
- /*backgroundUsageTimeInMs=*/ 0,
- /*screenOnTimeInMs=*/ 0,
- /*consumePower=*/ 0,
- /*foregroundUsageConsumePower=*/ 0,
- /*foregroundServiceUsageConsumePower=*/ 0,
- /*backgroundUsageConsumePower=*/ 0,
- /*cachedUsageConsumePower=*/ 0,
- new BatteryHistEntry(new ContentValues()));
- }
-
- @Override
- public String getKey() {
- return "S|Others";
- }
-
- @Override
- public String getAppLabel() {
- return mContext.getString(R.string.battery_usage_others);
- }
-
- @Override
- public Drawable getAppIcon() {
- return mContext.getDrawable(R.drawable.ic_settings_battery_usage_others);
- }
-
- @Override
- public boolean validForRestriction() {
- return false;
- }
-
- @Override
- public boolean isSystemEntry() {
- return true;
- }
-
- @Override
- public double getSortingKey() {
- // Always on the bottom of the system list.
- return -1;
- }
-
- @Override
- public BatteryDiffEntry clone() {
- OthersBatteryDiffEntry newEntry = new OthersBatteryDiffEntry(this.mContext);
- newEntry.mForegroundUsageTimeInMs = this.mForegroundUsageTimeInMs;
- newEntry.mBackgroundUsageTimeInMs = this.mBackgroundUsageTimeInMs;
- newEntry.mScreenOnTimeInMs = this.mScreenOnTimeInMs;
- newEntry.mConsumePower = this.mConsumePower;
- newEntry.mForegroundUsageConsumePower = this.mForegroundUsageConsumePower;
- newEntry.mForegroundServiceUsageConsumePower = this.mForegroundServiceUsageConsumePower;
- newEntry.mBackgroundUsageConsumePower = this.mBackgroundUsageConsumePower;
- newEntry.mCachedUsageConsumePower = this.mCachedUsageConsumePower;
- return newEntry;
- }
- }
}
diff --git a/src/com/android/settings/fuelgauge/batteryusage/BatteryEntry.java b/src/com/android/settings/fuelgauge/batteryusage/BatteryEntry.java
index 7f86b7c..86538ee 100644
--- a/src/com/android/settings/fuelgauge/batteryusage/BatteryEntry.java
+++ b/src/com/android/settings/fuelgauge/batteryusage/BatteryEntry.java
@@ -252,33 +252,6 @@
return mPowerComponentId;
}
- void getQuickNameIconForUid(
- final int uid, final String[] packages, final boolean loadDataInBackground) {
- // Locale sync to system config in Settings
- final Locale locale = Locale.getDefault();
- if (sCurrentLocale != locale) {
- clearUidCache();
- sCurrentLocale = locale;
- }
-
- final String uidString = Integer.toString(uid);
- if (sUidCache.containsKey(uidString)) {
- UidToDetail utd = sUidCache.get(uidString);
- mDefaultPackageName = utd.mPackageName;
- mName = utd.mName;
- mIcon = utd.mIcon;
- return;
- }
-
- if (packages == null || packages.length == 0) {
- final NameAndIcon nameAndIcon = getNameAndIconFromUid(mContext, mName, uid);
- mIcon = nameAndIcon.mIcon;
- mName = nameAndIcon.mName;
- } else {
- mIcon = mContext.getPackageManager().getDefaultActivityIcon();
- }
- }
-
/** Loads the app label and icon image and stores into the cache. */
public static NameAndIcon loadNameAndIcon(
Context context,
diff --git a/src/com/android/settings/fuelgauge/batteryusage/BatteryHistEntry.java b/src/com/android/settings/fuelgauge/batteryusage/BatteryHistEntry.java
index 827f0fc..6f78566 100644
--- a/src/com/android/settings/fuelgauge/batteryusage/BatteryHistEntry.java
+++ b/src/com/android/settings/fuelgauge/batteryusage/BatteryHistEntry.java
@@ -169,21 +169,6 @@
return mIsValidEntry;
}
- /** Whether this {@link BatteryHistEntry} is user consumer or not. */
- public boolean isUserEntry() {
- return mConsumerType == ConvertUtils.CONSUMER_TYPE_USER_BATTERY;
- }
-
- /** Whether this {@link BatteryHistEntry} is app consumer or not. */
- public boolean isAppEntry() {
- return mConsumerType == ConvertUtils.CONSUMER_TYPE_UID_BATTERY;
- }
-
- /** Whether this {@link BatteryHistEntry} is system consumer or not. */
- public boolean isSystemEntry() {
- return mConsumerType == ConvertUtils.CONSUMER_TYPE_SYSTEM_BATTERY;
- }
-
/** Gets an identifier to represent this {@link BatteryHistEntry}. */
public String getKey() {
if (mKey == null) {
diff --git a/src/com/android/settings/fuelgauge/batteryusage/BatteryHistoryLoader.java b/src/com/android/settings/fuelgauge/batteryusage/BatteryHistoryLoader.java
deleted file mode 100644
index 9a0e410..0000000
--- a/src/com/android/settings/fuelgauge/batteryusage/BatteryHistoryLoader.java
+++ /dev/null
@@ -1,45 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.settings.fuelgauge.batteryusage;
-
-import android.content.Context;
-
-import com.android.settingslib.utils.AsyncLoaderCompat;
-
-import java.util.Calendar;
-import java.util.Map;
-
-/** Loader that can be used to load battery history information. */
-public class BatteryHistoryLoader
- extends AsyncLoaderCompat<Map<Long, Map<String, BatteryHistEntry>>> {
- private static final String TAG = "BatteryHistoryLoader";
-
- private final Context mContext;
-
- public BatteryHistoryLoader(Context context) {
- super(context);
- mContext = context;
- }
-
- @Override
- protected void onDiscardResult(Map<Long, Map<String, BatteryHistEntry>> result) {
- }
-
- @Override
- public Map<Long, Map<String, BatteryHistEntry>> loadInBackground() {
- return DatabaseUtils.getHistoryMapSinceLastFullCharge(mContext, Calendar.getInstance());
- }
-}
diff --git a/src/com/android/settings/fuelgauge/batteryusage/BatteryHistoryPreference.java b/src/com/android/settings/fuelgauge/batteryusage/BatteryHistoryPreference.java
index c78b3c7..d64bf34 100644
--- a/src/com/android/settings/fuelgauge/batteryusage/BatteryHistoryPreference.java
+++ b/src/com/android/settings/fuelgauge/batteryusage/BatteryHistoryPreference.java
@@ -17,17 +17,13 @@
package com.android.settings.fuelgauge.batteryusage;
import android.content.Context;
-import android.os.BatteryUsageStats;
import android.util.AttributeSet;
import android.widget.TextView;
-import androidx.annotation.NonNull;
-import androidx.annotation.VisibleForTesting;
import androidx.preference.Preference;
import androidx.preference.PreferenceViewHolder;
import com.android.settings.R;
-import com.android.settings.fuelgauge.BatteryInfo;
import com.android.settings.fuelgauge.BatteryUtils;
/**
@@ -36,9 +32,6 @@
public class BatteryHistoryPreference extends Preference {
private static final String TAG = "BatteryHistoryPreference";
- @VisibleForTesting
- BatteryInfo mBatteryInfo;
-
private BatteryChartView mDailyChartView;
private BatteryChartView mHourlyChartView;
private BatteryChartPreferenceController mChartPreferenceController;
@@ -49,13 +42,6 @@
setSelectable(false);
}
- void setBatteryUsageStats(@NonNull BatteryUsageStats batteryUsageStats) {
- BatteryInfo.getBatteryInfo(getContext(), info -> {
- mBatteryInfo = info;
- notifyChanged();
- }, batteryUsageStats, false);
- }
-
void setChartPreferenceController(BatteryChartPreferenceController controller) {
mChartPreferenceController = controller;
if (mDailyChartView != null && mHourlyChartView != null) {
@@ -67,9 +53,6 @@
public void onBindViewHolder(PreferenceViewHolder view) {
super.onBindViewHolder(view);
final long startTime = System.currentTimeMillis();
- if (mBatteryInfo == null) {
- return;
- }
final TextView companionTextView = (TextView) view.findViewById(R.id.companion_text);
mDailyChartView = (BatteryChartView) view.findViewById(R.id.daily_battery_chart);
mDailyChartView.setCompanionTextView(companionTextView);
diff --git a/src/com/android/settings/fuelgauge/batteryusage/BatteryLevelData.java b/src/com/android/settings/fuelgauge/batteryusage/BatteryLevelData.java
index 4ff9eeb..09d66c7 100644
--- a/src/com/android/settings/fuelgauge/batteryusage/BatteryLevelData.java
+++ b/src/com/android/settings/fuelgauge/batteryusage/BatteryLevelData.java
@@ -16,15 +16,29 @@
package com.android.settings.fuelgauge.batteryusage;
+import static com.android.settingslib.fuelgauge.BatteryStatus.BATTERY_LEVEL_UNKNOWN;
+
+import android.text.format.DateUtils;
+import android.util.ArrayMap;
+import android.util.Pair;
+
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
import androidx.core.util.Preconditions;
+import java.util.ArrayList;
+import java.util.Collections;
import java.util.List;
import java.util.Locale;
+import java.util.Map;
import java.util.Objects;
/** Wraps the battery timestamp and level data used for battery usage chart. */
public final class BatteryLevelData {
+ private static final long MIN_SIZE = 2;
+ private static final long TIME_SLOT = DateUtils.HOUR_IN_MILLIS * 2;
+
/** A container for the battery timestamp and level data. */
public static final class PeriodBatteryLevelData {
// The length of mTimestamps and mLevels must be the same. mLevels[index] might be null when
@@ -33,12 +47,14 @@
private final List<Integer> mLevels;
public PeriodBatteryLevelData(
- @NonNull List<Long> timestamps, @NonNull List<Integer> levels) {
- Preconditions.checkArgument(timestamps.size() == levels.size(),
- /* errorMessage= */ "Timestamp: " + timestamps.size() + ", Level: "
- + levels.size());
+ @NonNull Map<Long, Integer> batteryLevelMap,
+ @NonNull List<Long> timestamps) {
mTimestamps = timestamps;
- mLevels = levels;
+ mLevels = new ArrayList<>(timestamps.size());
+ for (Long timestamp : timestamps) {
+ mLevels.add(batteryLevelMap.containsKey(timestamp)
+ ? batteryLevelMap.get(timestamp) : BATTERY_LEVEL_UNKNOWN);
+ }
}
public List<Long> getTimestamps() {
@@ -54,6 +70,16 @@
return String.format(Locale.ENGLISH, "timestamps: %s; levels: %s",
Objects.toString(mTimestamps), Objects.toString(mLevels));
}
+
+ private int getIndexByTimestamps(long startTimestamp, long endTimestamp) {
+ for (int index = 0; index < mTimestamps.size() - 1; index++) {
+ if (mTimestamps.get(index) <= startTimestamp
+ && endTimestamp <= mTimestamps.get(index + 1)) {
+ return index;
+ }
+ }
+ return BatteryChartViewModel.SELECTED_INDEX_INVALID;
+ }
}
/**
@@ -68,15 +94,33 @@
// The size of hourly data must be the size of daily data - 1.
private final List<PeriodBatteryLevelData> mHourlyBatteryLevelsPerDay;
- public BatteryLevelData(
- @NonNull PeriodBatteryLevelData dailyBatteryLevels,
- @NonNull List<PeriodBatteryLevelData> hourlyBatteryLevelsPerDay) {
- final long dailySize = dailyBatteryLevels.getTimestamps().size();
- final long hourlySize = hourlyBatteryLevelsPerDay.size();
- Preconditions.checkArgument(hourlySize == dailySize - 1,
- /* errorMessage= */ "DailySize: " + dailySize + ", HourlySize: " + hourlySize);
- mDailyBatteryLevels = dailyBatteryLevels;
- mHourlyBatteryLevelsPerDay = hourlyBatteryLevelsPerDay;
+ public BatteryLevelData(@NonNull Map<Long, Integer> batteryLevelMap) {
+ final int mapSize = batteryLevelMap.size();
+ Preconditions.checkArgument(mapSize >= MIN_SIZE, "batteryLevelMap size:" + mapSize);
+
+ final List<Long> timestampList = new ArrayList<>(batteryLevelMap.keySet());
+ Collections.sort(timestampList);
+ final List<Long> dailyTimestamps = getDailyTimestamps(timestampList);
+ final List<List<Long>> hourlyTimestamps = getHourlyTimestamps(dailyTimestamps);
+
+ mDailyBatteryLevels = new PeriodBatteryLevelData(batteryLevelMap, dailyTimestamps);
+ mHourlyBatteryLevelsPerDay = new ArrayList<>(hourlyTimestamps.size());
+ for (List<Long> hourlyTimestampsPerDay : hourlyTimestamps) {
+ mHourlyBatteryLevelsPerDay.add(
+ new PeriodBatteryLevelData(batteryLevelMap, hourlyTimestampsPerDay));
+ }
+ }
+
+ /** Gets daily and hourly index between start and end timestamps. */
+ public Pair<Integer, Integer> getIndexByTimestamps(long startTimestamp, long endTimestamp) {
+ final int dailyHighlightIndex =
+ mDailyBatteryLevels.getIndexByTimestamps(startTimestamp, endTimestamp);
+ final int hourlyHighlightIndex =
+ (dailyHighlightIndex == BatteryChartViewModel.SELECTED_INDEX_INVALID)
+ ? BatteryChartViewModel.SELECTED_INDEX_INVALID
+ : mHourlyBatteryLevelsPerDay.get(dailyHighlightIndex)
+ .getIndexByTimestamps(startTimestamp, endTimestamp);
+ return Pair.create(dailyHighlightIndex, hourlyHighlightIndex);
}
public PeriodBatteryLevelData getDailyBatteryLevels() {
@@ -94,5 +138,69 @@
Objects.toString(mDailyBatteryLevels),
Objects.toString(mHourlyBatteryLevelsPerDay));
}
+
+ @Nullable
+ static BatteryLevelData combine(@Nullable BatteryLevelData existingBatteryLevelData,
+ List<BatteryEvent> batteryLevelRecordEvents) {
+ final Map<Long, Integer> batteryLevelMap = new ArrayMap<>(batteryLevelRecordEvents.size());
+ for (BatteryEvent event : batteryLevelRecordEvents) {
+ batteryLevelMap.put(event.getTimestamp(), event.getBatteryLevel());
+ }
+ if (existingBatteryLevelData != null) {
+ List<PeriodBatteryLevelData> multiDaysData =
+ existingBatteryLevelData.getHourlyBatteryLevelsPerDay();
+ for (int dayIndex = 0; dayIndex < multiDaysData.size(); dayIndex++) {
+ PeriodBatteryLevelData oneDayData = multiDaysData.get(dayIndex);
+ for (int hourIndex = 0; hourIndex < oneDayData.getLevels().size(); hourIndex++) {
+ batteryLevelMap.put(oneDayData.getTimestamps().get(hourIndex),
+ oneDayData.getLevels().get(hourIndex));
+ }
+ }
+ }
+ return batteryLevelMap.size() < MIN_SIZE ? null : new BatteryLevelData(batteryLevelMap);
+ }
+
+ /**
+ * Computes expected daily timestamp slots.
+ *
+ * The valid result should be composed of 3 parts:
+ * 1) start timestamp
+ * 2) every 00:00 timestamp (default timezone) between the start and end
+ * 3) end timestamp
+ * Otherwise, returns an empty list.
+ */
+ @VisibleForTesting
+ static List<Long> getDailyTimestamps(final List<Long> timestampList) {
+ Preconditions.checkArgument(
+ timestampList.size() >= MIN_SIZE, "timestampList size:" + timestampList.size());
+ final List<Long> dailyTimestampList = new ArrayList<>();
+ final long startTimestamp = timestampList.get(0);
+ final long endTimestamp = timestampList.get(timestampList.size() - 1);
+ for (long timestamp = startTimestamp; timestamp < endTimestamp;
+ timestamp = TimestampUtils.getNextDayTimestamp(timestamp)) {
+ dailyTimestampList.add(timestamp);
+ }
+ dailyTimestampList.add(endTimestamp);
+ return dailyTimestampList;
+ }
+
+ private static List<List<Long>> getHourlyTimestamps(final List<Long> dailyTimestamps) {
+ final List<List<Long>> hourlyTimestamps = new ArrayList<>();
+ for (int dailyIndex = 0; dailyIndex < dailyTimestamps.size() - 1; dailyIndex++) {
+ final List<Long> hourlyTimestampsPerDay = new ArrayList<>();
+ final long startTime = dailyTimestamps.get(dailyIndex);
+ final long endTime = dailyTimestamps.get(dailyIndex + 1);
+
+ hourlyTimestampsPerDay.add(startTime);
+ for (long timestamp = TimestampUtils.getNextEvenHourTimestamp(startTime);
+ timestamp < endTime; timestamp += TIME_SLOT) {
+ hourlyTimestampsPerDay.add(timestamp);
+ }
+ hourlyTimestampsPerDay.add(endTime);
+
+ hourlyTimestamps.add(hourlyTimestampsPerDay);
+ }
+ return hourlyTimestamps;
+ }
}
diff --git a/src/com/android/settings/fuelgauge/batteryusage/BatteryTipsCardPreference.java b/src/com/android/settings/fuelgauge/batteryusage/BatteryTipsCardPreference.java
new file mode 100644
index 0000000..47d2ac3
--- /dev/null
+++ b/src/com/android/settings/fuelgauge/batteryusage/BatteryTipsCardPreference.java
@@ -0,0 +1,154 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.fuelgauge.batteryusage;
+
+import android.content.Context;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import androidx.annotation.VisibleForTesting;
+import androidx.preference.Preference;
+import androidx.preference.PreferenceViewHolder;
+
+import com.android.settings.R;
+import com.android.settings.overlay.FeatureFactory;
+import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
+
+import com.google.android.material.button.MaterialButton;
+
+/**
+ * A preference for displaying the battery tips card view.
+ */
+public class BatteryTipsCardPreference extends Preference implements View.OnClickListener {
+
+ private static final String TAG = "BatteryTipsCardPreference";
+
+ interface OnConfirmListener {
+ void onConfirm();
+ }
+
+ interface OnRejectListener {
+ void onReject();
+ }
+
+ private final MetricsFeatureProvider mMetricsFeatureProvider;
+ private OnConfirmListener mOnConfirmListener;
+ private OnRejectListener mOnRejectListener;
+ private int mIconResourceId = 0;
+ private int mMainButtonStrokeColorResourceId = 0;
+
+ @VisibleForTesting
+ CharSequence mMainButtonLabel;
+ @VisibleForTesting
+ CharSequence mDismissButtonLabel;
+
+ public BatteryTipsCardPreference(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ setLayoutResource(R.layout.battery_tips_card);
+ setSelectable(false);
+ final FeatureFactory featureFactory = FeatureFactory.getFactory(context);
+ mMetricsFeatureProvider = featureFactory.getMetricsFeatureProvider();
+ }
+
+ public void setOnConfirmListener(OnConfirmListener listener) {
+ mOnConfirmListener = listener;
+ }
+
+ public void setOnRejectListener(OnRejectListener listener) {
+ mOnRejectListener = listener;
+ }
+
+ /**
+ * Sets the icon in tips card.
+ */
+ public void setIconResourceId(int resourceId) {
+ if (mIconResourceId != resourceId) {
+ mIconResourceId = resourceId;
+ notifyChanged();
+ }
+ }
+
+ /**
+ * Sets the stroke color of main button in tips card.
+ */
+ public void setMainButtonStrokeColorResourceId(int resourceId) {
+ if (mMainButtonStrokeColorResourceId != resourceId) {
+ mMainButtonStrokeColorResourceId = resourceId;
+ notifyChanged();
+ }
+ }
+
+ /**
+ * Sets the label of main button in tips card.
+ */
+ public void setMainButtonLabel(CharSequence label) {
+ if (!TextUtils.equals(mMainButtonLabel, label)) {
+ mMainButtonLabel = label;
+ notifyChanged();
+ }
+ }
+
+ /**
+ * Sets the label of dismiss button in tips card.
+ */
+ public void setDismissButtonLabel(CharSequence label) {
+ if (!TextUtils.equals(mDismissButtonLabel, label)) {
+ mDismissButtonLabel = label;
+ notifyChanged();
+ }
+ }
+
+ @Override
+ public void onClick(View view) {
+ final int viewId = view.getId();
+ if (viewId == R.id.main_button || viewId == R.id.tips_card) {
+ if (mOnConfirmListener != null) {
+ mOnConfirmListener.onConfirm();
+ }
+ } else if (viewId == R.id.dismiss_button) {
+ if (mOnRejectListener != null) {
+ mOnRejectListener.onReject();
+ }
+ }
+ }
+
+ @Override
+ public void onBindViewHolder(PreferenceViewHolder view) {
+ super.onBindViewHolder(view);
+
+ ((TextView) view.findViewById(R.id.title)).setText(getTitle());
+
+ LinearLayout tipsCard = (LinearLayout) view.findViewById(R.id.tips_card);
+ tipsCard.setOnClickListener(this);
+ MaterialButton mainButton = (MaterialButton) view.findViewById(R.id.main_button);
+ mainButton.setOnClickListener(this);
+ mainButton.setText(mMainButtonLabel);
+ if (mMainButtonStrokeColorResourceId != 0) {
+ mainButton.setStrokeColorResource(mMainButtonStrokeColorResourceId);
+ }
+ MaterialButton dismissButton = (MaterialButton) view.findViewById(R.id.dismiss_button);
+ dismissButton.setOnClickListener(this);
+ dismissButton.setText(mDismissButtonLabel);
+ if (mIconResourceId != 0) {
+ ((ImageView) view.findViewById(R.id.icon)).setImageResource(mIconResourceId);
+ }
+ }
+}
diff --git a/src/com/android/settings/fuelgauge/batteryusage/BatteryTipsController.java b/src/com/android/settings/fuelgauge/batteryusage/BatteryTipsController.java
new file mode 100644
index 0000000..39ed0dc
--- /dev/null
+++ b/src/com/android/settings/fuelgauge/batteryusage/BatteryTipsController.java
@@ -0,0 +1,147 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.fuelgauge.batteryusage;
+
+import android.app.settings.SettingsEnums;
+import android.content.Context;
+import android.text.TextUtils;
+
+import androidx.preference.PreferenceScreen;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.settings.core.BasePreferenceController;
+import com.android.settings.overlay.FeatureFactory;
+import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
+
+/** Controls the update for battery tips card */
+public class BatteryTipsController extends BasePreferenceController {
+
+ private static final String TAG = "BatteryTipsController";
+ private static final String ROOT_PREFERENCE_KEY = "battery_tips_category";
+ private static final String CARD_PREFERENCE_KEY = "battery_tips_card";
+
+ private final MetricsFeatureProvider mMetricsFeatureProvider;
+
+ /** A callback listener for the battery tips is confirmed. */
+ interface OnAnomalyConfirmListener {
+ /** The callback function for the battery tips is confirmed. */
+ void onAnomalyConfirm();
+ }
+
+ /** A callback listener for the battery tips is rejected. */
+ interface OnAnomalyRejectListener {
+ /** The callback function for the battery tips is rejected. */
+ void onAnomalyReject();
+ }
+
+ private OnAnomalyConfirmListener mOnAnomalyConfirmListener;
+ private OnAnomalyRejectListener mOnAnomalyRejectListener;
+
+ @VisibleForTesting
+ BatteryTipsCardPreference mCardPreference;
+ @VisibleForTesting
+ AnomalyEventWrapper mAnomalyEventWrapper = null;
+ @VisibleForTesting
+ Boolean mIsAcceptable = false;
+
+ public BatteryTipsController(Context context) {
+ super(context, ROOT_PREFERENCE_KEY);
+ final FeatureFactory featureFactory = FeatureFactory.getFactory(context);
+ mMetricsFeatureProvider = featureFactory.getMetricsFeatureProvider();
+ }
+
+ @Override
+ public int getAvailabilityStatus() {
+ return AVAILABLE;
+ }
+
+ @Override
+ public void displayPreference(PreferenceScreen screen) {
+ super.displayPreference(screen);
+ mCardPreference = screen.findPreference(CARD_PREFERENCE_KEY);
+ }
+
+ void setOnAnomalyConfirmListener(OnAnomalyConfirmListener listener) {
+ mOnAnomalyConfirmListener = listener;
+ }
+
+ void setOnAnomalyRejectListener(OnAnomalyRejectListener listener) {
+ mOnAnomalyRejectListener = listener;
+ }
+
+ void acceptTipsCard() {
+ if (mAnomalyEventWrapper == null || !mIsAcceptable) {
+ return;
+ }
+ // For anomaly events with same record key, dismissed until next time full charged.
+ final String dismissRecordKey = mAnomalyEventWrapper.getDismissRecordKey();
+ if (!TextUtils.isEmpty(dismissRecordKey)) {
+ DatabaseUtils.setDismissedPowerAnomalyKeys(mContext, dismissRecordKey);
+ }
+ mCardPreference.setVisible(false);
+ mMetricsFeatureProvider.action(
+ mContext, SettingsEnums.ACTION_BATTERY_TIPS_CARD_ACCEPT,
+ mAnomalyEventWrapper.getEventId());
+ }
+
+ void handleBatteryTipsCardUpdated(
+ AnomalyEventWrapper anomalyEventWrapper, boolean isAcceptable) {
+ mAnomalyEventWrapper = anomalyEventWrapper;
+ mIsAcceptable = isAcceptable;
+ if (mAnomalyEventWrapper == null) {
+ mCardPreference.setVisible(false);
+ return;
+ }
+
+ // Get card preference strings and navigate fragment info
+ final String eventId = mAnomalyEventWrapper.getEventId();
+
+ // Update card & buttons preference
+ if (!mAnomalyEventWrapper.updateTipsCardPreference(mCardPreference)) {
+ mCardPreference.setVisible(false);
+ return;
+ }
+
+ // Set battery tips card listener
+ mCardPreference.setOnConfirmListener(() -> {
+ mCardPreference.setVisible(false);
+ if (mOnAnomalyConfirmListener != null) {
+ mOnAnomalyConfirmListener.onAnomalyConfirm();
+ } else if (mAnomalyEventWrapper.launchSubSetting()) {
+ mMetricsFeatureProvider.action(
+ mContext, SettingsEnums.ACTION_BATTERY_TIPS_CARD_ACCEPT, eventId);
+ }
+ });
+ mCardPreference.setOnRejectListener(() -> {
+ mCardPreference.setVisible(false);
+ if (mOnAnomalyRejectListener != null) {
+ mOnAnomalyRejectListener.onAnomalyReject();
+ }
+ // For anomaly events with same record key, dismissed until next time full charged.
+ final String dismissRecordKey = mAnomalyEventWrapper.getDismissRecordKey();
+ if (!TextUtils.isEmpty(dismissRecordKey)) {
+ DatabaseUtils.setDismissedPowerAnomalyKeys(mContext, dismissRecordKey);
+ }
+ mMetricsFeatureProvider.action(
+ mContext, SettingsEnums.ACTION_BATTERY_TIPS_CARD_DISMISS, eventId);
+ });
+
+ mCardPreference.setVisible(true);
+ mMetricsFeatureProvider.action(
+ mContext, SettingsEnums.ACTION_BATTERY_TIPS_CARD_SHOW, eventId);
+ }
+}
diff --git a/src/com/android/settings/fuelgauge/batteryusage/BatteryUsageBreakdownController.java b/src/com/android/settings/fuelgauge/batteryusage/BatteryUsageBreakdownController.java
index b262dee..b237ef6 100644
--- a/src/com/android/settings/fuelgauge/batteryusage/BatteryUsageBreakdownController.java
+++ b/src/com/android/settings/fuelgauge/batteryusage/BatteryUsageBreakdownController.java
@@ -53,6 +53,7 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.Optional;
import java.util.Set;
/** Controller for battery usage breakdown preference group. */
@@ -93,6 +94,14 @@
BatteryDiffData mBatteryDiffData;
@VisibleForTesting
String mPercentLessThanThresholdText;
+ @VisibleForTesting
+ boolean mIsHighlightSlot;
+ @VisibleForTesting
+ String mAnomalyEventId;
+ @VisibleForTesting
+ String mAnomalyEntryKey;
+ @VisibleForTesting
+ String mAnomalyHintString;
public BatteryUsageBreakdownController(
Context context, Lifecycle lifecycle, SettingsActivity activity,
@@ -137,6 +146,12 @@
return false;
}
+ private String getActionKey(String packageName) {
+ final String actionKey = TextUtils.isEmpty(packageName)
+ ? PACKAGE_NAME_NONE : packageName;
+ return mAnomalyEventId == null ? actionKey : actionKey + "|" + mAnomalyEventId;
+ }
+
@Override
public boolean handlePreferenceTreeClick(Preference preference) {
if (!(preference instanceof PowerGaugePreference)) {
@@ -144,19 +159,17 @@
}
final PowerGaugePreference powerPref = (PowerGaugePreference) preference;
final BatteryDiffEntry diffEntry = powerPref.getBatteryDiffEntry();
- final BatteryHistEntry histEntry = diffEntry.mBatteryHistEntry;
- final String packageName = histEntry.mPackageName;
- final boolean isAppEntry = histEntry.isAppEntry();
+ final String packageName = diffEntry.getPackageName();
mMetricsFeatureProvider.action(
/* attribution */ SettingsEnums.OPEN_BATTERY_USAGE,
- /* action */ isAppEntry
- ? SettingsEnums.ACTION_BATTERY_USAGE_APP_ITEM
- : SettingsEnums.ACTION_BATTERY_USAGE_SYSTEM_ITEM,
+ /* action */ diffEntry.isSystemEntry()
+ ? SettingsEnums.ACTION_BATTERY_USAGE_SYSTEM_ITEM
+ : SettingsEnums.ACTION_BATTERY_USAGE_APP_ITEM,
/* pageId */ SettingsEnums.OPEN_BATTERY_USAGE,
- TextUtils.isEmpty(packageName) ? PACKAGE_NAME_NONE : packageName,
+ getActionKey(packageName),
(int) Math.round(diffEntry.getPercentage()));
Log.d(TAG, String.format("handleClick() label=%s key=%s package=%s",
- diffEntry.getAppLabel(), histEntry.getKey(), histEntry.mPackageName));
+ diffEntry.getAppLabel(), diffEntry.getKey(), packageName));
AdvancedPowerUsageDetail.startBatteryDetailPage(
mActivity, mFragment, diffEntry, powerPref.getPercentage(), mSlotTimestamp);
return true;
@@ -213,9 +226,23 @@
* used when showing the footer.
*/
void handleBatteryUsageUpdated(
- BatteryDiffData slotUsageData, String slotTimestamp, boolean isAllUsageDataEmpty) {
+ BatteryDiffData slotUsageData, String slotTimestamp,
+ boolean isAllUsageDataEmpty, boolean isHighlightSlot,
+ Optional<AnomalyEventWrapper> optionalAnomalyEventWrapper) {
mBatteryDiffData = slotUsageData;
mSlotTimestamp = slotTimestamp;
+ mIsHighlightSlot = isHighlightSlot;
+
+ if (optionalAnomalyEventWrapper != null) {
+ final AnomalyEventWrapper anomalyEventWrapper =
+ optionalAnomalyEventWrapper.orElse(null);
+ mAnomalyEventId = anomalyEventWrapper != null
+ ? anomalyEventWrapper.getEventId() : null;
+ mAnomalyEntryKey = anomalyEventWrapper != null
+ ? anomalyEventWrapper.getAnomalyEntryKey() : null;
+ mAnomalyHintString = anomalyEventWrapper != null
+ ? anomalyEventWrapper.getAnomalyHintString() : null;
+ }
showCategoryTitle(slotTimestamp);
showSpinnerAndAppList();
@@ -280,15 +307,15 @@
continue;
}
final String prefKey = entry.getKey();
- PowerGaugePreference pref = mAppListPreferenceGroup.findPreference(prefKey);
+ AnomalyAppItemPreference pref = mAppListPreferenceGroup.findPreference(prefKey);
if (pref != null) {
isAdded = true;
} else {
- pref = (PowerGaugePreference) mPreferenceCache.get(prefKey);
+ pref = (AnomalyAppItemPreference) mPreferenceCache.get(prefKey);
}
- // Creates new innstance if cached preference is not found.
+ // Creates new instance if cached preference is not found.
if (pref == null) {
- pref = new PowerGaugePreference(mPrefContext);
+ pref = new AnomalyAppItemPreference(mPrefContext);
pref.setKey(prefKey);
mPreferenceCache.put(prefKey, pref);
}
@@ -296,6 +323,10 @@
pref.setTitle(appLabel);
pref.setOrder(prefIndex);
pref.setSingleLineTitle(true);
+ // Updates App item preference style
+ pref.setAnomalyHint(mIsHighlightSlot && mAnomalyEntryKey != null
+ && mAnomalyEntryKey.equals(entry.getKey())
+ ? mAnomalyHintString : null);
// Sets the BatteryDiffEntry to preference for launching detailed page.
pref.setBatteryDiffEntry(entry);
pref.setSelectable(entry.validForRestriction());
diff --git a/src/com/android/settings/fuelgauge/batteryusage/BatteryUsageBroadcastReceiver.java b/src/com/android/settings/fuelgauge/batteryusage/BatteryUsageBroadcastReceiver.java
index ed5f182..952b83f 100644
--- a/src/com/android/settings/fuelgauge/batteryusage/BatteryUsageBroadcastReceiver.java
+++ b/src/com/android/settings/fuelgauge/batteryusage/BatteryUsageBroadcastReceiver.java
@@ -120,6 +120,7 @@
mFetchBatteryUsageData = true;
BatteryUsageDataLoader.enqueueWork(context, /*isFullChargeStart=*/ true);
+ BootBroadcastReceiver.invokeJobRecheck(context);
}
private void sendBatteryEventData(Context context, BatteryEventType batteryEventType) {
diff --git a/src/com/android/settings/fuelgauge/batteryusage/BatteryUsageContentProvider.java b/src/com/android/settings/fuelgauge/batteryusage/BatteryUsageContentProvider.java
index 1b2d4cd..edba7c4 100644
--- a/src/com/android/settings/fuelgauge/batteryusage/BatteryUsageContentProvider.java
+++ b/src/com/android/settings/fuelgauge/batteryusage/BatteryUsageContentProvider.java
@@ -21,7 +21,6 @@
import android.content.UriMatcher;
import android.database.Cursor;
import android.net.Uri;
-import android.os.AsyncTask;
import android.text.TextUtils;
import android.util.Log;
@@ -36,12 +35,14 @@
import com.android.settings.fuelgauge.batteryusage.db.BatteryState;
import com.android.settings.fuelgauge.batteryusage.db.BatteryStateDao;
import com.android.settings.fuelgauge.batteryusage.db.BatteryStateDatabase;
+import com.android.settings.fuelgauge.batteryusage.db.BatteryUsageSlotDao;
+import com.android.settings.fuelgauge.batteryusage.db.BatteryUsageSlotEntity;
import java.time.Clock;
import java.time.Duration;
+import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
-import java.util.stream.Collectors;
/** {@link ContentProvider} class to fetch battery usage data. */
public class BatteryUsageContentProvider extends ContentProvider {
@@ -55,7 +56,12 @@
private static final int APP_USAGE_LATEST_TIMESTAMP_CODE = 2;
private static final int APP_USAGE_EVENT_CODE = 3;
private static final int BATTERY_EVENT_CODE = 4;
+ private static final int LAST_FULL_CHARGE_TIMESTAMP_CODE = 5;
+ private static final int BATTERY_STATE_LATEST_TIMESTAMP_CODE = 6;
+ private static final int BATTERY_USAGE_SLOT_CODE = 7;
+ private static final List<Integer> ALL_BATTERY_EVENT_TYPES =
+ Arrays.stream(BatteryEventType.values()).map(type -> type.getNumber()).toList();
private static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
static {
@@ -75,12 +81,25 @@
DatabaseUtils.AUTHORITY,
/*path=*/ DatabaseUtils.BATTERY_EVENT_TABLE,
/*code=*/ BATTERY_EVENT_CODE);
+ sUriMatcher.addURI(
+ DatabaseUtils.AUTHORITY,
+ /*path=*/ DatabaseUtils.LAST_FULL_CHARGE_TIMESTAMP_PATH,
+ /*code=*/ LAST_FULL_CHARGE_TIMESTAMP_CODE);
+ sUriMatcher.addURI(
+ DatabaseUtils.AUTHORITY,
+ /*path=*/ DatabaseUtils.BATTERY_STATE_LATEST_TIMESTAMP_PATH,
+ /*code=*/ BATTERY_STATE_LATEST_TIMESTAMP_CODE);
+ sUriMatcher.addURI(
+ DatabaseUtils.AUTHORITY,
+ /*path=*/ DatabaseUtils.BATTERY_USAGE_SLOT_TABLE,
+ /*code=*/ BATTERY_USAGE_SLOT_CODE);
}
private Clock mClock;
private BatteryStateDao mBatteryStateDao;
private AppUsageEventDao mAppUsageEventDao;
private BatteryEventDao mBatteryEventDao;
+ private BatteryUsageSlotDao mBatteryUsageSlotDao;
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
public void setClock(Clock clock) {
@@ -94,9 +113,11 @@
return false;
}
mClock = Clock.systemUTC();
- mBatteryStateDao = BatteryStateDatabase.getInstance(getContext()).batteryStateDao();
- mAppUsageEventDao = BatteryStateDatabase.getInstance(getContext()).appUsageEventDao();
- mBatteryEventDao = BatteryStateDatabase.getInstance(getContext()).batteryEventDao();
+ final BatteryStateDatabase database = BatteryStateDatabase.getInstance(getContext());
+ mBatteryStateDao = database.batteryStateDao();
+ mAppUsageEventDao = database.appUsageEventDao();
+ mBatteryEventDao = database.batteryEventDao();
+ mBatteryUsageSlotDao = database.batteryUsageSlotDao();
Log.w(TAG, "create content provider from " + getCallingPackage());
return true;
}
@@ -118,6 +139,12 @@
return getAppUsageLatestTimestamp(uri);
case BATTERY_EVENT_CODE:
return getBatteryEvents(uri);
+ case LAST_FULL_CHARGE_TIMESTAMP_CODE:
+ return getLastFullChargeTimestamp(uri);
+ case BATTERY_STATE_LATEST_TIMESTAMP_CODE:
+ return getBatteryStateLatestTimestamp(uri);
+ case BATTERY_USAGE_SLOT_CODE:
+ return getBatteryUsageSlots(uri);
default:
throw new IllegalArgumentException("unknown URI: " + uri);
}
@@ -132,34 +159,31 @@
@Nullable
@Override
public Uri insert(@NonNull Uri uri, @Nullable ContentValues contentValues) {
- switch (sUriMatcher.match(uri)) {
- case BATTERY_STATE_CODE:
- try {
+ try {
+ switch (sUriMatcher.match(uri)) {
+ case BATTERY_STATE_CODE:
mBatteryStateDao.insert(BatteryState.create(contentValues));
- return uri;
- } catch (RuntimeException e) {
- Log.e(TAG, "insert() from:" + uri + " error:" + e);
- return null;
- }
- case APP_USAGE_EVENT_CODE:
- try {
+ break;
+ case APP_USAGE_EVENT_CODE:
mAppUsageEventDao.insert(AppUsageEventEntity.create(contentValues));
- return uri;
- } catch (RuntimeException e) {
- Log.e(TAG, "insert() from:" + uri + " error:" + e);
- return null;
- }
- case BATTERY_EVENT_CODE:
- try {
+ break;
+ case BATTERY_EVENT_CODE:
mBatteryEventDao.insert(BatteryEventEntity.create(contentValues));
- return uri;
- } catch (RuntimeException e) {
- Log.e(TAG, "insert() from:" + uri + " error:" + e);
- return null;
- }
- default:
- throw new IllegalArgumentException("unknown URI: " + uri);
+ break;
+ case BATTERY_USAGE_SLOT_CODE:
+ mBatteryUsageSlotDao.insert(BatteryUsageSlotEntity.create(contentValues));
+ break;
+ default:
+ throw new IllegalArgumentException("unknown URI: " + uri);
+ }
+ } catch (RuntimeException e) {
+ if (e instanceof IllegalArgumentException) {
+ throw e;
+ }
+ Log.e(TAG, "insert() from:" + uri + " error:", e);
+ return null;
}
+ return uri;
}
@Override
@@ -176,21 +200,44 @@
throw new UnsupportedOperationException("unsupported!");
}
- private Cursor getBatteryStates(Uri uri) {
- final long queryTimestamp = getQueryTimestamp(uri);
- return getBatteryStates(uri, queryTimestamp);
- }
-
- private Cursor getBatteryStates(Uri uri, long firstTimestamp) {
+ private Cursor getLastFullChargeTimestamp(Uri uri) {
final long timestamp = mClock.millis();
Cursor cursor = null;
try {
- cursor = mBatteryStateDao.getCursorSinceLastFullCharge(firstTimestamp);
+ cursor = mBatteryEventDao.getLastFullChargeTimestamp();
} catch (RuntimeException e) {
- Log.e(TAG, "query() from:" + uri + " error:" + e);
+ Log.e(TAG, "query() from:" + uri + " error:", e);
}
- AsyncTask.execute(() -> BootBroadcastReceiver.invokeJobRecheck(getContext()));
- Log.d(TAG, "query battery states in " + (mClock.millis() - timestamp) + "/ms");
+ Log.d(TAG, String.format("getLastFullChargeTimestamp() in %d/ms",
+ mClock.millis() - timestamp));
+ return cursor;
+ }
+
+ private Cursor getBatteryStateLatestTimestamp(Uri uri) {
+ final long queryTimestamp = getQueryTimestamp(uri);
+ final long timestamp = mClock.millis();
+ Cursor cursor = null;
+ try {
+ cursor = mBatteryStateDao.getLatestTimestampBefore(queryTimestamp);
+ } catch (RuntimeException e) {
+ Log.e(TAG, "query() from:" + uri + " error:", e);
+ }
+ Log.d(TAG, String.format("getBatteryStateLatestTimestamp() no later than %d in %d/ms",
+ queryTimestamp, mClock.millis() - timestamp));
+ return cursor;
+ }
+
+ private Cursor getBatteryStates(Uri uri) {
+ final long queryTimestamp = getQueryTimestamp(uri);
+ final long timestamp = mClock.millis();
+ Cursor cursor = null;
+ try {
+ cursor = mBatteryStateDao.getBatteryStatesAfter(queryTimestamp);
+ } catch (RuntimeException e) {
+ Log.e(TAG, "query() from:" + uri + " error:", e);
+ }
+ Log.d(TAG, String.format("getBatteryStates() after %d in %d/ms",
+ queryTimestamp, mClock.millis() - timestamp));
return cursor;
}
@@ -205,9 +252,9 @@
try {
cursor = mAppUsageEventDao.getAllForUsersAfter(queryUserIds, queryTimestamp);
} catch (RuntimeException e) {
- Log.e(TAG, "query() from:" + uri + " error:" + e);
+ Log.e(TAG, "query() from:" + uri + " error:", e);
}
- Log.w(TAG, "query app usage events in " + (mClock.millis() - timestamp) + "/ms");
+ Log.w(TAG, "getAppUsageEvents() in " + (mClock.millis() - timestamp) + "/ms");
return cursor;
}
@@ -221,42 +268,78 @@
try {
cursor = mAppUsageEventDao.getLatestTimestampOfUser(queryUserId);
} catch (RuntimeException e) {
- Log.e(TAG, "query() from:" + uri + " error:" + e);
+ Log.e(TAG, "query() from:" + uri + " error:", e);
}
- Log.d(TAG, String.format("query app usage latest timestamp %d for user %d in %d/ms",
- timestamp, queryUserId, (mClock.millis() - timestamp)));
+ Log.d(TAG, String.format("getAppUsageLatestTimestamp() for user %d in %d/ms",
+ queryUserId, (mClock.millis() - timestamp)));
return cursor;
}
private Cursor getBatteryEvents(Uri uri) {
+ List<Integer> queryBatteryEventTypes = getQueryBatteryEventTypes(uri);
+ if (queryBatteryEventTypes == null || queryBatteryEventTypes.isEmpty()) {
+ queryBatteryEventTypes = ALL_BATTERY_EVENT_TYPES;
+ }
final long queryTimestamp = getQueryTimestamp(uri);
final long timestamp = mClock.millis();
Cursor cursor = null;
try {
- cursor = mBatteryEventDao.getAllAfter(queryTimestamp);
+ cursor = mBatteryEventDao.getAllAfter(queryTimestamp, queryBatteryEventTypes);
} catch (RuntimeException e) {
- Log.e(TAG, "query() from:" + uri + " error:" + e);
+ Log.e(TAG, "query() from:" + uri + " error:", e);
}
- Log.w(TAG, "query app usage events in " + (mClock.millis() - timestamp) + "/ms");
+ Log.w(TAG, "getBatteryEvents() in " + (mClock.millis() - timestamp) + "/ms");
return cursor;
}
+ private Cursor getBatteryUsageSlots(Uri uri) {
+ final long queryTimestamp = getQueryTimestamp(uri);
+ final long timestamp = mClock.millis();
+ Cursor cursor = null;
+ try {
+ cursor = mBatteryUsageSlotDao.getAllAfter(queryTimestamp);
+ } catch (RuntimeException e) {
+ Log.e(TAG, "query() from:" + uri + " error:", e);
+ }
+ Log.w(TAG, "getBatteryUsageSlots() in " + (mClock.millis() - timestamp) + "/ms");
+ return cursor;
+ }
+
+ private List<Integer> getQueryBatteryEventTypes(Uri uri) {
+ Log.d(TAG, "getQueryBatteryEventTypes from uri: " + uri);
+ final String batteryEventTypesParameter =
+ uri.getQueryParameter(DatabaseUtils.QUERY_BATTERY_EVENT_TYPE);
+ if (TextUtils.isEmpty(batteryEventTypesParameter)) {
+ return null;
+ }
+ try {
+ List<Integer> batteryEventTypes = new ArrayList<>();
+ for (String typeString : batteryEventTypesParameter.split(",")) {
+ batteryEventTypes.add(Integer.parseInt(typeString.trim()));
+ }
+ return batteryEventTypes;
+ } catch (NumberFormatException e) {
+ Log.e(TAG, "invalid query value: " + batteryEventTypesParameter, e);
+ return null;
+ }
+ }
+
// If URI contains query parameter QUERY_KEY_USERID, use the value directly.
// Otherwise, return null.
private List<Long> getQueryUserIds(Uri uri) {
Log.d(TAG, "getQueryUserIds from uri: " + uri);
- final String value = uri.getQueryParameter(DatabaseUtils.QUERY_KEY_USERID);
- if (TextUtils.isEmpty(value)) {
- Log.w(TAG, "empty query value");
+ final String userIdsParameter = uri.getQueryParameter(DatabaseUtils.QUERY_KEY_USERID);
+ if (TextUtils.isEmpty(userIdsParameter)) {
return null;
}
try {
- return Arrays.asList(value.split(","))
- .stream()
- .map(s -> Long.parseLong(s.trim()))
- .collect(Collectors.toList());
+ List<Long> userIds = new ArrayList<>();
+ for (String idString : userIdsParameter.split(",")) {
+ userIds.add(Long.parseLong(idString.trim()));
+ }
+ return userIds;
} catch (NumberFormatException e) {
- Log.e(TAG, "invalid query value: " + value, e);
+ Log.e(TAG, "invalid query value: " + userIdsParameter, e);
return null;
}
}
diff --git a/src/com/android/settings/fuelgauge/batteryusage/BatteryUsageDataLoader.java b/src/com/android/settings/fuelgauge/batteryusage/BatteryUsageDataLoader.java
index fb1be3e..55ef937 100644
--- a/src/com/android/settings/fuelgauge/batteryusage/BatteryUsageDataLoader.java
+++ b/src/com/android/settings/fuelgauge/batteryusage/BatteryUsageDataLoader.java
@@ -16,14 +16,22 @@
package com.android.settings.fuelgauge.batteryusage;
+import android.app.usage.UsageEvents;
import android.content.Context;
import android.os.AsyncTask;
import android.os.BatteryUsageStats;
+import android.os.Handler;
+import android.os.Looper;
import android.util.Log;
import androidx.annotation.VisibleForTesting;
+import com.android.settings.fuelgauge.BatteryUsageHistoricalLogEntry.Action;
+import com.android.settings.fuelgauge.batteryusage.bugreport.BatteryUsageLogUtils;
+import com.android.settings.overlay.FeatureFactory;
+
import java.util.List;
+import java.util.Map;
import java.util.function.Supplier;
/** Load battery usage data in the background. */
@@ -33,6 +41,10 @@
// For testing only.
@VisibleForTesting
static Supplier<List<BatteryEntry>> sFakeBatteryEntryListSupplier;
+ @VisibleForTesting
+ static Supplier<Map<Long, UsageEvents>> sFakeAppUsageEventsSupplier;
+ @VisibleForTesting
+ static Supplier<List<AppUsageEvent>> sFakeUsageEventsListSupplier;
private BatteryUsageDataLoader() {
}
@@ -45,8 +57,9 @@
}
@VisibleForTesting
- static void loadUsageData(final Context context, final boolean isFullChargeStart) {
- final long start = System.currentTimeMillis();
+ static void loadBatteryStatsData(final Context context, final boolean isFullChargeStart) {
+ BatteryUsageLogUtils.writeLog(context, Action.FETCH_USAGE_DATA, "");
+ final long currentTime = System.currentTimeMillis();
final BatteryUsageStats batteryUsageStats = DataProcessor.getBatteryUsageStats(context);
final List<BatteryEntry> batteryEntryList =
sFakeBatteryEntryListSupplier != null ? sFakeBatteryEntryListSupplier.get()
@@ -55,25 +68,90 @@
if (batteryEntryList == null || batteryEntryList.isEmpty()) {
Log.w(TAG, "getBatteryEntryList() returns null or empty content");
}
- final long elapsedTime = System.currentTimeMillis() - start;
+ final long elapsedTime = System.currentTimeMillis() - currentTime;
Log.d(TAG, String.format("getBatteryUsageStats() in %d/ms", elapsedTime));
if (isFullChargeStart) {
DatabaseUtils.recordDateTime(
context, DatabaseUtils.KEY_LAST_LOAD_FULL_CHARGE_TIME);
+ DatabaseUtils.sendBatteryEventData(context, ConvertUtils.convertToBatteryEvent(
+ currentTime, BatteryEventType.FULL_CHARGED, 100));
+ DatabaseUtils.removeDismissedPowerAnomalyKeys(context);
}
// Uploads the BatteryEntry data into database.
DatabaseUtils.sendBatteryEntryData(
- context, batteryEntryList, batteryUsageStats, isFullChargeStart);
+ context, currentTime, batteryEntryList, batteryUsageStats, isFullChargeStart);
DataProcessor.closeBatteryUsageStats(batteryUsageStats);
}
+ @VisibleForTesting
+ static void loadAppUsageData(final Context context) {
+ final long start = System.currentTimeMillis();
+ final Map<Long, UsageEvents> appUsageEvents =
+ sFakeAppUsageEventsSupplier != null
+ ? sFakeAppUsageEventsSupplier.get()
+ : DataProcessor.getAppUsageEvents(context);
+ if (appUsageEvents == null) {
+ Log.w(TAG, "loadAppUsageData() returns null");
+ return;
+ }
+ final List<AppUsageEvent> appUsageEventList =
+ sFakeUsageEventsListSupplier != null
+ ? sFakeUsageEventsListSupplier.get()
+ : DataProcessor.generateAppUsageEventListFromUsageEvents(
+ context, appUsageEvents);
+ if (appUsageEventList == null || appUsageEventList.isEmpty()) {
+ Log.w(TAG, "loadAppUsageData() returns null or empty content");
+ return;
+ }
+ final long elapsedTime = System.currentTimeMillis() - start;
+ Log.d(TAG, String.format("loadAppUsageData() size=%d in %d/ms", appUsageEventList.size(),
+ elapsedTime));
+ // Uploads the AppUsageEvent data into database.
+ DatabaseUtils.sendAppUsageEventData(context, appUsageEventList);
+ }
+
+ private static void preprocessBatteryUsageSlots(final Context context) {
+ final long start = System.currentTimeMillis();
+ final Handler handler = new Handler(Looper.getMainLooper());
+ final BatteryLevelData batteryLevelData = DataProcessManager.getBatteryLevelData(
+ context, handler, /*isFromPeriodJob=*/ true,
+ batteryDiffDataMap -> {
+ DatabaseUtils.sendBatteryUsageSlotData(context,
+ ConvertUtils.convertToBatteryUsageSlotList(batteryDiffDataMap));
+ if (batteryDiffDataMap.values().stream().anyMatch(data ->
+ data != null && (!data.getAppDiffEntryList().isEmpty()
+ || !data.getSystemDiffEntryList().isEmpty()))) {
+ FeatureFactory.getFactory(context).getPowerUsageFeatureProvider(context)
+ .detectSettingsAnomaly(context, /* displayDrain= */ 0);
+ }
+ });
+ if (batteryLevelData == null) {
+ Log.d(TAG, "preprocessBatteryUsageSlots() no new battery usage data.");
+ return;
+ }
+
+ DatabaseUtils.sendBatteryEventData(
+ context, ConvertUtils.convertToBatteryEventList(batteryLevelData));
+ Log.d(TAG, String.format(
+ "preprocessBatteryUsageSlots() batteryLevelData=%s in %d/ms",
+ batteryLevelData, System.currentTimeMillis() - start));
+ }
+
private static void loadUsageDataSafely(
final Context context, final boolean isFullChargeStart) {
try {
- loadUsageData(context, isFullChargeStart);
+ final long start = System.currentTimeMillis();
+ loadBatteryStatsData(context, isFullChargeStart);
+ if (!isFullChargeStart) {
+ // No app usage data or battery diff data at this time.
+ loadAppUsageData(context);
+ preprocessBatteryUsageSlots(context);
+ }
+ Log.d(TAG, String.format(
+ "loadUsageDataSafely() in %d/ms", System.currentTimeMillis() - start));
} catch (RuntimeException e) {
- Log.e(TAG, "loadUsageData:" + e);
+ Log.e(TAG, "loadUsageData:", e);
}
}
}
diff --git a/src/com/android/settings/fuelgauge/batteryusage/BootBroadcastReceiver.java b/src/com/android/settings/fuelgauge/batteryusage/BootBroadcastReceiver.java
index 64b5b77..6d14e1c 100644
--- a/src/com/android/settings/fuelgauge/batteryusage/BootBroadcastReceiver.java
+++ b/src/com/android/settings/fuelgauge/batteryusage/BootBroadcastReceiver.java
@@ -24,6 +24,8 @@
import android.util.Log;
import com.android.settings.core.instrumentation.ElapsedTimeUtils;
+import com.android.settings.fuelgauge.BatteryUsageHistoricalLogEntry.Action;
+import com.android.settings.fuelgauge.batteryusage.bugreport.BatteryUsageLogUtils;
import com.android.settings.overlay.FeatureFactory;
import java.time.Duration;
@@ -79,8 +81,13 @@
if (Intent.ACTION_BOOT_COMPLETED.equals(action)) {
final Intent recheckIntent = new Intent(ACTION_PERIODIC_JOB_RECHECK);
recheckIntent.setClass(context, BootBroadcastReceiver.class);
- mHandler.postDelayed(() -> context.sendBroadcast(recheckIntent),
- getRescheduleTimeForBootAction(context));
+ final long delayedTime = getRescheduleTimeForBootAction(context);
+ mHandler.postDelayed(() -> context.sendBroadcast(recheckIntent), delayedTime);
+
+ // Refreshes the usage source from UsageStatsManager when booting.
+ DatabaseUtils.removeUsageSource(context);
+
+ BatteryUsageLogUtils.writeLog(context, Action.RECHECK_JOB, "delay:" + delayedTime);
} else if (ACTION_SETUP_WIZARD_FINISHED.equals(action)) {
ElapsedTimeUtils.storeSuwFinishedTimestamp(context, System.currentTimeMillis());
}
diff --git a/src/com/android/settings/fuelgauge/batteryusage/ConvertUtils.java b/src/com/android/settings/fuelgauge/batteryusage/ConvertUtils.java
index 2c98c4b..a1987c9 100644
--- a/src/com/android/settings/fuelgauge/batteryusage/ConvertUtils.java
+++ b/src/com/android/settings/fuelgauge/batteryusage/ConvertUtils.java
@@ -27,22 +27,27 @@
import android.os.BatteryUsageStats;
import android.os.Build;
import android.os.LocaleList;
-import android.os.RemoteException;
import android.os.UserHandle;
import android.text.TextUtils;
import android.text.format.DateFormat;
import android.util.Base64;
import android.util.Log;
+import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import com.android.settings.fuelgauge.BatteryUtils;
import com.android.settings.fuelgauge.batteryusage.db.AppUsageEventEntity;
import com.android.settings.fuelgauge.batteryusage.db.BatteryEventEntity;
+import com.android.settings.fuelgauge.batteryusage.db.BatteryUsageSlotEntity;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.List;
import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
import java.util.TimeZone;
/** A utility class to convert data into another types. */
@@ -67,10 +72,31 @@
public static final int CONSUMER_TYPE_USER_BATTERY = 2;
public static final int CONSUMER_TYPE_SYSTEM_BATTERY = 3;
+ public static final int DEFAULT_USAGE_SOURCE = UsageStatsManager.USAGE_SOURCE_CURRENT_ACTIVITY;
+ public static final int EMPTY_USAGE_SOURCE = -1;
+
+ @VisibleForTesting
+ static int sUsageSource = EMPTY_USAGE_SOURCE;
+
private ConvertUtils() {
}
- /** Converts {@link BatteryEntry} to content values */
+ /** Whether {@code consumerType} is app consumer or not. */
+ public static boolean isUidConsumer(final int consumerType) {
+ return consumerType == CONSUMER_TYPE_UID_BATTERY;
+ }
+
+ /** Whether {@code consumerType} is user consumer or not. */
+ public static boolean isUserConsumer(final int consumerType) {
+ return consumerType == CONSUMER_TYPE_USER_BATTERY;
+ }
+
+ /** Whether {@code consumerType} is system consumer or not. */
+ public static boolean isSystemConsumer(final int consumerType) {
+ return consumerType == CONSUMER_TYPE_SYSTEM_BATTERY;
+ }
+
+ /** Converts {@link BatteryEntry} to {@link ContentValues} */
public static ContentValues convertBatteryEntryToContentValues(
final BatteryEntry entry,
final BatteryUsageStats batteryUsageStats,
@@ -113,7 +139,7 @@
return values;
}
- /** Converts {@link AppUsageEvent} to content values */
+ /** Converts {@link AppUsageEvent} to {@link ContentValues} */
public static ContentValues convertAppUsageEventToContentValues(final AppUsageEvent event) {
final ContentValues values = new ContentValues();
values.put(AppUsageEventEntity.KEY_UID, event.getUid());
@@ -126,7 +152,7 @@
return values;
}
- /** Converts {@link BatteryEvent} to content values */
+ /** Converts {@link BatteryEvent} to {@link ContentValues} */
public static ContentValues convertBatteryEventToContentValues(final BatteryEvent event) {
final ContentValues values = new ContentValues();
values.put(BatteryEventEntity.KEY_TIMESTAMP, event.getTimestamp());
@@ -135,6 +161,16 @@
return values;
}
+ /** Converts {@link BatteryUsageSlot} to {@link ContentValues} */
+ public static ContentValues convertBatteryUsageSlotToContentValues(
+ final BatteryUsageSlot batteryUsageSlot) {
+ final ContentValues values = new ContentValues(2);
+ values.put(BatteryUsageSlotEntity.KEY_TIMESTAMP, batteryUsageSlot.getStartTimestamp());
+ values.put(BatteryUsageSlotEntity.KEY_BATTERY_USAGE_SLOT,
+ Base64.encodeToString(batteryUsageSlot.toByteArray(), Base64.DEFAULT));
+ return values;
+ }
+
/** Gets the encoded string from {@link BatteryInformation} instance. */
public static String convertBatteryInformationToString(
final BatteryInformation batteryInformation) {
@@ -178,10 +214,10 @@
/*isFullChargeStart=*/ false));
}
- /** Converts to {@link AppUsageEvent} from {@link Event} */
+ /** Converts from {@link Event} to {@link AppUsageEvent} */
@Nullable
public static AppUsageEvent convertToAppUsageEvent(
- Context context, final IUsageStatsManager usageStatsManager, final Event event,
+ Context context, IUsageStatsManager usageStatsManager, final Event event,
final long userId) {
final String packageName = event.getPackageName();
if (packageName == null) {
@@ -207,7 +243,8 @@
}
final String effectivePackageName =
- getEffectivePackageName(usageStatsManager, packageName, taskRootPackageName);
+ getEffectivePackageName(
+ context, usageStatsManager, packageName, taskRootPackageName);
try {
final long uid = context
.getPackageManager()
@@ -228,8 +265,8 @@
return appUsageEventBuilder.build();
}
- /** Converts to {@link AppUsageEvent} from {@link Cursor} */
- public static AppUsageEvent convertToAppUsageEventFromCursor(final Cursor cursor) {
+ /** Converts from {@link Cursor} to {@link AppUsageEvent} */
+ public static AppUsageEvent convertToAppUsageEvent(final Cursor cursor) {
final AppUsageEvent.Builder eventBuilder = AppUsageEvent.newBuilder();
eventBuilder.setTimestamp(getLongFromCursor(cursor, AppUsageEventEntity.KEY_TIMESTAMP));
eventBuilder.setType(
@@ -247,7 +284,7 @@
return eventBuilder.build();
}
- /** Converts to {@link BatteryEvent} from {@link BatteryEventType} */
+ /** Converts from {@link BatteryEventType} to {@link BatteryEvent} */
public static BatteryEvent convertToBatteryEvent(
long timestamp, BatteryEventType type, int batteryLevel) {
final BatteryEvent.Builder eventBuilder = BatteryEvent.newBuilder();
@@ -257,8 +294,8 @@
return eventBuilder.build();
}
- /** Converts to {@link BatteryEvent} from {@link Cursor} */
- public static BatteryEvent convertToBatteryEventFromCursor(final Cursor cursor) {
+ /** Converts from {@link Cursor} to {@link BatteryEvent} */
+ public static BatteryEvent convertToBatteryEvent(final Cursor cursor) {
final BatteryEvent.Builder eventBuilder = BatteryEvent.newBuilder();
eventBuilder.setTimestamp(getLongFromCursor(cursor, BatteryEventEntity.KEY_TIMESTAMP));
eventBuilder.setType(
@@ -270,6 +307,42 @@
return eventBuilder.build();
}
+ /** Converts from {@link BatteryLevelData} to {@link List<BatteryEvent>} */
+ public static List<BatteryEvent> convertToBatteryEventList(
+ final BatteryLevelData batteryLevelData) {
+ final List<BatteryEvent> batteryEventList = new ArrayList<>();
+ final List<BatteryLevelData.PeriodBatteryLevelData> levelDataList =
+ batteryLevelData.getHourlyBatteryLevelsPerDay();
+ for (BatteryLevelData.PeriodBatteryLevelData oneDayData : levelDataList) {
+ for (int hourIndex = 0; hourIndex < oneDayData.getLevels().size() - 1; hourIndex++) {
+ batteryEventList.add(convertToBatteryEvent(
+ oneDayData.getTimestamps().get(hourIndex),
+ BatteryEventType.EVEN_HOUR,
+ oneDayData.getLevels().get(hourIndex)));
+ }
+ }
+ return batteryEventList;
+ }
+
+ /** Converts from {@link Cursor} to {@link BatteryUsageSlot} */
+ public static BatteryUsageSlot convertToBatteryUsageSlot(final Cursor cursor) {
+ final BatteryUsageSlot defaultInstance = BatteryUsageSlot.getDefaultInstance();
+ final int columnIndex =
+ cursor.getColumnIndex(BatteryUsageSlotEntity.KEY_BATTERY_USAGE_SLOT);
+ return columnIndex < 0 ? defaultInstance : BatteryUtils.parseProtoFromString(
+ cursor.getString(columnIndex), defaultInstance);
+ }
+
+ /** Converts from {@link Map<Long, BatteryDiffData>} to {@link List<BatteryUsageSlot>} */
+ public static List<BatteryUsageSlot> convertToBatteryUsageSlotList(
+ final Map<Long, BatteryDiffData> batteryDiffDataMap) {
+ List<BatteryUsageSlot> batteryUsageSlotList = new ArrayList<>();
+ for (BatteryDiffData batteryDiffData : batteryDiffDataMap.values()) {
+ batteryUsageSlotList.add(convertToBatteryUsageSlot(batteryDiffData));
+ }
+ return batteryUsageSlotList;
+ }
+
/** Converts UTC timestamp to local time string for logging only, so use the US locale for
* better readability in debugging. */
public static String utcToLocalTimeForLogging(long timestamp) {
@@ -323,9 +396,9 @@
*/
@VisibleForTesting
static String getEffectivePackageName(
- final IUsageStatsManager usageStatsManager, final String packageName,
+ Context context, IUsageStatsManager usageStatsManager, final String packageName,
final String taskRootPackageName) {
- int usageSource = getUsageSource(usageStatsManager);
+ final int usageSource = getUsageSource(context, usageStatsManager);
switch (usageSource) {
case UsageStatsManager.USAGE_SOURCE_TASK_ROOT_ACTIVITY:
return !TextUtils.isEmpty(taskRootPackageName)
@@ -370,18 +443,11 @@
}
}
- /**
- * Returns what App Usage Observers will consider the source of usage for an activity.
- *
- * @see UsageStatsManager#getUsageSource()
- */
- private static int getUsageSource(final IUsageStatsManager usageStatsManager) {
- try {
- return usageStatsManager.getUsageSource();
- } catch (RemoteException e) {
- Log.e(TAG, "Failed to getUsageSource", e);
- return UsageStatsManager.USAGE_SOURCE_CURRENT_ACTIVITY;
+ private static int getUsageSource(Context context, IUsageStatsManager usageStatsManager) {
+ if (sUsageSource == EMPTY_USAGE_SOURCE) {
+ sUsageSource = DatabaseUtils.getUsageSource(context, usageStatsManager);
}
+ return sUsageSource;
}
private static AppUsageEventType getAppUsageEventType(final int eventType) {
@@ -397,6 +463,103 @@
}
}
+ private static BatteryUsageDiff convertToBatteryUsageDiff(BatteryDiffEntry batteryDiffEntry) {
+ BatteryUsageDiff.Builder builder = BatteryUsageDiff.newBuilder()
+ .setUid(batteryDiffEntry.mUid)
+ .setUserId(batteryDiffEntry.mUserId)
+ .setIsHidden(batteryDiffEntry.mIsHidden)
+ .setComponentId(batteryDiffEntry.mComponentId)
+ .setConsumerType(batteryDiffEntry.mConsumerType)
+ .setConsumePower(batteryDiffEntry.mConsumePower)
+ .setForegroundUsageConsumePower(batteryDiffEntry.mForegroundUsageConsumePower)
+ .setBackgroundUsageConsumePower(batteryDiffEntry.mBackgroundUsageConsumePower)
+ .setForegroundServiceUsageConsumePower(
+ batteryDiffEntry.mForegroundServiceUsageConsumePower)
+ .setCachedUsageConsumePower(batteryDiffEntry.mCachedUsageConsumePower)
+ .setForegroundUsageTime(batteryDiffEntry.mForegroundUsageTimeInMs)
+ .setBackgroundUsageTime(batteryDiffEntry.mBackgroundUsageTimeInMs)
+ .setScreenOnTime(batteryDiffEntry.mScreenOnTimeInMs);
+ if (batteryDiffEntry.mKey != null) {
+ builder.setKey(batteryDiffEntry.mKey);
+ }
+ if (batteryDiffEntry.mLegacyPackageName != null) {
+ builder.setPackageName(batteryDiffEntry.mLegacyPackageName);
+ }
+ if (batteryDiffEntry.mLegacyLabel != null) {
+ builder.setLabel(batteryDiffEntry.mLegacyLabel);
+ }
+ return builder.build();
+ }
+
+ private static BatteryUsageSlot convertToBatteryUsageSlot(
+ final BatteryDiffData batteryDiffData) {
+ if (batteryDiffData == null) {
+ return BatteryUsageSlot.getDefaultInstance();
+ }
+ final BatteryUsageSlot.Builder builder = BatteryUsageSlot.newBuilder()
+ .setStartTimestamp(batteryDiffData.getStartTimestamp())
+ .setEndTimestamp(batteryDiffData.getEndTimestamp())
+ .setStartBatteryLevel(batteryDiffData.getStartBatteryLevel())
+ .setEndBatteryLevel(batteryDiffData.getEndBatteryLevel())
+ .setScreenOnTime(batteryDiffData.getScreenOnTime());
+ for (BatteryDiffEntry batteryDiffEntry : batteryDiffData.getAppDiffEntryList()) {
+ builder.addAppUsage(convertToBatteryUsageDiff(batteryDiffEntry));
+ }
+ for (BatteryDiffEntry batteryDiffEntry : batteryDiffData.getSystemDiffEntryList()) {
+ builder.addSystemUsage(convertToBatteryUsageDiff(batteryDiffEntry));
+ }
+ return builder.build();
+ }
+
+ private static BatteryDiffEntry convertToBatteryDiffEntry(
+ Context context, final BatteryUsageDiff batteryUsageDiff) {
+ return new BatteryDiffEntry(
+ context,
+ batteryUsageDiff.getUid(),
+ batteryUsageDiff.getUserId(),
+ batteryUsageDiff.getKey(),
+ batteryUsageDiff.getIsHidden(),
+ batteryUsageDiff.getComponentId(),
+ batteryUsageDiff.getPackageName(),
+ batteryUsageDiff.getLabel(),
+ batteryUsageDiff.getConsumerType(),
+ batteryUsageDiff.getForegroundUsageTime(),
+ batteryUsageDiff.getBackgroundUsageTime(),
+ batteryUsageDiff.getScreenOnTime(),
+ batteryUsageDiff.getConsumePower(),
+ batteryUsageDiff.getForegroundUsageConsumePower(),
+ batteryUsageDiff.getForegroundServiceUsageConsumePower(),
+ batteryUsageDiff.getBackgroundUsageConsumePower(),
+ batteryUsageDiff.getCachedUsageConsumePower());
+ }
+
+ static BatteryDiffData convertToBatteryDiffData(
+ Context context,
+ final BatteryUsageSlot batteryUsageSlot,
+ @NonNull final Set<String> systemAppsPackageNames,
+ @NonNull final Set<Integer> systemAppsUids) {
+ final List<BatteryDiffEntry> appDiffEntries = new ArrayList<>();
+ final List<BatteryDiffEntry> systemDiffEntries = new ArrayList<>();
+ for (BatteryUsageDiff batteryUsageDiff : batteryUsageSlot.getAppUsageList()) {
+ appDiffEntries.add(convertToBatteryDiffEntry(context, batteryUsageDiff));
+ }
+ for (BatteryUsageDiff batteryUsageDiff : batteryUsageSlot.getSystemUsageList()) {
+ systemDiffEntries.add(convertToBatteryDiffEntry(context, batteryUsageDiff));
+ }
+ return new BatteryDiffData(
+ context,
+ batteryUsageSlot.getStartTimestamp(),
+ batteryUsageSlot.getEndTimestamp(),
+ batteryUsageSlot.getStartBatteryLevel(),
+ batteryUsageSlot.getEndBatteryLevel(),
+ batteryUsageSlot.getScreenOnTime(),
+ appDiffEntries,
+ systemDiffEntries,
+ systemAppsPackageNames,
+ systemAppsUids,
+ /*isAccumulated=*/ false);
+ }
+
private static BatteryInformation constructBatteryInformation(
final BatteryEntry entry,
final BatteryUsageStats batteryUsageStats,
diff --git a/src/com/android/settings/fuelgauge/batteryusage/DataProcessManager.java b/src/com/android/settings/fuelgauge/batteryusage/DataProcessManager.java
index 0f67e6a..1a226fd 100644
--- a/src/com/android/settings/fuelgauge/batteryusage/DataProcessManager.java
+++ b/src/com/android/settings/fuelgauge/batteryusage/DataProcessManager.java
@@ -23,6 +23,7 @@
import android.os.Looper;
import android.os.UserHandle;
import android.os.UserManager;
+import android.util.ArrayMap;
import android.util.Log;
import androidx.annotation.NonNull;
@@ -33,10 +34,10 @@
import java.util.ArrayList;
import java.util.Calendar;
-import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.Set;
/**
* Manages the async tasks to process battery and app usage data.
@@ -69,28 +70,37 @@
*/
public class DataProcessManager {
private static final String TAG = "DataProcessManager";
+ private static final List<BatteryEventType> POWER_CONNECTION_EVENTS =
+ List.of(BatteryEventType.POWER_CONNECTED, BatteryEventType.POWER_DISCONNECTED);
+ private static final List<BatteryEventType> BATTERY_LEVEL_RECORD_EVENTS =
+ List.of(BatteryEventType.FULL_CHARGED, BatteryEventType.EVEN_HOUR);
- private final Handler mHandler;
- private final DataProcessor.UsageMapAsyncResponse mCallbackFunction;
- private final List<AppUsageEvent> mAppUsageEventList = new ArrayList<>();
- private final List<BatteryEvent> mBatteryEventList = new ArrayList<>();
-
- private Context mContext;
- private UserManager mUserManager;
- private List<BatteryLevelData.PeriodBatteryLevelData> mHourlyBatteryLevelsPerDay;
- private Map<Long, Map<String, BatteryHistEntry>> mBatteryHistoryMap;
+ // For testing only.
+ @VisibleForTesting
+ static Map<Long, Map<String, BatteryHistEntry>> sFakeBatteryHistoryMap;
// Raw start timestamp with round to the nearest hour.
- private long mRawStartTimestamp;
+ private final long mRawStartTimestamp;
+ private final long mLastFullChargeTimestamp;
+ private final Context mContext;
+ private final Handler mHandler;
+ private final UserManager mUserManager;
+ private final OnBatteryDiffDataMapLoadedListener mCallbackFunction;
+ private final List<AppUsageEvent> mAppUsageEventList = new ArrayList<>();
+ private final List<BatteryEvent> mBatteryEventList = new ArrayList<>();
+ private final List<BatteryUsageSlot> mBatteryUsageSlotList = new ArrayList<>();
+ private final List<BatteryLevelData.PeriodBatteryLevelData> mHourlyBatteryLevelsPerDay;
+ private final Map<Long, Map<String, BatteryHistEntry>> mBatteryHistoryMap;
private boolean mIsCurrentBatteryHistoryLoaded = false;
private boolean mIsCurrentAppUsageLoaded = false;
private boolean mIsDatabaseAppUsageLoaded = false;
private boolean mIsBatteryEventLoaded = false;
+ private boolean mIsBatteryUsageSlotLoaded = false;
// Used to identify whether screen-on time data should be shown in the UI.
private boolean mShowScreenOnTime = true;
- // Used to identify whether battery level data should be shown in the UI.
- private boolean mShowBatteryLevel = true;
+ private Set<String> mSystemAppsPackageNames = null;
+ private Set<Integer> mSystemAppsUids = null;
/**
* The indexed {@link AppUsagePeriod} list data for each corresponding time slot.
@@ -101,22 +111,33 @@
mAppUsagePeriodMap;
/**
+ * A callback listener when all the data is processed.
+ * This happens when all the async tasks complete and generate the final callback.
+ */
+ public interface OnBatteryDiffDataMapLoadedListener {
+ /** The callback function when all the data is processed. */
+ void onBatteryDiffDataMapLoaded(Map<Long, BatteryDiffData> batteryDiffDataMap);
+ }
+
+ /**
* Constructor when there exists battery level data.
*/
DataProcessManager(
Context context,
Handler handler,
final long rawStartTimestamp,
- @NonNull final DataProcessor.UsageMapAsyncResponse callbackFunction,
+ final long lastFullChargeTimestamp,
+ @NonNull final OnBatteryDiffDataMapLoadedListener callbackFunction,
@NonNull final List<BatteryLevelData.PeriodBatteryLevelData> hourlyBatteryLevelsPerDay,
@NonNull final Map<Long, Map<String, BatteryHistEntry>> batteryHistoryMap) {
mContext = context.getApplicationContext();
mHandler = handler;
mUserManager = mContext.getSystemService(UserManager.class);
+ mRawStartTimestamp = rawStartTimestamp;
+ mLastFullChargeTimestamp = lastFullChargeTimestamp;
mCallbackFunction = callbackFunction;
mHourlyBatteryLevelsPerDay = hourlyBatteryLevelsPerDay;
mBatteryHistoryMap = batteryHistoryMap;
- mRawStartTimestamp = rawStartTimestamp;
}
/**
@@ -125,31 +146,49 @@
DataProcessManager(
Context context,
Handler handler,
- @NonNull final DataProcessor.UsageMapAsyncResponse callbackFunction) {
+ @NonNull final OnBatteryDiffDataMapLoadedListener callbackFunction) {
mContext = context.getApplicationContext();
mHandler = handler;
mUserManager = mContext.getSystemService(UserManager.class);
mCallbackFunction = callbackFunction;
+ mRawStartTimestamp = 0L;
+ mLastFullChargeTimestamp = 0L;
+ mHourlyBatteryLevelsPerDay = null;
+ mBatteryHistoryMap = null;
// When there is no battery level data, don't show screen-on time and battery level chart on
// the UI.
mShowScreenOnTime = false;
- mShowBatteryLevel = false;
}
/**
* Starts the async tasks to load battery history data and app usage data.
*/
public void start() {
+ start(/*isFromPeriodJob=*/ false);
+ }
+
+ /**
+ * Starts the async tasks to load battery history data and app usage data.
+ */
+ public void start(boolean isFromPeriodJob) {
// If we have battery level data, load the battery history map and app usage simultaneously.
- if (mShowBatteryLevel) {
- // Loads the latest battery history data from the service.
- loadCurrentBatteryHistoryMap();
+ if (mHourlyBatteryLevelsPerDay != null) {
+ if (isFromPeriodJob) {
+ mIsCurrentBatteryHistoryLoaded = true;
+ mIsCurrentAppUsageLoaded = true;
+ mIsBatteryUsageSlotLoaded = true;
+ } else {
+ // Loads the latest battery history data from the service.
+ loadCurrentBatteryHistoryMap();
+ // Loads the latest app usage list from the service.
+ loadCurrentAppUsageList();
+ // Loads existing battery usage slots from database.
+ loadBatteryUsageSlotList();
+ }
// Loads app usage list from database.
loadDatabaseAppUsageList();
- // Loads the latest app usage list from the service.
- loadCurrentAppUsageList();
// Loads the battery event list from database.
- loadBatteryEventList();
+ loadPowerConnectionBatteryEventList();
} else {
// If there is no battery level data, only load the battery history data from service
// and show it as the app list directly.
@@ -193,11 +232,6 @@
return mShowScreenOnTime;
}
- @VisibleForTesting
- boolean getShowBatteryLevel() {
- return mShowBatteryLevel;
- }
-
private void loadCurrentBatteryHistoryMap() {
new AsyncTask<Void, Void, Map<String, BatteryHistEntry>>() {
@Override
@@ -323,7 +357,7 @@
}.execute();
}
- private void loadBatteryEventList() {
+ private void loadPowerConnectionBatteryEventList() {
new AsyncTask<Void, Void, List<BatteryEvent>>() {
@Override
protected List<BatteryEvent> doInBackground(Void... voids) {
@@ -331,8 +365,10 @@
// Loads the battery event data from the database.
final List<BatteryEvent> batteryEventList =
DatabaseUtils.getBatteryEvents(
- mContext, Calendar.getInstance(), mRawStartTimestamp);
- Log.d(TAG, String.format("execute loadBatteryEventList size=%d in %d/ms",
+ mContext, Calendar.getInstance(), mRawStartTimestamp,
+ POWER_CONNECTION_EVENTS);
+ Log.d(TAG, String.format(
+ "execute loadPowerConnectionBatteryEventList size=%d in %d/ms",
batteryEventList.size(), (System.currentTimeMillis() - startTime)));
return batteryEventList;
}
@@ -352,29 +388,55 @@
}.execute();
}
- private void loadAndApplyBatteryMapFromServiceOnly() {
- new AsyncTask<Void, Void, Map<Integer, Map<Integer, BatteryDiffData>>>() {
+ private void loadBatteryUsageSlotList() {
+ new AsyncTask<Void, Void, List<BatteryUsageSlot>>() {
@Override
- protected Map<Integer, Map<Integer, BatteryDiffData>> doInBackground(Void... voids) {
+ protected List<BatteryUsageSlot> doInBackground(Void... voids) {
final long startTime = System.currentTimeMillis();
- final Map<Integer, Map<Integer, BatteryDiffData>> batteryUsageMap =
- DataProcessor.getBatteryUsageMapFromStatsService(mContext);
- DataProcessor.loadLabelAndIcon(batteryUsageMap);
- Log.d(TAG, String.format(
- "execute loadAndApplyBatteryMapFromServiceOnly size=%d in %d/ms",
- batteryUsageMap.size(), (System.currentTimeMillis() - startTime)));
- return batteryUsageMap;
+ // Loads the battery usage slot data from the database.
+ final List<BatteryUsageSlot> batteryUsageSlotList =
+ DatabaseUtils.getBatteryUsageSlots(
+ mContext, Calendar.getInstance(), mLastFullChargeTimestamp);
+ Log.d(TAG, String.format("execute loadBatteryUsageSlotList size=%d in %d/ms",
+ batteryUsageSlotList.size(), (System.currentTimeMillis() - startTime)));
+ return batteryUsageSlotList;
}
@Override
- protected void onPostExecute(
- final Map<Integer, Map<Integer, BatteryDiffData>> batteryUsageMap) {
- // Set the unused variables to null.
- mContext = null;
+ protected void onPostExecute(final List<BatteryUsageSlot> batteryUsageSlotList) {
+ if (batteryUsageSlotList == null || batteryUsageSlotList.isEmpty()) {
+ Log.d(TAG, "batteryUsageSlotList is null or empty");
+ } else {
+ mBatteryUsageSlotList.clear();
+ mBatteryUsageSlotList.addAll(batteryUsageSlotList);
+ }
+ mIsBatteryUsageSlotLoaded = true;
+ tryToGenerateFinalDataAndApplyCallback();
+ }
+ }.execute();
+ }
+
+ private void loadAndApplyBatteryMapFromServiceOnly() {
+ new AsyncTask<Void, Void, Map<Long, BatteryDiffData>>() {
+ @Override
+ protected Map<Long, BatteryDiffData> doInBackground(Void... voids) {
+ final long startTime = System.currentTimeMillis();
+ final Map<Long, BatteryDiffData> batteryDiffDataMap =
+ DataProcessor.getBatteryDiffDataMapFromStatsService(
+ mContext, mRawStartTimestamp, getSystemAppsPackageNames(),
+ getSystemAppsUids());
+ Log.d(TAG, String.format(
+ "execute loadAndApplyBatteryMapFromServiceOnly size=%d in %d/ms",
+ batteryDiffDataMap.size(), (System.currentTimeMillis() - startTime)));
+ return batteryDiffDataMap;
+ }
+
+ @Override
+ protected void onPostExecute(final Map<Long, BatteryDiffData> batteryDiffDataMap) {
// Post results back to main thread to refresh UI.
if (mHandler != null && mCallbackFunction != null) {
mHandler.post(() -> {
- mCallbackFunction.onBatteryCallbackDataLoaded(batteryUsageMap);
+ mCallbackFunction.onBatteryDiffDataMapLoaded(batteryDiffDataMap);
});
}
}
@@ -397,8 +459,8 @@
}
// Generates the indexed AppUsagePeriod list data for each corresponding time slot for
// further use.
- mAppUsagePeriodMap = DataProcessor.generateAppUsagePeriodMap(mRawStartTimestamp,
- mHourlyBatteryLevelsPerDay, mAppUsageEventList, mBatteryEventList);
+ mAppUsagePeriodMap = DataProcessor.generateAppUsagePeriodMap(
+ mContext, mHourlyBatteryLevelsPerDay, mAppUsageEventList, mBatteryEventList);
}
private void tryToGenerateFinalDataAndApplyCallback() {
@@ -406,38 +468,41 @@
if (!mIsCurrentBatteryHistoryLoaded
|| !mIsCurrentAppUsageLoaded
|| !mIsDatabaseAppUsageLoaded
- || !mIsBatteryEventLoaded) {
+ || !mIsBatteryEventLoaded
+ || !mIsBatteryUsageSlotLoaded) {
return;
}
generateFinalDataAndApplyCallback();
}
- private void generateFinalDataAndApplyCallback() {
- new AsyncTask<Void, Void, Map<Integer, Map<Integer, BatteryDiffData>>>() {
+ private synchronized void generateFinalDataAndApplyCallback() {
+ new AsyncTask<Void, Void, Map<Long, BatteryDiffData>>() {
@Override
- protected Map<Integer, Map<Integer, BatteryDiffData>> doInBackground(Void... voids) {
+ protected Map<Long, BatteryDiffData> doInBackground(Void... voids) {
final long startTime = System.currentTimeMillis();
- final Map<Integer, Map<Integer, BatteryDiffData>> batteryUsageMap =
- DataProcessor.getBatteryUsageMap(
- mContext, mHourlyBatteryLevelsPerDay, mBatteryHistoryMap,
- mAppUsagePeriodMap);
- DataProcessor.loadLabelAndIcon(batteryUsageMap);
- Log.d(TAG, String.format("execute generateFinalDataAndApplyCallback in %d/ms",
- (System.currentTimeMillis() - startTime)));
- return batteryUsageMap;
+ final Map<Long, BatteryDiffData> batteryDiffDataMap = new ArrayMap<>();
+ for (BatteryUsageSlot batteryUsageSlot : mBatteryUsageSlotList) {
+ batteryDiffDataMap.put(batteryUsageSlot.getStartTimestamp(),
+ ConvertUtils.convertToBatteryDiffData(
+ mContext, batteryUsageSlot, getSystemAppsPackageNames(),
+ getSystemAppsUids()));
+ }
+ batteryDiffDataMap.putAll(DataProcessor.getBatteryDiffDataMap(mContext,
+ mHourlyBatteryLevelsPerDay, mBatteryHistoryMap, mAppUsagePeriodMap,
+ getSystemAppsPackageNames(), getSystemAppsUids()));
+
+ Log.d(TAG, String.format(
+ "execute generateFinalDataAndApplyCallback size=%d in %d/ms",
+ batteryDiffDataMap.size(), System.currentTimeMillis() - startTime));
+ return batteryDiffDataMap;
}
@Override
- protected void onPostExecute(
- final Map<Integer, Map<Integer, BatteryDiffData>> batteryUsageMap) {
- // Set the unused variables to null.
- mContext = null;
- mHourlyBatteryLevelsPerDay = null;
- mBatteryHistoryMap = null;
+ protected void onPostExecute(final Map<Long, BatteryDiffData> batteryDiffDataMap) {
// Post results back to main thread to refresh UI.
if (mHandler != null && mCallbackFunction != null) {
mHandler.post(() -> {
- mCallbackFunction.onBatteryCallbackDataLoaded(batteryUsageMap);
+ mCallbackFunction.onBatteryDiffDataMapLoaded(batteryDiffDataMap);
});
}
}
@@ -445,7 +510,7 @@
}
// Whether we should load app usage data from service or database.
- private boolean shouldLoadAppUsageData() {
+ private synchronized boolean shouldLoadAppUsageData() {
if (!mShowScreenOnTime) {
return false;
}
@@ -480,6 +545,20 @@
return userHandle != null ? userHandle.getIdentifier() : Integer.MIN_VALUE;
}
+ private synchronized Set<String> getSystemAppsPackageNames() {
+ if (mSystemAppsPackageNames == null) {
+ mSystemAppsPackageNames = DataProcessor.getSystemAppsPackageNames(mContext);
+ }
+ return mSystemAppsPackageNames;
+ }
+
+ private synchronized Set<Integer> getSystemAppsUids() {
+ if (mSystemAppsUids == null) {
+ mSystemAppsUids = DataProcessor.getSystemAppsUids(mContext);
+ }
+ return mSystemAppsUids;
+ }
+
/**
* @return Returns battery level data and start async task to compute battery diff usage data
* and load app labels + icons.
@@ -489,14 +568,55 @@
public static BatteryLevelData getBatteryLevelData(
Context context,
@Nullable Handler handler,
- @Nullable final Map<Long, Map<String, BatteryHistEntry>> batteryHistoryMap,
- final DataProcessor.UsageMapAsyncResponse asyncResponseDelegate) {
- if (batteryHistoryMap == null || batteryHistoryMap.isEmpty()) {
- Log.d(TAG, "batteryHistoryMap is null in getBatteryLevelData()");
- new DataProcessManager(context, handler, asyncResponseDelegate).start();
+ final boolean isFromPeriodJob,
+ final OnBatteryDiffDataMapLoadedListener onBatteryUsageMapLoadedListener) {
+ final long start = System.currentTimeMillis();
+ final long lastFullChargeTime = DatabaseUtils.getLastFullChargeTime(context);
+ final List<BatteryEvent> batteryLevelRecordEvents =
+ DatabaseUtils.getBatteryEvents(
+ context, Calendar.getInstance(), lastFullChargeTime,
+ BATTERY_LEVEL_RECORD_EVENTS);
+ final long startTimestamp = batteryLevelRecordEvents.isEmpty()
+ ? lastFullChargeTime : batteryLevelRecordEvents.get(0).getTimestamp();
+ final BatteryLevelData batteryLevelData = getPeriodBatteryLevelData(context, handler,
+ startTimestamp, lastFullChargeTime, isFromPeriodJob,
+ onBatteryUsageMapLoadedListener);
+ Log.d(TAG, String.format("execute getBatteryLevelData in %d/ms,"
+ + " batteryLevelRecordEvents.size=%d",
+ (System.currentTimeMillis() - start), batteryLevelRecordEvents.size()));
+
+ return isFromPeriodJob
+ ? batteryLevelData
+ : BatteryLevelData.combine(batteryLevelData, batteryLevelRecordEvents);
+ }
+
+ private static BatteryLevelData getPeriodBatteryLevelData(
+ Context context,
+ @Nullable Handler handler,
+ final long startTimestamp,
+ final long lastFullChargeTime,
+ final boolean isFromPeriodJob,
+ final OnBatteryDiffDataMapLoadedListener onBatteryDiffDataMapLoadedListener) {
+ final long currentTime = System.currentTimeMillis();
+ Log.d(TAG, String.format("getPeriodBatteryLevelData() startTimestamp=%s",
+ ConvertUtils.utcToLocalTimeForLogging(startTimestamp)));
+ if (isFromPeriodJob
+ && startTimestamp >= TimestampUtils.getLastEvenHourTimestamp(currentTime)) {
+ // Nothing needs to be loaded for period job.
return null;
}
+
handler = handler != null ? handler : new Handler(Looper.getMainLooper());
+ final Map<Long, Map<String, BatteryHistEntry>> batteryHistoryMap =
+ sFakeBatteryHistoryMap != null ? sFakeBatteryHistoryMap
+ : DatabaseUtils.getHistoryMapSinceLatestRecordBeforeQueryTimestamp(context,
+ Calendar.getInstance(), startTimestamp, lastFullChargeTime);
+ if (batteryHistoryMap == null || batteryHistoryMap.isEmpty()) {
+ Log.d(TAG, "batteryHistoryMap is null in getPeriodBatteryLevelData()");
+ new DataProcessManager(context, handler, onBatteryDiffDataMapLoadedListener).start();
+ return null;
+ }
+
// Process raw history map data into hourly timestamps.
final Map<Long, Map<String, BatteryHistEntry>> processedBatteryHistoryMap =
DataProcessor.getHistoryMapWithExpectedTimestamps(context, batteryHistoryMap);
@@ -505,20 +625,20 @@
DataProcessor.getLevelDataThroughProcessedHistoryMap(
context, processedBatteryHistoryMap);
if (batteryLevelData == null) {
- new DataProcessManager(context, handler, asyncResponseDelegate).start();
+ new DataProcessManager(context, handler, onBatteryDiffDataMapLoadedListener).start();
Log.d(TAG, "getBatteryLevelData() returns null");
return null;
}
- final long rawStartTimestamp = Collections.min(batteryHistoryMap.keySet());
// Start the async task to compute diff usage data and load labels and icons.
new DataProcessManager(
context,
handler,
- rawStartTimestamp,
- asyncResponseDelegate,
+ startTimestamp,
+ lastFullChargeTime,
+ onBatteryDiffDataMapLoadedListener,
batteryLevelData.getHourlyBatteryLevelsPerDay(),
- processedBatteryHistoryMap).start();
+ processedBatteryHistoryMap).start(isFromPeriodJob);
return batteryLevelData;
}
diff --git a/src/com/android/settings/fuelgauge/batteryusage/DataProcessor.java b/src/com/android/settings/fuelgauge/batteryusage/DataProcessor.java
index 6914c30..925cb3a 100644
--- a/src/com/android/settings/fuelgauge/batteryusage/DataProcessor.java
+++ b/src/com/android/settings/fuelgauge/batteryusage/DataProcessor.java
@@ -17,6 +17,9 @@
package com.android.settings.fuelgauge.batteryusage;
import static com.android.settings.fuelgauge.batteryusage.ConvertUtils.getEffectivePackageName;
+import static com.android.settings.fuelgauge.batteryusage.ConvertUtils.isSystemConsumer;
+import static com.android.settings.fuelgauge.batteryusage.ConvertUtils.isUidConsumer;
+import static com.android.settingslib.fuelgauge.BatteryStatus.BATTERY_LEVEL_UNKNOWN;
import android.app.usage.IUsageStatsManager;
import android.app.usage.UsageEvents;
@@ -44,6 +47,7 @@
import android.util.Log;
import android.util.SparseArray;
+import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.android.internal.annotations.VisibleForTesting;
@@ -54,6 +58,8 @@
import com.android.settingslib.fuelgauge.BatteryStatus;
import com.android.settingslib.spaprivileged.model.app.AppListRepositoryUtil;
+import com.google.common.base.Preconditions;
+
import java.time.Duration;
import java.util.ArrayList;
import java.util.Calendar;
@@ -76,11 +82,7 @@
private static final int POWER_COMPONENT_WAKELOCK = 12;
private static final int MIN_AVERAGE_POWER_THRESHOLD_MILLI_AMP = 10;
private static final int MIN_DAILY_DATA_SIZE = 2;
- private static final int MIN_TIMESTAMP_DATA_SIZE = 2;
private static final int MAX_DIFF_SECONDS_OF_UPPER_TIMESTAMP = 5;
- // Maximum total time value for each hourly slot cumulative data at most 2 hours.
- private static final float TOTAL_HOURLY_TIME_THRESHOLD = DateUtils.HOUR_IN_MILLIS * 2;
- private static final long MIN_TIME_SLOT = DateUtils.HOUR_IN_MILLIS * 2;
private static final String MEDIASERVER_PACKAGE_NAME = "mediaserver";
private static final String ANDROID_CORE_APPS_SHARED_USER_ID = "android.uid.shared";
private static final Map<String, BatteryHistEntry> EMPTY_BATTERY_MAP = new ArrayMap<>();
@@ -159,11 +161,14 @@
}
return batteryLevelData == null
? null
- : getBatteryUsageMap(
- context,
- batteryLevelData.getHourlyBatteryLevelsPerDay(),
- processedBatteryHistoryMap,
- /*appUsagePeriodMap=*/ null);
+ : generateBatteryUsageMap(context,
+ getBatteryDiffDataMap(context,
+ batteryLevelData.getHourlyBatteryLevelsPerDay(),
+ processedBatteryHistoryMap,
+ /*appUsagePeriodMap=*/ null,
+ getSystemAppsPackageNames(context),
+ getSystemAppsUids(context)),
+ batteryLevelData);
}
/**
@@ -263,7 +268,7 @@
* </ul>
*
* <p>The structure is consistent with the battery usage map returned by
- * {@code getBatteryUsageMap}.</p>
+ * {@code generateBatteryUsageMap}.</p>
*
* <p>{@code Long} stands for the userId.</p>
* <p>{@code String} stands for the packageName.</p>
@@ -271,7 +276,7 @@
@Nullable
public static Map<Integer, Map<Integer, Map<Long, Map<String, List<AppUsagePeriod>>>>>
generateAppUsagePeriodMap(
- final long rawStartTimestamp,
+ Context context,
final List<BatteryLevelData.PeriodBatteryLevelData> hourlyBatteryLevelsPerDay,
final List<AppUsageEvent> appUsageEventList,
final List<BatteryEvent> batteryEventList) {
@@ -305,7 +310,7 @@
// The value could be null when there is no data in the hourly slot.
dailyMap.put(
hourlyIndex,
- buildAppUsagePeriodList(hourlyAppUsageEventList, batteryEventList,
+ buildAppUsagePeriodList(context, hourlyAppUsageEventList, batteryEventList,
startTimestamp, endTimestamp));
}
}
@@ -405,8 +410,8 @@
/**
* @return Returns the processed history map which has interpolated to every hour data.
- * The start and end timestamp must be the even hours.
- * The keys of processed history map should contain every hour between the start and end
+ * The start timestamp is the first timestamp in batteryHistoryMap. The end timestamp is current
+ * time. The keys of processed history map should contain every hour between the start and end
* timestamp. If there's no data in some key, the value will be the empty map.
*/
static Map<Long, Map<String, BatteryHistEntry>> getHistoryMapWithExpectedTimestamps(
@@ -433,28 +438,23 @@
static BatteryLevelData getLevelDataThroughProcessedHistoryMap(
Context context,
final Map<Long, Map<String, BatteryHistEntry>> processedBatteryHistoryMap) {
- final List<Long> timestampList = new ArrayList<>(processedBatteryHistoryMap.keySet());
- Collections.sort(timestampList);
- final List<Long> dailyTimestamps = getDailyTimestamps(timestampList);
// There should be at least the start and end timestamps. Otherwise, return null to not show
// data in usage chart.
- if (dailyTimestamps.size() < MIN_DAILY_DATA_SIZE) {
+ if (processedBatteryHistoryMap.size() < MIN_DAILY_DATA_SIZE) {
return null;
}
-
- final List<List<Long>> hourlyTimestamps = getHourlyTimestamps(dailyTimestamps);
- final BatteryLevelData.PeriodBatteryLevelData dailyLevelData =
- getPeriodBatteryLevelData(context, processedBatteryHistoryMap, dailyTimestamps);
- final List<BatteryLevelData.PeriodBatteryLevelData> hourlyLevelData =
- getHourlyPeriodBatteryLevelData(
- context, processedBatteryHistoryMap, hourlyTimestamps);
- return new BatteryLevelData(dailyLevelData, hourlyLevelData);
+ Map<Long, Integer> batteryLevelMap = new ArrayMap<>();
+ for (Long timestamp : processedBatteryHistoryMap.keySet()) {
+ batteryLevelMap.put(
+ timestamp, getLevel(context, processedBatteryHistoryMap, timestamp));
+ }
+ return new BatteryLevelData(batteryLevelMap);
}
/**
- * Computes expected timestamp slots. The start timestamp is the last full charge time.
- * The end timestamp is current time. The middle timestamps are the sharp hour timestamps
- * between the start and end timestamps.
+ * Computes expected timestamp slots. The start timestamp is the first timestamp in
+ * rawTimestampList. The end timestamp is current time. The middle timestamps are the sharp hour
+ * timestamps between the start and end timestamps.
*/
@VisibleForTesting
static List<Long> getTimestampSlots(final List<Long> rawTimestampList, final long currentTime) {
@@ -477,56 +477,6 @@
return timestampSlots;
}
- /**
- * Computes expected daily timestamp slots.
- *
- * The valid result should be composed of 3 parts:
- * 1) start timestamp
- * 2) every 00:00 timestamp (default timezone) between the start and end
- * 3) end timestamp
- * Otherwise, returns an empty list.
- */
- @VisibleForTesting
- static List<Long> getDailyTimestamps(final List<Long> timestampList) {
- final List<Long> dailyTimestampList = new ArrayList<>();
- // If timestamp number is smaller than 2, the following computation is not necessary.
- if (timestampList.size() < MIN_TIMESTAMP_DATA_SIZE) {
- return dailyTimestampList;
- }
- final long startTime = timestampList.get(0);
- final long endTime = timestampList.get(timestampList.size() - 1);
- for (long timestamp = startTime; timestamp < endTime;
- timestamp = TimestampUtils.getNextDayTimestamp(timestamp)) {
- dailyTimestampList.add(timestamp);
- }
- dailyTimestampList.add(endTime);
- return dailyTimestampList;
- }
-
- @VisibleForTesting
- static List<List<Long>> getHourlyTimestamps(final List<Long> dailyTimestamps) {
- final List<List<Long>> hourlyTimestamps = new ArrayList<>();
- if (dailyTimestamps.size() < MIN_DAILY_DATA_SIZE) {
- return hourlyTimestamps;
- }
-
- for (int dailyIndex = 0; dailyIndex < dailyTimestamps.size() - 1; dailyIndex++) {
- final List<Long> hourlyTimestampsPerDay = new ArrayList<>();
- final long startTime = dailyTimestamps.get(dailyIndex);
- final long endTime = dailyTimestamps.get(dailyIndex + 1);
-
- hourlyTimestampsPerDay.add(startTime);
- for (long timestamp = TimestampUtils.getNextEvenHourTimestamp(startTime);
- timestamp < endTime; timestamp += MIN_TIME_SLOT) {
- hourlyTimestampsPerDay.add(timestamp);
- }
- hourlyTimestampsPerDay.add(endTime);
-
- hourlyTimestamps.add(hourlyTimestampsPerDay);
- }
- return hourlyTimestamps;
- }
-
@VisibleForTesting
static boolean isFromFullCharge(@Nullable final Map<String, BatteryHistEntry> entryList) {
if (entryList == null) {
@@ -562,34 +512,102 @@
return results;
}
+ static Map<Long, BatteryDiffData> getBatteryDiffDataMap(
+ Context context,
+ final List<BatteryLevelData.PeriodBatteryLevelData> hourlyBatteryLevelsPerDay,
+ final Map<Long, Map<String, BatteryHistEntry>> batteryHistoryMap,
+ final Map<Integer, Map<Integer, Map<Long, Map<String, List<AppUsagePeriod>>>>>
+ appUsagePeriodMap,
+ final @NonNull Set<String> systemAppsPackageNames,
+ final @NonNull Set<Integer> systemAppsUids) {
+ final Map<Long, BatteryDiffData> batteryDiffDataMap = new ArrayMap<>();
+ final int currentUserId = context.getUserId();
+ final UserHandle userHandle =
+ Utils.getManagedProfile(context.getSystemService(UserManager.class));
+ final int workProfileUserId =
+ userHandle != null ? userHandle.getIdentifier() : Integer.MIN_VALUE;
+ // Each time slot usage diff data =
+ // sum(Math.abs(timestamp[i+1] data - timestamp[i] data));
+ // since we want to aggregate every hour usage diff data into a single time slot.
+ for (int dailyIndex = 0; dailyIndex < hourlyBatteryLevelsPerDay.size(); dailyIndex++) {
+ if (hourlyBatteryLevelsPerDay.get(dailyIndex) == null) {
+ continue;
+ }
+ final List<Long> hourlyTimestamps =
+ hourlyBatteryLevelsPerDay.get(dailyIndex).getTimestamps();
+ for (int hourlyIndex = 0; hourlyIndex < hourlyTimestamps.size() - 1; hourlyIndex++) {
+ final Long startTimestamp = hourlyTimestamps.get(hourlyIndex);
+ final Long endTimestamp = hourlyTimestamps.get(hourlyIndex + 1);
+ final int startBatteryLevel =
+ hourlyBatteryLevelsPerDay.get(dailyIndex).getLevels().get(hourlyIndex);
+ final int endBatteryLevel =
+ hourlyBatteryLevelsPerDay.get(dailyIndex).getLevels().get(hourlyIndex + 1);
+ final long slotDuration = endTimestamp - startTimestamp;
+ List<Map<String, BatteryHistEntry>> slotBatteryHistoryList = new ArrayList<>();
+ slotBatteryHistoryList.add(
+ batteryHistoryMap.getOrDefault(startTimestamp, EMPTY_BATTERY_MAP));
+ for (Long timestamp = TimestampUtils.getNextHourTimestamp(startTimestamp);
+ timestamp < endTimestamp; timestamp += DateUtils.HOUR_IN_MILLIS) {
+ slotBatteryHistoryList.add(
+ batteryHistoryMap.getOrDefault(timestamp, EMPTY_BATTERY_MAP));
+ }
+ slotBatteryHistoryList.add(
+ batteryHistoryMap.getOrDefault(endTimestamp, EMPTY_BATTERY_MAP));
+
+ final BatteryDiffData hourlyBatteryDiffData =
+ insertHourlyUsageDiffDataPerSlot(
+ context,
+ startTimestamp,
+ endTimestamp,
+ startBatteryLevel,
+ endBatteryLevel,
+ currentUserId,
+ workProfileUserId,
+ slotDuration,
+ systemAppsPackageNames,
+ systemAppsUids,
+ appUsagePeriodMap == null
+ || appUsagePeriodMap.get(dailyIndex) == null
+ ? null
+ : appUsagePeriodMap.get(dailyIndex).get(hourlyIndex),
+ slotBatteryHistoryList);
+ batteryDiffDataMap.put(startTimestamp, hourlyBatteryDiffData);
+ }
+ }
+ return batteryDiffDataMap;
+ }
+
/**
* @return Returns the indexed battery usage data for each corresponding time slot.
*
* <p>There could be 2 cases of the returned value:</p>
* <ul>
- * <li>null: empty or invalid data.</li>
- * <li>non-null: must be a 2d map and composed by 3 parts:</li>
+ * <li> null: empty or invalid data.</li>
+ * <li> 1 part: if batteryLevelData is null.</li>
+ * <p> [SELECTED_INDEX_ALL][SELECTED_INDEX_ALL]</p>
+ * <li> 3 parts: if batteryLevelData is not null.</li>
* <p> 1 - [SELECTED_INDEX_ALL][SELECTED_INDEX_ALL]</p>
* <p> 2 - [0][SELECTED_INDEX_ALL] ~ [maxDailyIndex][SELECTED_INDEX_ALL]</p>
* <p> 3 - [0][0] ~ [maxDailyIndex][maxHourlyIndex]</p>
* </ul>
*/
- @Nullable
- static Map<Integer, Map<Integer, BatteryDiffData>> getBatteryUsageMap(
+ static Map<Integer, Map<Integer, BatteryDiffData>> generateBatteryUsageMap(
final Context context,
- final List<BatteryLevelData.PeriodBatteryLevelData> hourlyBatteryLevelsPerDay,
- final Map<Long, Map<String, BatteryHistEntry>> batteryHistoryMap,
- final Map<Integer, Map<Integer, Map<Long, Map<String, List<AppUsagePeriod>>>>>
- appUsagePeriodMap) {
- if (batteryHistoryMap.isEmpty()) {
- return null;
- }
+ final Map<Long, BatteryDiffData> batteryDiffDataMap,
+ final @Nullable BatteryLevelData batteryLevelData) {
final Map<Integer, Map<Integer, BatteryDiffData>> resultMap = new ArrayMap<>();
- final Set<String> systemAppsPackageNames = getSystemAppsPackageNames(context);
- final Set<Integer> systemAppsUids = getSystemAppsUids(context);
+ if (batteryLevelData == null) {
+ Preconditions.checkArgument(batteryDiffDataMap.size() == 1);
+ BatteryDiffData batteryDiffData = batteryDiffDataMap.values().stream().toList().get(0);
+ final Map<Integer, BatteryDiffData> allUsageMap = new ArrayMap<>();
+ allUsageMap.put(SELECTED_INDEX_ALL, batteryDiffData);
+ resultMap.put(SELECTED_INDEX_ALL, allUsageMap);
+ return resultMap;
+ }
+ List<BatteryLevelData.PeriodBatteryLevelData> hourlyBatteryLevelsPerDay =
+ batteryLevelData.getHourlyBatteryLevelsPerDay();
// Insert diff data from [0][0] to [maxDailyIndex][maxHourlyIndex].
- insertHourlyUsageDiffData(context, systemAppsPackageNames, systemAppsUids,
- hourlyBatteryLevelsPerDay, batteryHistoryMap, appUsagePeriodMap, resultMap);
+ insertHourlyUsageDiffData(hourlyBatteryLevelsPerDay, batteryDiffDataMap, resultMap);
// Insert diff data from [0][SELECTED_INDEX_ALL] to [maxDailyIndex][SELECTED_INDEX_ALL].
insertDailyUsageDiffData(context, hourlyBatteryLevelsPerDay, resultMap);
// Insert diff data [SELECTED_INDEX_ALL][SELECTED_INDEX_ALL].
@@ -604,7 +622,10 @@
@Nullable
static BatteryDiffData generateBatteryDiffData(
final Context context,
- final List<BatteryHistEntry> batteryHistEntryList) {
+ final long startTimestamp,
+ final List<BatteryHistEntry> batteryHistEntryList,
+ final @NonNull Set<String> systemAppsPackageNames,
+ final @NonNull Set<Integer> systemAppsUids) {
if (batteryHistEntryList == null || batteryHistEntryList.isEmpty()) {
Log.w(TAG, "batteryHistEntryList is null or empty in generateBatteryDiffData()");
return null;
@@ -626,6 +647,14 @@
} else {
final BatteryDiffEntry currentBatteryDiffEntry = new BatteryDiffEntry(
context,
+ entry.mUid,
+ entry.mUserId,
+ entry.getKey(),
+ entry.mIsHidden,
+ entry.mDrainType,
+ entry.mPackageName,
+ entry.mAppLabel,
+ entry.mConsumerType,
entry.mForegroundUsageTimeInMs,
entry.mBackgroundUsageTimeInMs,
/*screenOnTimeInMs=*/ 0,
@@ -633,8 +662,7 @@
entry.mForegroundUsageConsumePower,
entry.mForegroundServiceUsageConsumePower,
entry.mBackgroundUsageConsumePower,
- entry.mCachedUsageConsumePower,
- entry);
+ entry.mCachedUsageConsumePower);
if (currentBatteryDiffEntry.isSystemEntry()) {
systemEntries.add(currentBatteryDiffEntry);
} else {
@@ -647,11 +675,10 @@
if (appEntries.isEmpty() && systemEntries.isEmpty()) {
return null;
}
-
- final Set<String> systemAppsPackageNames = getSystemAppsPackageNames(context);
- final Set<Integer> systemAppsUids = getSystemAppsUids(context);
- return new BatteryDiffData(context, /* screenOnTime= */ 0L, appEntries, systemEntries,
- systemAppsPackageNames, systemAppsUids, /* isAccumulated= */ false);
+ return new BatteryDiffData(context, startTimestamp, getCurrentTimeMillis(),
+ /* startBatteryLevel =*/ 100, getCurrentLevel(context), /* screenOnTime= */ 0L,
+ appEntries, systemEntries, systemAppsPackageNames, systemAppsUids,
+ /* isAccumulated= */ false);
}
/**
@@ -661,8 +688,8 @@
@VisibleForTesting
@Nullable
static Map<Long, Map<String, List<AppUsagePeriod>>> buildAppUsagePeriodList(
- final List<AppUsageEvent> appUsageEvents, final List<BatteryEvent> batteryEventList,
- final long startTime, final long endTime) {
+ Context context, final List<AppUsageEvent> appUsageEvents,
+ final List<BatteryEvent> batteryEventList, final long startTime, final long endTime) {
if (appUsageEvents.isEmpty()) {
return null;
}
@@ -702,6 +729,7 @@
final AppUsageEvent firstEvent = usageEvents.get(0);
final long eventUserId = firstEvent.getUserId();
final String packageName = getEffectivePackageName(
+ context,
sUsageStatsManager,
firstEvent.getPackageName(),
firstEvent.getTaskRootPackageName());
@@ -846,21 +874,15 @@
return getScreenOnTime(appUsageMap.get(userId).get(packageName));
}
- /**
- * @return Returns the overall battery usage data from battery stats service directly.
- *
- * The returned value should be always a 2d map and composed by only 1 part:
- * - [SELECTED_INDEX_ALL][SELECTED_INDEX_ALL]
- */
- static Map<Integer, Map<Integer, BatteryDiffData>> getBatteryUsageMapFromStatsService(
- final Context context) {
- final Map<Integer, Map<Integer, BatteryDiffData>> resultMap = new ArrayMap<>();
- final Map<Integer, BatteryDiffData> allUsageMap = new ArrayMap<>();
- // Always construct the map whether the value is null or not.
- allUsageMap.put(SELECTED_INDEX_ALL,
- generateBatteryDiffData(context, getBatteryHistListFromFromStatsService(context)));
- resultMap.put(SELECTED_INDEX_ALL, allUsageMap);
- return resultMap;
+ static Map<Long, BatteryDiffData> getBatteryDiffDataMapFromStatsService(
+ final Context context, final long startTimestamp,
+ @NonNull final Set<String> systemAppsPackageNames,
+ @NonNull final Set<Integer> systemAppsUids) {
+ Map<Long, BatteryDiffData> batteryDiffDataMap = new ArrayMap<>(1);
+ batteryDiffDataMap.put(startTimestamp, generateBatteryDiffData(
+ context, startTimestamp, getBatteryHistListFromFromStatsService(context),
+ systemAppsPackageNames, systemAppsUids));
+ return batteryDiffDataMap;
}
static void loadLabelAndIcon(
@@ -879,6 +901,22 @@
}
}
+ static Set<String> getSystemAppsPackageNames(Context context) {
+ return sTestSystemAppsPackageNames != null ? sTestSystemAppsPackageNames
+ : AppListRepositoryUtil.getSystemPackageNames(context, context.getUserId());
+ }
+
+ static Set<Integer> getSystemAppsUids(Context context) {
+ Set<Integer> result = new ArraySet<>(1);
+ try {
+ result.add(context.getPackageManager().getUidForSharedUser(
+ ANDROID_CORE_APPS_SHARED_USER_ID));
+ } catch (PackageManager.NameNotFoundException e) {
+ // No Android Core Apps
+ }
+ return result;
+ }
+
/**
* Generates the list of {@link AppUsageEvent} within the specific time range.
* The buffer is added to make sure the app usage calculation near the boundaries is correct.
@@ -1159,28 +1197,6 @@
resultMap.put(currentSlot, newHistEntryMap);
}
- private static List<BatteryLevelData.PeriodBatteryLevelData> getHourlyPeriodBatteryLevelData(
- Context context,
- final Map<Long, Map<String, BatteryHistEntry>> processedBatteryHistoryMap,
- final List<List<Long>> timestamps) {
- final List<BatteryLevelData.PeriodBatteryLevelData> levelData = new ArrayList<>();
- timestamps.forEach(
- timestampList -> levelData.add(
- getPeriodBatteryLevelData(
- context, processedBatteryHistoryMap, timestampList)));
- return levelData;
- }
-
- private static BatteryLevelData.PeriodBatteryLevelData getPeriodBatteryLevelData(
- Context context,
- final Map<Long, Map<String, BatteryHistEntry>> processedBatteryHistoryMap,
- final List<Long> timestamps) {
- final List<Integer> levels = new ArrayList<>();
- timestamps.forEach(
- timestamp -> levels.add(getLevel(context, processedBatteryHistoryMap, timestamp)));
- return new BatteryLevelData.PeriodBatteryLevelData(timestamps, levels);
- }
-
private static Integer getLevel(
Context context,
final Map<Long, Map<String, BatteryHistEntry>> processedBatteryHistoryMap,
@@ -1189,13 +1205,12 @@
if (entryMap == null || entryMap.isEmpty()) {
Log.e(TAG, "abnormal entry list in the timestamp:"
+ ConvertUtils.utcToLocalTimeForLogging(timestamp));
- return null;
+ return BATTERY_LEVEL_UNKNOWN;
}
// The current time battery history hasn't been loaded yet, returns the current battery
// level.
if (entryMap.containsKey(CURRENT_TIME_BATTERY_HISTORY_PLACEHOLDER)) {
- final Intent intent = BatteryUtils.getBatteryIntent(context);
- return BatteryStatus.getBatteryLevel(intent);
+ return getCurrentLevel(context);
}
// Averages the battery level in each time slot to avoid corner conditions.
float batteryLevelCounter = 0;
@@ -1205,20 +1220,15 @@
return Math.round(batteryLevelCounter / entryMap.size());
}
+ private static int getCurrentLevel(Context context) {
+ final Intent intent = BatteryUtils.getBatteryIntent(context);
+ return BatteryStatus.getBatteryLevel(intent);
+ }
+
private static void insertHourlyUsageDiffData(
- Context context,
- final Set<String> systemAppsPackageNames,
- final Set<Integer> systemAppsUids,
final List<BatteryLevelData.PeriodBatteryLevelData> hourlyBatteryLevelsPerDay,
- final Map<Long, Map<String, BatteryHistEntry>> batteryHistoryMap,
- final Map<Integer, Map<Integer, Map<Long, Map<String, List<AppUsagePeriod>>>>>
- appUsagePeriodMap,
+ final Map<Long, BatteryDiffData> batteryDiffDataMap,
final Map<Integer, Map<Integer, BatteryDiffData>> resultMap) {
- final int currentUserId = context.getUserId();
- final UserHandle userHandle =
- Utils.getManagedProfile(context.getSystemService(UserManager.class));
- final int workProfileUserId =
- userHandle != null ? userHandle.getIdentifier() : Integer.MIN_VALUE;
// Each time slot usage diff data =
// sum(Math.abs(timestamp[i+1] data - timestamp[i] data));
// since we want to aggregate every hour usage diff data into a single time slot.
@@ -1232,33 +1242,7 @@
hourlyBatteryLevelsPerDay.get(dailyIndex).getTimestamps();
for (int hourlyIndex = 0; hourlyIndex < hourlyTimestamps.size() - 1; hourlyIndex++) {
final Long startTimestamp = hourlyTimestamps.get(hourlyIndex);
- final Long endTimestamp = hourlyTimestamps.get(hourlyIndex + 1);
- final long slotDuration = endTimestamp - startTimestamp;
- List<Map<String, BatteryHistEntry>> slotBatteryHistoryList = new ArrayList<>();
- slotBatteryHistoryList.add(
- batteryHistoryMap.getOrDefault(startTimestamp, EMPTY_BATTERY_MAP));
- for (Long timestamp = TimestampUtils.getNextHourTimestamp(startTimestamp);
- timestamp < endTimestamp; timestamp += DateUtils.HOUR_IN_MILLIS) {
- slotBatteryHistoryList.add(
- batteryHistoryMap.getOrDefault(timestamp, EMPTY_BATTERY_MAP));
- }
- slotBatteryHistoryList.add(
- batteryHistoryMap.getOrDefault(endTimestamp, EMPTY_BATTERY_MAP));
-
- final BatteryDiffData hourlyBatteryDiffData =
- insertHourlyUsageDiffDataPerSlot(
- context,
- currentUserId,
- workProfileUserId,
- slotDuration,
- systemAppsPackageNames,
- systemAppsUids,
- appUsagePeriodMap == null
- || appUsagePeriodMap.get(dailyIndex) == null
- ? null
- : appUsagePeriodMap.get(dailyIndex).get(hourlyIndex),
- slotBatteryHistoryList);
- dailyDiffMap.put(hourlyIndex, hourlyBatteryDiffData);
+ dailyDiffMap.put(hourlyIndex, batteryDiffDataMap.get(startTimestamp));
}
}
}
@@ -1293,6 +1277,10 @@
@Nullable
private static BatteryDiffData insertHourlyUsageDiffDataPerSlot(
final Context context,
+ final long startTimestamp,
+ final long endTimestamp,
+ final int startBatteryLevel,
+ final int endBatteryLevel,
final int currentUserId,
final int workProfileUserId,
final long slotDuration,
@@ -1402,7 +1390,7 @@
currentEntry.mCachedUsageConsumePower,
nextEntry.mCachedUsageConsumePower);
}
- if (selectedBatteryEntry.isSystemEntry()
+ if (isSystemConsumer(selectedBatteryEntry.mConsumerType)
&& selectedBatteryEntry.mDrainType == BatteryConsumer.POWER_COMPONENT_SCREEN) {
// Replace Screen system component time with screen on time.
foregroundUsageTimeInMs = slotScreenOnTime;
@@ -1448,6 +1436,14 @@
backgroundUsageTimeInMs, (long) slotDuration - screenOnTime);
final BatteryDiffEntry currentBatteryDiffEntry = new BatteryDiffEntry(
context,
+ selectedBatteryEntry.mUid,
+ selectedBatteryEntry.mUserId,
+ selectedBatteryEntry.getKey(),
+ selectedBatteryEntry.mIsHidden,
+ selectedBatteryEntry.mDrainType,
+ selectedBatteryEntry.mPackageName,
+ selectedBatteryEntry.mAppLabel,
+ selectedBatteryEntry.mConsumerType,
foregroundUsageTimeInMs,
backgroundUsageTimeInMs,
screenOnTime,
@@ -1455,8 +1451,7 @@
foregroundUsageConsumePower,
foregroundServiceUsageConsumePower,
backgroundUsageConsumePower,
- cachedUsageConsumePower,
- selectedBatteryEntry);
+ cachedUsageConsumePower);
if (currentBatteryDiffEntry.isSystemEntry()) {
systemEntries.add(currentBatteryDiffEntry);
} else {
@@ -1469,7 +1464,8 @@
return null;
}
- return new BatteryDiffData(context, slotScreenOnTime, appEntries, systemEntries,
+ return new BatteryDiffData(context, startTimestamp, endTimestamp, startBatteryLevel,
+ endBatteryLevel, slotScreenOnTime, appEntries, systemEntries,
systemAppsPackageNames, systemAppsUids, /* isAccumulated= */ false);
}
@@ -1520,7 +1516,7 @@
final int currentUserId,
final int workProfileUserId,
final BatteryHistEntry batteryHistEntry) {
- return batteryHistEntry.mConsumerType == ConvertUtils.CONSUMER_TYPE_UID_BATTERY
+ return isUidConsumer(batteryHistEntry.mConsumerType)
&& batteryHistEntry.mUserId != currentUserId
&& batteryHistEntry.mUserId != workProfileUserId;
}
@@ -1532,11 +1528,23 @@
final List<BatteryDiffEntry> appEntries = new ArrayList<>();
final List<BatteryDiffEntry> systemEntries = new ArrayList<>();
+ long startTimestamp = Long.MAX_VALUE;
+ long endTimestamp = 0;
+ int startBatteryLevel = BATTERY_LEVEL_UNKNOWN;
+ int endBatteryLevel = BATTERY_LEVEL_UNKNOWN;
long totalScreenOnTime = 0;
for (BatteryDiffData batteryDiffData : batteryDiffDataList) {
if (batteryDiffData == null) {
continue;
}
+ if (startTimestamp > batteryDiffData.getStartTimestamp()) {
+ startTimestamp = batteryDiffData.getStartTimestamp();
+ startBatteryLevel = batteryDiffData.getStartBatteryLevel();
+ }
+ if (endTimestamp > batteryDiffData.getEndTimestamp()) {
+ endTimestamp = batteryDiffData.getEndTimestamp();
+ endBatteryLevel = batteryDiffData.getEndBatteryLevel();
+ }
totalScreenOnTime += batteryDiffData.getScreenOnTime();
for (BatteryDiffEntry entry : batteryDiffData.getAppDiffEntryList()) {
computeUsageDiffDataPerEntry(entry, diffEntryMap);
@@ -1555,8 +1563,9 @@
}
}
- return diffEntryList.isEmpty() ? null : new BatteryDiffData(context, totalScreenOnTime,
- appEntries, systemEntries, /* systemAppsPackageNames= */ new ArraySet<>(),
+ return diffEntryList.isEmpty() ? null : new BatteryDiffData(context, startTimestamp,
+ endTimestamp, startBatteryLevel, endBatteryLevel, totalScreenOnTime, appEntries,
+ systemEntries, /* systemAppsPackageNames= */ new ArraySet<>(),
/* systemAppsUids= */ new ArraySet<>(), /* isAccumulated= */ true);
}
@@ -1752,22 +1761,6 @@
return v2 > v1 ? v2 - v1 : 0;
}
- private static Set<String> getSystemAppsPackageNames(Context context) {
- return sTestSystemAppsPackageNames != null ? sTestSystemAppsPackageNames
- : AppListRepositoryUtil.getSystemPackageNames(context, context.getUserId());
- }
-
- private static Set<Integer> getSystemAppsUids(Context context) {
- Set<Integer> result = new ArraySet<>();
- try {
- result.add(context.getPackageManager().getUidForSharedUser(
- ANDROID_CORE_APPS_SHARED_USER_ID));
- } catch (PackageManager.NameNotFoundException e) {
- // No Android Core Apps
- }
- return result;
- }
-
private static long getCurrentTimeMillis() {
return sTestCurrentTimeMillis > 0 ? sTestCurrentTimeMillis : System.currentTimeMillis();
}
diff --git a/src/com/android/settings/fuelgauge/batteryusage/DatabaseUtils.java b/src/com/android/settings/fuelgauge/batteryusage/DatabaseUtils.java
index 0435e45..e78d25c 100644
--- a/src/com/android/settings/fuelgauge/batteryusage/DatabaseUtils.java
+++ b/src/com/android/settings/fuelgauge/batteryusage/DatabaseUtils.java
@@ -15,6 +15,10 @@
*/
package com.android.settings.fuelgauge.batteryusage;
+import static com.android.settings.fuelgauge.batteryusage.ConvertUtils.utcToLocalTimeForLogging;
+
+import android.app.usage.IUsageStatsManager;
+import android.app.usage.UsageStatsManager;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
@@ -28,13 +32,18 @@
import android.os.BatteryUsageStats;
import android.os.Handler;
import android.os.Looper;
+import android.os.RemoteException;
import android.os.SystemClock;
import android.os.UserManager;
+import android.util.ArrayMap;
+import android.util.ArraySet;
import android.util.Log;
import androidx.annotation.VisibleForTesting;
+import com.android.settings.fuelgauge.BatteryUsageHistoricalLogEntry.Action;
import com.android.settings.fuelgauge.BatteryUtils;
+import com.android.settings.fuelgauge.batteryusage.bugreport.BatteryUsageLogUtils;
import com.android.settings.fuelgauge.batteryusage.db.BatteryStateDatabase;
import com.android.settingslib.fuelgauge.BatteryStatus;
@@ -43,9 +52,10 @@
import java.time.Duration;
import java.util.ArrayList;
import java.util.Calendar;
-import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.Set;
+import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;
@@ -57,10 +67,13 @@
/** Clear memory threshold for device booting phase. **/
private static final long CLEAR_MEMORY_THRESHOLD_MS = Duration.ofMinutes(5).toMillis();
private static final long CLEAR_MEMORY_DELAYED_MS = Duration.ofSeconds(2).toMillis();
+ private static final long INVALID_TIMESTAMP = 0L;
static final int DATA_RETENTION_INTERVAL_DAY = 9;
static final String KEY_LAST_LOAD_FULL_CHARGE_TIME = "last_load_full_charge_time";
static final String KEY_LAST_UPLOAD_FULL_CHARGE_TIME = "last_upload_full_charge_time";
+ static final String KEY_LAST_USAGE_SOURCE = "last_usage_source";
+ static final String KEY_DISMISSED_POWER_ANOMALY_KEYS = "dismissed_power_anomaly_keys";
/** An authority name of the battery content provider. */
public static final String AUTHORITY = "com.android.settings.battery.usage.provider";
@@ -70,14 +83,20 @@
public static final String BATTERY_EVENT_TABLE = "BatteryEvent";
/** A table name for battery usage history. */
public static final String BATTERY_STATE_TABLE = "BatteryState";
+ /** A table name for battery usage slot. */
+ public static final String BATTERY_USAGE_SLOT_TABLE = "BatteryUsageSlot";
+ /** A path name for last full charge time query. */
+ public static final String LAST_FULL_CHARGE_TIMESTAMP_PATH = "lastFullChargeTimestamp";
+ /** A path name for querying the latest record timestamp in battery state table. */
+ public static final String BATTERY_STATE_LATEST_TIMESTAMP_PATH = "batteryStateLatestTimestamp";
/** A path name for app usage latest timestamp query. */
public static final String APP_USAGE_LATEST_TIMESTAMP_PATH = "appUsageLatestTimestamp";
- /** A class name for battery usage data provider. */
- public static final String SETTINGS_PACKAGE_PATH = "com.android.settings";
/** Key for query parameter timestamp used in BATTERY_CONTENT_URI **/
public static final String QUERY_KEY_TIMESTAMP = "timestamp";
/** Key for query parameter userid used in APP_USAGE_EVENT_URI **/
public static final String QUERY_KEY_USERID = "userid";
+ /** Key for query parameter battery event type used in BATTERY_EVENT_URI **/
+ public static final String QUERY_BATTERY_EVENT_TYPE = "batteryEventType";
public static final long INVALID_USER_ID = Integer.MIN_VALUE;
/**
@@ -107,6 +126,13 @@
.authority(AUTHORITY)
.appendPath(BATTERY_STATE_TABLE)
.build();
+ /** A content URI to access battery usage slots data. */
+ public static final Uri BATTERY_USAGE_SLOT_URI =
+ new Uri.Builder()
+ .scheme(ContentResolver.SCHEME_CONTENT)
+ .authority(AUTHORITY)
+ .appendPath(BATTERY_USAGE_SLOT_TABLE)
+ .build();
// For testing only.
@VisibleForTesting
@@ -134,9 +160,9 @@
.appendQueryParameter(
QUERY_KEY_USERID, Long.toString(userId))
.build();
- final long latestTimestamp =
- loadAppUsageLatestTimestampFromContentProvider(context, appUsageLatestTimestampUri);
- final String latestTimestampString = ConvertUtils.utcToLocalTimeForLogging(latestTimestamp);
+ final long latestTimestamp = loadLongFromContentProvider(
+ context, appUsageLatestTimestampUri, /*defaultValue=*/ INVALID_TIMESTAMP);
+ final String latestTimestampString = utcToLocalTimeForLogging(latestTimestamp);
Log.d(TAG, String.format(
"getAppUsageStartTimestampOfUser() userId=%d latestTimestamp=%s in %d/ms",
userId, latestTimestampString, (System.currentTimeMillis() - startTime)));
@@ -157,8 +183,7 @@
// sure the app usage calculation near the boundaries is correct.
final long queryTimestamp =
Math.max(rawStartTimestamp, sixDaysAgoTimestamp) - USAGE_QUERY_BUFFER_HOURS;
- Log.d(TAG, "sixDayAgoTimestamp: " + ConvertUtils.utcToLocalTimeForLogging(
- sixDaysAgoTimestamp));
+ Log.d(TAG, "sixDaysAgoTimestamp: " + utcToLocalTimeForLogging(sixDaysAgoTimestamp));
final String queryUserIdString = userIds.stream()
.map(userId -> String.valueOf(userId))
.collect(Collectors.joining(","));
@@ -173,8 +198,8 @@
.appendQueryParameter(QUERY_KEY_USERID, queryUserIdString)
.build();
- final List<AppUsageEvent> appUsageEventList =
- loadAppUsageEventsFromContentProvider(context, appUsageEventUri);
+ final List<AppUsageEvent> appUsageEventList = loadListFromContentProvider(
+ context, appUsageEventUri, ConvertUtils::convertToAppUsageEvent);
Log.d(TAG, String.format("getAppUsageEventForUser userId=%s size=%d in %d/ms",
queryUserIdString, appUsageEventList.size(),
(System.currentTimeMillis() - startTime)));
@@ -185,11 +210,15 @@
public static List<BatteryEvent> getBatteryEvents(
Context context,
final Calendar calendar,
- final long rawStartTimestamp) {
+ final long rawStartTimestamp,
+ final List<BatteryEventType> queryBatteryEventTypes) {
final long startTime = System.currentTimeMillis();
final long sixDaysAgoTimestamp = getTimestampSixDaysAgo(calendar);
final long queryTimestamp = Math.max(rawStartTimestamp, sixDaysAgoTimestamp);
Log.d(TAG, "getBatteryEvents for timestamp: " + queryTimestamp);
+ final String queryBatteryEventTypesString = queryBatteryEventTypes.stream()
+ .map(type -> String.valueOf(type.getNumber()))
+ .collect(Collectors.joining(","));
// Builds the content uri everytime to avoid cache.
final Uri batteryEventUri =
new Uri.Builder()
@@ -198,22 +227,93 @@
.appendPath(BATTERY_EVENT_TABLE)
.appendQueryParameter(
QUERY_KEY_TIMESTAMP, Long.toString(queryTimestamp))
+ .appendQueryParameter(
+ QUERY_BATTERY_EVENT_TYPE, queryBatteryEventTypesString)
.build();
- final List<BatteryEvent> batteryEventList =
- loadBatteryEventsFromContentProvider(context, batteryEventUri);
+ final List<BatteryEvent> batteryEventList = loadListFromContentProvider(
+ context, batteryEventUri, ConvertUtils::convertToBatteryEvent);
Log.d(TAG, String.format("getBatteryEvents size=%d in %d/ms", batteryEventList.size(),
(System.currentTimeMillis() - startTime)));
return batteryEventList;
}
- /** Long: for timestamp and String: for BatteryHistEntry.getKey() */
- public static Map<Long, Map<String, BatteryHistEntry>> getHistoryMapSinceLastFullCharge(
- Context context, Calendar calendar) {
+ /**
+ * Returns the battery usage slot data after {@code rawStartTimestamp} in battery event table.
+ */
+ public static List<BatteryUsageSlot> getBatteryUsageSlots(
+ Context context,
+ final Calendar calendar,
+ final long rawStartTimestamp) {
final long startTime = System.currentTimeMillis();
final long sixDaysAgoTimestamp = getTimestampSixDaysAgo(calendar);
- Log.d(TAG, "sixDayAgoTimestamp: " + ConvertUtils.utcToLocalTimeForLogging(
- sixDaysAgoTimestamp));
+ final long queryTimestamp = Math.max(rawStartTimestamp, sixDaysAgoTimestamp);
+ Log.d(TAG, "getBatteryUsageSlots for timestamp: " + queryTimestamp);
+ // Builds the content uri everytime to avoid cache.
+ final Uri batteryUsageSlotUri =
+ new Uri.Builder()
+ .scheme(ContentResolver.SCHEME_CONTENT)
+ .authority(AUTHORITY)
+ .appendPath(BATTERY_USAGE_SLOT_TABLE)
+ .appendQueryParameter(
+ QUERY_KEY_TIMESTAMP, Long.toString(queryTimestamp))
+ .build();
+
+ final List<BatteryUsageSlot> batteryUsageSlotList = loadListFromContentProvider(
+ context, batteryUsageSlotUri, ConvertUtils::convertToBatteryUsageSlot);
+ Log.d(TAG, String.format("getBatteryUsageSlots size=%d in %d/ms",
+ batteryUsageSlotList.size(), (System.currentTimeMillis() - startTime)));
+ return batteryUsageSlotList;
+ }
+
+ /** Returns the last full charge time. */
+ public static long getLastFullChargeTime(Context context) {
+ final long startTime = System.currentTimeMillis();
+ // Builds the content uri everytime to avoid cache.
+ final Uri lastFullChargeTimeUri =
+ new Uri.Builder()
+ .scheme(ContentResolver.SCHEME_CONTENT)
+ .authority(AUTHORITY)
+ .appendPath(LAST_FULL_CHARGE_TIMESTAMP_PATH)
+ .build();
+ final long lastFullChargeTime = loadLongFromContentProvider(
+ context, lastFullChargeTimeUri, /*defaultValue=*/ INVALID_TIMESTAMP);
+ final String lastFullChargeTimeString = utcToLocalTimeForLogging(lastFullChargeTime);
+ Log.d(TAG, String.format(
+ "getLastFullChargeTime() lastFullChargeTime=%s in %d/ms",
+ lastFullChargeTimeString, (System.currentTimeMillis() - startTime)));
+ return lastFullChargeTime;
+ }
+
+ /** Returns the first battery state timestamp no later than the {@code queryTimestamp}. */
+ @VisibleForTesting
+ static long getBatteryStateLatestTimestampBeforeQueryTimestamp(
+ Context context, final long queryTimestamp) {
+ final long startTime = System.currentTimeMillis();
+ // Builds the content uri everytime to avoid cache.
+ final Uri batteryStateLatestTimestampUri =
+ new Uri.Builder()
+ .scheme(ContentResolver.SCHEME_CONTENT)
+ .authority(AUTHORITY)
+ .appendPath(BATTERY_STATE_LATEST_TIMESTAMP_PATH)
+ .appendQueryParameter(
+ QUERY_KEY_TIMESTAMP, Long.toString(queryTimestamp))
+ .build();
+ final long batteryStateLatestTimestamp = loadLongFromContentProvider(
+ context, batteryStateLatestTimestampUri, /*defaultValue=*/ INVALID_TIMESTAMP);
+ final String batteryStateLatestTimestampString =
+ utcToLocalTimeForLogging(batteryStateLatestTimestamp);
+ Log.d(TAG, String.format(
+ "getBatteryStateLatestTimestamp() batteryStateLatestTimestamp=%s in %d/ms",
+ batteryStateLatestTimestampString, (System.currentTimeMillis() - startTime)));
+ return batteryStateLatestTimestamp;
+ }
+
+ /** Returns the battery history map after the given timestamp. */
+ @VisibleForTesting
+ static Map<Long, Map<String, BatteryHistEntry>> getHistoryMapSinceQueryTimestamp(
+ Context context, final long queryTimestamp) {
+ final long startTime = System.currentTimeMillis();
// Builds the content uri everytime to avoid cache.
final Uri batteryStateUri =
new Uri.Builder()
@@ -221,20 +321,59 @@
.authority(AUTHORITY)
.appendPath(BATTERY_STATE_TABLE)
.appendQueryParameter(
- QUERY_KEY_TIMESTAMP, Long.toString(sixDaysAgoTimestamp))
+ QUERY_KEY_TIMESTAMP, Long.toString(queryTimestamp))
.build();
- final Map<Long, Map<String, BatteryHistEntry>> resultMap =
- loadHistoryMapFromContentProvider(context, batteryStateUri);
+ final List<BatteryHistEntry> batteryHistEntryList = loadListFromContentProvider(
+ context, batteryStateUri, cursor -> new BatteryHistEntry(cursor));
+ final Map<Long, Map<String, BatteryHistEntry>> resultMap = new ArrayMap();
+ for (final BatteryHistEntry entry : batteryHistEntryList) {
+ final long timestamp = entry.mTimestamp;
+ final String key = entry.getKey();
+ Map batteryHistEntryMap = resultMap.get(timestamp);
+ // Creates new one if there is no corresponding map.
+ if (batteryHistEntryMap == null) {
+ batteryHistEntryMap = new ArrayMap();
+ resultMap.put(timestamp, batteryHistEntryMap);
+ }
+ batteryHistEntryMap.put(key, entry);
+ }
+
if (resultMap == null || resultMap.isEmpty()) {
- Log.d(TAG, "getHistoryMapSinceLastFullCharge() returns empty or null");
+ Log.d(TAG, "getBatteryHistoryMap() returns empty or null");
} else {
- Log.d(TAG, String.format("getHistoryMapSinceLastFullCharge() size=%d in %d/ms",
+ Log.d(TAG, String.format("getBatteryHistoryMap() size=%d in %d/ms",
resultMap.size(), (System.currentTimeMillis() - startTime)));
}
return resultMap;
}
+ /**
+ * Returns the battery history map since the latest record no later than the given timestamp.
+ * If there is no record before the given timestamp or the given timestamp is before last full
+ * charge time, returns the history map since last full charge time.
+ */
+ public static Map<Long, Map<String, BatteryHistEntry>>
+ getHistoryMapSinceLatestRecordBeforeQueryTimestamp(Context context, Calendar calendar,
+ final long queryTimestamp, final long lastFullChargeTime) {
+ final long sixDaysAgoTimestamp = getTimestampSixDaysAgo(calendar);
+ Log.d(TAG, "sixDaysAgoTimestamp: " + utcToLocalTimeForLogging(sixDaysAgoTimestamp));
+ final long batteryStateLatestTimestamp =
+ queryTimestamp == 0L ? 0L : getBatteryStateLatestTimestampBeforeQueryTimestamp(
+ context, queryTimestamp);
+ final long maxTimestamp = Math.max(Math.max(
+ sixDaysAgoTimestamp, lastFullChargeTime), batteryStateLatestTimestamp);
+ return getHistoryMapSinceQueryTimestamp(context, maxTimestamp);
+ }
+
+ /** Returns the history map since last full charge time. */
+ public static Map<Long, Map<String, BatteryHistEntry>> getHistoryMapSinceLastFullCharge(
+ Context context, Calendar calendar) {
+ final long lastFullChargeTime = getLastFullChargeTime(context);
+ return getHistoryMapSinceLatestRecordBeforeQueryTimestamp(
+ context, calendar, 0, lastFullChargeTime);
+ }
+
/** Clears all data in the battery usage database. */
public static void clearAll(Context context) {
AsyncTask.execute(() -> {
@@ -244,6 +383,7 @@
database.appUsageEventDao().clearAll();
database.batteryEventDao().clearAll();
database.batteryStateDao().clearAll();
+ database.batteryUsageSlotDao().clearAll();
} catch (RuntimeException e) {
Log.e(TAG, "clearAll() failed", e);
}
@@ -261,6 +401,7 @@
database.appUsageEventDao().clearAllBefore(earliestTimestamp);
database.batteryEventDao().clearAllBefore(earliestTimestamp);
database.batteryStateDao().clearAllBefore(earliestTimestamp);
+ database.batteryUsageSlotDao().clearAllBefore(earliestTimestamp);
} catch (RuntimeException e) {
Log.e(TAG, "clearAllBefore() failed", e);
}
@@ -289,7 +430,7 @@
/*user=*/ context.getSystemService(UserManager.class)
.getProfileParent(context.getUser()));
} catch (PackageManager.NameNotFoundException e) {
- Log.e(TAG, "context.createPackageContextAsUser() fail:" + e);
+ Log.e(TAG, "context.createPackageContextAsUser() fail:", e);
return null;
}
}
@@ -316,7 +457,7 @@
resolver.notifyChange(APP_USAGE_EVENT_URI, /*observer=*/ null);
Log.d(TAG, "insert() app usage events data into database");
} catch (Exception e) {
- Log.e(TAG, "bulkInsert() app usage data into database error:\n" + e);
+ Log.e(TAG, "bulkInsert() app usage data into database error:", e);
}
}
Log.d(TAG, String.format("sendAppUsageEventData() size=%d in %d/ms",
@@ -342,8 +483,65 @@
return contentValues;
}
+ static List<ContentValues> sendBatteryEventData(
+ final Context context, final List<BatteryEvent> batteryEventList) {
+ final long startTime = System.currentTimeMillis();
+ // Creates the ContentValues list to insert them into provider.
+ final List<ContentValues> valuesList = new ArrayList<>();
+ batteryEventList.stream()
+ .forEach(batteryEvent -> valuesList.add(
+ ConvertUtils.convertBatteryEventToContentValues(batteryEvent)));
+ int size = 0;
+ final ContentResolver resolver = context.getContentResolver();
+ // Inserts all ContentValues into battery provider.
+ if (!valuesList.isEmpty()) {
+ final ContentValues[] valuesArray = new ContentValues[valuesList.size()];
+ valuesList.toArray(valuesArray);
+ try {
+ size = resolver.bulkInsert(BATTERY_EVENT_URI, valuesArray);
+ resolver.notifyChange(BATTERY_EVENT_URI, /*observer=*/ null);
+ Log.d(TAG, "insert() battery event data into database");
+ } catch (Exception e) {
+ Log.e(TAG, "bulkInsert() battery event data into database error:", e);
+ }
+ }
+ Log.d(TAG, String.format("sendBatteryEventData() size=%d in %d/ms",
+ size, (System.currentTimeMillis() - startTime)));
+ clearMemory();
+ return valuesList;
+ }
+
+ static List<ContentValues> sendBatteryUsageSlotData(
+ final Context context, final List<BatteryUsageSlot> batteryUsageSlotList) {
+ final long startTime = System.currentTimeMillis();
+ // Creates the ContentValues list to insert them into provider.
+ final List<ContentValues> valuesList = new ArrayList<>();
+ batteryUsageSlotList.stream()
+ .forEach(batteryUsageSlot -> valuesList.add(
+ ConvertUtils.convertBatteryUsageSlotToContentValues(batteryUsageSlot)));
+ int size = 0;
+ final ContentResolver resolver = context.getContentResolver();
+ // Inserts all ContentValues into battery provider.
+ if (!valuesList.isEmpty()) {
+ final ContentValues[] valuesArray = new ContentValues[valuesList.size()];
+ valuesList.toArray(valuesArray);
+ try {
+ size = resolver.bulkInsert(BATTERY_USAGE_SLOT_URI, valuesArray);
+ resolver.notifyChange(BATTERY_USAGE_SLOT_URI, /*observer=*/ null);
+ Log.d(TAG, "insert() battery usage slots data into database");
+ } catch (Exception e) {
+ Log.e(TAG, "bulkInsert() battery usage slots data into database error:", e);
+ }
+ }
+ Log.d(TAG, String.format("sendBatteryUsageSlotData() size=%d in %d/ms",
+ size, (System.currentTimeMillis() - startTime)));
+ clearMemory();
+ return valuesList;
+ }
+
static List<ContentValues> sendBatteryEntryData(
final Context context,
+ final long snapshotTimestamp,
final List<BatteryEntry> batteryEntryList,
final BatteryUsageStats batteryUsageStats,
final boolean isFullChargeStart) {
@@ -360,7 +558,6 @@
final int batteryHealth = intent.getIntExtra(
BatteryManager.EXTRA_HEALTH, BatteryManager.BATTERY_HEALTH_UNKNOWN);
// We should use the same timestamp for each data snapshot.
- final long snapshotTimestamp = Clock.systemUTC().millis();
final long snapshotBootTimestamp = SystemClock.elapsedRealtime();
// Creates the ContentValues list to insert them into provider.
@@ -395,6 +592,7 @@
int size = 1;
final ContentResolver resolver = context.getContentResolver();
+ String errorMessage = "";
// Inserts all ContentValues into battery provider.
if (!valuesList.isEmpty()) {
final ContentValues[] valuesArray = new ContentValues[valuesList.size()];
@@ -404,7 +602,7 @@
Log.d(TAG, "insert() battery states data into database with isFullChargeStart:"
+ isFullChargeStart);
} catch (Exception e) {
- Log.e(TAG, "bulkInsert() battery states data into database error:\n" + e);
+ Log.e(TAG, "bulkInsert() data into database error:", e);
}
} else {
// Inserts one fake data into battery provider.
@@ -424,11 +622,15 @@
+ isFullChargeStart);
} catch (Exception e) {
- Log.e(TAG, "insert() data into database error:\n" + e);
+ Log.e(TAG, "insert() data into database error:", e);
}
valuesList.add(contentValues);
}
resolver.notifyChange(BATTERY_CONTENT_URI, /*observer=*/ null);
+ BatteryUsageLogUtils.writeLog(
+ context,
+ Action.INSERT_USAGE_DATA,
+ "size=" + size + " " + errorMessage);
Log.d(TAG, String.format("sendBatteryEntryData() size=%d in %d/ms",
size, (System.currentTimeMillis() - startTime)));
if (isFullChargeStart) {
@@ -452,6 +654,8 @@
KEY_LAST_LOAD_FULL_CHARGE_TIME);
writeString(context, writer, "LastUploadFullChargeTime",
KEY_LAST_UPLOAD_FULL_CHARGE_TIME);
+ writeString(context, writer, "DismissedPowerAnomalyKeys",
+ KEY_DISMISSED_POWER_ANOMALY_KEYS);
}
static SharedPreferences getSharedPreferences(Context context) {
@@ -459,15 +663,104 @@
SHARED_PREFS_FILE, Context.MODE_PRIVATE);
}
+ static void removeUsageSource(Context context) {
+ final SharedPreferences sharedPreferences = getSharedPreferences(context);
+ if (sharedPreferences != null && sharedPreferences.contains(KEY_LAST_USAGE_SOURCE)) {
+ sharedPreferences.edit().remove(KEY_LAST_USAGE_SOURCE).apply();
+ }
+ }
+
+ /**
+ * Returns what App Usage Observers will consider the source of usage for an activity.
+ *
+ * @see UsageStatsManager#getUsageSource()
+ */
+ static int getUsageSource(Context context, IUsageStatsManager usageStatsManager) {
+ final SharedPreferences sharedPreferences = getSharedPreferences(context);
+ if (sharedPreferences != null && sharedPreferences.contains(KEY_LAST_USAGE_SOURCE)) {
+ return sharedPreferences
+ .getInt(KEY_LAST_USAGE_SOURCE, ConvertUtils.DEFAULT_USAGE_SOURCE);
+ }
+ int usageSource = ConvertUtils.DEFAULT_USAGE_SOURCE;
+
+ try {
+ usageSource = usageStatsManager.getUsageSource();
+ } catch (RemoteException e) {
+ Log.e(TAG, "Failed to getUsageSource", e);
+ }
+ if (sharedPreferences != null) {
+ sharedPreferences.edit().putInt(KEY_LAST_USAGE_SOURCE, usageSource).apply();
+ }
+ return usageSource;
+ }
+
+ static void removeDismissedPowerAnomalyKeys(Context context) {
+ final SharedPreferences sharedPreferences = getSharedPreferences(context);
+ if (sharedPreferences != null
+ && sharedPreferences.contains(KEY_DISMISSED_POWER_ANOMALY_KEYS)) {
+ sharedPreferences.edit().remove(KEY_DISMISSED_POWER_ANOMALY_KEYS).apply();
+ }
+ }
+
+ static Set<String> getDismissedPowerAnomalyKeys(Context context) {
+ final SharedPreferences sharedPreferences = getSharedPreferences(context);
+ return sharedPreferences != null
+ ? sharedPreferences.getStringSet(KEY_DISMISSED_POWER_ANOMALY_KEYS, new ArraySet<>())
+ : new ArraySet<>();
+ }
+
+ static void setDismissedPowerAnomalyKeys(Context context, String dismissedPowerAnomalyKey) {
+ final SharedPreferences sharedPreferences = getSharedPreferences(context);
+ if (sharedPreferences != null) {
+ final Set<String> dismissedPowerAnomalyKeys = getDismissedPowerAnomalyKeys(context);
+ dismissedPowerAnomalyKeys.add(dismissedPowerAnomalyKey);
+ sharedPreferences.edit()
+ .putStringSet(KEY_DISMISSED_POWER_ANOMALY_KEYS, dismissedPowerAnomalyKeys)
+ .apply();
+ }
+ }
+
static void recordDateTime(Context context, String preferenceKey) {
final SharedPreferences sharedPreferences = getSharedPreferences(context);
if (sharedPreferences != null) {
- final String currentTime = ConvertUtils.utcToLocalTimeForLogging(
- System.currentTimeMillis());
+ final String currentTime = utcToLocalTimeForLogging(System.currentTimeMillis());
sharedPreferences.edit().putString(preferenceKey, currentTime).apply();
}
}
+ @VisibleForTesting
+ static <T> T loadFromContentProvider(
+ Context context, Uri uri, T defaultValue, Function<Cursor, T> cursorReader) {
+ // Transfer work profile to user profile. Please see b/297036263.
+ context = getParentContext(context);
+ if (context == null) {
+ return defaultValue;
+ }
+ try (Cursor cursor = sFakeSupplier != null ? sFakeSupplier.get() :
+ context.getContentResolver().query(uri, null, null, null)) {
+ return (cursor == null || cursor.getCount() == 0)
+ ? defaultValue : cursorReader.apply(cursor);
+ }
+ }
+
+ private static long loadLongFromContentProvider(
+ Context context, Uri uri, final long defaultValue) {
+ return loadFromContentProvider(context, uri, defaultValue,
+ cursor -> cursor.moveToFirst() ? cursor.getLong(/*columnIndex=*/ 0) : defaultValue);
+ }
+
+ private static <E> List<E> loadListFromContentProvider(
+ Context context, Uri uri, Function<Cursor, E> converter) {
+ return loadFromContentProvider(context, uri, new ArrayList<>(),
+ cursor -> {
+ final List<E> list = new ArrayList<>();
+ while (cursor.moveToNext()) {
+ list.add(converter.apply(cursor));
+ }
+ return list;
+ });
+ }
+
private static void writeString(
Context context, PrintWriter writer, String prefix, String key) {
final SharedPreferences sharedPreferences = getSharedPreferences(context);
@@ -477,116 +770,6 @@
}
}
- private static long loadAppUsageLatestTimestampFromContentProvider(
- Context context, final Uri appUsageLatestTimestampUri) {
- // We have already make sure the context here is with profile parent's user identity. Don't
- // need to check whether current user is work profile.
- try (Cursor cursor = sFakeSupplier != null
- ? sFakeSupplier.get()
- : context.getContentResolver().query(
- appUsageLatestTimestampUri, null, null, null)) {
- if (cursor == null || cursor.getCount() == 0) {
- return INVALID_USER_ID;
- }
- cursor.moveToFirst();
- // There is only one column returned so use the index 0 directly.
- final long latestTimestamp = cursor.getLong(/*columnIndex=*/ 0);
- try {
- cursor.close();
- } catch (Exception e) {
- Log.e(TAG, "cursor.close() failed", e);
- }
- // If there is no data for this user, 0 will be returned from the database.
- return latestTimestamp == 0 ? INVALID_USER_ID : latestTimestamp;
- }
- }
-
- private static List<AppUsageEvent> loadAppUsageEventsFromContentProvider(
- Context context, Uri appUsageEventUri) {
- final List<AppUsageEvent> appUsageEventList = new ArrayList<>();
- context = getParentContext(context);
- if (context == null) {
- return appUsageEventList;
- }
- try (Cursor cursor = sFakeSupplier != null
- ? sFakeSupplier.get()
- : context.getContentResolver().query(appUsageEventUri, null, null, null)) {
- if (cursor == null || cursor.getCount() == 0) {
- return appUsageEventList;
- }
- // Loads and recovers all AppUsageEvent data from cursor.
- while (cursor.moveToNext()) {
- appUsageEventList.add(ConvertUtils.convertToAppUsageEventFromCursor(cursor));
- }
- try {
- cursor.close();
- } catch (Exception e) {
- Log.e(TAG, "cursor.close() failed", e);
- }
- }
- return appUsageEventList;
- }
-
- private static List<BatteryEvent> loadBatteryEventsFromContentProvider(
- Context context, Uri batteryEventUri) {
- final List<BatteryEvent> batteryEventList = new ArrayList<>();
- context = getParentContext(context);
- if (context == null) {
- return batteryEventList;
- }
- try (Cursor cursor = sFakeSupplier != null
- ? sFakeSupplier.get()
- : context.getContentResolver().query(batteryEventUri, null, null, null)) {
- if (cursor == null || cursor.getCount() == 0) {
- return batteryEventList;
- }
- // Loads and recovers all AppUsageEvent data from cursor.
- while (cursor.moveToNext()) {
- batteryEventList.add(ConvertUtils.convertToBatteryEventFromCursor(cursor));
- }
- try {
- cursor.close();
- } catch (Exception e) {
- Log.e(TAG, "cursor.close() failed", e);
- }
- }
- return batteryEventList;
- }
-
- private static Map<Long, Map<String, BatteryHistEntry>> loadHistoryMapFromContentProvider(
- Context context, Uri batteryStateUri) {
- context = DatabaseUtils.getParentContext(context);
- if (context == null) {
- return null;
- }
- final Map<Long, Map<String, BatteryHistEntry>> resultMap = new HashMap();
- try (Cursor cursor = sFakeSupplier != null ? sFakeSupplier.get() :
- context.getContentResolver().query(batteryStateUri, null, null, null)) {
- if (cursor == null || cursor.getCount() == 0) {
- return resultMap;
- }
- // Loads and recovers all BatteryHistEntry data from cursor.
- while (cursor.moveToNext()) {
- final BatteryHistEntry entry = new BatteryHistEntry(cursor);
- final long timestamp = entry.mTimestamp;
- final String key = entry.getKey();
- Map batteryHistEntryMap = resultMap.get(timestamp);
- // Creates new one if there is no corresponding map.
- if (batteryHistEntryMap == null) {
- batteryHistEntryMap = new HashMap<>();
- resultMap.put(timestamp, batteryHistEntryMap);
- }
- batteryHistEntryMap.put(key, entry);
- }
- try {
- cursor.close();
- } catch (Exception e) {
- Log.e(TAG, "cursor.close() failed", e);
- }
- }
- return resultMap;
- }
-
private static void clearMemory() {
if (SystemClock.uptimeMillis() > CLEAR_MEMORY_THRESHOLD_MS) {
return;
diff --git a/src/com/android/settings/fuelgauge/batteryusage/PeriodicJobManager.java b/src/com/android/settings/fuelgauge/batteryusage/PeriodicJobManager.java
index 3d78c00..43cd69d 100644
--- a/src/com/android/settings/fuelgauge/batteryusage/PeriodicJobManager.java
+++ b/src/com/android/settings/fuelgauge/batteryusage/PeriodicJobManager.java
@@ -24,6 +24,8 @@
import androidx.annotation.VisibleForTesting;
+import com.android.settings.fuelgauge.BatteryUsageHistoricalLogEntry.Action;
+import com.android.settings.fuelgauge.batteryusage.bugreport.BatteryUsageLogUtils;
import com.android.settings.overlay.FeatureFactory;
import java.time.Clock;
@@ -66,6 +68,8 @@
/** Schedules the next alarm job if it is available. */
public void refreshJob(final boolean fromBoot) {
if (mAlarmManager == null) {
+ BatteryUsageLogUtils.writeLog(mContext, Action.SCHEDULE_JOB,
+ "cannot schedule next alarm job due to AlarmManager is null");
Log.e(TAG, "cannot schedule next alarm job");
return;
}
@@ -76,8 +80,11 @@
final long triggerAtMillis = getTriggerAtMillis(mContext, Clock.systemUTC(), fromBoot);
mAlarmManager.setExactAndAllowWhileIdle(
AlarmManager.RTC_WAKEUP, triggerAtMillis, pendingIntent);
- Log.d(TAG, "schedule next alarm job at "
- + ConvertUtils.utcToLocalTimeForLogging(triggerAtMillis));
+
+ final String utcToLocalTime = ConvertUtils.utcToLocalTimeForLogging(triggerAtMillis);
+ BatteryUsageLogUtils.writeLog(mContext, Action.SCHEDULE_JOB,
+ String.format("triggerTime=%s, fromBoot=%b", utcToLocalTime, fromBoot));
+ Log.d(TAG, "schedule next alarm job at " + utcToLocalTime);
}
void cancelJob(PendingIntent pendingIntent) {
diff --git a/src/com/android/settings/fuelgauge/batteryusage/PeriodicJobReceiver.java b/src/com/android/settings/fuelgauge/batteryusage/PeriodicJobReceiver.java
index 3ca4532..dccca43 100644
--- a/src/com/android/settings/fuelgauge/batteryusage/PeriodicJobReceiver.java
+++ b/src/com/android/settings/fuelgauge/batteryusage/PeriodicJobReceiver.java
@@ -22,6 +22,9 @@
import android.content.Intent;
import android.util.Log;
+import com.android.settings.fuelgauge.BatteryUsageHistoricalLogEntry.Action;
+import com.android.settings.fuelgauge.batteryusage.bugreport.BatteryUsageLogUtils;
+
/** Receives the periodic alarm {@link PendingIntent} callback. */
public final class PeriodicJobReceiver extends BroadcastReceiver {
private static final String TAG = "PeriodicJobReceiver";
@@ -30,17 +33,28 @@
@Override
public void onReceive(Context context, Intent intent) {
+ try {
+ loadDataAndRefreshJob(context, intent);
+ } catch (Exception e) {
+ BatteryUsageLogUtils.writeLog(context, Action.SCHEDULE_JOB,
+ String.format("loadDataAndRefreshJob() failed: %s", e));
+ }
+ }
+
+ private static void loadDataAndRefreshJob(Context context, Intent intent) {
final String action = intent == null ? "" : intent.getAction();
if (!ACTION_PERIODIC_JOB_UPDATE.equals(action)) {
Log.w(TAG, "receive unexpected action=" + action);
return;
}
if (DatabaseUtils.isWorkProfile(context)) {
+ BatteryUsageLogUtils.writeLog(context, Action.SCHEDULE_JOB,
+ "do not refresh job for work profile");
Log.w(TAG, "do not refresh job for work profile action=" + action);
return;
}
+ BatteryUsageLogUtils.writeLog(context, Action.EXECUTE_JOB, "");
BatteryUsageDataLoader.enqueueWork(context, /*isFullChargeStart=*/ false);
- AppUsageDataLoader.enqueueWork(context);
Log.d(TAG, "refresh periodic job from action=" + action);
PeriodicJobManager.getInstance(context).refreshJob(/*fromBoot=*/ false);
DatabaseUtils.clearExpiredDataIfNeeded(context);
diff --git a/src/com/android/settings/fuelgauge/batteryusage/PowerUsageAdvanced.java b/src/com/android/settings/fuelgauge/batteryusage/PowerUsageAdvanced.java
index 7c4478e..fb92a76 100644
--- a/src/com/android/settings/fuelgauge/batteryusage/PowerUsageAdvanced.java
+++ b/src/com/android/settings/fuelgauge/batteryusage/PowerUsageAdvanced.java
@@ -21,12 +21,14 @@
import android.content.Context;
import android.database.ContentObserver;
import android.net.Uri;
+import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Handler;
+import android.os.Looper;
import android.provider.SearchIndexableResource;
import android.util.Log;
+import android.util.Pair;
-import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import androidx.loader.app.LoaderManager;
import androidx.loader.content.Loader;
@@ -34,14 +36,23 @@
import com.android.settings.R;
import com.android.settings.SettingsActivity;
import com.android.settings.fuelgauge.BatteryBroadcastReceiver;
+import com.android.settings.fuelgauge.PowerUsageFeatureProvider;
+import com.android.settings.overlay.FeatureFactory;
import com.android.settings.search.BaseSearchIndexProvider;
import com.android.settingslib.core.AbstractPreferenceController;
import com.android.settingslib.search.SearchIndexable;
+import com.android.settingslib.utils.AsyncLoaderCompat;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Comparator;
import java.util.List;
import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.function.Predicate;
/** Advanced power usage. */
@SearchIndexable(forTarget = SearchIndexable.ALL & ~SearchIndexable.ARC)
@@ -53,16 +64,17 @@
@VisibleForTesting
BatteryHistoryPreference mHistPref;
@VisibleForTesting
- Map<Long, Map<String, BatteryHistEntry>> mBatteryHistoryMap;
- @VisibleForTesting
- final BatteryHistoryLoaderCallbacks mBatteryHistoryLoaderCallbacks =
- new BatteryHistoryLoaderCallbacks();
+ final BatteryLevelDataLoaderCallbacks mBatteryLevelDataLoaderCallbacks =
+ new BatteryLevelDataLoaderCallbacks();
private boolean mIsChartDataLoaded = false;
- private BatteryChartPreferenceController mBatteryChartPreferenceController;
+ private long mResumeTimestamp;
+ private Map<Integer, Map<Integer, BatteryDiffData>> mBatteryUsageMap;
+ private final ExecutorService mExecutor = Executors.newSingleThreadExecutor();
+ private final Handler mHandler = new Handler(Looper.getMainLooper());
private final ContentObserver mBatteryObserver =
- new ContentObserver(new Handler()) {
+ new ContentObserver(mHandler) {
@Override
public void onChange(boolean selfChange) {
Log.d(TAG, "onBatteryContentChange: " + selfChange);
@@ -72,11 +84,25 @@
}
};
+ @VisibleForTesting
+ BatteryTipsController mBatteryTipsController;
+ @VisibleForTesting
+ BatteryChartPreferenceController mBatteryChartPreferenceController;
+ @VisibleForTesting
+ ScreenOnTimeController mScreenOnTimeController;
+ @VisibleForTesting
+ BatteryUsageBreakdownController mBatteryUsageBreakdownController;
+ @VisibleForTesting
+ Optional<BatteryLevelData> mBatteryLevelData;
+ @VisibleForTesting
+ Optional<AnomalyEventWrapper> mHighlightEventWrapper;
+
@Override
public void onCreate(Bundle icicle) {
super.onCreate(icicle);
mHistPref = findPreference(KEY_BATTERY_CHART);
setBatteryChartPreferenceController();
+ AsyncTask.execute(() -> BootBroadcastReceiver.invokeJobRecheck(getContext()));
}
@Override
@@ -85,6 +111,7 @@
if (getActivity().isChangingConfigurations()) {
BatteryEntry.clearUidCache();
}
+ mExecutor.shutdown();
}
@Override
@@ -116,6 +143,7 @@
@Override
public void onResume() {
super.onResume();
+ mResumeTimestamp = System.currentTimeMillis();
final Uri uri = DatabaseUtils.BATTERY_CONTENT_URI;
if (uri != null) {
getContext().getContentResolver().registerContentObserver(
@@ -126,41 +154,31 @@
@Override
protected List<AbstractPreferenceController> createPreferenceControllers(Context context) {
final List<AbstractPreferenceController> controllers = new ArrayList<>();
+ mBatteryTipsController = new BatteryTipsController(context);
mBatteryChartPreferenceController =
new BatteryChartPreferenceController(
context, getSettingsLifecycle(), (SettingsActivity) getActivity());
- ScreenOnTimeController screenOnTimeController = new ScreenOnTimeController(context);
- BatteryUsageBreakdownController batteryUsageBreakdownController =
+ mScreenOnTimeController = new ScreenOnTimeController(context);
+ mBatteryUsageBreakdownController =
new BatteryUsageBreakdownController(
context, getSettingsLifecycle(), (SettingsActivity) getActivity(), this);
- mBatteryChartPreferenceController.setOnScreenOnTimeUpdatedListener(
- screenOnTimeController::handleSceenOnTimeUpdated);
- mBatteryChartPreferenceController.setOnBatteryUsageUpdatedListener(
- batteryUsageBreakdownController::handleBatteryUsageUpdated);
-
+ controllers.add(mBatteryTipsController);
controllers.add(mBatteryChartPreferenceController);
- controllers.add(screenOnTimeController);
- controllers.add(batteryUsageBreakdownController);
+ controllers.add(mScreenOnTimeController);
+ controllers.add(mBatteryUsageBreakdownController);
setBatteryChartPreferenceController();
+ mBatteryChartPreferenceController.setOnSelectedIndexUpdatedListener(
+ this::onSelectedSlotDataUpdated);
+
+ // Force UI refresh if battery usage data was loaded before UI initialization.
+ onSelectedSlotDataUpdated();
return controllers;
}
@Override
- protected boolean isBatteryHistoryNeeded() {
- return true;
- }
-
- @Override
protected void refreshUi(@BatteryUpdateType int refreshType) {
- final Context context = getContext();
- if (context == null) {
- return;
- }
- updatePreference(mHistPref);
- if (mBatteryChartPreferenceController != null && mBatteryHistoryMap != null) {
- mBatteryChartPreferenceController.setBatteryHistoryMap(mBatteryHistoryMap);
- }
+ // Do nothing
}
@Override
@@ -169,17 +187,238 @@
bundle.putInt(KEY_REFRESH_TYPE, refreshType);
if (!mIsChartDataLoaded) {
mIsChartDataLoaded = true;
- restartLoader(LoaderIndex.BATTERY_HISTORY_LOADER, bundle,
- mBatteryHistoryLoaderCallbacks);
+ mBatteryLevelData = null;
+ mBatteryUsageMap = null;
+ mHighlightEventWrapper = null;
+ restartLoader(LoaderIndex.BATTERY_LEVEL_DATA_LOADER, bundle,
+ mBatteryLevelDataLoaderCallbacks);
}
}
+ private void onBatteryLevelDataUpdate(BatteryLevelData batteryLevelData) {
+ if (!isResumed()) {
+ return;
+ }
+ mBatteryLevelData = Optional.ofNullable(batteryLevelData);
+ if (mBatteryChartPreferenceController != null) {
+ mBatteryChartPreferenceController.onBatteryLevelDataUpdate(batteryLevelData);
+ Log.d(TAG, String.format("Battery chart shows in %d millis",
+ System.currentTimeMillis() - mResumeTimestamp));
+ }
+ }
+
+ private void onBatteryDiffDataMapUpdate(Map<Long, BatteryDiffData> batteryDiffDataMap) {
+ if (!isResumed() || mBatteryLevelData == null) {
+ return;
+ }
+ mBatteryUsageMap = DataProcessor.generateBatteryUsageMap(
+ getContext(), batteryDiffDataMap, mBatteryLevelData.orElse(null));
+ Log.d(TAG, "onBatteryDiffDataMapUpdate: " + mBatteryUsageMap);
+ DataProcessor.loadLabelAndIcon(mBatteryUsageMap);
+ onSelectedSlotDataUpdated();
+ detectAnomaly();
+ logScreenUsageTime();
+ if (mBatteryChartPreferenceController != null
+ && mBatteryLevelData.isEmpty() && isBatteryUsageMapNullOrEmpty()) {
+ // No available battery usage and battery level data.
+ mBatteryChartPreferenceController.showEmptyChart();
+ }
+ }
+
+ private void onSelectedSlotDataUpdated() {
+ if (mBatteryChartPreferenceController == null
+ || mScreenOnTimeController == null
+ || mBatteryUsageBreakdownController == null
+ || mBatteryUsageMap == null) {
+ return;
+ }
+ final int dailyIndex = mBatteryChartPreferenceController.getDailyChartIndex();
+ final int hourlyIndex = mBatteryChartPreferenceController.getHourlyChartIndex();
+ final String slotInformation = mBatteryChartPreferenceController.getSlotInformation();
+ final BatteryDiffData slotUsageData = mBatteryUsageMap.get(dailyIndex).get(hourlyIndex);
+ if (slotUsageData != null) {
+ mScreenOnTimeController.handleSceenOnTimeUpdated(
+ slotUsageData.getScreenOnTime(), slotInformation);
+ }
+ // Hide card tips if the related highlight slot was clicked.
+ if (isAppsAnomalyEventFocused()) {
+ mBatteryTipsController.acceptTipsCard();
+ }
+ mBatteryUsageBreakdownController.handleBatteryUsageUpdated(
+ slotUsageData, slotInformation, isBatteryUsageMapNullOrEmpty(),
+ isAppsAnomalyEventFocused(), mHighlightEventWrapper);
+ Log.d(TAG, String.format("Battery usage list shows in %d millis",
+ System.currentTimeMillis() - mResumeTimestamp));
+ }
+
+ private void detectAnomaly() {
+ mExecutor.execute(() -> {
+ final PowerUsageFeatureProvider powerUsageFeatureProvider =
+ FeatureFactory.getFactory(getContext())
+ .getPowerUsageFeatureProvider(getContext());
+ final PowerAnomalyEventList anomalyEventList =
+ powerUsageFeatureProvider.detectSettingsAnomaly(
+ getContext(), /* displayDrain= */ 0);
+ mHandler.post(() -> onAnomalyDetected(anomalyEventList));
+ });
+ }
+
+ private void onAnomalyDetected(PowerAnomalyEventList anomalyEventList) {
+ if (!isResumed() || anomalyEventList == null) {
+ return;
+ }
+ Log.d(TAG, "anomalyEventList = " + anomalyEventList);
+
+ final Set<String> dismissedPowerAnomalyKeys =
+ DatabaseUtils.getDismissedPowerAnomalyKeys(getContext());
+ Log.d(TAG, "dismissedPowerAnomalyKeys = " + dismissedPowerAnomalyKeys);
+
+ // Choose an app anomaly event with highest score to show highlight slot
+ final PowerAnomalyEvent highlightEvent =
+ getAnomalyEvent(anomalyEventList, PowerAnomalyEvent::hasWarningItemInfo);
+ // Choose an event never dismissed to show as card.
+ // If the slot is already highlighted, the tips card should be the corresponding app
+ // or settings anomaly event.
+ final PowerAnomalyEvent tipsCardEvent =
+ getAnomalyEvent(anomalyEventList,
+ event -> !dismissedPowerAnomalyKeys.contains(event.getDismissRecordKey())
+ && (event.equals(highlightEvent) || !event.hasWarningItemInfo()));
+ onDisplayAnomalyEventUpdated(tipsCardEvent, highlightEvent);
+ }
+
+ @VisibleForTesting
+ void onDisplayAnomalyEventUpdated(
+ PowerAnomalyEvent tipsCardEvent, PowerAnomalyEvent highlightEvent) {
+ if (mBatteryTipsController == null
+ || mBatteryChartPreferenceController == null
+ || mBatteryUsageBreakdownController == null) {
+ return;
+ }
+
+ final boolean isSameAnomalyEvent = (tipsCardEvent == highlightEvent);
+ // Update battery tips card preference & behaviour
+ mBatteryTipsController.setOnAnomalyConfirmListener(null);
+ mBatteryTipsController.setOnAnomalyRejectListener(null);
+ final AnomalyEventWrapper tipsCardEventWrapper = (tipsCardEvent == null) ? null :
+ new AnomalyEventWrapper(getContext(), tipsCardEvent);
+ if (tipsCardEventWrapper != null) {
+ tipsCardEventWrapper.setRelatedBatteryDiffEntry(
+ findRelatedBatteryDiffEntry(tipsCardEventWrapper));
+ }
+ mBatteryTipsController.handleBatteryTipsCardUpdated(
+ tipsCardEventWrapper, isSameAnomalyEvent);
+
+ // Update highlight slot effect in battery chart view
+ Pair<Integer, Integer> highlightSlotIndexPair = Pair.create(
+ BatteryChartViewModel.SELECTED_INDEX_INVALID,
+ BatteryChartViewModel.SELECTED_INDEX_INVALID);
+ mHighlightEventWrapper = Optional.ofNullable(isSameAnomalyEvent ? tipsCardEventWrapper :
+ ((highlightEvent != null)
+ ? new AnomalyEventWrapper(getContext(), highlightEvent) : null));
+ if (mBatteryLevelData != null && mBatteryLevelData.isPresent()
+ && mHighlightEventWrapper.isPresent()
+ && mHighlightEventWrapper.get().hasHighlightSlotPair(mBatteryLevelData.get())) {
+ highlightSlotIndexPair = mHighlightEventWrapper.get()
+ .getHighlightSlotPair(mBatteryLevelData.get());
+ if (isSameAnomalyEvent) {
+ // For main button, focus on highlight slot when clicked
+ mBatteryTipsController.setOnAnomalyConfirmListener(() -> {
+ mBatteryChartPreferenceController.selectHighlightSlotIndex();
+ mBatteryTipsController.acceptTipsCard();
+ });
+ }
+ }
+ mBatteryChartPreferenceController.onHighlightSlotIndexUpdate(
+ highlightSlotIndexPair.first, highlightSlotIndexPair.second);
+ }
+
+ @VisibleForTesting
+ BatteryDiffEntry findRelatedBatteryDiffEntry(AnomalyEventWrapper eventWrapper) {
+ if (eventWrapper == null
+ || mBatteryLevelData == null || mBatteryLevelData.isEmpty()
+ || !eventWrapper.hasHighlightSlotPair(mBatteryLevelData.get())
+ || !eventWrapper.hasAnomalyEntryKey()
+ || mBatteryUsageMap == null) {
+ return null;
+ }
+ final Pair<Integer, Integer> highlightSlotIndexPair =
+ eventWrapper.getHighlightSlotPair(mBatteryLevelData.get());
+ final BatteryDiffData relatedDiffData = mBatteryUsageMap
+ .get(highlightSlotIndexPair.first).get(highlightSlotIndexPair.second);
+ final String anomalyEntryKey = eventWrapper.getAnomalyEntryKey();
+ if (relatedDiffData == null || anomalyEntryKey == null) {
+ return null;
+ }
+ for (BatteryDiffEntry entry : relatedDiffData.getAppDiffEntryList()) {
+ if (anomalyEntryKey.equals(entry.getKey())) {
+ return entry;
+ }
+ }
+ return null;
+ }
+
private void setBatteryChartPreferenceController() {
if (mHistPref != null && mBatteryChartPreferenceController != null) {
mHistPref.setChartPreferenceController(mBatteryChartPreferenceController);
}
}
+ private boolean isBatteryUsageMapNullOrEmpty() {
+ final BatteryDiffData allBatteryDiffData = getAllBatteryDiffData(mBatteryUsageMap);
+ // If all data is null or empty, each slot must be null or empty.
+ return allBatteryDiffData == null
+ || (allBatteryDiffData.getAppDiffEntryList().isEmpty()
+ && allBatteryDiffData.getSystemDiffEntryList().isEmpty());
+ }
+
+ private boolean isAppsAnomalyEventFocused() {
+ return mBatteryChartPreferenceController != null
+ && mBatteryChartPreferenceController.isHighlightSlotFocused();
+ }
+
+ private void logScreenUsageTime() {
+ final BatteryDiffData allBatteryDiffData = getAllBatteryDiffData(mBatteryUsageMap);
+ if (allBatteryDiffData == null) {
+ return;
+ }
+ long totalForegroundUsageTime = 0;
+ for (final BatteryDiffEntry entry : allBatteryDiffData.getAppDiffEntryList()) {
+ totalForegroundUsageTime += entry.mForegroundUsageTimeInMs;
+ }
+ mMetricsFeatureProvider.action(
+ getContext(),
+ SettingsEnums.ACTION_BATTERY_USAGE_SCREEN_ON_TIME,
+ (int) allBatteryDiffData.getScreenOnTime());
+ mMetricsFeatureProvider.action(
+ getContext(),
+ SettingsEnums.ACTION_BATTERY_USAGE_FOREGROUND_USAGE_TIME,
+ (int) totalForegroundUsageTime);
+ }
+
+ @VisibleForTesting
+ static PowerAnomalyEvent getAnomalyEvent(
+ PowerAnomalyEventList anomalyEventList, Predicate<PowerAnomalyEvent> predicate) {
+ if (anomalyEventList == null || anomalyEventList.getPowerAnomalyEventsCount() == 0) {
+ return null;
+ }
+
+ final PowerAnomalyEvent filterAnomalyEvent = anomalyEventList.getPowerAnomalyEventsList()
+ .stream()
+ .filter(predicate)
+ .max(Comparator.comparing(PowerAnomalyEvent::getScore))
+ .orElse(null);
+ Log.d(TAG, "filterAnomalyEvent = " + filterAnomalyEvent);
+ return filterAnomalyEvent;
+ }
+
+
+ private static BatteryDiffData getAllBatteryDiffData(
+ Map<Integer, Map<Integer, BatteryDiffData>> batteryUsageMap) {
+ return batteryUsageMap == null ? null : batteryUsageMap
+ .get(BatteryChartViewModel.SELECTED_INDEX_ALL)
+ .get(BatteryChartViewModel.SELECTED_INDEX_ALL);
+ }
+
public static final BaseSearchIndexProvider SEARCH_INDEX_DATA_PROVIDER =
new BaseSearchIndexProvider() {
@Override
@@ -200,32 +439,36 @@
controllers.add(new BatteryUsageBreakdownController(
context, null /* lifecycle */, null /* activity */,
null /* fragment */));
+ controllers.add(new BatteryTipsController(context));
return controllers;
}
};
- private class BatteryHistoryLoaderCallbacks
- implements LoaderManager.LoaderCallbacks<Map<Long, Map<String, BatteryHistEntry>>> {
- private int mRefreshType;
-
+ private class BatteryLevelDataLoaderCallbacks
+ implements LoaderManager.LoaderCallbacks<BatteryLevelData> {
@Override
- @NonNull
- public Loader<Map<Long, Map<String, BatteryHistEntry>>> onCreateLoader(
- int id, Bundle bundle) {
- mRefreshType = bundle.getInt(KEY_REFRESH_TYPE);
- return new BatteryHistoryLoader(getContext());
+ public Loader<BatteryLevelData> onCreateLoader(int id, Bundle bundle) {
+ return new AsyncLoaderCompat<BatteryLevelData>(getContext().getApplicationContext()) {
+ @Override
+ protected void onDiscardResult(BatteryLevelData result) {}
+
+ @Override
+ public BatteryLevelData loadInBackground() {
+ return DataProcessManager.getBatteryLevelData(
+ getContext(), mHandler, /*isFromPeriodJob=*/ false,
+ PowerUsageAdvanced.this::onBatteryDiffDataMapUpdate);
+ }
+ };
}
@Override
- public void onLoadFinished(Loader<Map<Long, Map<String, BatteryHistEntry>>> loader,
- Map<Long, Map<String, BatteryHistEntry>> batteryHistoryMap) {
- mBatteryHistoryMap = batteryHistoryMap;
- PowerUsageAdvanced.this.onLoadFinished(mRefreshType);
+ public void onLoadFinished(Loader<BatteryLevelData> loader,
+ BatteryLevelData batteryLevelData) {
+ PowerUsageAdvanced.this.onBatteryLevelDataUpdate(batteryLevelData);
}
@Override
- public void onLoaderReset(Loader<Map<Long, Map<String, BatteryHistEntry>>> loader) {
+ public void onLoaderReset(Loader<BatteryLevelData> loader) {
}
}
-
}
diff --git a/src/com/android/settings/fuelgauge/batteryusage/PowerUsageBase.java b/src/com/android/settings/fuelgauge/batteryusage/PowerUsageBase.java
index ed3a921..22856b6 100644
--- a/src/com/android/settings/fuelgauge/batteryusage/PowerUsageBase.java
+++ b/src/com/android/settings/fuelgauge/batteryusage/PowerUsageBase.java
@@ -32,7 +32,6 @@
import com.android.settings.dashboard.DashboardFragment;
import com.android.settings.fuelgauge.BatteryBroadcastReceiver;
-import com.android.settings.fuelgauge.BatteryUtils;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -63,14 +62,14 @@
LoaderIndex.BATTERY_USAGE_STATS_LOADER,
LoaderIndex.BATTERY_INFO_LOADER,
LoaderIndex.BATTERY_TIP_LOADER,
- LoaderIndex.BATTERY_HISTORY_LOADER
+ LoaderIndex.BATTERY_LEVEL_DATA_LOADER
})
public @interface LoaderIndex {
int BATTERY_USAGE_STATS_LOADER = 0;
int BATTERY_INFO_LOADER = 1;
int BATTERY_TIP_LOADER = 2;
- int BATTERY_HISTORY_LOADER = 3;
+ int BATTERY_LEVEL_DATA_LOADER = 3;
}
@Override
@@ -108,7 +107,7 @@
protected void restartBatteryStatsLoader(int refreshType) {
final Bundle bundle = new Bundle();
bundle.putInt(KEY_REFRESH_TYPE, refreshType);
- bundle.putBoolean(KEY_INCLUDE_HISTORY, isBatteryHistoryNeeded());
+ bundle.putBoolean(KEY_INCLUDE_HISTORY, false);
restartLoader(LoaderIndex.BATTERY_USAGE_STATS_LOADER, bundle,
mBatteryUsageStatsLoaderCallbacks);
}
@@ -137,14 +136,6 @@
protected abstract void refreshUi(@BatteryUpdateType int refreshType);
- protected abstract boolean isBatteryHistoryNeeded();
-
- protected void updatePreference(BatteryHistoryPreference historyPref) {
- final long startTime = System.currentTimeMillis();
- historyPref.setBatteryUsageStats(mBatteryUsageStats);
- BatteryUtils.logRuntime(TAG, "updatePreference", startTime);
- }
-
private class BatteryUsageStatsLoaderCallbacks
implements LoaderManager.LoaderCallbacks<BatteryUsageStats> {
private int mRefreshType;
diff --git a/src/com/android/settings/fuelgauge/batteryusage/PowerUsageSummary.java b/src/com/android/settings/fuelgauge/batteryusage/PowerUsageSummary.java
index 0bec490..40fd3f4 100644
--- a/src/com/android/settings/fuelgauge/batteryusage/PowerUsageSummary.java
+++ b/src/com/android/settings/fuelgauge/batteryusage/PowerUsageSummary.java
@@ -45,7 +45,6 @@
import com.android.settings.overlay.FeatureFactory;
import com.android.settings.search.BaseSearchIndexProvider;
import com.android.settingslib.search.SearchIndexable;
-import com.android.settingslib.widget.LayoutPreference;
import java.util.List;
@@ -69,8 +68,6 @@
@VisibleForTesting
BatteryUtils mBatteryUtils;
@VisibleForTesting
- LayoutPreference mBatteryLayoutPref;
- @VisibleForTesting
BatteryInfo mBatteryInfo;
@VisibleForTesting
@@ -208,11 +205,6 @@
return R.string.help_url_battery;
}
- @Override
- protected boolean isBatteryHistoryNeeded() {
- return false;
- }
-
protected void refreshUi(@BatteryUpdateType int refreshType) {
final Context context = getContext();
if (context == null) {
@@ -240,11 +232,6 @@
}
@VisibleForTesting
- void setBatteryLayoutPreference(LayoutPreference layoutPreference) {
- mBatteryLayoutPref = layoutPreference;
- }
-
- @VisibleForTesting
void initFeatureProvider() {
final Context context = getContext();
mPowerFeatureProvider = FeatureFactory.getFactory(context)
diff --git a/src/com/android/settings/fuelgauge/batteryusage/bugreport/BatteryUsageLogUtils.java b/src/com/android/settings/fuelgauge/batteryusage/bugreport/BatteryUsageLogUtils.java
new file mode 100644
index 0000000..cb2f394
--- /dev/null
+++ b/src/com/android/settings/fuelgauge/batteryusage/bugreport/BatteryUsageLogUtils.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.fuelgauge.batteryusage.bugreport;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.util.Base64;
+
+import com.android.settings.fuelgauge.BatteryUsageHistoricalLog;
+import com.android.settings.fuelgauge.BatteryUsageHistoricalLogEntry;
+import com.android.settings.fuelgauge.BatteryUsageHistoricalLogEntry.Action;
+import com.android.settings.fuelgauge.BatteryUtils;
+import com.android.settings.fuelgauge.batteryusage.ConvertUtils;
+import com.google.common.annotations.VisibleForTesting;
+
+import java.io.PrintWriter;
+import java.util.List;
+
+/** Writes and reads a historical log of battery usage periodic job events. */
+public final class BatteryUsageLogUtils {
+ private static final String TAG = "BatteryUsageLogUtils";
+ private static final String BATTERY_USAGE_FILE_NAME = "battery_usage_historical_logs";
+ private static final String LOGS_KEY = "battery_usage_logs_key";
+
+ // 24 hours x 4 events every hour x 3 days
+ static final int MAX_ENTRIES = 288;
+
+ private BatteryUsageLogUtils() {}
+
+ /** Write the log into the {@link SharedPreferences}. */
+ public static void writeLog(Context context, Action action, String actionDescription) {
+ final SharedPreferences sharedPreferences = getSharedPreferences(context);
+ final BatteryUsageHistoricalLogEntry newLogEntry =
+ BatteryUsageHistoricalLogEntry.newBuilder()
+ .setTimestamp(System.currentTimeMillis())
+ .setAction(action)
+ .setActionDescription(actionDescription)
+ .build();
+
+ final BatteryUsageHistoricalLog existingLog =
+ parseLogFromString(sharedPreferences.getString(LOGS_KEY, ""));
+ final BatteryUsageHistoricalLog.Builder newLogBuilder = existingLog.toBuilder();
+ // Prune old entries to limit the max logging data count.
+ if (existingLog.getLogEntryCount() >= MAX_ENTRIES) {
+ newLogBuilder.removeLogEntry(0);
+ }
+ newLogBuilder.addLogEntry(newLogEntry);
+
+ final String loggingContent =
+ Base64.encodeToString(newLogBuilder.build().toByteArray(), Base64.DEFAULT);
+ sharedPreferences
+ .edit()
+ .putString(LOGS_KEY, loggingContent)
+ .apply();
+ }
+
+ /** Prints the historical log that has previously been stored by this utility. */
+ public static void printHistoricalLog(Context context, PrintWriter writer) {
+ final BatteryUsageHistoricalLog existingLog = parseLogFromString(
+ getSharedPreferences(context).getString(LOGS_KEY, ""));
+ final List<BatteryUsageHistoricalLogEntry> logEntryList = existingLog.getLogEntryList();
+ if (logEntryList.isEmpty()) {
+ writer.println("\tnothing to dump");
+ } else {
+ logEntryList.forEach(entry -> writer.println(toString(entry)));
+ }
+ }
+
+ @VisibleForTesting
+ static SharedPreferences getSharedPreferences(Context context) {
+ return context.getApplicationContext()
+ .getSharedPreferences(BATTERY_USAGE_FILE_NAME, Context.MODE_PRIVATE);
+ }
+
+ private static BatteryUsageHistoricalLog parseLogFromString(String storedLogs) {
+ return BatteryUtils.parseProtoFromString(
+ storedLogs, BatteryUsageHistoricalLog.getDefaultInstance());
+ }
+
+ private static String toString(BatteryUsageHistoricalLogEntry entry) {
+ final StringBuilder builder = new StringBuilder("\t")
+ .append(ConvertUtils.utcToLocalTimeForLogging(entry.getTimestamp()))
+ .append(" " + entry.getAction());
+ final String description = entry.getActionDescription();
+ if (description != null && !description.isEmpty()) {
+ builder.append(" " + description);
+ }
+ return builder.toString();
+ }
+}
diff --git a/src/com/android/settings/fuelgauge/batteryusage/bugreport/LogUtils.java b/src/com/android/settings/fuelgauge/batteryusage/bugreport/LogUtils.java
index 9be378b..6d5082c 100644
--- a/src/com/android/settings/fuelgauge/batteryusage/bugreport/LogUtils.java
+++ b/src/com/android/settings/fuelgauge/batteryusage/bugreport/LogUtils.java
@@ -39,6 +39,12 @@
private static final Duration DUMP_TIME_OFFSET_FOR_ENTRY = Duration.ofHours(4);
static void dumpBatteryUsageDatabaseHist(Context context, PrintWriter writer) {
+ // Dumps periodic job events.
+ writer.println("\nBattery PeriodicJob History:");
+ BatteryUsageLogUtils.printHistoricalLog(context, writer);
+ writer.flush();
+
+ // Dumps phenotype environments.
DatabaseUtils.dump(context, writer);
writer.flush();
final BatteryStateDao dao =
@@ -47,6 +53,7 @@
.batteryStateDao();
final long timeOffset =
Clock.systemUTC().millis() - DUMP_TIME_OFFSET.toMillis();
+
// Gets all distinct timestamps.
final List<Long> timestamps = dao.getDistinctTimestamps(timeOffset);
final int distinctCount = timestamps.size();
diff --git a/src/com/android/settings/fuelgauge/batteryusage/db/BatteryEventDao.java b/src/com/android/settings/fuelgauge/batteryusage/db/BatteryEventDao.java
index a638d09..0a6de71 100644
--- a/src/com/android/settings/fuelgauge/batteryusage/db/BatteryEventDao.java
+++ b/src/com/android/settings/fuelgauge/batteryusage/db/BatteryEventDao.java
@@ -36,9 +36,16 @@
@Query("SELECT * FROM BatteryEventEntity ORDER BY timestamp DESC")
List<BatteryEventEntity> getAll();
+ /** Gets the {@link Cursor} of the last full charge time . */
+ @Query("SELECT MAX(timestamp) FROM BatteryEventEntity"
+ + " WHERE batteryEventType = 3") // BatteryEventType.FULL_CHARGED = 3
+ Cursor getLastFullChargeTimestamp();
+
/** Gets the {@link Cursor} of all recorded data after a specific timestamp. */
- @Query("SELECT * FROM BatteryEventEntity WHERE timestamp > :timestamp ORDER BY timestamp DESC")
- Cursor getAllAfter(long timestamp);
+ @Query("SELECT * FROM BatteryEventEntity"
+ + " WHERE timestamp > :timestamp AND batteryEventType IN (:batteryEventTypes)"
+ + " ORDER BY timestamp DESC")
+ Cursor getAllAfter(long timestamp, List<Integer> batteryEventTypes);
/** Deletes all recorded data before a specific timestamp. */
@Query("DELETE FROM BatteryEventEntity WHERE timestamp <= :timestamp")
diff --git a/src/com/android/settings/fuelgauge/batteryusage/db/BatteryStateDao.java b/src/com/android/settings/fuelgauge/batteryusage/db/BatteryStateDao.java
index 6d2ab8d..520c6be 100644
--- a/src/com/android/settings/fuelgauge/batteryusage/db/BatteryStateDao.java
+++ b/src/com/android/settings/fuelgauge/batteryusage/db/BatteryStateDao.java
@@ -37,16 +37,18 @@
@Insert(onConflict = OnConflictStrategy.REPLACE)
void insertAll(List<BatteryState> states);
+ /** Gets the {@link Cursor} of the latest record timestamp no later than the given timestamp. */
+ @Query("SELECT MAX(timestamp) FROM BatteryState WHERE timestamp <= :timestamp")
+ Cursor getLatestTimestampBefore(long timestamp);
+
+ /** Lists all recorded battery states after a specific timestamp. */
+ @Query("SELECT * FROM BatteryState WHERE timestamp >= :timestamp ORDER BY timestamp ASC")
+ Cursor getBatteryStatesAfter(long timestamp);
+
/** Lists all recorded data after a specific timestamp. */
@Query("SELECT * FROM BatteryState WHERE timestamp > :timestamp ORDER BY timestamp DESC")
List<BatteryState> getAllAfter(long timestamp);
- /** Gets the {@link Cursor} of all recorded data since last full charge within 7 days. */
- @Query("SELECT * FROM BatteryState WHERE timestamp >= :timestampSixDaysAgo AND timestamp >= "
- + "(SELECT IFNULL((SELECT MAX(timestamp) FROM BatteryState "
- + "WHERE isFullChargeCycleStart = 1), 0)) ORDER BY timestamp ASC")
- Cursor getCursorSinceLastFullCharge(long timestampSixDaysAgo);
-
/** Get the count of distinct timestamp after a specific timestamp. */
@Query("SELECT COUNT(DISTINCT timestamp) FROM BatteryState WHERE timestamp > :timestamp")
int getDistinctTimestampCount(long timestamp);
diff --git a/src/com/android/settings/fuelgauge/batteryusage/db/BatteryStateDatabase.java b/src/com/android/settings/fuelgauge/batteryusage/db/BatteryStateDatabase.java
index 466a7ca..28a0012 100644
--- a/src/com/android/settings/fuelgauge/batteryusage/db/BatteryStateDatabase.java
+++ b/src/com/android/settings/fuelgauge/batteryusage/db/BatteryStateDatabase.java
@@ -25,7 +25,8 @@
/** A {@link RoomDatabase} for battery usage states history. */
@Database(
- entities = {AppUsageEventEntity.class, BatteryEventEntity.class, BatteryState.class},
+ entities = {AppUsageEventEntity.class, BatteryEventEntity.class, BatteryState.class,
+ BatteryUsageSlotEntity.class},
version = 1)
public abstract class BatteryStateDatabase extends RoomDatabase {
private static final String TAG = "BatteryStateDatabase";
@@ -38,13 +39,15 @@
public abstract BatteryEventDao batteryEventDao();
/** Provides DAO for battery state table. */
public abstract BatteryStateDao batteryStateDao();
+ /** Provides DAO for battery usage slot table. */
+ public abstract BatteryUsageSlotDao batteryUsageSlotDao();
/** Gets or creates an instance of {@link RoomDatabase}. */
public static BatteryStateDatabase getInstance(Context context) {
if (sBatteryStateDatabase == null) {
sBatteryStateDatabase =
Room.databaseBuilder(
- context, BatteryStateDatabase.class, "battery-usage-db-v8")
+ context, BatteryStateDatabase.class, "battery-usage-db-v9")
// Allows accessing data in the main thread for dumping bugreport.
.allowMainThreadQueries()
.fallbackToDestructiveMigration()
diff --git a/src/com/android/settings/fuelgauge/batteryusage/db/BatteryUsageSlotDao.java b/src/com/android/settings/fuelgauge/batteryusage/db/BatteryUsageSlotDao.java
new file mode 100644
index 0000000..a695f6a
--- /dev/null
+++ b/src/com/android/settings/fuelgauge/batteryusage/db/BatteryUsageSlotDao.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.fuelgauge.batteryusage.db;
+
+import android.database.Cursor;
+
+import androidx.room.Dao;
+import androidx.room.Insert;
+import androidx.room.OnConflictStrategy;
+import androidx.room.Query;
+
+import java.util.List;
+
+/** Data access object for accessing {@link BatteryUsageSlotEntity} in the database. */
+@Dao
+public interface BatteryUsageSlotDao {
+ /** Inserts a {@link BatteryUsageSlotEntity} data into the database. */
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ void insert(BatteryUsageSlotEntity event);
+
+ /** Gets all recorded data. */
+ @Query("SELECT * FROM BatteryUsageSlotEntity ORDER BY timestamp ASC")
+ List<BatteryUsageSlotEntity> getAll();
+
+ /** Gets the {@link Cursor} of all recorded data after a specific timestamp. */
+ @Query("SELECT * FROM BatteryUsageSlotEntity WHERE timestamp >= :timestamp"
+ + " ORDER BY timestamp ASC")
+ Cursor getAllAfter(long timestamp);
+
+ /** Deletes all recorded data before a specific timestamp. */
+ @Query("DELETE FROM BatteryUsageSlotEntity WHERE timestamp <= :timestamp")
+ void clearAllBefore(long timestamp);
+
+ /** Clears all recorded data in the database. */
+ @Query("DELETE FROM BatteryUsageSlotEntity")
+ void clearAll();
+}
diff --git a/src/com/android/settings/fuelgauge/batteryusage/db/BatteryUsageSlotEntity.java b/src/com/android/settings/fuelgauge/batteryusage/db/BatteryUsageSlotEntity.java
new file mode 100644
index 0000000..c2d5631
--- /dev/null
+++ b/src/com/android/settings/fuelgauge/batteryusage/db/BatteryUsageSlotEntity.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.fuelgauge.batteryusage.db;
+
+import android.content.ContentValues;
+
+import androidx.room.Entity;
+import androidx.room.PrimaryKey;
+
+import com.android.settings.fuelgauge.batteryusage.ConvertUtils;
+
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+
+import java.util.Locale;
+
+/** A {@link Entity} class to save battery usage slot into database. */
+@Entity
+public class BatteryUsageSlotEntity {
+ /** Keys for accessing {@link ContentValues}. */
+ public static final String KEY_TIMESTAMP = "timestamp";
+ public static final String KEY_BATTERY_USAGE_SLOT = "batteryUsageSlot";
+
+ @PrimaryKey(autoGenerate = true)
+ private long mId;
+
+ public final long timestamp;
+ public final String batteryUsageSlot;
+
+ public BatteryUsageSlotEntity(final long timestamp, final String batteryUsageSlot) {
+ this.timestamp = timestamp;
+ this.batteryUsageSlot = batteryUsageSlot;
+ }
+
+ /** Sets the auto-generated content ID. */
+ public void setId(long id) {
+ this.mId = id;
+ }
+
+ /** Gets the auto-generated content ID. */
+ public long getId() {
+ return mId;
+ }
+
+ @Override
+ public String toString() {
+ final String recordAtDateTime = ConvertUtils.utcToLocalTimeForLogging(timestamp);
+ final StringBuilder builder = new StringBuilder()
+ .append("\nBatteryUsageSlot{")
+ .append(String.format(Locale.US, "\n\ttimestamp=%s|batteryUsageSlot=%s",
+ recordAtDateTime, batteryUsageSlot))
+ .append("\n}");
+ return builder.toString();
+ }
+
+ /** Creates new {@link BatteryUsageSlotEntity} from {@link ContentValues}. */
+ public static BatteryUsageSlotEntity create(ContentValues contentValues) {
+ Builder builder = BatteryUsageSlotEntity.newBuilder();
+ if (contentValues.containsKey(KEY_TIMESTAMP)) {
+ builder.setTimestamp(contentValues.getAsLong(KEY_TIMESTAMP));
+ }
+ if (contentValues.containsKey(KEY_BATTERY_USAGE_SLOT)) {
+ builder.setBatteryUsageSlot(contentValues.getAsString(KEY_BATTERY_USAGE_SLOT));
+ }
+ return builder.build();
+ }
+
+ /** Creates a new {@link Builder} instance. */
+ public static Builder newBuilder() {
+ return new Builder();
+ }
+
+ /** A convenience builder class to improve readability. */
+ public static class Builder {
+ private long mTimestamp;
+ private String mBatteryUsageSlot;
+
+ /** Sets the timestamp. */
+ @CanIgnoreReturnValue
+ public Builder setTimestamp(final long timestamp) {
+ mTimestamp = timestamp;
+ return this;
+ }
+
+ /** Sets the battery usage slot. */
+ @CanIgnoreReturnValue
+ public Builder setBatteryUsageSlot(final String batteryUsageSlot) {
+ mBatteryUsageSlot = batteryUsageSlot;
+ return this;
+ }
+
+ /** Builds the {@link BatteryUsageSlotEntity}. */
+ public BatteryUsageSlotEntity build() {
+ return new BatteryUsageSlotEntity(mTimestamp, mBatteryUsageSlot);
+ }
+
+ private Builder() {}
+ }
+}
diff --git a/src/com/android/settings/fuelgauge/protos/Android.bp b/src/com/android/settings/fuelgauge/protos/Android.bp
index 3af2aef..531bdc32 100644
--- a/src/com/android/settings/fuelgauge/protos/Android.bp
+++ b/src/com/android/settings/fuelgauge/protos/Android.bp
@@ -24,9 +24,25 @@
}
java_library {
+ name: "battery-usage-slot-protos-lite",
+ proto: {
+ type: "lite",
+ },
+ srcs: ["battery_usage_slot.proto"],
+}
+
+java_library {
name: "fuelgauge-usage-state-protos-lite",
proto: {
type: "lite",
},
srcs: ["fuelgauge_usage_state.proto"],
}
+
+java_library {
+ name: "power-anomaly-event-protos-lite",
+ proto: {
+ type: "lite",
+ },
+ srcs: ["power_anomaly_event.proto"],
+}
diff --git a/src/com/android/settings/fuelgauge/protos/battery_event.proto b/src/com/android/settings/fuelgauge/protos/battery_event.proto
index 80ccb3b..58ab3be 100644
--- a/src/com/android/settings/fuelgauge/protos/battery_event.proto
+++ b/src/com/android/settings/fuelgauge/protos/battery_event.proto
@@ -8,6 +8,8 @@
UNKNOWN_EVENT = 0;
POWER_CONNECTED = 1;
POWER_DISCONNECTED = 2;
+ FULL_CHARGED = 3;
+ EVEN_HOUR = 4;
}
message BatteryEvent {
diff --git a/src/com/android/settings/fuelgauge/protos/battery_usage_slot.proto b/src/com/android/settings/fuelgauge/protos/battery_usage_slot.proto
new file mode 100644
index 0000000..5bc1a3e
--- /dev/null
+++ b/src/com/android/settings/fuelgauge/protos/battery_usage_slot.proto
@@ -0,0 +1,34 @@
+syntax = "proto2";
+
+option java_multiple_files = true;
+option java_package = "com.android.settings.fuelgauge.batteryusage";
+option java_outer_classname = "BatteryUsageSlotProto";
+
+message BatteryUsageSlot {
+ optional int64 start_timestamp = 1;
+ optional int64 end_timestamp = 2;
+ optional int32 start_battery_level = 3;
+ optional int32 end_battery_level = 4;
+ optional int64 screen_on_time = 5;
+ repeated BatteryUsageDiff app_usage = 6;
+ repeated BatteryUsageDiff system_usage = 7;
+}
+
+message BatteryUsageDiff {
+ optional int64 uid = 1;
+ optional int64 user_id = 2;
+ optional string package_name = 3;
+ optional string label = 4;
+ optional string key = 5;
+ optional bool is_hidden = 6;
+ optional int32 component_id = 7;
+ optional int32 consumer_type = 8;
+ optional double consume_power = 9;
+ optional double foreground_usage_consume_power = 10;
+ optional double background_usage_consume_power = 11;
+ optional double foreground_service_usage_consume_power = 12;
+ optional double cached_usage_consume_power = 13;
+ optional int64 foreground_usage_time = 14;
+ optional int64 background_usage_time = 15;
+ optional int64 screen_on_time = 16;
+}
diff --git a/src/com/android/settings/fuelgauge/protos/power_anomaly_event.proto b/src/com/android/settings/fuelgauge/protos/power_anomaly_event.proto
new file mode 100644
index 0000000..caa9c35
--- /dev/null
+++ b/src/com/android/settings/fuelgauge/protos/power_anomaly_event.proto
@@ -0,0 +1,70 @@
+syntax = "proto2";
+
+option java_multiple_files = true;
+option java_package = "com.android.settings.fuelgauge.batteryusage";
+option java_outer_classname = "PowerAnomalyEventProto";
+
+message PowerAnomalyEventList {
+ repeated PowerAnomalyEvent power_anomaly_events = 1;
+}
+
+message PowerAnomalyEvent {
+ optional string event_id = 1;
+ optional int64 timestamp = 2;
+ optional PowerAnomalyType type = 3;
+ optional PowerAnomalyKey key = 4;
+ optional float score = 5;
+ oneof info {
+ WarningBannerInfo warning_banner_info = 6;
+ WarningItemInfo warning_item_info = 7;
+ }
+ optional string dismiss_record_key = 8;
+}
+
+// NOTE: Please DO NOT delete enum items or change enum values. Use [deprecated = true] instead.
+// The enum value will be used to decide the tips card style like icons and colors.
+//
+// Next id: 2
+enum PowerAnomalyType{
+ TYPE_SETTINGS_BANNER = 0;
+ TYPE_APPS_ITEM = 1;
+}
+
+// NOTE: Please DO NOT delete enum items or change enum values. Use [deprecated = true] instead.
+// The enum value will be used to decide pre-defined title and button labels.
+//
+// Next id: 8
+enum PowerAnomalyKey{
+ KEY_BRIGHTNESS = 0;
+ KEY_SCREEN_TIMEOUT = 1;
+ KEY_APP_TOTAL_ALWAYS_HIGH = 2;
+ KEY_APP_TOTAL_HIGHER_THAN_USUAL = 3;
+ KEY_APP_BACKGROUND_ALWAYS_HIGH = 4;
+ KEY_APP_BACKGROUND_HIGHER_THAN_USUAL = 5;
+ KEY_APP_FOREGROUND_ALWAYS_HIGH = 6;
+ KEY_APP_FOREGROUND_HIGHER_THAN_USUAL = 7;
+}
+
+message WarningBannerInfo {
+ optional string title_string = 1;
+ optional string description_string = 2;
+ optional string main_button_string = 3;
+ // Used in the SubSettingLauncher.setDestination().
+ optional string main_button_destination = 4;
+ // Used in the SubSettingLauncher.setSourceMetricsCategory().
+ optional int32 main_button_source_metrics_category = 5;
+ // Used in the SubSettingLauncher.setArguments().
+ optional string main_button_source_highlight_key = 6;
+ optional string cancel_button_string = 7;
+}
+
+message WarningItemInfo {
+ optional int64 start_timestamp = 1;
+ optional int64 end_timestamp = 2;
+ optional string top_card_string = 3;
+ optional string title_string = 4;
+ optional string description_string = 5;
+ optional string main_button_string = 6;
+ optional string cancel_button_string = 7;
+ optional string item_key = 8;
+}
diff --git a/src/com/android/settings/gestures/SystemNavigationPreferenceController.java b/src/com/android/settings/gestures/SystemNavigationPreferenceController.java
index ab83a9d..ccdb257 100644
--- a/src/com/android/settings/gestures/SystemNavigationPreferenceController.java
+++ b/src/com/android/settings/gestures/SystemNavigationPreferenceController.java
@@ -52,7 +52,8 @@
}
}
- static boolean isGestureAvailable(Context context) {
+ /** Returns {@code true} if gesture is available. */
+ public static boolean isGestureAvailable(Context context) {
// Skip if the swipe up settings are not available
if (!context.getResources().getBoolean(
com.android.internal.R.bool.config_swipe_up_gesture_setting_available)) {
diff --git a/src/com/android/settings/inputmethod/KeyboardSettingsFeatureProvider.java b/src/com/android/settings/inputmethod/KeyboardSettingsFeatureProvider.java
new file mode 100644
index 0000000..7255107
--- /dev/null
+++ b/src/com/android/settings/inputmethod/KeyboardSettingsFeatureProvider.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.inputmethod;
+
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+
+import androidx.annotation.Nullable;
+import androidx.preference.PreferenceScreen;
+
+/**
+ * Provider for Keyboard settings related features.
+ */
+public interface KeyboardSettingsFeatureProvider {
+
+ /**
+ * Checks whether the connected device supports firmware update.
+ *
+ * @return true if the connected device supports firmware update.
+ */
+ boolean supportsFirmwareUpdate();
+
+ /**
+ * Add firmware update preference category .
+ *
+ * @param context The context to initialize the application with.
+ * @param screen The {@link PreferenceScreen} to add the firmware update preference category.
+ *
+ * @return true if the category is added successfully.
+ */
+ boolean addFirmwareUpdateCategory(Context context, PreferenceScreen screen);
+
+ /**
+ * Get custom action key icon.
+ *
+ * @param context Context for accessing resources.
+ *
+ * @return Returns the image of the icon, or null if there is no any custom icon.
+ */
+ @Nullable
+ Drawable getActionKeyIcon(Context context);
+}
diff --git a/src/com/android/settings/inputmethod/KeyboardSettingsFeatureProviderImpl.java b/src/com/android/settings/inputmethod/KeyboardSettingsFeatureProviderImpl.java
new file mode 100644
index 0000000..26b10e5
--- /dev/null
+++ b/src/com/android/settings/inputmethod/KeyboardSettingsFeatureProviderImpl.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.inputmethod;
+
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+
+import androidx.preference.PreferenceScreen;
+
+/**
+ * Provider implementation for keyboard settings related features.
+ */
+public class KeyboardSettingsFeatureProviderImpl implements KeyboardSettingsFeatureProvider {
+
+ @Override
+ public boolean supportsFirmwareUpdate() {
+ return false;
+ }
+
+ @Override
+ public boolean addFirmwareUpdateCategory(Context context, PreferenceScreen screen) {
+ return false;
+ }
+
+ @Override
+ public Drawable getActionKeyIcon(Context context) {
+ return null;
+ };
+}
diff --git a/src/com/android/settings/inputmethod/KeyboardSettingsPreferenceController.java b/src/com/android/settings/inputmethod/KeyboardSettingsPreferenceController.java
index 03461af..ae6a24a 100644
--- a/src/com/android/settings/inputmethod/KeyboardSettingsPreferenceController.java
+++ b/src/com/android/settings/inputmethod/KeyboardSettingsPreferenceController.java
@@ -16,6 +16,7 @@
package com.android.settings.inputmethod;
+import android.app.settings.SettingsEnums;
import android.content.Context;
import android.content.Intent;
import android.provider.Settings;
@@ -53,8 +54,7 @@
if (mCachedDevice.getAddress().equals(hardKeyboardDeviceInfo.mBluetoothAddress)) {
Intent intent = new Intent(Settings.ACTION_HARD_KEYBOARD_SETTINGS);
intent.putExtra(
- NewKeyboardSettingsUtils.EXTRA_INTENT_FROM,
- "com.android.settings.inputmethod.KeyboardSettingsPreferenceController");
+ Settings.EXTRA_ENTRYPOINT, SettingsEnums.CONNECTED_DEVICES_SETTINGS);
intent.putExtra(
Settings.EXTRA_INPUT_DEVICE_IDENTIFIER,
hardKeyboardDeviceInfo.mDeviceIdentifier);
diff --git a/src/com/android/settings/inputmethod/ModifierKeysPickerDialogFragment.java b/src/com/android/settings/inputmethod/ModifierKeysPickerDialogFragment.java
index 949e656..076173a 100644
--- a/src/com/android/settings/inputmethod/ModifierKeysPickerDialogFragment.java
+++ b/src/com/android/settings/inputmethod/ModifierKeysPickerDialogFragment.java
@@ -22,6 +22,7 @@
import android.app.AlertDialog;
import android.app.Dialog;
import android.content.Context;
+import android.graphics.drawable.Drawable;
import android.hardware.input.InputManager;
import android.os.Bundle;
import android.text.Spannable;
@@ -39,10 +40,12 @@
import android.widget.ListView;
import android.widget.TextView;
+import androidx.core.graphics.drawable.DrawableCompat;
import androidx.fragment.app.DialogFragment;
import androidx.preference.Preference;
import com.android.settings.R;
+import com.android.settings.overlay.FeatureFactory;
import com.android.settingslib.Utils;
import java.util.ArrayList;
@@ -60,6 +63,11 @@
private String mKeyDefaultName;
private String mKeyFocus;
private Activity mActivity;
+ private KeyboardSettingsFeatureProvider mFeatureProvider;
+ private Drawable mActionKeyDrawable;
+ private TextView mLeftBracket;
+ private TextView mRightBracket;
+ private ImageView mActionKeyIcon;
private List<int[]> mRemappableKeyList =
new ArrayList<>(Arrays.asList(
@@ -83,6 +91,8 @@
super.onCreateDialog(savedInstanceState);
mActivity = getActivity();
+ FeatureFactory featureFactory = FeatureFactory.getFactory(mActivity);
+ mFeatureProvider = featureFactory.getKeyboardSettingsFeatureProvider();
InputManager inputManager = mActivity.getSystemService(InputManager.class);
mKeyDefaultName = getArguments().getString(DEFAULT_KEY);
mKeyFocus = getArguments().getString(SELECTION_KEY);
@@ -97,6 +107,10 @@
for (int i = 0; i < modifierKeys.size(); i++) {
mRemappableKeyMap.put(modifierKeys.get(i), mRemappableKeyList.get(i));
}
+ Drawable drawable = mFeatureProvider.getActionKeyIcon(mActivity);
+ if (drawable != null) {
+ mActionKeyDrawable = DrawableCompat.wrap(drawable);
+ }
View dialoglayout =
LayoutInflater.from(mActivity).inflate(R.layout.modifier_key_picker_dialog, null);
@@ -226,10 +240,18 @@
checkIcon.setImageAlpha(255);
view.setBackground(
mActivity.getDrawable(R.drawable.modifier_key_lisetview_background));
+ if (mActionKeyDrawable != null && i == 2) {
+ setActionKeyIcon(view);
+ setActionKeyColor(getColorOfMaterialColorPrimary());
+ }
} else {
textView.setTextColor(getColorOfTextColorPrimary());
checkIcon.setImageAlpha(0);
view.setBackground(null);
+ if (mActionKeyDrawable != null && i == 2) {
+ setActionKeyIcon(view);
+ setActionKeyColor(getColorOfTextColorPrimary());
+ }
}
return view;
}
@@ -243,6 +265,21 @@
}
}
+ private void setActionKeyIcon(View view) {
+ mLeftBracket = view.findViewById(R.id.modifier_key_left_bracket);
+ mRightBracket = view.findViewById(R.id.modifier_key_right_bracket);
+ mActionKeyIcon = view.findViewById(R.id.modifier_key_action_key_icon);
+ mLeftBracket.setText("(");
+ mRightBracket.setText(")");
+ mActionKeyIcon.setImageDrawable(mActionKeyDrawable);
+ }
+
+ private void setActionKeyColor(int color) {
+ mLeftBracket.setTextColor(color);
+ mRightBracket.setTextColor(color);
+ DrawableCompat.setTint(mActionKeyDrawable, color);
+ }
+
private int getColorOfTextColorPrimary() {
return Utils.getColorAttrDefaultColor(mActivity, android.R.attr.textColorPrimary);
}
diff --git a/src/com/android/settings/inputmethod/ModifierKeysPreferenceController.java b/src/com/android/settings/inputmethod/ModifierKeysPreferenceController.java
index 5d8149a..77def48 100644
--- a/src/com/android/settings/inputmethod/ModifierKeysPreferenceController.java
+++ b/src/com/android/settings/inputmethod/ModifierKeysPreferenceController.java
@@ -17,12 +17,16 @@
package com.android.settings.inputmethod;
import android.content.Context;
+import android.graphics.drawable.Drawable;
import android.hardware.input.InputManager;
import android.os.Bundle;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.style.ForegroundColorSpan;
+import android.util.Pair;
import android.view.KeyEvent;
+import android.widget.ImageView;
+import android.widget.TextView;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
@@ -31,7 +35,9 @@
import com.android.settings.R;
import com.android.settings.core.BasePreferenceController;
+import com.android.settings.overlay.FeatureFactory;
import com.android.settingslib.Utils;
+import com.android.settingslib.widget.LayoutPreference;
import java.util.ArrayList;
import java.util.Arrays;
@@ -53,6 +59,7 @@
private FragmentManager mFragmentManager;
private final InputManager mIm;
private PreferenceScreen mScreen;
+ private Drawable mDrawable;
private final List<Integer> mRemappableKeys = new ArrayList<>(
Arrays.asList(
@@ -61,6 +68,14 @@
KeyEvent.KEYCODE_ALT_LEFT, KeyEvent.KEYCODE_ALT_RIGHT,
KeyEvent.KEYCODE_CAPS_LOCK));
+ private final List<Pair<String, Integer>> mKeys = new ArrayList<>(
+ Arrays.asList(
+ Pair.create(KEY_PREFERENCE_CTRL, R.string.modifier_keys_ctrl),
+ Pair.create(KEY_PREFERENCE_META, R.string.modifier_keys_meta),
+ Pair.create(KEY_PREFERENCE_ALT, R.string.modifier_keys_alt),
+ Pair.create(KEY_PREFERENCE_CAPS_LOCK, R.string.modifier_keys_caps_lock)
+ ));
+
private String[] mKeyNames = new String[] {
mContext.getString(R.string.modifier_keys_ctrl),
mContext.getString(R.string.modifier_keys_ctrl),
@@ -74,6 +89,9 @@
super(context, key);
mIm = context.getSystemService(InputManager.class);
Objects.requireNonNull(mIm, "InputManager service cannot be null");
+ KeyboardSettingsFeatureProvider featureProvider =
+ FeatureFactory.getFactory(context).getKeyboardSettingsFeatureProvider();
+ mDrawable = featureProvider.getActionKeyIcon(context);
}
public void setFragment(Fragment parent) {
@@ -91,33 +109,59 @@
}
private void refreshUi() {
+ initDefaultKeysName();
for (Map.Entry<Integer, Integer> entry : mIm.getModifierKeyRemapping().entrySet()) {
int fromKey = entry.getKey();
int toKey = entry.getValue();
int index = mRemappableKeys.indexOf(toKey);
if (isCtrl(fromKey) && mRemappableKeys.contains(toKey)) {
- Preference preference = mScreen.findPreference(KEY_PREFERENCE_CTRL);
- preference.setSummary(changeSummaryColor(mKeyNames[index]));
+ setSummaryColor(KEY_PREFERENCE_CTRL, index);
}
if (isMeta(fromKey) && mRemappableKeys.contains(toKey)) {
- Preference preference = mScreen.findPreference(KEY_PREFERENCE_META);
- preference.setSummary(changeSummaryColor(mKeyNames[index]));
+ setSummaryColor(KEY_PREFERENCE_META, index);
}
if (isAlt(fromKey) && mRemappableKeys.contains(toKey)) {
- Preference preference = mScreen.findPreference(KEY_PREFERENCE_ALT);
- preference.setSummary(changeSummaryColor(mKeyNames[index]));
+ setSummaryColor(KEY_PREFERENCE_ALT, index);
}
if (isCapLock(fromKey) && mRemappableKeys.contains(toKey)) {
- Preference preference = mScreen.findPreference(KEY_PREFERENCE_CAPS_LOCK);
- preference.setSummary(changeSummaryColor(mKeyNames[index]));
+ setSummaryColor(KEY_PREFERENCE_CAPS_LOCK, index);
}
}
}
+ private void initDefaultKeysName() {
+ for (Pair<String, Integer> key : mKeys) {
+ LayoutPreference layoutPreference = mScreen.findPreference(key.first);
+ TextView title = layoutPreference.findViewById(R.id.title);
+ TextView summary = layoutPreference.findViewById(R.id.summary);
+ title.setText(key.second);
+ summary.setText(R.string.modifier_keys_default_summary);
+
+ if (key.first.equals(KEY_PREFERENCE_META) && mDrawable != null) {
+ setActionKeyIcon(layoutPreference, mDrawable);
+ }
+ }
+ }
+
+ private static void setActionKeyIcon(LayoutPreference preference, Drawable drawable) {
+ TextView leftBracket = preference.findViewById(R.id.modifier_key_left_bracket);
+ TextView rightBracket = preference.findViewById(R.id.modifier_key_right_bracket);
+ ImageView actionKeyIcon = preference.findViewById(R.id.modifier_key_action_key_icon);
+ leftBracket.setText("(");
+ rightBracket.setText(")");
+ actionKeyIcon.setImageDrawable(drawable);
+ }
+
+ private void setSummaryColor(String key, int targetIndex) {
+ LayoutPreference layoutPreference = mScreen.findPreference(key);
+ TextView summary = layoutPreference.findViewById(R.id.summary);
+ summary.setText(changeSummaryColor(mKeyNames[targetIndex]));
+ }
+
@Override
public boolean handlePreferenceTreeClick(Preference preference) {
if (preference.getKey().equals(KEY_RESTORE_PREFERENCE)) {
@@ -137,12 +181,14 @@
ModifierKeysPickerDialogFragment fragment = new ModifierKeysPickerDialogFragment();
fragment.setTargetFragment(mParent, 0);
Bundle bundle = new Bundle();
+ TextView title = ((LayoutPreference) preference).findViewById(R.id.title);
+ TextView summary = ((LayoutPreference) preference).findViewById(R.id.summary);
bundle.putString(
ModifierKeysPickerDialogFragment.DEFAULT_KEY,
- preference.getTitle().toString());
+ title.getText().toString());
bundle.putString(
ModifierKeysPickerDialogFragment.SELECTION_KEY,
- preference.getSummary().toString());
+ summary.getText().toString());
fragment.setArguments(bundle);
fragment.show(mFragmentManager, KEY_TAG);
}
diff --git a/src/com/android/settings/inputmethod/ModifierKeysResetDialogFragment.java b/src/com/android/settings/inputmethod/ModifierKeysResetDialogFragment.java
index 755e9dd..070968c 100644
--- a/src/com/android/settings/inputmethod/ModifierKeysResetDialogFragment.java
+++ b/src/com/android/settings/inputmethod/ModifierKeysResetDialogFragment.java
@@ -33,6 +33,7 @@
import com.android.settings.R;
public class ModifierKeysResetDialogFragment extends DialogFragment {
+
private static final String MODIFIER_KEYS_CAPS_LOCK = "modifier_keys_caps_lock";
private static final String MODIFIER_KEYS_CTRL = "modifier_keys_ctrl";
private static final String MODIFIER_KEYS_META = "modifier_keys_meta";
diff --git a/src/com/android/settings/inputmethod/NewKeyboardLayoutPickerContent.java b/src/com/android/settings/inputmethod/NewKeyboardLayoutPickerContent.java
index 1af001b..11740ec 100644
--- a/src/com/android/settings/inputmethod/NewKeyboardLayoutPickerContent.java
+++ b/src/com/android/settings/inputmethod/NewKeyboardLayoutPickerContent.java
@@ -20,10 +20,6 @@
import android.content.Context;
import android.hardware.input.InputDeviceIdentifier;
import android.hardware.input.InputManager;
-import android.hardware.input.KeyboardLayout;
-import android.os.Bundle;
-import android.view.inputmethod.InputMethodInfo;
-import android.view.inputmethod.InputMethodSubtype;
import com.android.settings.R;
import com.android.settings.dashboard.DashboardFragment;
@@ -32,55 +28,23 @@
private static final String TAG = "KeyboardLayoutPicker";
- private InputManager mIm;
- private int mUserId;
- private InputDeviceIdentifier mIdentifier;
- private InputMethodInfo mInputMethodInfo;
- private InputMethodSubtype mInputMethodSubtype;
-
@Override
public void onAttach(Context context) {
super.onAttach(context);
- mIm = getContext().getSystemService(InputManager.class);
- Bundle arguments = getArguments();
- final CharSequence title = arguments.getCharSequence(NewKeyboardSettingsUtils.EXTRA_TITLE);
- mUserId = arguments.getInt(NewKeyboardSettingsUtils.EXTRA_USER_ID);
- mIdentifier =
- arguments.getParcelable(NewKeyboardSettingsUtils.EXTRA_INPUT_DEVICE_IDENTIFIER);
- mInputMethodInfo =
- arguments.getParcelable(NewKeyboardSettingsUtils.EXTRA_INPUT_METHOD_INFO);
- mInputMethodSubtype =
- arguments.getParcelable(NewKeyboardSettingsUtils.EXTRA_INPUT_METHOD_SUBTYPE);
- if (mIdentifier == null
- || NewKeyboardSettingsUtils.getInputDevice(mIm, mIdentifier) == null) {
+ InputManager im = getContext().getSystemService(InputManager.class);
+ InputDeviceIdentifier identifier =
+ getArguments().getParcelable(
+ NewKeyboardSettingsUtils.EXTRA_INPUT_DEVICE_IDENTIFIER);
+ if (identifier == null
+ || NewKeyboardSettingsUtils.getInputDevice(im, identifier) == null) {
getActivity().finish();
return;
}
- getActivity().setTitle(title);
- use(NewKeyboardLayoutPickerController.class).initialize(this /*parent*/, mUserId,
- mIdentifier, mInputMethodInfo, mInputMethodSubtype, getSelectedLayoutLabel());
- }
-
- private String getSelectedLayoutLabel() {
- String label = getContext().getString(R.string.keyboard_default_layout);
- String layout = NewKeyboardSettingsUtils.getKeyboardLayout(
- mIm, mUserId, mIdentifier, mInputMethodInfo, mInputMethodSubtype);
- KeyboardLayout[] keyboardLayouts = NewKeyboardSettingsUtils.getKeyboardLayouts(
- mIm, mUserId, mIdentifier, mInputMethodInfo, mInputMethodSubtype);
- if (layout != null) {
- for (int i = 0; i < keyboardLayouts.length; i++) {
- if (keyboardLayouts[i].getDescriptor().equals(layout)) {
- label = keyboardLayouts[i].getLabel();
- break;
- }
- }
- }
- return label;
+ use(NewKeyboardLayoutPickerController.class).initialize(this);
}
@Override
public int getMetricsCategory() {
- // TODO: add new SettingsEnums SETTINGS_KEYBOARDS_LAYOUT_PICKER_CONTENT
return SettingsEnums.SETTINGS_KEYBOARDS_LAYOUT_PICKER;
}
diff --git a/src/com/android/settings/inputmethod/NewKeyboardLayoutPickerController.java b/src/com/android/settings/inputmethod/NewKeyboardLayoutPickerController.java
index 8278be8..e63d7d1 100644
--- a/src/com/android/settings/inputmethod/NewKeyboardLayoutPickerController.java
+++ b/src/com/android/settings/inputmethod/NewKeyboardLayoutPickerController.java
@@ -20,6 +20,7 @@
import android.hardware.input.InputDeviceIdentifier;
import android.hardware.input.InputManager;
import android.hardware.input.KeyboardLayout;
+import android.os.Bundle;
import android.view.inputmethod.InputMethodInfo;
import android.view.inputmethod.InputMethodSubtype;
@@ -27,6 +28,7 @@
import androidx.preference.Preference;
import androidx.preference.PreferenceScreen;
+import com.android.settings.R;
import com.android.settings.core.BasePreferenceController;
import com.android.settings.widget.TickButtonPreference;
import com.android.settingslib.core.lifecycle.LifecycleObserver;
@@ -38,16 +40,16 @@
public class NewKeyboardLayoutPickerController extends BasePreferenceController implements
InputManager.InputDeviceListener, LifecycleObserver, OnStart, OnStop {
+
private final InputManager mIm;
private final Map<TickButtonPreference, KeyboardLayout> mPreferenceMap;
-
private Fragment mParent;
+ private CharSequence mTitle;
private int mInputDeviceId;
private int mUserId;
private InputDeviceIdentifier mInputDeviceIdentifier;
private InputMethodInfo mInputMethodInfo;
private InputMethodSubtype mInputMethodSubtype;
-
private KeyboardLayout[] mKeyboardLayouts;
private PreferenceScreen mScreen;
private String mPreviousSelection;
@@ -60,16 +62,21 @@
mPreferenceMap = new HashMap<>();
}
- public void initialize(Fragment parent, int userId, InputDeviceIdentifier inputDeviceIdentifier,
- InputMethodInfo imeInfo, InputMethodSubtype imeSubtype, String layout) {
+ public void initialize(Fragment parent) {
mParent = parent;
- mUserId = userId;
- mInputDeviceIdentifier = inputDeviceIdentifier;
- mInputMethodInfo = imeInfo;
- mInputMethodSubtype = imeSubtype;
- mLayout = layout;
+ Bundle arguments = parent.getArguments();
+ mTitle = arguments.getCharSequence(NewKeyboardSettingsUtils.EXTRA_TITLE);
+ mUserId = arguments.getInt(NewKeyboardSettingsUtils.EXTRA_USER_ID);
+ mInputDeviceIdentifier =
+ arguments.getParcelable(NewKeyboardSettingsUtils.EXTRA_INPUT_DEVICE_IDENTIFIER);
+ mInputMethodInfo =
+ arguments.getParcelable(NewKeyboardSettingsUtils.EXTRA_INPUT_METHOD_INFO);
+ mInputMethodSubtype =
+ arguments.getParcelable(NewKeyboardSettingsUtils.EXTRA_INPUT_METHOD_SUBTYPE);
+ mLayout = getSelectedLayoutLabel();
mKeyboardLayouts = mIm.getKeyboardLayoutListForInputDevice(
- inputDeviceIdentifier, userId, imeInfo, imeSubtype);
+ mInputDeviceIdentifier, mUserId, mInputMethodInfo, mInputMethodSubtype);
+ parent.getActivity().setTitle(mTitle);
}
@Override
@@ -162,4 +169,21 @@
mInputMethodSubtype,
mPreferenceMap.get(preference).getDescriptor());
}
+
+ private String getSelectedLayoutLabel() {
+ String label = mContext.getString(R.string.keyboard_default_layout);
+ String layout = NewKeyboardSettingsUtils.getKeyboardLayout(
+ mIm, mUserId, mInputDeviceIdentifier, mInputMethodInfo, mInputMethodSubtype);
+ KeyboardLayout[] keyboardLayouts = NewKeyboardSettingsUtils.getKeyboardLayouts(
+ mIm, mUserId, mInputDeviceIdentifier, mInputMethodInfo, mInputMethodSubtype);
+ if (layout != null) {
+ for (KeyboardLayout keyboardLayout : keyboardLayouts) {
+ if (keyboardLayout.getDescriptor().equals(layout)) {
+ label = keyboardLayout.getLabel();
+ break;
+ }
+ }
+ }
+ return label;
+ }
}
diff --git a/src/com/android/settings/inputmethod/NewKeyboardLayoutPickerTitle.java b/src/com/android/settings/inputmethod/NewKeyboardLayoutPickerTitle.java
index abcad27..7f87826 100644
--- a/src/com/android/settings/inputmethod/NewKeyboardLayoutPickerTitle.java
+++ b/src/com/android/settings/inputmethod/NewKeyboardLayoutPickerTitle.java
@@ -34,8 +34,7 @@
@Override
public int getMetricsCategory() {
- // TODO: add new SettingsEnums SETTINGS_KEYBOARDS_LAYOUT_PICKER_TITLE
- return SettingsEnums.SETTINGS_KEYBOARDS_LAYOUT_PICKER;
+ return SettingsEnums.SETTINGS_KEYBOARDS_LAYOUT_PICKER_TITLE;
}
public static final BaseSearchIndexProvider SEARCH_INDEX_DATA_PROVIDER =
diff --git a/src/com/android/settings/inputmethod/NewKeyboardSettingsUtils.java b/src/com/android/settings/inputmethod/NewKeyboardSettingsUtils.java
index 697c0f0..ad68c43 100644
--- a/src/com/android/settings/inputmethod/NewKeyboardSettingsUtils.java
+++ b/src/com/android/settings/inputmethod/NewKeyboardSettingsUtils.java
@@ -33,12 +33,6 @@
*/
public class NewKeyboardSettingsUtils {
- /**
- * Record the class name of the intent sender for metrics.
- */
- public static final String EXTRA_INTENT_FROM =
- "com.android.settings.inputmethod.EXTRA_INTENT_FROM";
-
static final String EXTRA_TITLE = "keyboard_layout_picker_title";
static final String EXTRA_USER_ID = "user_id";
static final String EXTRA_INPUT_DEVICE_IDENTIFIER = "input_device_identifier";
diff --git a/src/com/android/settings/inputmethod/PhysicalKeyboardFragment.java b/src/com/android/settings/inputmethod/PhysicalKeyboardFragment.java
index 936de38..9cfa4cd 100644
--- a/src/com/android/settings/inputmethod/PhysicalKeyboardFragment.java
+++ b/src/com/android/settings/inputmethod/PhysicalKeyboardFragment.java
@@ -48,6 +48,7 @@
import com.android.settings.Settings;
import com.android.settings.SettingsPreferenceFragment;
import com.android.settings.core.SubSettingLauncher;
+import com.android.settings.overlay.FeatureFactory;
import com.android.settings.search.BaseSearchIndexProvider;
import com.android.settingslib.search.SearchIndexable;
import com.android.settingslib.utils.ThreadUtils;
@@ -75,6 +76,7 @@
private InputManager mIm;
private InputMethodManager mImm;
private InputDeviceIdentifier mAutoInputDeviceIdentifier;
+ private KeyboardSettingsFeatureProvider mFeatureProvider;
@NonNull
private PreferenceCategory mKeyboardAssistanceCategory;
@NonNull
@@ -82,6 +84,7 @@
private Intent mIntentWaitingForResult;
private boolean mIsNewKeyboardSettings;
+ private boolean mSupportsFirmwareUpdate;
static final String EXTRA_BT_ADDRESS = "extra_bt_address";
private String mBluetoothAddress;
@@ -104,6 +107,12 @@
(SwitchPreference) mKeyboardAssistanceCategory.findPreference(
SHOW_VIRTUAL_KEYBOARD_SWITCH));
+ FeatureFactory featureFactory = FeatureFactory.getFactory(getContext());
+ mFeatureProvider = featureFactory.getKeyboardSettingsFeatureProvider();
+ mSupportsFirmwareUpdate = mFeatureProvider.supportsFirmwareUpdate();
+ if (mSupportsFirmwareUpdate) {
+ mFeatureProvider.addFirmwareUpdateCategory(getContext(), getPreferenceScreen());
+ }
mIsNewKeyboardSettings = FeatureFlagUtils.isEnabled(
getContext(), FeatureFlagUtils.SETTINGS_NEW_KEYBOARD_UI);
boolean isModifierKeySettingsEnabled = FeatureFlagUtils
@@ -113,7 +122,8 @@
}
InputDeviceIdentifier inputDeviceIdentifier = activity.getIntent().getParcelableExtra(
KeyboardLayoutPickerFragment.EXTRA_INPUT_DEVICE_IDENTIFIER);
- // TODO (b/271391879): The EXTRA_INTENT_FROM is used for the future metrics.
+ int intentFromWhere =
+ activity.getIntent().getIntExtra(android.provider.Settings.EXTRA_ENTRYPOINT, -1);
if (inputDeviceIdentifier != null) {
mAutoInputDeviceIdentifier = inputDeviceIdentifier;
}
@@ -247,6 +257,9 @@
}
mKeyboardAssistanceCategory.setOrder(1);
preferenceScreen.addPreference(mKeyboardAssistanceCategory);
+ if (mSupportsFirmwareUpdate) {
+ mFeatureProvider.addFirmwareUpdateCategory(getPrefContext(), preferenceScreen);
+ }
updateShowVirtualKeyboardSwitch();
}
diff --git a/src/com/android/settings/inputmethod/PhysicalKeyboardPreferenceController.java b/src/com/android/settings/inputmethod/PhysicalKeyboardPreferenceController.java
index 1f01b98..b88928c 100644
--- a/src/com/android/settings/inputmethod/PhysicalKeyboardPreferenceController.java
+++ b/src/com/android/settings/inputmethod/PhysicalKeyboardPreferenceController.java
@@ -16,6 +16,7 @@
package com.android.settings.inputmethod;
+import android.app.settings.SettingsEnums;
import android.content.Context;
import android.content.Intent;
import android.hardware.input.InputManager;
@@ -66,9 +67,7 @@
return false;
}
Intent intent = new Intent(Settings.ACTION_HARD_KEYBOARD_SETTINGS);
- intent.putExtra(
- NewKeyboardSettingsUtils.EXTRA_INTENT_FROM,
- "com.android.settings.inputmethod.PhysicalKeyboardPreferenceController");
+ intent.putExtra(Settings.EXTRA_ENTRYPOINT, SettingsEnums.KEYBOARD_SETTINGS);
mContext.startActivity(intent);
return true;
}
diff --git a/src/com/android/settings/inputmethod/TouchGesturesButtonPreferenceController.java b/src/com/android/settings/inputmethod/TouchGesturesButtonPreferenceController.java
index 7efa637..bbe65c1 100644
--- a/src/com/android/settings/inputmethod/TouchGesturesButtonPreferenceController.java
+++ b/src/com/android/settings/inputmethod/TouchGesturesButtonPreferenceController.java
@@ -63,9 +63,7 @@
@Override
public int getAvailabilityStatus() {
- boolean touchGestureDeveloperMode = FeatureFlagUtils
- .isEnabled(mContext, FeatureFlagUtils.SETTINGS_NEW_KEYBOARD_TRACKPAD_GESTURE);
- return touchGestureDeveloperMode ? AVAILABLE : CONDITIONALLY_UNAVAILABLE;
+ return AVAILABLE;
}
private void showTouchpadGestureEducation() {
diff --git a/src/com/android/settings/localepicker/LocaleDialogFragment.java b/src/com/android/settings/localepicker/LocaleDialogFragment.java
index ad9e10f..2457e79 100644
--- a/src/com/android/settings/localepicker/LocaleDialogFragment.java
+++ b/src/com/android/settings/localepicker/LocaleDialogFragment.java
@@ -16,6 +16,8 @@
package com.android.settings.localepicker;
+import static android.window.OnBackInvokedDispatcher.PRIORITY_DEFAULT;
+
import android.app.Activity;
import android.app.Dialog;
import android.app.settings.SettingsEnums;
@@ -23,15 +25,17 @@
import android.content.DialogInterface;
import android.content.Intent;
import android.os.Bundle;
+import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
+import android.window.OnBackInvokedCallback;
+import android.window.OnBackInvokedDispatcher;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import androidx.appcompat.app.AlertDialog;
-import androidx.fragment.app.FragmentManager;
import com.android.internal.app.LocaleStore;
import com.android.settings.R;
@@ -53,6 +57,12 @@
static final String ARG_SHOW_DIALOG = "arg_show_dialog";
private boolean mShouldKeepDialog;
+ private AlertDialog mAlertDialog;
+ private OnBackInvokedDispatcher mBackDispatcher;
+
+ private OnBackInvokedCallback mBackCallback = () -> {
+ Log.d(TAG, "Do not back to previous page if the dialog is displaying.");
+ };
public static LocaleDialogFragment newInstance() {
return new LocaleDialogFragment();
@@ -108,9 +118,15 @@
if (!dialogContent.mNegativeButton.isEmpty()) {
builder.setNegativeButton(dialogContent.mNegativeButton, controller);
}
- AlertDialog alertDialog = builder.create();
- alertDialog.setCanceledOnTouchOutside(false);
- return alertDialog;
+ mAlertDialog = builder.create();
+ getOnBackInvokedDispatcher().registerOnBackInvokedCallback(PRIORITY_DEFAULT, mBackCallback);
+ mAlertDialog.setCanceledOnTouchOutside(false);
+ mAlertDialog.setOnDismissListener(dialogInterface -> {
+ mAlertDialog.getOnBackInvokedDispatcher().unregisterOnBackInvokedCallback(
+ mBackCallback);
+ });
+
+ return mAlertDialog;
}
private static void setDialogTitle(View root, String content) {
@@ -130,6 +146,25 @@
}
@VisibleForTesting
+ public OnBackInvokedCallback getBackInvokedCallback() {
+ return mBackCallback;
+ }
+
+ @VisibleForTesting
+ public void setBackDispatcher(OnBackInvokedDispatcher dispatcher) {
+ mBackDispatcher = dispatcher;
+ }
+
+ @VisibleForTesting
+ public OnBackInvokedDispatcher getOnBackInvokedDispatcher() {
+ if (mBackDispatcher != null) {
+ return mBackDispatcher;
+ } else {
+ return mAlertDialog.getOnBackInvokedDispatcher();
+ }
+ }
+
+ @VisibleForTesting
LocaleDialogController getLocaleDialogController(Context context,
LocaleDialogFragment dialogFragment, LocaleListEditor parentFragment) {
return new LocaleDialogController(context, dialogFragment, parentFragment);
@@ -155,24 +190,20 @@
mParent = parentFragment;
}
- LocaleDialogController(@NonNull LocaleDialogFragment dialogFragment,
- LocaleListEditor parent) {
- this(dialogFragment.getContext(), dialogFragment, parent);
- }
-
@Override
public void onClick(DialogInterface dialog, int which) {
if (mDialogType == DIALOG_CONFIRM_SYSTEM_DEFAULT) {
int result = Activity.RESULT_CANCELED;
+ boolean changed = false;
if (which == DialogInterface.BUTTON_POSITIVE) {
result = Activity.RESULT_OK;
+ changed = true;
}
Intent intent = new Intent();
Bundle bundle = new Bundle();
bundle.putInt(ARG_DIALOG_TYPE, DIALOG_CONFIRM_SYSTEM_DEFAULT);
intent.putExtras(bundle);
mParent.onActivityResult(DIALOG_CONFIRM_SYSTEM_DEFAULT, result, intent);
- mMetricsFeatureProvider.action(mContext, SettingsEnums.ACTION_CHANGE_LANGUAGE);
}
mShouldKeepDialog = false;
}
diff --git a/src/com/android/settings/localepicker/LocaleDragAndDropAdapter.java b/src/com/android/settings/localepicker/LocaleDragAndDropAdapter.java
index edd3026..bfe0749 100644
--- a/src/com/android/settings/localepicker/LocaleDragAndDropAdapter.java
+++ b/src/com/android/settings/localepicker/LocaleDragAndDropAdapter.java
@@ -30,6 +30,7 @@
import android.widget.CheckBox;
import android.widget.CompoundButton;
+import androidx.annotation.VisibleForTesting;
import androidx.core.view.MotionEventCompat;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.RecyclerView;
@@ -50,7 +51,6 @@
private static final String TAG = "LocaleDragAndDropAdapter";
private static final String CFGKEY_SELECTED_LOCALES = "selectedLocales";
private static final String CFGKEY_DRAG_LOCALE = "dragLocales";
- private static final String CFGKEY_DRAG_LOCALES_TO_POSITION = "dragLocales_end";
private final Context mContext;
private final ItemTouchHelper mItemTouchHelper;
@@ -58,7 +58,6 @@
private List<LocaleStore.LocaleInfo> mFeedItemList;
private List<LocaleStore.LocaleInfo> mCacheItemList;
private RecyclerView mParentView = null;
- private LocaleListEditor mParent;
private boolean mRemoveMode = false;
private boolean mDragEnabled = true;
private NumberFormat mNumberFormatter = NumberFormat.getNumberInstance();
@@ -91,7 +90,6 @@
LocaleDragAndDropAdapter(LocaleListEditor parent, List<LocaleStore.LocaleInfo> feedItemList) {
mFeedItemList = feedItemList;
- mParent = parent;
mCacheItemList = new ArrayList<>(feedItemList);
mContext = parent.getContext();
@@ -176,17 +174,33 @@
// clear listener before setChecked() in case another item already bind to
// current ViewHolder and checked event is triggered on stale listener mistakenly.
checkbox.setOnCheckedChangeListener(null);
- checkbox.setChecked(mRemoveMode ? feedItem.getChecked() : false);
+ boolean isChecked = mRemoveMode ? feedItem.getChecked() : false;
+ checkbox.setChecked(isChecked);
+ setCheckBoxDescription(dragCell, checkbox, isChecked);
+
checkbox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
LocaleStore.LocaleInfo feedItem =
(LocaleStore.LocaleInfo) dragCell.getTag();
feedItem.setChecked(isChecked);
+ setCheckBoxDescription(dragCell, checkbox, isChecked);
}
});
}
+ @VisibleForTesting
+ protected void setCheckBoxDescription(LocaleDragCell dragCell, CheckBox checkbox,
+ boolean isChecked) {
+ CharSequence checkedStatus = mContext.getText(
+ isChecked ? com.android.internal.R.string.checked
+ : com.android.internal.R.string.not_checked);
+ // Talkback
+ dragCell.setStateDescription(checkedStatus);
+ // Select to Speak
+ checkbox.setContentDescription(checkedStatus);
+ }
+
@Override
public int getItemCount() {
int itemCount = (null != mFeedItemList ? mFeedItemList.size() : 0);
@@ -210,6 +224,7 @@
Log.e(TAG, String.format(Locale.US,
"Negative position in onItemMove %d -> %d", fromPosition, toPosition));
}
+
notifyItemChanged(fromPosition); // to update the numbers
notifyItemChanged(toPosition);
notifyItemMoved(fromPosition, toPosition);
@@ -244,8 +259,10 @@
void removeChecked() {
int itemCount = mFeedItemList.size();
+ LocaleStore.LocaleInfo localeInfo;
for (int i = itemCount - 1; i >= 0; i--) {
- if (mFeedItemList.get(i).getChecked()) {
+ localeInfo = mFeedItemList.get(i);
+ if (localeInfo.getChecked()) {
mFeedItemList.remove(i);
}
}
@@ -381,10 +398,13 @@
// drag locale's original position to the top.
mDragLocale = (LocaleStore.LocaleInfo) savedInstanceState.getSerializable(
CFGKEY_DRAG_LOCALE);
- mFeedItemList.removeIf(
- localeInfo -> TextUtils.equals(localeInfo.getId(), mDragLocale.getId()));
- mFeedItemList.add(0, mDragLocale);
- notifyItemRangeChanged(0, mFeedItemList.size());
+ if (mDragLocale != null) {
+ mFeedItemList.removeIf(
+ localeInfo -> TextUtils.equals(localeInfo.getId(),
+ mDragLocale.getId()));
+ mFeedItemList.add(0, mDragLocale);
+ notifyItemRangeChanged(0, mFeedItemList.size());
+ }
}
}
}
diff --git a/src/com/android/settings/localepicker/LocaleHelperPreferenceController.java b/src/com/android/settings/localepicker/LocaleHelperPreferenceController.java
index 1227683..a639c9d 100644
--- a/src/com/android/settings/localepicker/LocaleHelperPreferenceController.java
+++ b/src/com/android/settings/localepicker/LocaleHelperPreferenceController.java
@@ -16,7 +16,6 @@
package com.android.settings.localepicker;
-import android.app.settings.SettingsEnums;
import android.content.Context;
import android.content.Intent;
import android.util.Log;
@@ -25,10 +24,8 @@
import androidx.preference.PreferenceScreen;
import com.android.settings.R;
-import com.android.settings.overlay.FeatureFactory;
import com.android.settingslib.HelpUtils;
import com.android.settingslib.core.AbstractPreferenceController;
-import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
import com.android.settingslib.widget.FooterPreference;
/**
@@ -39,11 +36,8 @@
private static final String KEY_FOOTER_LANGUAGE_PICKER = "footer_languages_picker";
- private final MetricsFeatureProvider mMetricsFeatureProvider;
-
public LocaleHelperPreferenceController(Context context) {
super(context);
- mMetricsFeatureProvider = FeatureFactory.getFactory(context).getMetricsFeatureProvider();
}
@Override
@@ -78,7 +72,6 @@
mContext.getString(R.string.link_locale_picker_footer_learn_more),
mContext.getClass().getName());
if (intent != null) {
- mMetricsFeatureProvider.action(mContext, SettingsEnums.ACTION_LANGUAGES_LEARN_MORE);
mContext.startActivity(intent);
} else {
Log.w(TAG, "HelpIntent is null");
diff --git a/src/com/android/settings/localepicker/LocaleListEditor.java b/src/com/android/settings/localepicker/LocaleListEditor.java
index 7ec08f7..a9ea42c 100644
--- a/src/com/android/settings/localepicker/LocaleListEditor.java
+++ b/src/com/android/settings/localepicker/LocaleListEditor.java
@@ -104,7 +104,6 @@
addPreferencesFromResource(R.xml.languages);
final Activity activity = getActivity();
- activity.setTitle(R.string.language_picker_title);
mLocaleHelperPreferenceController = new LocaleHelperPreferenceController(activity);
final PreferenceScreen screen = getPreferenceScreen();
mLocalePickerPreference = screen.findPreference(KEY_LANGUAGES_PICKER);
@@ -200,8 +199,8 @@
localeInfo = (LocaleStore.LocaleInfo) data.getSerializableExtra(INTENT_LOCALE_KEY);
String preferencesTags = Settings.System.getString(
getContext().getContentResolver(), Settings.System.LOCALE_PREFERENCES);
-
- mAdapter.addLocale(mayAppendUnicodeTags(localeInfo, preferencesTags));
+ localeInfo = mayAppendUnicodeTags(localeInfo, preferencesTags);
+ mAdapter.addLocale(localeInfo);
updateVisibilityOfRemoveMenu();
} else if (requestCode == DIALOG_CONFIRM_SYSTEM_DEFAULT) {
localeInfo = mAdapter.getFeedItemList().get(0);
@@ -215,6 +214,9 @@
LocaleDialogFragment localeDialogFragment = LocaleDialogFragment.newInstance();
localeDialogFragment.setArguments(args);
localeDialogFragment.show(mFragmentManager, TAG_DIALOG_NOT_AVAILABLE);
+ mMetricsFeatureProvider.action(getContext(),
+ SettingsEnums.ACTION_NOT_SUPPORTED_SYSTEM_LANGUAGE,
+ localeInfo.getLocale().toLanguageTag());
}
} else {
mAdapter.notifyListChanged(localeInfo);
@@ -318,7 +320,13 @@
// to remove.
mRemoveMode = false;
mShowingRemoveDialog = false;
+ LocaleStore.LocaleInfo firstLocale =
+ mAdapter.getFeedItemList().get(0);
mAdapter.removeChecked();
+ boolean isFirstRemoved =
+ firstLocale != mAdapter.getFeedItemList().get(0);
+ showConfirmDialog(isFirstRemoved, isFirstRemoved ? firstLocale
+ : mAdapter.getFeedItemList().get(0));
setRemoveMode(false);
}
})
@@ -358,12 +366,12 @@
final LocaleLinearLayoutManager llm = new LocaleLinearLayoutManager(getContext(), mAdapter);
llm.setAutoMeasureEnabled(true);
list.setLayoutManager(llm);
-
list.setHasFixedSize(true);
list.setNestedScrollingEnabled(false);
mAdapter.setRecyclerView(list);
list.setAdapter(mAdapter);
list.setOnTouchListener(this);
+ list.requestFocus();
mAddLanguage = layout.findViewById(R.id.add_language);
mAddLanguage.setOnClickListener(new View.OnClickListener() {
@@ -384,22 +392,27 @@
public boolean onTouch(View v, MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_UP
|| event.getAction() == MotionEvent.ACTION_CANCEL) {
- LocaleStore.LocaleInfo localeInfo = mAdapter.getFeedItemList().get(0);
- if (!localeInfo.getLocale().equals(LocalePicker.getLocales().get(0))) {
- final LocaleDialogFragment localeDialogFragment =
- LocaleDialogFragment.newInstance();
- Bundle args = new Bundle();
- args.putInt(LocaleDialogFragment.ARG_DIALOG_TYPE, DIALOG_CONFIRM_SYSTEM_DEFAULT);
- args.putSerializable(LocaleDialogFragment.ARG_TARGET_LOCALE, localeInfo);
- localeDialogFragment.setArguments(args);
- localeDialogFragment.show(mFragmentManager, TAG_DIALOG_CONFIRM_SYSTEM_DEFAULT);
- } else {
- mAdapter.doTheUpdate();
- }
+ showConfirmDialog(false, mAdapter.getFeedItemList().get(0));
}
return false;
}
+ private void showConfirmDialog(boolean isFirstRemoved, LocaleStore.LocaleInfo localeInfo) {
+ Locale currentSystemLocale = LocalePicker.getLocales().get(0);
+ if (!localeInfo.getLocale().equals(currentSystemLocale)) {
+ final LocaleDialogFragment localeDialogFragment =
+ LocaleDialogFragment.newInstance();
+ Bundle args = new Bundle();
+ args.putInt(LocaleDialogFragment.ARG_DIALOG_TYPE, DIALOG_CONFIRM_SYSTEM_DEFAULT);
+ args.putSerializable(LocaleDialogFragment.ARG_TARGET_LOCALE,
+ isFirstRemoved ? LocaleStore.getLocaleInfo(currentSystemLocale) : localeInfo);
+ localeDialogFragment.setArguments(args);
+ localeDialogFragment.show(mFragmentManager, TAG_DIALOG_CONFIRM_SYSTEM_DEFAULT);
+ } else {
+ mAdapter.doTheUpdate();
+ }
+ }
+
// Hide the "Remove" menu if there is only one locale in the list, show it otherwise
// This is called when the menu is first created, and then one add / remove locale
private void updateVisibilityOfRemoveMenu() {
diff --git a/src/com/android/settings/localepicker/LocaleRecyclerView.java b/src/com/android/settings/localepicker/LocaleRecyclerView.java
deleted file mode 100644
index 4a5f28b..0000000
--- a/src/com/android/settings/localepicker/LocaleRecyclerView.java
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
- * Copyright (C) 2016 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.settings.localepicker;
-
-import android.content.Context;
-import android.util.AttributeSet;
-import android.view.MotionEvent;
-
-import androidx.recyclerview.widget.RecyclerView;
-
-class LocaleRecyclerView extends RecyclerView {
- public LocaleRecyclerView(Context context) {
- super(context);
- }
-
- public LocaleRecyclerView(Context context, AttributeSet attrs) {
- super(context, attrs);
- }
-
- public LocaleRecyclerView(Context context, AttributeSet attrs, int defStyle) {
- super(context, attrs, defStyle);
- }
-}
diff --git a/src/com/android/settings/media/MediaOutputIndicatorWorker.java b/src/com/android/settings/media/MediaOutputIndicatorWorker.java
index bf1e06e..159efa0 100644
--- a/src/com/android/settings/media/MediaOutputIndicatorWorker.java
+++ b/src/com/android/settings/media/MediaOutputIndicatorWorker.java
@@ -167,12 +167,20 @@
/** Check if this device supports LE Audio Broadcast feature */
public boolean isBroadcastSupported() {
+ if (mLocalBluetoothManager == null) {
+ Log.e(TAG, "isBroadcastSupported: Bluetooth is not supported on this device");
+ return false;
+ }
LocalBluetoothLeBroadcast broadcast =
mLocalBluetoothManager.getProfileManager().getLeAudioBroadcastProfile();
return broadcast != null ? true : false;
}
public boolean isDeviceBroadcasting() {
+ if (mLocalBluetoothManager == null) {
+ Log.e(TAG, "isDeviceBroadcasting: Bluetooth is not supported on this device");
+ return false;
+ }
LocalBluetoothLeBroadcast broadcast =
mLocalBluetoothManager.getProfileManager().getLeAudioBroadcastProfile();
if (broadcast == null) {
diff --git a/src/com/android/settings/network/EraseEuiccDataDialogFragment.java b/src/com/android/settings/network/EraseEuiccDataDialogFragment.java
index 32903bd..0200e52 100644
--- a/src/com/android/settings/network/EraseEuiccDataDialogFragment.java
+++ b/src/com/android/settings/network/EraseEuiccDataDialogFragment.java
@@ -23,7 +23,6 @@
import android.content.DialogInterface;
import android.os.AsyncTask;
import android.os.Bundle;
-import android.os.RecoverySystem;
import android.util.Log;
import androidx.annotation.NonNull;
@@ -62,7 +61,7 @@
return new AlertDialog.Builder(getActivity())
.setTitle(R.string.reset_esim_title)
.setMessage(R.string.reset_esim_desc)
- .setPositiveButton(R.string.erase_euicc_data_button, this)
+ .setPositiveButton(R.string.erase_sim_confirm_button, this)
.setNegativeButton(R.string.cancel, null)
.setOnDismissListener(this)
.create();
diff --git a/src/com/android/settings/network/SubscriptionUtil.java b/src/com/android/settings/network/SubscriptionUtil.java
index 9d953bf..9974ba2 100644
--- a/src/com/android/settings/network/SubscriptionUtil.java
+++ b/src/com/android/settings/network/SubscriptionUtil.java
@@ -18,11 +18,11 @@
import static android.telephony.SubscriptionManager.INVALID_SIM_SLOT_INDEX;
import static android.telephony.UiccSlotInfo.CARD_STATE_INFO_PRESENT;
-
import static com.android.internal.util.CollectionUtils.emptyIfNull;
import android.annotation.Nullable;
import android.content.Context;
+import android.content.SharedPreferences;
import android.os.ParcelUuid;
import android.provider.Settings;
import android.telephony.PhoneNumberUtils;
@@ -55,12 +55,21 @@
import java.util.Set;
import java.util.function.Function;
import java.util.function.Supplier;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class SubscriptionUtil {
private static final String TAG = "SubscriptionUtil";
private static final String PROFILE_GENERIC_DISPLAY_NAME = "CARD";
+ @VisibleForTesting
+ static final String SUB_ID = "sub_id";
+ @VisibleForTesting
+ static final String KEY_UNIQUE_SUBSCRIPTION_DISPLAYNAME = "unique_subscription_displayName";
+ private static final String REGEX_DISPLAY_NAME_PREFIXES = "^";
+ private static final String REGEX_DISPLAY_NAME_SUFFIXES = "\\s[0-9]+";
+
private static List<SubscriptionInfo> sAvailableResultsForTesting;
private static List<SubscriptionInfo> sActiveResultsForTesting;
@@ -265,20 +274,21 @@
// Map of SubscriptionId to DisplayName
final Supplier<Stream<DisplayInfo>> originalInfos =
() -> getAvailableSubscriptions(context)
- .stream()
- .filter(i -> {
- // Filter out null values.
- return (i != null && i.getDisplayName() != null);
- })
- .map(i -> {
- DisplayInfo info = new DisplayInfo();
- info.subscriptionInfo = i;
- String displayName = i.getDisplayName().toString();
- info.originalName = TextUtils.equals(displayName, PROFILE_GENERIC_DISPLAY_NAME)
- ? context.getResources().getString(R.string.sim_card)
- : displayName.trim();
- return info;
- });
+ .stream()
+ .filter(i -> {
+ // Filter out null values.
+ return (i != null && i.getDisplayName() != null);
+ })
+ .map(i -> {
+ DisplayInfo info = new DisplayInfo();
+ info.subscriptionInfo = i;
+ String displayName = i.getDisplayName().toString();
+ info.originalName =
+ TextUtils.equals(displayName, PROFILE_GENERIC_DISPLAY_NAME)
+ ? context.getResources().getString(R.string.sim_card)
+ : displayName.trim();
+ return info;
+ });
// TODO(goldmanj) consider using a map of DisplayName to SubscriptionInfos.
// A Unique set of display names
@@ -292,6 +302,19 @@
// If a display name is duplicate, append the final 4 digits of the phone number.
// Creates a mapping of Subscription id to original display name + phone number display name
final Supplier<Stream<DisplayInfo>> uniqueInfos = () -> originalInfos.get().map(info -> {
+ int infoSubId = info.subscriptionInfo.getSubscriptionId();
+ String cachedDisplayName = getDisplayNameFromSharedPreference(
+ context, infoSubId);
+ if (isValidCachedDisplayName(cachedDisplayName, info.originalName.toString())) {
+ Log.d(TAG, "use cached display name : for subId : " + infoSubId
+ + "cached display name : " + cachedDisplayName);
+ info.uniqueName = cachedDisplayName;
+ return info;
+ } else {
+ Log.d(TAG, "remove cached display name : " + infoSubId);
+ removeItemFromDisplayNameSharedPreference(context, infoSubId);
+ }
+
if (duplicateOriginalNames.contains(info.originalName)) {
// This may return null, if the user cannot view the phone number itself.
final String phoneNumber = getBidiFormattedPhoneNumber(context,
@@ -299,15 +322,16 @@
String lastFourDigits = "";
if (phoneNumber != null) {
lastFourDigits = (phoneNumber.length() > 4)
- ? phoneNumber.substring(phoneNumber.length() - 4) : phoneNumber;
+ ? phoneNumber.substring(phoneNumber.length() - 4) : phoneNumber;
}
-
if (TextUtils.isEmpty(lastFourDigits)) {
info.uniqueName = info.originalName;
} else {
info.uniqueName = info.originalName + " " + lastFourDigits;
+ Log.d(TAG, "Cache display name [" + info.uniqueName + "] for sub id "
+ + infoSubId);
+ saveDisplayNameToSharedPreference(context, infoSubId, info.uniqueName);
}
-
} else {
info.uniqueName = info.originalName;
}
@@ -371,6 +395,44 @@
return getUniqueSubscriptionDisplayName(info.getSubscriptionId(), context);
}
+
+ private static SharedPreferences getDisplayNameSharedPreferences(Context context) {
+ return context.getSharedPreferences(
+ KEY_UNIQUE_SUBSCRIPTION_DISPLAYNAME, Context.MODE_PRIVATE);
+ }
+
+ private static SharedPreferences.Editor getDisplayNameSharedPreferenceEditor(Context context) {
+ return getDisplayNameSharedPreferences(context).edit();
+ }
+
+ private static void saveDisplayNameToSharedPreference(
+ Context context, int subId, CharSequence displayName) {
+ getDisplayNameSharedPreferenceEditor(context)
+ .putString(SUB_ID + subId, String.valueOf(displayName))
+ .apply();
+ }
+
+ private static void removeItemFromDisplayNameSharedPreference(Context context, int subId) {
+ getDisplayNameSharedPreferenceEditor(context)
+ .remove(SUB_ID + subId)
+ .commit();
+ }
+
+ private static String getDisplayNameFromSharedPreference(Context context, int subid) {
+ return getDisplayNameSharedPreferences(context).getString(SUB_ID + subid, "");
+ }
+
+ @VisibleForTesting
+ static boolean isValidCachedDisplayName(String cachedDisplayName, String originalName) {
+ if (TextUtils.isEmpty(cachedDisplayName) || TextUtils.isEmpty(originalName)) {
+ return false;
+ }
+ String regex = REGEX_DISPLAY_NAME_PREFIXES + originalName + REGEX_DISPLAY_NAME_SUFFIXES;
+ Pattern pattern = Pattern.compile(regex);
+ Matcher matcher = pattern.matcher(cachedDisplayName);
+ return matcher.matches();
+ }
+
public static String getDisplayName(SubscriptionInfo info) {
final CharSequence name = info.getDisplayName();
if (name != null) {
diff --git a/src/com/android/settings/network/UiccSlotUtil.java b/src/com/android/settings/network/UiccSlotUtil.java
index 95a0e4d..49a1a85 100644
--- a/src/com/android/settings/network/UiccSlotUtil.java
+++ b/src/com/android/settings/network/UiccSlotUtil.java
@@ -17,7 +17,10 @@
package com.android.settings.network;
import android.annotation.IntDef;
+import android.content.BroadcastReceiver;
import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
import android.provider.Settings;
import android.telephony.SubscriptionInfo;
import android.telephony.SubscriptionManager;
@@ -54,6 +57,28 @@
public static final int INVALID_PHYSICAL_SLOT_ID = -1;
public static final int INVALID_PORT_ID = -1;
+ @VisibleForTesting
+ static class SimSlotChangeReceiver extends BroadcastReceiver{
+ private final CountDownLatch mLatch;
+ SimSlotChangeReceiver(CountDownLatch latch) {
+ mLatch = latch;
+ }
+
+ public void registerOn(Context context) {
+ context.registerReceiver(this,
+ new IntentFilter(TelephonyManager.ACTION_SIM_SLOT_STATUS_CHANGED),
+ Context.RECEIVER_EXPORTED/*UNAUDITED*/);
+ }
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ Log.i(TAG, "Action: " + intent.getAction());
+ if (TelephonyManager.ACTION_SIM_SLOT_STATUS_CHANGED.equals(intent.getAction())) {
+ mLatch.countDown();
+ }
+ }
+ }
+
/**
* Mode for switching to eSIM slot which decides whether there is cleanup process, e.g.
* disabling test profile, after eSIM slot is activated and whether we will wait it finished.
@@ -229,19 +254,27 @@
&& uiccSlotMapping.getPortIndex() == port);
}
- private static void performSwitchToSlot(TelephonyManager telMgr,
+ @VisibleForTesting
+ static void performSwitchToSlot(TelephonyManager telMgr,
Collection<UiccSlotMapping> uiccSlotMappings, Context context)
throws UiccSlotsException {
- CarrierConfigChangedReceiver receiver = null;
+ BroadcastReceiver receiver = null;
long waitingTimeMillis =
Settings.Global.getLong(
context.getContentResolver(),
Settings.Global.EUICC_SWITCH_SLOT_TIMEOUT_MILLIS,
DEFAULT_WAIT_AFTER_SWITCH_TIMEOUT_MILLIS);
+ Log.d(TAG, "Set waitingTime as " + waitingTimeMillis);
+
try {
CountDownLatch latch = new CountDownLatch(1);
- receiver = new CarrierConfigChangedReceiver(latch);
- receiver.registerOn(context);
+ if (isMultipleEnabledProfilesSupported(telMgr)) {
+ receiver = new SimSlotChangeReceiver(latch);
+ ((SimSlotChangeReceiver) receiver).registerOn(context);
+ } else {
+ receiver = new CarrierConfigChangedReceiver(latch);
+ ((CarrierConfigChangedReceiver) receiver).registerOn(context);
+ }
telMgr.setSimSlotMapping(uiccSlotMappings);
latch.await(waitingTimeMillis, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
@@ -435,4 +468,14 @@
Log.i(TAG, "isRemovableSimEnabled: " + isRemovableSimEnabled);
return isRemovableSimEnabled;
}
+
+ private static boolean isMultipleEnabledProfilesSupported(TelephonyManager telMgr) {
+ List<UiccCardInfo> cardInfos = telMgr.getUiccCardsInfo();
+ if (cardInfos == null) {
+ Log.w(TAG, "UICC cards info list is empty.");
+ return false;
+ }
+ return cardInfos.stream().anyMatch(
+ cardInfo -> cardInfo.isMultipleEnabledProfilesSupported());
+ }
}
diff --git a/src/com/android/settings/network/telephony/AbstractMobileNetworkSettings.java b/src/com/android/settings/network/telephony/AbstractMobileNetworkSettings.java
index 245ac83..7addb59 100644
--- a/src/com/android/settings/network/telephony/AbstractMobileNetworkSettings.java
+++ b/src/com/android/settings/network/telephony/AbstractMobileNetworkSettings.java
@@ -18,7 +18,6 @@
import android.os.SystemClock;
import android.text.TextUtils;
-import android.util.Log;
import androidx.preference.Preference;
import androidx.preference.PreferenceScreen;
@@ -66,8 +65,7 @@
TelephonyStatusControlSession setTelephonyAvailabilityStatus(
Collection<AbstractPreferenceController> listOfPrefControllers) {
- return (new TelephonyStatusControlSession.Builder(listOfPrefControllers))
- .build();
+ return new TelephonyStatusControlSession(listOfPrefControllers, getLifecycle());
}
@Override
diff --git a/src/com/android/settings/network/telephony/DataUsagePreferenceController.java b/src/com/android/settings/network/telephony/DataUsagePreferenceController.java
deleted file mode 100644
index a536c1d..0000000
--- a/src/com/android/settings/network/telephony/DataUsagePreferenceController.java
+++ /dev/null
@@ -1,151 +0,0 @@
-/*
- * Copyright (C) 2018 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.settings.network.telephony;
-
-import android.content.Context;
-import android.content.Intent;
-import android.net.NetworkTemplate;
-import android.provider.Settings;
-import android.telephony.SubscriptionManager;
-import android.text.TextUtils;
-import android.util.Log;
-
-import androidx.preference.Preference;
-
-import com.android.internal.annotations.VisibleForTesting;
-import com.android.settings.R;
-import com.android.settings.datausage.DataUsageUtils;
-import com.android.settings.datausage.lib.DataUsageLib;
-import com.android.settingslib.net.DataUsageController;
-import com.android.settingslib.utils.ThreadUtils;
-
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.Future;
-import java.util.concurrent.atomic.AtomicReference;
-
-/**
- * Preference controller for "Data usage"
- */
-public class DataUsagePreferenceController extends TelephonyBasePreferenceController {
-
- private static final String LOG_TAG = "DataUsagePreferCtrl";
-
- private Future<NetworkTemplate> mTemplateFuture;
- private AtomicReference<NetworkTemplate> mTemplate;
- private Future<Long> mHistoricalUsageLevel;
-
- public DataUsagePreferenceController(Context context, String key) {
- super(context, key);
- mTemplate = new AtomicReference<NetworkTemplate>();
- }
-
- @Override
- public int getAvailabilityStatus(int subId) {
- return (SubscriptionManager.isValidSubscriptionId(subId))
- && DataUsageUtils.hasMobileData(mContext)
- ? AVAILABLE
- : AVAILABLE_UNSEARCHABLE;
- }
-
- @Override
- public boolean handlePreferenceTreeClick(Preference preference) {
- if (!TextUtils.equals(preference.getKey(), getPreferenceKey())) {
- return false;
- }
- final Intent intent = new Intent(Settings.ACTION_MOBILE_DATA_USAGE);
- intent.putExtra(Settings.EXTRA_NETWORK_TEMPLATE, getNetworkTemplate());
- intent.putExtra(Settings.EXTRA_SUB_ID, mSubId);
-
- mContext.startActivity(intent);
- return true;
- }
-
- @Override
- public void updateState(Preference preference) {
- super.updateState(preference);
- if (!SubscriptionManager.isValidSubscriptionId(mSubId)) {
- preference.setEnabled(false);
- return;
- }
- final CharSequence summary = getDataUsageSummary(mContext, mSubId);
- if (summary == null) {
- preference.setEnabled(false);
- } else {
- preference.setEnabled(true);
- preference.setSummary(summary);
- }
- }
-
- public void init(int subId) {
- mSubId = subId;
- mTemplate.set(null);
- mTemplateFuture = ThreadUtils.postOnBackgroundThread(()
- -> fetchMobileTemplate(mContext, mSubId));
- }
-
- private NetworkTemplate fetchMobileTemplate(Context context, int subId) {
- if (!SubscriptionManager.isValidSubscriptionId(subId)) {
- return null;
- }
- return DataUsageLib.getMobileTemplate(context, subId);
- }
-
- private NetworkTemplate getNetworkTemplate() {
- if (!SubscriptionManager.isValidSubscriptionId(mSubId)) {
- return null;
- }
- NetworkTemplate template = mTemplate.get();
- if (template != null) {
- return template;
- }
- try {
- template = mTemplateFuture.get();
- mTemplate.set(template);
- } catch (ExecutionException | InterruptedException | NullPointerException exception) {
- Log.e(LOG_TAG, "Fail to get data usage template", exception);
- }
- return template;
- }
-
- @VisibleForTesting
- DataUsageController.DataUsageInfo getDataUsageInfo(DataUsageController controller) {
- return controller.getDataUsageInfo(getNetworkTemplate());
- }
-
- private CharSequence getDataUsageSummary(Context context, int subId) {
- final DataUsageController controller = new DataUsageController(context);
- controller.setSubscriptionId(subId);
-
- mHistoricalUsageLevel = ThreadUtils.postOnBackgroundThread(() ->
- controller.getHistoricalUsageLevel(getNetworkTemplate()));
-
- final DataUsageController.DataUsageInfo usageInfo = getDataUsageInfo(controller);
-
- long usageLevel = usageInfo.usageLevel;
- if (usageLevel <= 0L) {
- try {
- usageLevel = mHistoricalUsageLevel.get();
- } catch (Exception exception) {
- }
- }
- if (usageLevel <= 0L) {
- return null;
- }
- return context.getString(R.string.data_usage_template,
- DataUsageUtils.formatDataUsage(context, usageLevel), usageInfo.period);
- }
-}
diff --git a/src/com/android/settings/network/telephony/DataUsagePreferenceController.kt b/src/com/android/settings/network/telephony/DataUsagePreferenceController.kt
new file mode 100644
index 0000000..88da325
--- /dev/null
+++ b/src/com/android/settings/network/telephony/DataUsagePreferenceController.kt
@@ -0,0 +1,127 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.network.telephony
+
+import android.content.Context
+import android.content.Intent
+import android.net.NetworkTemplate
+import android.provider.Settings
+import android.telephony.SubscriptionManager
+import androidx.annotation.VisibleForTesting
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
+import androidx.preference.Preference
+import androidx.preference.PreferenceScreen
+import com.android.settings.R
+import com.android.settings.datausage.DataUsageUtils
+import com.android.settings.datausage.lib.DataUsageLib
+import com.android.settingslib.net.DataUsageController
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+
+/**
+ * Preference controller for "Data usage"
+ */
+class DataUsagePreferenceController(context: Context, key: String) :
+ TelephonyBasePreferenceController(context, key) {
+
+ private lateinit var preference: Preference
+ private var networkTemplate: NetworkTemplate? = null
+
+ @VisibleForTesting
+ var dataUsageControllerFactory: (Context) -> DataUsageController = { DataUsageController(it) }
+
+ fun init(subId: Int) {
+ mSubId = subId
+ }
+
+ override fun getAvailabilityStatus(subId: Int): Int = when {
+ SubscriptionManager.isValidSubscriptionId(subId) &&
+ DataUsageUtils.hasMobileData(mContext) -> AVAILABLE
+
+ else -> AVAILABLE_UNSEARCHABLE
+ }
+
+ override fun displayPreference(screen: PreferenceScreen) {
+ super.displayPreference(screen)
+ preference = screen.findPreference(preferenceKey)!!
+ }
+
+ fun whenViewCreated(viewLifecycleOwner: LifecycleOwner) {
+ viewLifecycleOwner.lifecycleScope.launch {
+ viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
+ update()
+ }
+ }
+ }
+
+ override fun handlePreferenceTreeClick(preference: Preference): Boolean {
+ if (preference.key != preferenceKey || networkTemplate == null) return false
+ val intent = Intent(Settings.ACTION_MOBILE_DATA_USAGE).apply {
+ putExtra(Settings.EXTRA_NETWORK_TEMPLATE, networkTemplate)
+ putExtra(Settings.EXTRA_SUB_ID, mSubId)
+ }
+ mContext.startActivity(intent)
+ return true
+ }
+
+ private suspend fun update() {
+ val summary = withContext(Dispatchers.Default) {
+ networkTemplate = getNetworkTemplate()
+ getDataUsageSummary()
+ }
+ if (summary == null) {
+ preference.isEnabled = false
+ } else {
+ preference.isEnabled = true
+ preference.summary = summary
+ }
+ }
+
+ private fun getNetworkTemplate(): NetworkTemplate? = when {
+ SubscriptionManager.isValidSubscriptionId(mSubId) -> {
+ DataUsageLib.getMobileTemplate(mContext, mSubId)
+ }
+
+ else -> null
+ }
+
+ private fun getDataUsageSummary(): String? {
+ val networkTemplate = networkTemplate ?: return null
+ val controller = dataUsageControllerFactory(mContext).apply {
+ setSubscriptionId(mSubId)
+ }
+ val usageInfo = controller.getDataUsageInfo(networkTemplate)
+ if (usageInfo != null && usageInfo.usageLevel > 0) {
+ return mContext.getString(
+ R.string.data_usage_template,
+ DataUsageUtils.formatDataUsage(mContext, usageInfo.usageLevel),
+ usageInfo.period,
+ )
+ }
+
+ return controller.getHistoricalUsageLevel(networkTemplate).takeIf { it > 0 }?.let {
+ mContext.getString(
+ R.string.data_used_template,
+ DataUsageUtils.formatDataUsage(mContext, it),
+ )
+ }
+ }
+}
diff --git a/src/com/android/settings/network/telephony/Enable2gPreferenceController.java b/src/com/android/settings/network/telephony/Enable2gPreferenceController.java
index 106aa02..03f3be4 100644
--- a/src/com/android/settings/network/telephony/Enable2gPreferenceController.java
+++ b/src/com/android/settings/network/telephony/Enable2gPreferenceController.java
@@ -119,20 +119,21 @@
String summary;
if (isDisabledByCarrier) {
summary = mContext.getString(R.string.enable_2g_summary_disabled_carrier,
- getCarrierName());
+ getSimCardName());
} else {
summary = mContext.getString(R.string.enable_2g_summary);
}
preference.setSummary(summary);
}
- private String getCarrierName() {
+ private String getSimCardName() {
SubscriptionInfo subInfo = SubscriptionUtil.getSubById(mSubscriptionManager, mSubId);
if (subInfo == null) {
return "";
}
- CharSequence carrierName = subInfo.getCarrierName();
- return TextUtils.isEmpty(carrierName) ? "" : carrierName.toString();
+ // It is the sim card name, and it should be the same name as the sim page.
+ CharSequence simCardName = subInfo.getDisplayName();
+ return TextUtils.isEmpty(simCardName) ? "" : simCardName.toString();
}
/**
diff --git a/src/com/android/settings/network/telephony/MmsMessagePreferenceController.java b/src/com/android/settings/network/telephony/MmsMessagePreferenceController.java
index 5908ecd..8f59f0e 100644
--- a/src/com/android/settings/network/telephony/MmsMessagePreferenceController.java
+++ b/src/com/android/settings/network/telephony/MmsMessagePreferenceController.java
@@ -23,6 +23,7 @@
import android.telephony.TelephonyManager;
import android.telephony.data.ApnSetting;
+import androidx.preference.Preference;
import androidx.preference.PreferenceScreen;
import com.android.settings.network.MobileDataContentObserver;
@@ -31,21 +32,20 @@
import com.android.settingslib.core.lifecycle.events.OnStop;
/**
- * Preference controller for "Mobile data"
+ * Preference controller for "MMS messages"
*/
public class MmsMessagePreferenceController extends TelephonyTogglePreferenceController implements
LifecycleObserver, OnStart, OnStop {
private TelephonyManager mTelephonyManager;
- private SubscriptionManager mSubscriptionManager;
private MobileDataContentObserver mMobileDataContentObserver;
private PreferenceScreen mScreen;
+ private Preference mPreference;
public MmsMessagePreferenceController(Context context, String key) {
super(context, key);
- mSubscriptionManager = context.getSystemService(SubscriptionManager.class);
mMobileDataContentObserver = new MobileDataContentObserver(
new Handler(Looper.getMainLooper()));
- mMobileDataContentObserver.setOnMobileDataChangedListener(()->refreshPreference());
+ mMobileDataContentObserver.setOnMobileDataChangedListener(() -> refreshPreference());
}
@Override
@@ -63,6 +63,7 @@
public void onStart() {
if (mSubId != SubscriptionManager.INVALID_SUBSCRIPTION_ID) {
mMobileDataContentObserver.register(mContext, mSubId);
+ updateState(mPreference);
}
}
@@ -77,6 +78,7 @@
public void displayPreference(PreferenceScreen screen) {
super.displayPreference(screen);
mScreen = screen;
+ mPreference = screen.findPreference(getPreferenceKey());
}
@@ -88,12 +90,15 @@
@Override
public boolean setChecked(boolean isChecked) {
+ if (mTelephonyManager == null) {
+ return false;
+ }
mTelephonyManager.setMobileDataPolicyEnabled(
TelephonyManager.MOBILE_DATA_POLICY_MMS_ALWAYS_ALLOWED, isChecked);
- return isChecked == mTelephonyManager.isMobileDataPolicyEnabled(
- TelephonyManager.MOBILE_DATA_POLICY_MMS_ALWAYS_ALLOWED);
+ return true;
}
+
@Override
public boolean isChecked() {
return mTelephonyManager != null && mTelephonyManager.isDataEnabledForApn(
diff --git a/src/com/android/settings/network/telephony/MobileNetworkSettings.java b/src/com/android/settings/network/telephony/MobileNetworkSettings.java
index 1d862f3..5b57ede 100644
--- a/src/com/android/settings/network/telephony/MobileNetworkSettings.java
+++ b/src/com/android/settings/network/telephony/MobileNetworkSettings.java
@@ -31,7 +31,10 @@
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
+import android.view.View;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.preference.Preference;
@@ -328,6 +331,12 @@
}
@Override
+ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ use(DataUsagePreferenceController.class).whenViewCreated(getViewLifecycleOwner());
+ }
+
+ @Override
public void onResume() {
super.onResume();
mMobileNetworkRepository.addRegister(this, this, mSubId);
diff --git a/src/com/android/settings/network/telephony/MobileNetworkSwitchController.java b/src/com/android/settings/network/telephony/MobileNetworkSwitchController.java
index 5014634..453941d 100644
--- a/src/com/android/settings/network/telephony/MobileNetworkSwitchController.java
+++ b/src/com/android/settings/network/telephony/MobileNetworkSwitchController.java
@@ -16,12 +16,16 @@
package com.android.settings.network.telephony;
+import static android.telephony.TelephonyManager.CALL_STATE_IDLE;
+
import static androidx.lifecycle.Lifecycle.Event.ON_PAUSE;
import static androidx.lifecycle.Lifecycle.Event.ON_RESUME;
import android.content.Context;
import android.telephony.SubscriptionInfo;
import android.telephony.SubscriptionManager;
+import android.telephony.TelephonyCallback;
+import android.telephony.TelephonyManager;
import androidx.lifecycle.LifecycleObserver;
import androidx.lifecycle.OnLifecycleEvent;
@@ -40,26 +44,40 @@
private int mSubId;
private SubscriptionsChangeListener mChangeListener;
private SubscriptionManager mSubscriptionManager;
+ private TelephonyManager mTelephonyManager;
+ private CallStateTelephonyCallback mCallStateCallback;
public MobileNetworkSwitchController(Context context, String preferenceKey) {
super(context, preferenceKey);
mSubId = SubscriptionManager.INVALID_SUBSCRIPTION_ID;
mSubscriptionManager = mContext.getSystemService(SubscriptionManager.class);
+ mTelephonyManager = mContext.getSystemService(TelephonyManager.class);
mChangeListener = new SubscriptionsChangeListener(context, this);
}
void init(int subId) {
mSubId = subId;
+ mTelephonyManager = mTelephonyManager.createForSubscriptionId(mSubId);
}
@OnLifecycleEvent(ON_RESUME)
public void onResume() {
mChangeListener.start();
+
+ if (mCallStateCallback == null) {
+ mCallStateCallback = new CallStateTelephonyCallback();
+ mTelephonyManager.registerTelephonyCallback(
+ mContext.getMainExecutor(), mCallStateCallback);
+ }
update();
}
@OnLifecycleEvent(ON_PAUSE)
public void onPause() {
+ if (mCallStateCallback != null) {
+ mTelephonyManager.unregisterTelephonyCallback(mCallStateCallback);
+ mCallStateCallback = null;
+ }
mChangeListener.stop();
}
@@ -118,4 +136,12 @@
public void onSubscriptionsChanged() {
update();
}
+
+ private class CallStateTelephonyCallback extends TelephonyCallback implements
+ TelephonyCallback.CallStateListener {
+ @Override
+ public void onCallStateChanged(int state) {
+ mSwitchBar.setSwitchBarEnabled(state == CALL_STATE_IDLE);
+ }
+ }
}
diff --git a/src/com/android/settings/network/telephony/NetworkSelectSettings.java b/src/com/android/settings/network/telephony/NetworkSelectSettings.java
index ebc3ea6..f20fe1f 100644
--- a/src/com/android/settings/network/telephony/NetworkSelectSettings.java
+++ b/src/com/android/settings/network/telephony/NetworkSelectSettings.java
@@ -236,37 +236,44 @@
@Override
public boolean onPreferenceTreeClick(Preference preference) {
- if (preference != mSelectedPreference) {
- stopNetworkQuery();
-
- // Refresh the last selected item in case users reselect network.
- clearPreferenceSummary();
- if (mSelectedPreference != null) {
- // Set summary as "Disconnected" to the previously connected network
- mSelectedPreference.setSummary(R.string.network_disconnected);
- }
-
- mSelectedPreference = (NetworkOperatorPreference) preference;
- mSelectedPreference.setSummary(R.string.network_connecting);
-
- mMetricsFeatureProvider.action(getContext(),
- SettingsEnums.ACTION_MOBILE_NETWORK_MANUAL_SELECT_NETWORK);
-
- setProgressBarVisible(true);
- // Disable the screen until network is manually set
- enablePreferenceScreen(false);
-
- mRequestIdManualNetworkSelect = getNewRequestId();
- mWaitingForNumberOfScanResults = MIN_NUMBER_OF_SCAN_REQUIRED;
- final OperatorInfo operator = mSelectedPreference.getOperatorInfo();
- ThreadUtils.postOnBackgroundThread(() -> {
- final Message msg = mHandler.obtainMessage(
- EVENT_SET_NETWORK_SELECTION_MANUALLY_DONE);
- msg.obj = mTelephonyManager.setNetworkSelectionModeManual(
- operator, true /* persistSelection */);
- msg.sendToTarget();
- });
+ if (preference == mSelectedPreference) {
+ Log.d(TAG, "onPreferenceTreeClick: preference is mSelectedPreference. Do nothing.");
+ return true;
}
+ if (!(preference instanceof NetworkOperatorPreference)) {
+ Log.d(TAG, "onPreferenceTreeClick: preference is not the NetworkOperatorPreference.");
+ return false;
+ }
+
+ stopNetworkQuery();
+
+ // Refresh the last selected item in case users reselect network.
+ clearPreferenceSummary();
+ if (mSelectedPreference != null) {
+ // Set summary as "Disconnected" to the previously connected network
+ mSelectedPreference.setSummary(R.string.network_disconnected);
+ }
+
+ mSelectedPreference = (NetworkOperatorPreference) preference;
+ mSelectedPreference.setSummary(R.string.network_connecting);
+
+ mMetricsFeatureProvider.action(getContext(),
+ SettingsEnums.ACTION_MOBILE_NETWORK_MANUAL_SELECT_NETWORK);
+
+ setProgressBarVisible(true);
+ // Disable the screen until network is manually set
+ enablePreferenceScreen(false);
+
+ mRequestIdManualNetworkSelect = getNewRequestId();
+ mWaitingForNumberOfScanResults = MIN_NUMBER_OF_SCAN_REQUIRED;
+ final OperatorInfo operator = mSelectedPreference.getOperatorInfo();
+ ThreadUtils.postOnBackgroundThread(() -> {
+ final Message msg = mHandler.obtainMessage(
+ EVENT_SET_NETWORK_SELECTION_MANUALLY_DONE);
+ msg.obj = mTelephonyManager.setNetworkSelectionModeManual(
+ operator, true /* persistSelection */);
+ msg.sendToTarget();
+ });
return true;
}
@@ -563,8 +570,7 @@
int idxPreference = mPreferenceCategory.getPreferenceCount();
while (idxPreference > 0) {
idxPreference--;
- final NetworkOperatorPreference networkOperator = (NetworkOperatorPreference)
- (mPreferenceCategory.getPreference(idxPreference));
+ final Preference networkOperator = mPreferenceCategory.getPreference(idxPreference);
networkOperator.setSummary(null);
}
}
diff --git a/src/com/android/settings/network/telephony/TelephonyStatusControlSession.java b/src/com/android/settings/network/telephony/TelephonyStatusControlSession.java
deleted file mode 100644
index 3716f1f..0000000
--- a/src/com/android/settings/network/telephony/TelephonyStatusControlSession.java
+++ /dev/null
@@ -1,117 +0,0 @@
-/*
- * Copyright (C) 2020 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.settings.network.telephony;
-
-import android.util.Log;
-
-import com.android.settings.core.BasePreferenceController;
-import com.android.settingslib.core.AbstractPreferenceController;
-import com.android.settingslib.utils.ThreadUtils;
-
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.Future;
-
-/**
- * Session for controlling the status of TelephonyPreferenceController(s).
- *
- * Within this session, result of {@link BasePreferenceController#availabilityStatus()}
- * would be under control.
- */
-public class TelephonyStatusControlSession implements AutoCloseable {
-
- private static final String LOG_TAG = "TelephonyStatusControlSS";
-
- private Collection<AbstractPreferenceController> mControllers;
- private Collection<Future<Boolean>> mResult = new ArrayList<>();
-
- /**
- * Buider of session
- */
- public static class Builder {
- private Collection<AbstractPreferenceController> mControllers;
-
- /**
- * Constructor
- *
- * @param controllers is a collection of {@link AbstractPreferenceController}
- * which would have {@link BasePreferenceController#availabilityStatus()}
- * under control within this session.
- */
- public Builder(Collection<AbstractPreferenceController> controllers) {
- mControllers = controllers;
- }
-
- /**
- * Method to build this session.
- * @return {@link TelephonyStatusControlSession} session been setup.
- */
- public TelephonyStatusControlSession build() {
- return new TelephonyStatusControlSession(mControllers);
- }
- }
-
- private TelephonyStatusControlSession(Collection<AbstractPreferenceController> controllers) {
- mControllers = controllers;
- controllers.forEach(prefCtrl -> mResult
- .add(ThreadUtils.postOnBackgroundThread(() -> setupAvailabilityStatus(prefCtrl))));
-
- }
-
- /**
- * Close the session.
- *
- * No longer control the status.
- */
- public void close() {
- //check the background thread is finished then unset the status of availability.
-
- for (Future<Boolean> result : mResult) {
- try {
- result.get();
- } catch (ExecutionException | InterruptedException exception) {
- Log.e(LOG_TAG, "setup availability status failed!", exception);
- }
- }
- unsetAvailabilityStatus(mControllers);
- }
-
- private Boolean setupAvailabilityStatus(AbstractPreferenceController controller) {
- try {
- if (controller instanceof TelephonyAvailabilityHandler) {
- int status = ((BasePreferenceController) controller)
- .getAvailabilityStatus();
- ((TelephonyAvailabilityHandler) controller).setAvailabilityStatus(status);
- }
- return true;
- } catch (Exception exception) {
- Log.e(LOG_TAG, "Setup availability status failed!", exception);
- return false;
- }
- }
-
- private void unsetAvailabilityStatus(
- Collection<AbstractPreferenceController> controllerLists) {
- controllerLists.stream()
- .filter(controller -> controller instanceof TelephonyAvailabilityHandler)
- .map(TelephonyAvailabilityHandler.class::cast)
- .forEach(controller -> {
- controller.unsetAvailabilityStatus();
- });
- }
-}
diff --git a/src/com/android/settings/network/telephony/TelephonyStatusControlSession.kt b/src/com/android/settings/network/telephony/TelephonyStatusControlSession.kt
new file mode 100644
index 0000000..0e63c8c
--- /dev/null
+++ b/src/com/android/settings/network/telephony/TelephonyStatusControlSession.kt
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.settings.network.telephony
+
+import android.util.Log
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.coroutineScope
+import com.android.settings.core.BasePreferenceController
+import com.android.settingslib.core.AbstractPreferenceController
+import com.google.common.collect.Sets
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.yield
+
+/**
+ * Session for controlling the status of TelephonyPreferenceController(s).
+ *
+ * Within this session, result of [BasePreferenceController.getAvailabilityStatus]
+ * would be under control.
+ */
+class TelephonyStatusControlSession(
+ private val controllers: Collection<AbstractPreferenceController>,
+ lifecycle: Lifecycle,
+) : AutoCloseable {
+ private var job: Job? = null
+ private val controllerSet = Sets.newConcurrentHashSet<TelephonyAvailabilityHandler>()
+
+ init {
+ job = lifecycle.coroutineScope.launch(Dispatchers.Default) {
+ for (controller in controllers) {
+ launch {
+ setupAvailabilityStatus(controller)
+ }
+ }
+ }
+ }
+
+ /**
+ * Close the session.
+ *
+ * No longer control the status.
+ */
+ override fun close() {
+ job?.cancel()
+ unsetAvailabilityStatus()
+ }
+
+ private suspend fun setupAvailabilityStatus(controller: AbstractPreferenceController): Boolean =
+ try {
+ if (controller is TelephonyAvailabilityHandler) {
+ val status = (controller as BasePreferenceController).availabilityStatus
+ yield() // prompt cancellation guarantee
+ if (controllerSet.add(controller)) {
+ controller.setAvailabilityStatus(status)
+ }
+ }
+ true
+ } catch (exception: Exception) {
+ Log.e(LOG_TAG, "Setup availability status failed!", exception)
+ false
+ }
+
+ private fun unsetAvailabilityStatus() {
+ for (controller in controllerSet) {
+ controller.unsetAvailabilityStatus()
+ }
+ }
+
+ companion object {
+ private const val LOG_TAG = "TelephonyStatusControlSS"
+ }
+}
diff --git a/src/com/android/settings/network/telephony/gsm/AutoSelectPreferenceController.java b/src/com/android/settings/network/telephony/gsm/AutoSelectPreferenceController.java
index e3e83dc..400d261 100644
--- a/src/com/android/settings/network/telephony/gsm/AutoSelectPreferenceController.java
+++ b/src/com/android/settings/network/telephony/gsm/AutoSelectPreferenceController.java
@@ -16,6 +16,7 @@
package com.android.settings.network.telephony.gsm;
+import static androidx.lifecycle.Lifecycle.Event.ON_RESUME;
import static androidx.lifecycle.Lifecycle.Event.ON_START;
import static androidx.lifecycle.Lifecycle.Event.ON_STOP;
@@ -36,10 +37,11 @@
import android.telephony.TelephonyManager;
import android.util.Log;
+import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import androidx.lifecycle.Lifecycle;
-import androidx.lifecycle.LifecycleObserver;
-import androidx.lifecycle.OnLifecycleEvent;
+import androidx.lifecycle.LifecycleEventObserver;
+import androidx.lifecycle.LifecycleOwner;
import androidx.preference.Preference;
import androidx.preference.PreferenceScreen;
import androidx.preference.SwitchPreference;
@@ -63,10 +65,10 @@
* Preference controller for "Auto Select Network"
*/
public class AutoSelectPreferenceController extends TelephonyTogglePreferenceController
- implements LifecycleObserver{
+ implements LifecycleEventObserver{
private static final long MINIMUM_DIALOG_TIME_MILLIS = TimeUnit.SECONDS.toMillis(1);
private static final String LOG_TAG = "AutoSelectPreferenceController";
- private static final String INTERNAL_LOG_TAG_INIT = "Init";
+ private static final String INTERNAL_LOG_TAG_ONRESUME = "OnResume";
private static final String INTERNAL_LOG_TAG_AFTERSET = "AfterSet";
private final Handler mUiHandler;
@@ -110,14 +112,37 @@
}
}
- @OnLifecycleEvent(ON_START)
- public void onStart() {
- mAllowedNetworkTypesListener.register(mContext, mSubId);
- }
-
- @OnLifecycleEvent(ON_STOP)
- public void onStop() {
- mAllowedNetworkTypesListener.unregister(mContext, mSubId);
+ /**
+ * Implementation of LifecycleEventObserver.
+ */
+ @SuppressWarnings("FutureReturnValueIgnored")
+ public void onStateChanged(@NonNull LifecycleOwner lifecycleOwner,
+ @NonNull Lifecycle.Event event) {
+ switch (event) {
+ case ON_START:
+ mAllowedNetworkTypesListener.register(mContext, mSubId);
+ break;
+ case ON_RESUME:
+ ThreadUtils.postOnBackgroundThread(() -> {
+ queryNetworkSelectionMode(INTERNAL_LOG_TAG_ONRESUME);
+ //Update UI in UI thread
+ mUiHandler.post(() -> {
+ if (mSwitchPreference != null) {
+ mRecursiveUpdate.getAndIncrement();
+ mSwitchPreference.setChecked(isChecked());
+ mRecursiveUpdate.decrementAndGet();
+ updateListenerValue();
+ }
+ });
+ });
+ break;
+ case ON_STOP:
+ mAllowedNetworkTypesListener.unregister(mContext, mSubId);
+ break;
+ default:
+ // Do nothing
+ break;
+ }
}
@Override
@@ -243,19 +268,6 @@
updateUiAutoSelectValue(status);
}
};
-
- ThreadUtils.postOnBackgroundThread(() -> {
- queryNetworkSelectionMode(INTERNAL_LOG_TAG_INIT);
- //Update UI in UI thread
- mUiHandler.post(() -> {
- if (mSwitchPreference != null) {
- mRecursiveUpdate.getAndIncrement();
- mSwitchPreference.setChecked(isChecked());
- mRecursiveUpdate.decrementAndGet();
- updateListenerValue();
- }
- });
- });
return this;
}
diff --git a/src/com/android/settings/network/tether/TetherSettings.java b/src/com/android/settings/network/tether/TetherSettings.java
index 6f6ba8e..5398e9b 100644
--- a/src/com/android/settings/network/tether/TetherSettings.java
+++ b/src/com/android/settings/network/tether/TetherSettings.java
@@ -148,12 +148,13 @@
@Override
public void onCreate(Bundle icicle) {
super.onCreate(icicle);
+ // Even when the UI is restricted, addPreferencesFromResource cannot be omitted.
+ addPreferencesFromResource(R.xml.tether_prefs);
setIfOnlyAvailableForAdmins(true);
if (isUiRestricted()) {
return;
}
- addPreferencesFromResource(R.xml.tether_prefs);
mContext = getContext();
mDataSaverBackend = new DataSaverBackend(mContext);
mDataSaverEnabled = mDataSaverBackend.isDataSaverEnabled();
diff --git a/src/com/android/settings/notification/SeekBarVolumizerFactory.java b/src/com/android/settings/notification/SeekBarVolumizerFactory.java
new file mode 100644
index 0000000..6fac2c1
--- /dev/null
+++ b/src/com/android/settings/notification/SeekBarVolumizerFactory.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.notification;
+
+import android.content.Context;
+import android.net.Uri;
+import android.preference.SeekBarVolumizer;
+
+/**
+ * Testable wrapper around {@link SeekBarVolumizer} constructor.
+ */
+public class SeekBarVolumizerFactory {
+ private final Context mContext;
+
+ public SeekBarVolumizerFactory(Context context) {
+ mContext = context;
+ }
+
+ /**
+ * Creates a new SeekBarVolumizer.
+ *
+ * @param streamType of the audio manager.
+ * @param defaultUri of the volume.
+ * @param sbvc callback of the seekbar volumizer.
+ * @return a SeekBarVolumizer.
+ */
+ public SeekBarVolumizer create(int streamType, Uri defaultUri, SeekBarVolumizer.Callback sbvc) {
+ return new SeekBarVolumizer(mContext, streamType, defaultUri, sbvc);
+ }
+}
diff --git a/src/com/android/settings/notification/VolumeSeekBarPreference.java b/src/com/android/settings/notification/VolumeSeekBarPreference.java
index 0000eba..9f14b73 100644
--- a/src/com/android/settings/notification/VolumeSeekBarPreference.java
+++ b/src/com/android/settings/notification/VolumeSeekBarPreference.java
@@ -37,6 +37,8 @@
import com.android.settings.R;
import com.android.settings.widget.SeekBarPreference;
+import java.text.NumberFormat;
+import java.util.Locale;
import java.util.Objects;
/** A slider preference that directly controls an audio stream volume (no dialog) **/
@@ -47,8 +49,9 @@
protected SeekBar mSeekBar;
private int mStream;
+ private SeekBarVolumizer mVolumizer;
@VisibleForTesting
- SeekBarVolumizer mVolumizer;
+ SeekBarVolumizerFactory mSeekBarVolumizerFactory;
private Callback mCallback;
private Listener mListener;
private ImageView mIconView;
@@ -62,30 +65,36 @@
private boolean mStopped;
@VisibleForTesting
AudioManager mAudioManager;
+ private Locale mLocale;
+ private NumberFormat mNumberFormat;
public VolumeSeekBarPreference(Context context, AttributeSet attrs, int defStyleAttr,
int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
setLayoutResource(R.layout.preference_volume_slider);
mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
+ mSeekBarVolumizerFactory = new SeekBarVolumizerFactory(context);
}
public VolumeSeekBarPreference(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
setLayoutResource(R.layout.preference_volume_slider);
mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
+ mSeekBarVolumizerFactory = new SeekBarVolumizerFactory(context);
}
public VolumeSeekBarPreference(Context context, AttributeSet attrs) {
super(context, attrs);
setLayoutResource(R.layout.preference_volume_slider);
mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
+ mSeekBarVolumizerFactory = new SeekBarVolumizerFactory(context);
}
public VolumeSeekBarPreference(Context context) {
super(context);
setLayoutResource(R.layout.preference_volume_slider);
mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
+ mSeekBarVolumizerFactory = new SeekBarVolumizerFactory(context);
}
public void setStream(int stream) {
@@ -143,6 +152,7 @@
if (mCallback != null) {
mCallback.onStreamValueChanged(mStream, progress);
}
+ overrideSeekBarStateDescription(formatStateDescription(progress));
}
@Override
public void onMuted(boolean muted, boolean zenMuted) {
@@ -170,7 +180,7 @@
};
final Uri sampleUri = mStream == AudioManager.STREAM_MUSIC ? getMediaVolumeUri() : null;
if (mVolumizer == null) {
- mVolumizer = new SeekBarVolumizer(getContext(), mStream, sampleUri, sbvc);
+ mVolumizer = mSeekBarVolumizerFactory.create(mStream, sampleUri, sbvc);
}
mVolumizer.start();
mVolumizer.setSeekBar(mSeekBar);
@@ -216,6 +226,33 @@
+ "/" + R.raw.media_volume);
}
+ @VisibleForTesting
+ CharSequence formatStateDescription(int progress) {
+ // This code follows the same approach in ProgressBar.java, but it rounds down the percent
+ // to match it with what the talkback feature says after any progress change. (b/285458191)
+ // Cache the locale-appropriate NumberFormat. Configuration locale is guaranteed
+ // non-null, so the first time this is called we will always get the appropriate
+ // NumberFormat, then never regenerate it unless the locale changes on the fly.
+ Locale curLocale = getContext().getResources().getConfiguration().getLocales().get(0);
+ if (mLocale == null || !mLocale.equals(curLocale)) {
+ mLocale = curLocale;
+ mNumberFormat = NumberFormat.getPercentInstance(mLocale);
+ }
+ return mNumberFormat.format(getPercent(progress));
+ }
+
+ @VisibleForTesting
+ double getPercent(float progress) {
+ final float maxProgress = getMax();
+ final float minProgress = getMin();
+ final float diffProgress = maxProgress - minProgress;
+ if (diffProgress <= 0.0f) {
+ return 0.0f;
+ }
+ final float percent = (progress - minProgress) / diffProgress;
+ return Math.floor(Math.max(0.0f, Math.min(1.0f, percent)) * 100) / 100;
+ }
+
public void setSuppressionText(String text) {
if (Objects.equals(text, mSuppressionText)) return;
mSuppressionText = text;
diff --git a/src/com/android/settings/notification/app/ConversationListPreferenceController.java b/src/com/android/settings/notification/app/ConversationListPreferenceController.java
index f893df3..6703e4e 100644
--- a/src/com/android/settings/notification/app/ConversationListPreferenceController.java
+++ b/src/com/android/settings/notification/app/ConversationListPreferenceController.java
@@ -23,6 +23,7 @@
import android.os.UserHandle;
import android.provider.Settings;
import android.service.notification.ConversationChannelWrapper;
+import android.text.BidiFormatter;
import android.text.TextUtils;
import androidx.annotation.VisibleForTesting;
@@ -132,7 +133,7 @@
CharSequence getTitle(ConversationChannelWrapper conversation) {
ShortcutInfo si = conversation.getShortcutInfo();
return si != null
- ? si.getLabel()
+ ? BidiFormatter.getInstance().unicodeWrap(si.getLabel())
: conversation.getNotificationChannel().getName();
}
diff --git a/src/com/android/settings/notification/history/NotificationHistoryActivity.java b/src/com/android/settings/notification/history/NotificationHistoryActivity.java
index b71d295..4808773 100644
--- a/src/com/android/settings/notification/history/NotificationHistoryActivity.java
+++ b/src/com/android/settings/notification/history/NotificationHistoryActivity.java
@@ -50,6 +50,7 @@
import android.widget.ImageView;
import android.widget.TextView;
+import androidx.core.graphics.ColorUtils;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
@@ -176,7 +177,8 @@
com.android.internal.R.id.expand_button);
int textColor = obtainThemeColor(android.R.attr.textColorPrimary);
int backgroundColor = obtainThemeColor(android.R.attr.colorBackgroundFloating);
- expand.setDefaultPillColor(backgroundColor);
+ int pillColor = ColorUtils.blendARGB(textColor, backgroundColor, 0.9f);
+ expand.setDefaultPillColor(pillColor);
expand.setDefaultTextColor(textColor);
expand.setExpanded(false);
header.setStateDescription(container.getVisibility() == View.VISIBLE
diff --git a/src/com/android/settings/overlay/FeatureFactory.java b/src/com/android/settings/overlay/FeatureFactory.java
index c536a38..97fc343 100644
--- a/src/com/android/settings/overlay/FeatureFactory.java
+++ b/src/com/android/settings/overlay/FeatureFactory.java
@@ -31,6 +31,7 @@
import com.android.settings.biometrics.face.FaceFeatureProvider;
import com.android.settings.biometrics2.factory.BiometricsRepositoryProvider;
import com.android.settings.bluetooth.BluetoothFeatureProvider;
+import com.android.settings.connecteddevice.stylus.StylusFeatureProvider;
import com.android.settings.dashboard.DashboardFeatureProvider;
import com.android.settings.dashboard.suggestions.SuggestionFeatureProvider;
import com.android.settings.deviceinfo.hardwareinfo.HardwareInfoFeatureProvider;
@@ -40,6 +41,7 @@
import com.android.settings.fuelgauge.PowerUsageFeatureProvider;
import com.android.settings.gestures.AssistGestureFeatureProvider;
import com.android.settings.homepage.contextualcards.ContextualCardFeatureProvider;
+import com.android.settings.inputmethod.KeyboardSettingsFeatureProvider;
import com.android.settings.localepicker.LocaleFeatureProvider;
import com.android.settings.panel.PanelFeatureProvider;
import com.android.settings.search.SearchFeatureProvider;
@@ -130,8 +132,7 @@
/**
* Gets implementation for Battery Settings provider.
*/
- public abstract BatterySettingsFeatureProvider getBatterySettingsFeatureProvider(
- Context context);
+ public abstract BatterySettingsFeatureProvider getBatterySettingsFeatureProvider();
public abstract DashboardFeatureProvider getDashboardFeatureProvider(Context context);
@@ -204,6 +205,16 @@
*/
public abstract WifiFeatureProvider getWifiFeatureProvider();
+ /**
+ * Retrieves implementation for keyboard settings feature.
+ */
+ public abstract KeyboardSettingsFeatureProvider getKeyboardSettingsFeatureProvider();
+
+ /**
+ * Retrieves implementation for stylus settings feature.
+ */
+ public abstract StylusFeatureProvider getStylusFeatureProvider();
+
public static final class FactoryNotFoundException extends RuntimeException {
public FactoryNotFoundException(Throwable throwable) {
super("Unable to create factory. Did you misconfigure Proguard?", throwable);
diff --git a/src/com/android/settings/overlay/FeatureFactoryImpl.java b/src/com/android/settings/overlay/FeatureFactoryImpl.java
index 3ddda47..8c92792 100644
--- a/src/com/android/settings/overlay/FeatureFactoryImpl.java
+++ b/src/com/android/settings/overlay/FeatureFactoryImpl.java
@@ -42,6 +42,8 @@
import com.android.settings.bluetooth.BluetoothFeatureProvider;
import com.android.settings.bluetooth.BluetoothFeatureProviderImpl;
import com.android.settings.connecteddevice.dock.DockUpdaterFeatureProviderImpl;
+import com.android.settings.connecteddevice.stylus.StylusFeatureProvider;
+import com.android.settings.connecteddevice.stylus.StylusFeatureProviderImpl;
import com.android.settings.core.instrumentation.SettingsMetricsFeatureProvider;
import com.android.settings.dashboard.DashboardFeatureProvider;
import com.android.settings.dashboard.DashboardFeatureProviderImpl;
@@ -61,6 +63,8 @@
import com.android.settings.gestures.AssistGestureFeatureProviderImpl;
import com.android.settings.homepage.contextualcards.ContextualCardFeatureProvider;
import com.android.settings.homepage.contextualcards.ContextualCardFeatureProviderImpl;
+import com.android.settings.inputmethod.KeyboardSettingsFeatureProvider;
+import com.android.settings.inputmethod.KeyboardSettingsFeatureProviderImpl;
import com.android.settings.localepicker.LocaleFeatureProvider;
import com.android.settings.localepicker.LocaleFeatureProviderImpl;
import com.android.settings.panel.PanelFeatureProvider;
@@ -116,6 +120,8 @@
private AccessibilityMetricsFeatureProvider mAccessibilityMetricsFeatureProvider;
private AdvancedVpnFeatureProvider mAdvancedVpnFeatureProvider;
private WifiFeatureProvider mWifiFeatureProvider;
+ private KeyboardSettingsFeatureProvider mKeyboardSettingsFeatureProvider;
+ private StylusFeatureProvider mStylusFeatureProvider;
@Override
public HardwareInfoFeatureProvider getHardwareInfoFeatureProvider() {
@@ -154,9 +160,9 @@
}
@Override
- public BatterySettingsFeatureProvider getBatterySettingsFeatureProvider(Context context) {
+ public BatterySettingsFeatureProvider getBatterySettingsFeatureProvider() {
if (mBatterySettingsFeatureProvider == null) {
- mBatterySettingsFeatureProvider = new BatterySettingsFeatureProviderImpl(context);
+ mBatterySettingsFeatureProvider = new BatterySettingsFeatureProviderImpl();
}
return mBatterySettingsFeatureProvider;
}
@@ -372,4 +378,20 @@
}
return mWifiFeatureProvider;
}
+
+ @Override
+ public KeyboardSettingsFeatureProvider getKeyboardSettingsFeatureProvider() {
+ if (mKeyboardSettingsFeatureProvider == null) {
+ mKeyboardSettingsFeatureProvider = new KeyboardSettingsFeatureProviderImpl();
+ }
+ return mKeyboardSettingsFeatureProvider;
+ }
+
+ @Override
+ public StylusFeatureProvider getStylusFeatureProvider() {
+ if (mStylusFeatureProvider == null) {
+ mStylusFeatureProvider = new StylusFeatureProviderImpl();
+ }
+ return mStylusFeatureProvider;
+ }
}
diff --git a/src/com/android/settings/panel/PanelSlicesAdapter.java b/src/com/android/settings/panel/PanelSlicesAdapter.java
index 1bced76..fb41879 100644
--- a/src/com/android/settings/panel/PanelSlicesAdapter.java
+++ b/src/com/android/settings/panel/PanelSlicesAdapter.java
@@ -18,6 +18,7 @@
import static android.app.slice.Slice.HINT_ERROR;
import static android.app.slice.SliceItem.FORMAT_SLICE;
+import static android.view.accessibility.AccessibilityNodeInfo.ACTION_CLICK;
import android.app.settings.SettingsEnums;
import android.content.Context;
@@ -25,6 +26,7 @@
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
+import android.view.accessibility.AccessibilityNodeInfo;
import android.widget.LinearLayout;
import androidx.annotation.NonNull;
@@ -72,13 +74,12 @@
public SliceRowViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int viewType) {
final Context context = viewGroup.getContext();
final LayoutInflater inflater = LayoutInflater.from(context);
- View view;
+ final View view;
if (viewType == PanelContent.VIEW_TYPE_SLIDER) {
view = inflater.inflate(R.layout.panel_slice_slider_row, viewGroup, false);
} else {
view = inflater.inflate(R.layout.panel_slice_row, viewGroup, false);
}
-
return new SliceRowViewHolder(view);
}
@@ -115,6 +116,9 @@
public class SliceRowViewHolder extends RecyclerView.ViewHolder
implements DividerItemDecoration.DividedViewHolder {
+ private static final int ROW_VIEW_ID = androidx.slice.view.R.id.row_view;
+ private static final int ROW_VIEW_TAG = R.id.tag_row_view;
+
@VisibleForTesting
final SliceView sliceView;
@VisibleForTesting
@@ -135,6 +139,7 @@
public void onBind(Slice slice) {
// Hides slice which reports with error hint or not contain any slice sub-item.
if (slice == null || !isValidSlice(slice)) {
+ updateActionLabel();
sliceView.setVisibility(View.GONE);
return;
} else {
@@ -158,6 +163,61 @@
eventInfo.actionType /* value */);
})
);
+ updateActionLabel();
+ }
+
+ /**
+ * Either set the action label if the row view is inflated into Slice, or set a listener to
+ * do so later when the row is available.
+ */
+ @VisibleForTesting void updateActionLabel() {
+ if (sliceView == null) {
+ return;
+ }
+
+ final LinearLayout llRow = sliceView.findViewById(ROW_VIEW_ID);
+ if (llRow != null) {
+ // Just set the label for the row. if is already laid out, there is no need for
+ // listening to future changes.
+ setActionLabel(llRow);
+ } else { // set the accessibility delegate when row_view is laid out
+ Object alreadyAddedListener = sliceView.getTag(ROW_VIEW_TAG);
+ if (alreadyAddedListener != null) {
+ return;
+ }
+ sliceView.setTag(ROW_VIEW_TAG, new Object());
+ sliceView.addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
+ @Override
+ public void onLayoutChange(View v, int left, int top, int right, int bottom,
+ int oldLeft, int oldTop, int oldRight, int oldBottom) {
+ LinearLayout row = sliceView.findViewById(ROW_VIEW_ID);
+ if (row != null) {
+ setActionLabel(row);
+ sliceView.removeOnLayoutChangeListener(this);
+ }
+ }
+ });
+ }
+ }
+
+ /**
+ * Update the action label for TalkBack to be more specific
+ * @param view the RowView within the Slice
+ */
+ @VisibleForTesting void setActionLabel(View view) {
+ view.setAccessibilityDelegate(new View.AccessibilityDelegate() {
+ @Override
+ public void onInitializeAccessibilityNodeInfo(View host,
+ AccessibilityNodeInfo info) {
+ super.onInitializeAccessibilityNodeInfo(host, info);
+
+ AccessibilityNodeInfo.AccessibilityAction customClick =
+ new AccessibilityNodeInfo.AccessibilityAction(ACTION_CLICK, host
+ .getResources()
+ .getString(R.string.accessibility_action_label_panel_slice));
+ info.addAction(customClick);
+ }
+ });
}
private boolean isValidSlice(Slice slice) {
diff --git a/src/com/android/settings/password/ChooseLockGeneric.java b/src/com/android/settings/password/ChooseLockGeneric.java
index 4c4795c..0bf1255 100644
--- a/src/com/android/settings/password/ChooseLockGeneric.java
+++ b/src/com/android/settings/password/ChooseLockGeneric.java
@@ -33,6 +33,7 @@
import static com.android.settings.password.ChooseLockSettingsHelper.EXTRA_KEY_DEVICE_PASSWORD_REQUIREMENT_ONLY;
import static com.android.settings.password.ChooseLockSettingsHelper.EXTRA_KEY_IS_CALLING_APP_ADMIN;
import static com.android.settings.password.ChooseLockSettingsHelper.EXTRA_KEY_REQUESTED_MIN_COMPLEXITY;
+import static com.android.settings.password.ChooseLockSettingsHelper.EXTRA_KEY_REQUEST_WRITE_REPAIR_MODE_PW;
import android.app.Activity;
import android.app.Dialog;
@@ -795,6 +796,9 @@
if (getIntent().getBooleanExtra(EXTRA_SHOW_OPTIONS_BUTTON, false)) {
intent.putExtra(EXTRA_SHOW_OPTIONS_BUTTON, chooseLockSkipped);
}
+ if (getIntent().getBooleanExtra(EXTRA_KEY_REQUEST_WRITE_REPAIR_MODE_PW, false)) {
+ intent.putExtra(EXTRA_KEY_REQUEST_WRITE_REPAIR_MODE_PW, true);
+ }
intent.putExtra(EXTRA_CHOOSE_LOCK_GENERIC_EXTRAS, getIntent().getExtras());
// If the caller requested Gatekeeper Password Handle to be returned, we assume it
// came from biometric enrollment. onActivityResult will put the LockSettingsService
diff --git a/src/com/android/settings/password/ChooseLockPassword.java b/src/com/android/settings/password/ChooseLockPassword.java
index 9cce7ac..45db22c 100644
--- a/src/com/android/settings/password/ChooseLockPassword.java
+++ b/src/com/android/settings/password/ChooseLockPassword.java
@@ -65,7 +65,6 @@
import android.text.TextUtils;
import android.text.TextWatcher;
import android.util.Log;
-import android.util.Pair;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.View;
@@ -74,9 +73,11 @@
import android.view.inputmethod.EditorInfo;
import android.widget.CheckBox;
import android.widget.ImeAwareEditText;
+import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.TextView.OnEditorActionListener;
+import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.LinearLayoutManager;
@@ -87,7 +88,6 @@
import com.android.internal.widget.LockscreenCredential;
import com.android.internal.widget.PasswordValidationError;
import com.android.internal.widget.TextViewInputDisabler;
-import com.android.internal.widget.VerifyCredentialResponse;
import com.android.settings.R;
import com.android.settings.SettingsActivity;
import com.android.settings.SetupWizardUtils;
@@ -234,6 +234,7 @@
private LockscreenCredential mCurrentCredential;
private LockscreenCredential mChosenPassword;
private boolean mRequestGatekeeperPassword;
+ private boolean mRequestWriteRepairModePassword;
private ImeAwareEditText mPasswordEntry;
private TextViewInputDisabler mPasswordEntryInputDisabler;
@@ -517,7 +518,9 @@
|| DevicePolicyManager.PASSWORD_QUALITY_ALPHANUMERIC == mPasswordType
|| DevicePolicyManager.PASSWORD_QUALITY_COMPLEX == mPasswordType;
- setupPasswordRequirementsView(view);
+ final LinearLayout headerLayout = view.findViewById(
+ R.id.sud_layout_header);
+ setupPasswordRequirementsView(headerLayout);
mPasswordRestrictionView.setLayoutManager(new LinearLayoutManager(getActivity()));
mPasswordEntry = view.findViewById(R.id.password_entry);
@@ -561,6 +564,8 @@
ChooseLockSettingsHelper.EXTRA_KEY_PASSWORD);
mRequestGatekeeperPassword = intent.getBooleanExtra(
ChooseLockSettingsHelper.EXTRA_KEY_REQUEST_GK_PW_HANDLE, false);
+ mRequestWriteRepairModePassword = intent.getBooleanExtra(
+ ChooseLockSettingsHelper.EXTRA_KEY_REQUEST_WRITE_REPAIR_MODE_PW, false);
if (savedInstanceState == null) {
updateStage(Stage.Introduction);
if (confirmCredentials) {
@@ -570,6 +575,7 @@
.setTitle(getString(R.string.unlock_set_unlock_launch_picker_title))
.setReturnCredentials(true)
.setRequestGatekeeperPasswordHandle(mRequestGatekeeperPassword)
+ .setRequestWriteRepairModePassword(mRequestWriteRepairModePassword)
.setUserId(mUserId)
.show();
}
@@ -626,11 +632,33 @@
}
}
- private void setupPasswordRequirementsView(View view) {
- mPasswordRestrictionView = view.findViewById(R.id.password_requirements_view);
+ private void setupPasswordRequirementsView(@Nullable ViewGroup view) {
+ if (view == null) {
+ return;
+ }
+
+ createHintMessageView(view);
mPasswordRestrictionView.setLayoutManager(new LinearLayoutManager(getActivity()));
- mPasswordRequirementAdapter = new PasswordRequirementAdapter();
+ mPasswordRequirementAdapter = new PasswordRequirementAdapter(getActivity());
mPasswordRestrictionView.setAdapter(mPasswordRequirementAdapter);
+ view.addView(mPasswordRestrictionView);
+ }
+
+ private void createHintMessageView(ViewGroup view) {
+ if (mPasswordRestrictionView != null) {
+ return;
+ }
+
+ final TextView sucTitleView = view.findViewById(R.id.suc_layout_title);
+ final ViewGroup.MarginLayoutParams titleLayoutParams =
+ (ViewGroup.MarginLayoutParams) sucTitleView.getLayoutParams();
+ mPasswordRestrictionView = new RecyclerView(getActivity());
+ final LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(
+ LinearLayout.LayoutParams.MATCH_PARENT,
+ LinearLayout.LayoutParams.WRAP_CONTENT);
+ lp.setMargins(titleLayoutParams.leftMargin, getResources().getDimensionPixelSize(
+ R.dimen.password_requirement_view_margin_top), titleLayoutParams.leftMargin, 0);
+ mPasswordRestrictionView.setLayoutParams(lp);
}
@Override
@@ -1005,10 +1033,11 @@
getActivity().getWindow().getDecorView());
mPasswordEntryInputDisabler.setInputEnabled(false);
- setNextEnabled(false);
-
mSaveAndFinishWorker = new SaveAndFinishWorker();
- mSaveAndFinishWorker.setListener(this);
+ mSaveAndFinishWorker
+ .setListener(this)
+ .setRequestGatekeeperPasswordHandle(mRequestGatekeeperPassword)
+ .setRequestWriteRepairModePassword(mRequestWriteRepairModePassword);
getFragmentManager().beginTransaction().add(mSaveAndFinishWorker,
FRAGMENT_TAG_SAVE_AND_FINISH).commit();
@@ -1028,7 +1057,7 @@
(mAutoPinConfirmOption != null && mAutoPinConfirmOption.isChecked()),
mUserId);
- mSaveAndFinishWorker.start(mLockPatternUtils, mRequestGatekeeperPassword,
+ mSaveAndFinishWorker.start(mLockPatternUtils,
mChosenPassword, mCurrentCredential, mUserId);
}
@@ -1081,50 +1110,4 @@
}
}
}
-
- public static class SaveAndFinishWorker extends SaveChosenLockWorkerBase {
-
- private LockscreenCredential mChosenPassword;
- private LockscreenCredential mCurrentCredential;
-
- public void start(LockPatternUtils utils, boolean requestGatekeeperPassword,
- LockscreenCredential chosenPassword, LockscreenCredential currentCredential,
- int userId) {
- prepare(utils, requestGatekeeperPassword, userId);
-
- mChosenPassword = chosenPassword;
- mCurrentCredential = currentCredential != null ? currentCredential
- : LockscreenCredential.createNone();
- mUserId = userId;
-
- start();
- }
-
- @Override
- protected Pair<Boolean, Intent> saveAndVerifyInBackground() {
- final boolean success = mUtils.setLockCredential(
- mChosenPassword, mCurrentCredential, mUserId);
- if (success) {
- unifyProfileCredentialIfRequested();
- }
- Intent result = null;
- if (success && mRequestGatekeeperPassword) {
- // If a Gatekeeper Password was requested, invoke the LockSettingsService code
- // path to return a Gatekeeper Password based on the credential that the user
- // chose. This should only be run if the credential was successfully set.
- final VerifyCredentialResponse response = mUtils.verifyCredential(mChosenPassword,
- mUserId, LockPatternUtils.VERIFY_FLAG_REQUEST_GK_PW_HANDLE);
-
- if (!response.isMatched() || !response.containsGatekeeperPasswordHandle()) {
- Log.e(TAG, "critical: bad response or missing GK PW handle for known good"
- + " password: " + response.toString());
- }
-
- result = new Intent();
- result.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_GK_PW_HANDLE,
- response.getGatekeeperPasswordHandle());
- }
- return Pair.create(success, result);
- }
- }
}
diff --git a/src/com/android/settings/password/ChooseLockPattern.java b/src/com/android/settings/password/ChooseLockPattern.java
index 9ac9ef8..7569c15 100644
--- a/src/com/android/settings/password/ChooseLockPattern.java
+++ b/src/com/android/settings/password/ChooseLockPattern.java
@@ -34,7 +34,6 @@
import android.os.UserHandle;
import android.os.UserManager;
import android.util.Log;
-import android.util.Pair;
import android.util.TypedValue;
import android.view.KeyEvent;
import android.view.LayoutInflater;
@@ -53,7 +52,6 @@
import com.android.internal.widget.LockPatternView.Cell;
import com.android.internal.widget.LockPatternView.DisplayMode;
import com.android.internal.widget.LockscreenCredential;
-import com.android.internal.widget.VerifyCredentialResponse;
import com.android.settings.R;
import com.android.settings.SettingsActivity;
import com.android.settings.SetupWizardUtils;
@@ -206,6 +204,7 @@
private LockscreenCredential mCurrentCredential;
private boolean mRequestGatekeeperPassword;
+ private boolean mRequestWriteRepairModePassword;
protected TextView mHeaderText;
protected LockPatternView mLockPatternView;
protected TextView mFooterText;
@@ -442,7 +441,8 @@
protected boolean mForFace;
protected boolean mForBiometrics;
- private static final String KEY_UI_STAGE = "uiStage";
+ @VisibleForTesting
+ static final String KEY_UI_STAGE = "uiStage";
private static final String KEY_PATTERN_CHOICE = "chosenPattern";
private static final String KEY_CURRENT_PATTERN = "currentPattern";
@@ -562,6 +562,8 @@
intent.getParcelableExtra(ChooseLockSettingsHelper.EXTRA_KEY_PASSWORD);
mRequestGatekeeperPassword = intent.getBooleanExtra(
ChooseLockSettingsHelper.EXTRA_KEY_REQUEST_GK_PW_HANDLE, false);
+ mRequestWriteRepairModePassword = intent.getBooleanExtra(
+ ChooseLockSettingsHelper.EXTRA_KEY_REQUEST_WRITE_REPAIR_MODE_PW, false);
if (savedInstanceState == null) {
if (confirmCredentials) {
@@ -575,6 +577,7 @@
.setTitle(getString(R.string.unlock_set_unlock_launch_picker_title))
.setReturnCredentials(true)
.setRequestGatekeeperPasswordHandle(mRequestGatekeeperPassword)
+ .setRequestWriteRepairModePassword(mRequestWriteRepairModePassword)
.setUserId(mUserId)
.show();
@@ -718,10 +721,6 @@
final GlifLayout layout = getActivity().findViewById(R.id.setup_wizard_layout);
mUiStage = stage;
- if (stage == Stage.Introduction) {
- layout.setDescriptionText(stage.headerMessage);
- }
-
// header text, footer text, visibility and
// enabled state all known from the stage
if (stage == Stage.ChoiceTooShort) {
@@ -744,16 +743,13 @@
Theme theme = getActivity().getTheme();
theme.resolveAttribute(R.attr.colorError, typedValue, true);
mHeaderText.setTextColor(typedValue.data);
+ } else if (mDefaultHeaderColorList != null) {
+ mHeaderText.setTextColor(mDefaultHeaderColorList);
+ }
- } else {
- if (mDefaultHeaderColorList != null) {
- mHeaderText.setTextColor(mDefaultHeaderColorList);
- }
- if (stage == Stage.NeedToConfirm) {
- mHeaderText.setText(stage.headerMessage);
- layout.setHeaderText(R.string.lockpassword_draw_your_pattern_again_header);
- }
+ if (stage == Stage.ConfirmWrong || stage == Stage.NeedToConfirm) {
+ layout.setHeaderText(R.string.lockpassword_draw_your_pattern_again_header);
}
updateFooterLeftButton(stage);
@@ -833,7 +829,10 @@
setRightButtonEnabled(false);
mSaveAndFinishWorker = new SaveAndFinishWorker();
- mSaveAndFinishWorker.setListener(this);
+ mSaveAndFinishWorker
+ .setListener(this)
+ .setRequestGatekeeperPasswordHandle(mRequestGatekeeperPassword)
+ .setRequestWriteRepairModePassword(mRequestWriteRepairModePassword);
getFragmentManager().beginTransaction().add(mSaveAndFinishWorker,
FRAGMENT_TAG_SAVE_AND_FINISH).commit();
@@ -849,7 +848,7 @@
profileCredential);
}
}
- mSaveAndFinishWorker.start(mLockPatternUtils, mRequestGatekeeperPassword,
+ mSaveAndFinishWorker.start(mLockPatternUtils,
mChosenPattern, mCurrentCredential, mUserId);
}
@@ -873,51 +872,4 @@
getActivity().finish();
}
}
-
- public static class SaveAndFinishWorker extends SaveChosenLockWorkerBase {
-
- private LockscreenCredential mChosenPattern;
- private LockscreenCredential mCurrentCredential;
-
- public void start(LockPatternUtils utils, boolean requestGatekeeperPassword,
- LockscreenCredential chosenPattern, LockscreenCredential currentCredential,
- int userId) {
- prepare(utils, requestGatekeeperPassword, userId);
-
- mCurrentCredential = currentCredential != null ? currentCredential
- : LockscreenCredential.createNone();
- mChosenPattern = chosenPattern;
- mUserId = userId;
-
- start();
- }
-
- @Override
- protected Pair<Boolean, Intent> saveAndVerifyInBackground() {
- final int userId = mUserId;
- final boolean success = mUtils.setLockCredential(mChosenPattern, mCurrentCredential,
- userId);
- if (success) {
- unifyProfileCredentialIfRequested();
- }
- Intent result = null;
- if (success && mRequestGatekeeperPassword) {
- // If a Gatekeeper Password was requested, invoke the LockSettingsService code
- // path to return a Gatekeeper Password based on the credential that the user
- // chose. This should only be run if the credential was successfully set.
- final VerifyCredentialResponse response = mUtils.verifyCredential(mChosenPattern,
- userId, LockPatternUtils.VERIFY_FLAG_REQUEST_GK_PW_HANDLE);
-
- if (!response.isMatched() || !response.containsGatekeeperPasswordHandle()) {
- Log.e(TAG, "critical: bad response or missing GK PW handle for known good"
- + " pattern: " + response.toString());
- }
-
- result = new Intent();
- result.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_GK_PW_HANDLE,
- response.getGatekeeperPasswordHandle());
- }
- return Pair.create(success, result);
- }
- }
}
diff --git a/src/com/android/settings/password/ChooseLockSettingsHelper.java b/src/com/android/settings/password/ChooseLockSettingsHelper.java
index 216f7db..e5fc550 100644
--- a/src/com/android/settings/password/ChooseLockSettingsHelper.java
+++ b/src/com/android/settings/password/ChooseLockSettingsHelper.java
@@ -71,6 +71,10 @@
// Gatekeeper password handle, which can subsequently be used to generate Gatekeeper
// HardwareAuthToken(s) via LockSettingsService#verifyGatekeeperPasswordHandle
public static final String EXTRA_KEY_GK_PW_HANDLE = "gk_pw_handle";
+ public static final String EXTRA_KEY_REQUEST_WRITE_REPAIR_MODE_PW =
+ "request_write_repair_mode_pw";
+ public static final String EXTRA_KEY_WROTE_REPAIR_MODE_CREDENTIAL =
+ "wrote_repair_mode_credential";
/**
* When EXTRA_KEY_UNIFICATION_PROFILE_CREDENTIAL and EXTRA_KEY_UNIFICATION_PROFILE_ID are
@@ -152,6 +156,7 @@
@Nullable private RemoteLockscreenValidationSession mRemoteLockscreenValidationSession;
@Nullable private ComponentName mRemoteLockscreenValidationServiceComponent;
private boolean mRequestGatekeeperPasswordHandle;
+ private boolean mRequestWriteRepairModePassword;
private boolean mTaskOverlay;
public Builder(@NonNull Activity activity) {
@@ -336,6 +341,17 @@
}
/**
+ * @param requestWriteRepairModePassword Set {@code true} to request that
+ * LockSettingsService writes the password data to the repair mode file after the user
+ * credential is verified successfully.
+ */
+ @NonNull public Builder setRequestWriteRepairModePassword(
+ boolean requestWriteRepairModePassword) {
+ mRequestWriteRepairModePassword = requestWriteRepairModePassword;
+ return this;
+ }
+
+ /**
* Support of ActivityResultLauncher.
*
* Which allowing the launch operation be controlled externally.
@@ -348,7 +364,8 @@
}
@NonNull public ChooseLockSettingsHelper build() {
- if (!mAllowAnyUserId && mUserId != LockPatternUtils.USER_FRP) {
+ if (!mAllowAnyUserId && mUserId != LockPatternUtils.USER_FRP
+ && mUserId != LockPatternUtils.USER_REPAIR_MODE) {
Utils.enforceSameOwner(mActivity, mUserId);
}
@@ -385,7 +402,7 @@
mBuilder.mRemoteLockscreenValidationSession,
mBuilder.mRemoteLockscreenValidationServiceComponent, mBuilder.mAllowAnyUserId,
mBuilder.mForegroundOnly, mBuilder.mRequestGatekeeperPasswordHandle,
- mBuilder.mTaskOverlay);
+ mBuilder.mRequestWriteRepairModePassword, mBuilder.mTaskOverlay);
}
private boolean launchConfirmationActivity(int request, @Nullable CharSequence title,
@@ -396,7 +413,7 @@
@Nullable RemoteLockscreenValidationSession remoteLockscreenValidationSession,
@Nullable ComponentName remoteLockscreenValidationServiceComponent,
boolean allowAnyUser, boolean foregroundOnly, boolean requestGatekeeperPasswordHandle,
- boolean taskOverlay) {
+ boolean requestWriteRepairModePassword, boolean taskOverlay) {
Optional<Class<?>> activityClass = determineAppropriateActivityClass(
returnCredentials, forceVerifyPath, userId, remoteLockscreenValidationSession);
if (activityClass.isEmpty()) {
@@ -407,7 +424,7 @@
returnCredentials, external, forceVerifyPath, userId, alternateButton,
checkboxLabel, remoteLockscreenValidation, remoteLockscreenValidationSession,
remoteLockscreenValidationServiceComponent, allowAnyUser, foregroundOnly,
- requestGatekeeperPasswordHandle, taskOverlay);
+ requestGatekeeperPasswordHandle, requestWriteRepairModePassword, taskOverlay);
}
private boolean launchConfirmationActivity(int request, CharSequence title, CharSequence header,
@@ -418,7 +435,7 @@
@Nullable RemoteLockscreenValidationSession remoteLockscreenValidationSession,
@Nullable ComponentName remoteLockscreenValidationServiceComponent,
boolean allowAnyUser, boolean foregroundOnly, boolean requestGatekeeperPasswordHandle,
- boolean taskOverlay) {
+ boolean requestWriteRepairModePassword, boolean taskOverlay) {
final Intent intent = new Intent();
intent.putExtra(ConfirmDeviceCredentialBaseFragment.TITLE_TEXT, title);
intent.putExtra(ConfirmDeviceCredentialBaseFragment.HEADER_TEXT, header);
@@ -442,6 +459,8 @@
intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_ALLOW_ANY_USER, allowAnyUser);
intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_REQUEST_GK_PW_HANDLE,
requestGatekeeperPasswordHandle);
+ intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_REQUEST_WRITE_REPAIR_MODE_PW,
+ requestWriteRepairModePassword);
intent.setClassName(SETTINGS_PACKAGE_NAME, activityClass.getName());
intent.putExtra(SettingsBaseActivity.EXTRA_PAGE_TRANSITION_TYPE,
diff --git a/src/com/android/settings/password/ConfirmDeviceCredentialActivity.java b/src/com/android/settings/password/ConfirmDeviceCredentialActivity.java
index fabca6b..e4ebad7 100644
--- a/src/com/android/settings/password/ConfirmDeviceCredentialActivity.java
+++ b/src/com/android/settings/password/ConfirmDeviceCredentialActivity.java
@@ -20,9 +20,7 @@
import static android.app.admin.DevicePolicyResources.Strings.Settings.CONFIRM_WORK_PROFILE_PASSWORD_HEADER;
import static android.app.admin.DevicePolicyResources.Strings.Settings.CONFIRM_WORK_PROFILE_PATTERN_HEADER;
import static android.app.admin.DevicePolicyResources.Strings.Settings.CONFIRM_WORK_PROFILE_PIN_HEADER;
-import static android.app.admin.DevicePolicyResources.Strings.Settings.WORK_PROFILE_CONFIRM_PASSWORD;
-import static android.app.admin.DevicePolicyResources.Strings.Settings.WORK_PROFILE_CONFIRM_PATTERN;
-import static android.app.admin.DevicePolicyResources.Strings.Settings.WORK_PROFILE_CONFIRM_PIN;
+import static android.view.WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS;
import android.app.Activity;
import android.app.KeyguardManager;
@@ -32,6 +30,7 @@
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
+import android.content.res.Configuration;
import android.graphics.Color;
import android.hardware.biometrics.BiometricConstants;
import android.hardware.biometrics.BiometricPrompt;
@@ -166,11 +165,18 @@
mDetails = intent.getCharSequenceExtra(KeyguardManager.EXTRA_DESCRIPTION);
String alternateButton = intent.getStringExtra(
KeyguardManager.EXTRA_ALTERNATE_BUTTON_LABEL);
- boolean frp = KeyguardManager.ACTION_CONFIRM_FRP_CREDENTIAL.equals(intent.getAction());
- boolean remoteValidation =
+ final boolean frp =
+ KeyguardManager.ACTION_CONFIRM_FRP_CREDENTIAL.equals(intent.getAction());
+ final boolean repairMode =
+ KeyguardManager.ACTION_CONFIRM_REPAIR_MODE_DEVICE_CREDENTIAL
+ .equals(intent.getAction());
+ final boolean remoteValidation =
KeyguardManager.ACTION_CONFIRM_REMOTE_DEVICE_CREDENTIAL.equals(intent.getAction());
mTaskOverlay = isInternalActivity()
&& intent.getBooleanExtra(KeyguardManager.EXTRA_FORCE_TASK_OVERLAY, false);
+ final boolean prepareRepairMode =
+ KeyguardManager.ACTION_PREPARE_REPAIR_MODE_DEVICE_CREDENTIAL.equals(
+ intent.getAction());
mUserId = UserHandle.myUserId();
if (isInternalActivity()) {
@@ -202,7 +208,7 @@
}
if (mDetails == null) {
promptInfo.setDeviceCredentialSubtitle(
- getDetailsFromCredentialType(credentialType, isEffectiveUserManagedProfile));
+ Utils.getConfirmCredentialStringForUser(this, mUserId, credentialType));
}
boolean launchedBiometric = false;
@@ -219,6 +225,14 @@
.setExternal(true)
.setUserId(LockPatternUtils.USER_FRP)
.show();
+ } else if (repairMode) {
+ final ChooseLockSettingsHelper.Builder builder =
+ new ChooseLockSettingsHelper.Builder(this);
+ launchedCDC = builder.setHeader(mTitle)
+ .setDescription(mDetails)
+ .setExternal(true)
+ .setUserId(LockPatternUtils.USER_REPAIR_MODE)
+ .show();
} else if (remoteValidation) {
RemoteLockscreenValidationSession remoteLockscreenValidationSession =
intent.getParcelableExtra(
@@ -244,6 +258,17 @@
.setExternal(true)
.show();
return;
+ } else if (prepareRepairMode) {
+ final ChooseLockSettingsHelper.Builder builder =
+ new ChooseLockSettingsHelper.Builder(this);
+ launchedCDC = builder.setHeader(mTitle)
+ .setDescription(mDetails)
+ .setExternal(true)
+ .setUserId(mUserId)
+ .setTaskOverlay(mTaskOverlay)
+ .setRequestWriteRepairModePassword(true)
+ .setForceVerifyPath(true)
+ .show();
} else if (isEffectiveUserManagedProfile && isInternalActivity()) {
mCredentialMode = CREDENTIAL_MANAGED;
if (isBiometricAllowed(effectiveUserId, mUserId)) {
@@ -314,45 +339,18 @@
return null;
}
- private String getDetailsFromCredentialType(@LockPatternUtils.CredentialType int credentialType,
- boolean isEffectiveUserManagedProfile) {
- switch (credentialType) {
- case LockPatternUtils.CREDENTIAL_TYPE_PIN:
- if (isEffectiveUserManagedProfile) {
- return mDevicePolicyManager.getResources().getString(WORK_PROFILE_CONFIRM_PIN,
- () -> getString(
- R.string.lockpassword_confirm_your_pin_generic_profile));
- }
-
- return getString(R.string.lockpassword_confirm_your_pin_generic);
- case LockPatternUtils.CREDENTIAL_TYPE_PATTERN:
- if (isEffectiveUserManagedProfile) {
- return mDevicePolicyManager.getResources().getString(
- WORK_PROFILE_CONFIRM_PATTERN,
- () -> getString(
- R.string.lockpassword_confirm_your_pattern_generic_profile));
- }
-
- return getString(R.string.lockpassword_confirm_your_pattern_generic);
- case LockPatternUtils.CREDENTIAL_TYPE_PASSWORD:
- if (isEffectiveUserManagedProfile) {
- return mDevicePolicyManager.getResources().getString(
- WORK_PROFILE_CONFIRM_PASSWORD,
- () -> getString(
- R.string.lockpassword_confirm_your_password_generic_profile));
- }
-
- return getString(R.string.lockpassword_confirm_your_password_generic);
- }
- return null;
- }
-
@Override
protected void onStart() {
super.onStart();
// Translucent activity that is "visible", so it doesn't complain about finish()
// not being called before onResume().
setVisible(true);
+
+ if ((getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK)
+ != Configuration.UI_MODE_NIGHT_YES) {
+ getWindow().getInsetsController().setSystemBarsAppearance(
+ APPEARANCE_LIGHT_STATUS_BARS, APPEARANCE_LIGHT_STATUS_BARS);
+ }
}
@Override
diff --git a/src/com/android/settings/password/ConfirmDeviceCredentialBaseFragment.java b/src/com/android/settings/password/ConfirmDeviceCredentialBaseFragment.java
index f4cfabc..43d8440 100644
--- a/src/com/android/settings/password/ConfirmDeviceCredentialBaseFragment.java
+++ b/src/com/android/settings/password/ConfirmDeviceCredentialBaseFragment.java
@@ -105,6 +105,8 @@
protected final Handler mHandler = new Handler();
protected boolean mFrp;
protected boolean mRemoteValidation;
+ protected boolean mRequestWriteRepairModePassword;
+ protected boolean mRepairMode;
protected CharSequence mAlternateButtonText;
protected BiometricManager mBiometricManager;
@Nullable protected RemoteLockscreenValidationSession mRemoteLockscreenValidationSession;
@@ -130,6 +132,8 @@
ChooseLockSettingsHelper.EXTRA_KEY_REQUEST_GK_PW_HANDLE, false);
mForceVerifyPath = intent.getBooleanExtra(
ChooseLockSettingsHelper.EXTRA_KEY_FORCE_VERIFY, false);
+ mRequestWriteRepairModePassword = intent.getBooleanExtra(
+ ChooseLockSettingsHelper.EXTRA_KEY_REQUEST_WRITE_REPAIR_MODE_PW, false);
if (intent.getBooleanExtra(IS_REMOTE_LOCKSCREEN_VALIDATION, false)) {
if (FeatureFlagUtils.isEnabled(getContext(),
@@ -178,6 +182,7 @@
mUserId = Utils.getUserIdFromBundle(getActivity(), intent.getExtras(),
isInternalActivity());
mFrp = (mUserId == LockPatternUtils.USER_FRP);
+ mRepairMode = (mUserId == LockPatternUtils.USER_REPAIR_MODE);
mUserManager = UserManager.get(getActivity());
mEffectiveUserId = mUserManager.getCredentialOwnerProfile(mUserId);
mLockPatternUtils = new LockPatternUtils(getActivity());
@@ -266,7 +271,7 @@
// verifyTiedProfileChallenge. In such case, we also wanna show the user message that
// fingerprint is disabled due to device restart.
protected boolean isStrongAuthRequired() {
- return mFrp
+ return mFrp || mRepairMode
|| !mLockPatternUtils.isBiometricAllowedForUser(mEffectiveUserId)
|| !mUserManager.isUserUnlocked(mUserId);
}
diff --git a/src/com/android/settings/password/ConfirmLockPassword.java b/src/com/android/settings/password/ConfirmLockPassword.java
index 03b89f2..b203015 100644
--- a/src/com/android/settings/password/ConfirmLockPassword.java
+++ b/src/com/android/settings/password/ConfirmLockPassword.java
@@ -18,12 +18,8 @@
import static android.app.admin.DevicePolicyResources.Strings.Settings.CONFIRM_WORK_PROFILE_PASSWORD_HEADER;
import static android.app.admin.DevicePolicyResources.Strings.Settings.CONFIRM_WORK_PROFILE_PIN_HEADER;
-import static android.app.admin.DevicePolicyResources.Strings.Settings.WORK_PROFILE_CONFIRM_PASSWORD;
-import static android.app.admin.DevicePolicyResources.Strings.Settings.WORK_PROFILE_CONFIRM_PIN;
import static android.app.admin.DevicePolicyResources.Strings.Settings.WORK_PROFILE_LAST_PASSWORD_ATTEMPT_BEFORE_WIPE;
import static android.app.admin.DevicePolicyResources.Strings.Settings.WORK_PROFILE_LAST_PIN_ATTEMPT_BEFORE_WIPE;
-import static android.app.admin.DevicePolicyResources.Strings.Settings.WORK_PROFILE_PASSWORD_REQUIRED;
-import static android.app.admin.DevicePolicyResources.Strings.Settings.WORK_PROFILE_PIN_REQUIRED;
import static android.app.admin.DevicePolicyResources.UNDEFINED;
import static com.android.settings.biometrics.GatekeeperPasswordProvider.containsGatekeeperPasswordHandle;
@@ -75,27 +71,12 @@
public class ConfirmLockPassword extends ConfirmDeviceCredentialBaseActivity {
- // The index of the array is isStrongAuth << 2 + isManagedProfile << 1 + isAlpha.
+ // The index of the array is isStrongAuth << 1 + isAlpha.
private static final int[] DETAIL_TEXTS = new int[] {
R.string.lockpassword_confirm_your_pin_generic,
R.string.lockpassword_confirm_your_password_generic,
- R.string.lockpassword_confirm_your_pin_generic_profile,
- R.string.lockpassword_confirm_your_password_generic_profile,
R.string.lockpassword_strong_auth_required_device_pin,
R.string.lockpassword_strong_auth_required_device_password,
- R.string.lockpassword_strong_auth_required_work_pin,
- R.string.lockpassword_strong_auth_required_work_password
- };
-
- private static final String[] DETAIL_TEXT_OVERRIDES = new String[] {
- UNDEFINED,
- UNDEFINED,
- WORK_PROFILE_CONFIRM_PIN,
- WORK_PROFILE_CONFIRM_PASSWORD,
- UNDEFINED,
- UNDEFINED,
- WORK_PROFILE_PIN_REQUIRED,
- WORK_PROFILE_PASSWORD_REQUIRED
};
public static class InternalActivity extends ConfirmLockPassword {
@@ -125,7 +106,7 @@
public static class ConfirmLockPasswordFragment extends ConfirmDeviceCredentialBaseFragment
implements OnClickListener, OnEditorActionListener,
- CredentialCheckResultTracker.Listener, SaveChosenLockWorkerBase.Listener,
+ CredentialCheckResultTracker.Listener, SaveAndFinishWorker.Listener,
RemoteLockscreenValidationFragment.Listener {
private static final String FRAGMENT_TAG_CHECK_LOCK_RESULT = "check_lock_result";
private ImeAwareEditText mPasswordEntry;
@@ -200,7 +181,12 @@
detailsMessage = getDefaultDetails();
}
mGlifLayout.setHeaderText(headerMessage);
- mGlifLayout.setDescriptionText(detailsMessage);
+
+ if (mIsManagedProfile) {
+ mGlifLayout.getDescriptionTextView().setVisibility(View.GONE);
+ } else {
+ mGlifLayout.setDescriptionText(detailsMessage);
+ }
mCheckBoxLabel = intent.getCharSequenceExtra(KeyguardManager.EXTRA_CHECKBOX_LABEL);
}
int currentType = mPasswordEntry.getInputType();
@@ -284,6 +270,11 @@
return mIsAlpha ? getString(R.string.lockpassword_confirm_your_password_header_frp)
: getString(R.string.lockpassword_confirm_your_pin_header_frp);
}
+ if (mRepairMode) {
+ return mIsAlpha
+ ? getString(R.string.lockpassword_confirm_repair_mode_password_header)
+ : getString(R.string.lockpassword_confirm_repair_mode_pin_header);
+ }
if (mRemoteValidation) {
return getString(R.string.lockpassword_remote_validation_header);
}
@@ -307,17 +298,20 @@
return mIsAlpha ? getString(R.string.lockpassword_confirm_your_password_details_frp)
: getString(R.string.lockpassword_confirm_your_pin_details_frp);
}
+ if (mRepairMode) {
+ return mIsAlpha
+ ? getString(R.string.lockpassword_confirm_repair_mode_password_details)
+ : getString(R.string.lockpassword_confirm_repair_mode_pin_details);
+ }
if (mRemoteValidation) {
return getContext().getString(mIsAlpha
? R.string.lockpassword_remote_validation_password_details
: R.string.lockpassword_remote_validation_pin_details);
}
boolean isStrongAuthRequired = isStrongAuthRequired();
- // Map boolean flags to an index by isStrongAuth << 2 + isManagedProfile << 1 + isAlpha.
- int index = ((isStrongAuthRequired ? 1 : 0) << 2) + ((mIsManagedProfile ? 1 : 0) << 1)
- + (mIsAlpha ? 1 : 0);
- return mDevicePolicyManager.getResources().getString(
- DETAIL_TEXT_OVERRIDES[index], () -> getString(DETAIL_TEXTS[index]));
+ // Map boolean flags to an index by isStrongAuth << 1 + isAlpha.
+ int index = ((isStrongAuthRequired ? 1 : 0) << 1) + (mIsAlpha ? 1 : 0);
+ return getString(DETAIL_TEXTS[index]);
}
private String getDefaultCheckboxLabel() {
@@ -496,7 +490,9 @@
}
} else if (mForceVerifyPath) {
if (isInternalActivity()) {
- startVerifyPassword(credential, intent, 0 /* flags */);
+ final int flags = mRequestWriteRepairModePassword
+ ? LockPatternUtils.VERIFY_FLAG_WRITE_REPAIR_MODE_PW : 0;
+ startVerifyPassword(credential, intent, flags);
return;
}
} else {
@@ -621,15 +617,15 @@
if (mCheckBox.isChecked() && mRemoteLockscreenValidationFragment
.getLockscreenCredential() != null) {
Log.i(TAG, "Setting device screen lock to the other device's screen lock.");
- ChooseLockPassword.SaveAndFinishWorker saveAndFinishWorker =
- new ChooseLockPassword.SaveAndFinishWorker();
+ SaveAndFinishWorker saveAndFinishWorker = new SaveAndFinishWorker();
getFragmentManager().beginTransaction().add(saveAndFinishWorker, null)
.commit();
getFragmentManager().executePendingTransactions();
- saveAndFinishWorker.setListener(this);
+ saveAndFinishWorker
+ .setListener(this)
+ .setRequestGatekeeperPasswordHandle(true);
saveAndFinishWorker.start(
mLockPatternUtils,
- /* requestGatekeeperPassword= */ true,
mRemoteLockscreenValidationFragment.getLockscreenCredential(),
/* currentCredential= */ null,
mEffectiveUserId);
diff --git a/src/com/android/settings/password/ConfirmLockPattern.java b/src/com/android/settings/password/ConfirmLockPattern.java
index e99a986..7160d64 100644
--- a/src/com/android/settings/password/ConfirmLockPattern.java
+++ b/src/com/android/settings/password/ConfirmLockPattern.java
@@ -93,7 +93,7 @@
public static class ConfirmLockPatternFragment extends ConfirmDeviceCredentialBaseFragment
implements AppearAnimationCreator<Object>, CredentialCheckResultTracker.Listener,
- SaveChosenLockWorkerBase.Listener, RemoteLockscreenValidationFragment.Listener {
+ SaveAndFinishWorker.Listener, RemoteLockscreenValidationFragment.Listener {
private static final String FRAGMENT_TAG_CHECK_LOCK_RESULT = "check_lock_result";
@@ -179,7 +179,7 @@
// ability to disable the pattern in L. Remove this block after
// ensuring it's safe to do so. (Note that ConfirmLockPassword
// doesn't have this).
- if (!mFrp && !mRemoteValidation
+ if (!mFrp && !mRemoteValidation && !mRepairMode
&& !mLockPatternUtils.isLockPatternEnabled(mEffectiveUserId)) {
getActivity().setResult(Activity.RESULT_OK);
getActivity().finish();
@@ -308,17 +308,17 @@
if (mFrp) {
return getString(R.string.lockpassword_confirm_your_pattern_details_frp);
}
+ if (mRepairMode) {
+ return getString(R.string.lockpassword_confirm_repair_mode_pattern_details);
+ }
if (mRemoteValidation) {
return getString(
R.string.lockpassword_remote_validation_pattern_details);
}
final boolean isStrongAuthRequired = isStrongAuthRequired();
- if (!mIsManagedProfile) {
- return isStrongAuthRequired
- ? getString(R.string.lockpassword_strong_auth_required_device_pattern)
- : getString(R.string.lockpassword_confirm_your_pattern_generic);
- }
- return null;
+ return isStrongAuthRequired
+ ? getString(R.string.lockpassword_strong_auth_required_device_pattern)
+ : getString(R.string.lockpassword_confirm_your_pattern_generic);
}
private Object[][] getActiveViews() {
@@ -368,7 +368,10 @@
CharSequence detailsText =
mDetailsText == null ? getDefaultDetails() : mDetailsText;
- if (detailsText != null) {
+
+ if (mIsManagedProfile) {
+ mGlifLayout.getDescriptionTextView().setVisibility(View.GONE);
+ } else {
mGlifLayout.setDescriptionText(detailsText);
}
@@ -402,7 +405,12 @@
}
private String getDefaultHeader() {
- if (mFrp) return getString(R.string.lockpassword_confirm_your_pattern_header_frp);
+ if (mFrp) {
+ return getString(R.string.lockpassword_confirm_your_pattern_header_frp);
+ }
+ if (mRepairMode) {
+ return getString(R.string.lockpassword_confirm_repair_mode_pattern_header);
+ }
if (mRemoteValidation) {
return getString(R.string.lockpassword_remote_validation_header);
}
@@ -512,7 +520,9 @@
}
} else if (mForceVerifyPath) {
if (isInternalActivity()) {
- startVerifyPattern(credential, intent, 0 /* flags */);
+ final int flags = mRequestWriteRepairModePassword
+ ? LockPatternUtils.VERIFY_FLAG_WRITE_REPAIR_MODE_PW : 0;
+ startVerifyPattern(credential, intent, flags);
return;
}
} else {
@@ -620,15 +630,15 @@
if (mCheckBox.isChecked() && mRemoteLockscreenValidationFragment
.getLockscreenCredential() != null) {
Log.i(TAG, "Setting device screen lock to the other device's screen lock.");
- ChooseLockPattern.SaveAndFinishWorker saveAndFinishWorker =
- new ChooseLockPattern.SaveAndFinishWorker();
+ SaveAndFinishWorker saveAndFinishWorker = new SaveAndFinishWorker();
getFragmentManager().beginTransaction().add(saveAndFinishWorker, null)
.commit();
getFragmentManager().executePendingTransactions();
- saveAndFinishWorker.setListener(this);
+ saveAndFinishWorker
+ .setListener(this)
+ .setRequestGatekeeperPasswordHandle(true);
saveAndFinishWorker.start(
mLockPatternUtils,
- /* requestGatekeeperPassword= */ true,
mRemoteLockscreenValidationFragment.getLockscreenCredential(),
/* currentCredential= */ null,
mEffectiveUserId);
diff --git a/src/com/android/settings/password/ForgotPasswordActivity.java b/src/com/android/settings/password/ForgotPasswordActivity.java
index 9afda18..92dc336 100644
--- a/src/com/android/settings/password/ForgotPasswordActivity.java
+++ b/src/com/android/settings/password/ForgotPasswordActivity.java
@@ -50,6 +50,7 @@
finish();
return;
}
+ ThemeHelper.trySetDynamicColor(this);
setContentView(R.layout.forgot_password_activity);
DevicePolicyManager devicePolicyManager = getSystemService(DevicePolicyManager.class);
diff --git a/src/com/android/settings/password/PasswordRequirementAdapter.java b/src/com/android/settings/password/PasswordRequirementAdapter.java
index 0e194af..0d8f02e 100644
--- a/src/com/android/settings/password/PasswordRequirementAdapter.java
+++ b/src/com/android/settings/password/PasswordRequirementAdapter.java
@@ -16,6 +16,8 @@
package com.android.settings.password;
+import android.annotation.NonNull;
+import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@@ -31,9 +33,12 @@
*/
public class PasswordRequirementAdapter extends
RecyclerView.Adapter<PasswordRequirementViewHolder> {
- private String[] mRequirements;
- public PasswordRequirementAdapter() {
+ private String[] mRequirements;
+ private Context mContext;
+
+ public PasswordRequirementAdapter(Context context) {
+ mContext = context;
setHasStableIds(true);
}
@@ -60,8 +65,18 @@
}
@Override
+ public void onViewAttachedToWindow(@NonNull PasswordRequirementViewHolder holder) {
+ holder.mDescriptionText.announceForAccessibility(holder.mDescriptionText.getText());
+ }
+
+ @Override
public void onBindViewHolder(PasswordRequirementViewHolder holder, int position) {
+ final int fontSize = mContext.getResources().getDimensionPixelSize(
+ R.dimen.password_requirement_font_size);
holder.mDescriptionText.setText(mRequirements[position]);
+ holder.mDescriptionText.setTextAppearance(R.style.ScreenLockPasswordHintTextFontStyle);
+ holder.mDescriptionText.setTextSize(fontSize / mContext.getResources()
+ .getDisplayMetrics().scaledDensity);
}
public static class PasswordRequirementViewHolder extends RecyclerView.ViewHolder {
diff --git a/src/com/android/settings/password/PasswordUtils.java b/src/com/android/settings/password/PasswordUtils.java
index e8e309c..a7edc89 100644
--- a/src/com/android/settings/password/PasswordUtils.java
+++ b/src/com/android/settings/password/PasswordUtils.java
@@ -27,7 +27,13 @@
import android.os.RemoteException;
import android.os.UserHandle;
import android.util.Log;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Button;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+import com.android.settings.R;
import com.android.settings.Utils;
public final class PasswordUtils extends com.android.settingslib.Utils {
@@ -97,4 +103,25 @@
Log.v(TAG, "Could not talk to activity manager.", e);
}
}
+
+ /** Setup screen lock options button under the Glif Header. */
+ public static void setupScreenLockOptionsButton(Context context, View view, Button optButton) {
+ final LinearLayout headerLayout = view.findViewById(
+ R.id.sud_layout_header);
+ final TextView sucTitleView = headerLayout.findViewById(R.id.suc_layout_title);
+ if (headerLayout != null && sucTitleView != null) {
+ final ViewGroup.MarginLayoutParams layoutTitleParams =
+ (ViewGroup.MarginLayoutParams) sucTitleView.getLayoutParams();
+ final ViewGroup.MarginLayoutParams lp = new ViewGroup.MarginLayoutParams(
+ ViewGroup.LayoutParams.WRAP_CONTENT,
+ ViewGroup.LayoutParams.WRAP_CONTENT);
+ lp.leftMargin = layoutTitleParams.leftMargin;
+ lp.topMargin = (int) context.getResources().getDimensionPixelSize(
+ R.dimen.screen_lock_options_button_margin_top);
+ optButton.setPadding(0, 0, 0, 0);
+ optButton.setLayoutParams(lp);
+ optButton.setText(context.getString(R.string.setup_lock_settings_options_button_label));
+ headerLayout.addView(optButton);
+ }
+ }
}
diff --git a/src/com/android/settings/password/SaveAndFinishWorker.java b/src/com/android/settings/password/SaveAndFinishWorker.java
new file mode 100644
index 0000000..40054b7
--- /dev/null
+++ b/src/com/android/settings/password/SaveAndFinishWorker.java
@@ -0,0 +1,218 @@
+/*
+ * Copyright (C) 2015 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.settings.password;
+
+import android.content.Intent;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.os.UserHandle;
+import android.util.Log;
+import android.util.Pair;
+import android.widget.Toast;
+
+import androidx.annotation.VisibleForTesting;
+import androidx.fragment.app.Fragment;
+
+import com.android.internal.widget.LockPatternUtils;
+import com.android.internal.widget.LockscreenCredential;
+import com.android.internal.widget.VerifyCredentialResponse;
+import com.android.settings.R;
+import com.android.settings.safetycenter.LockScreenSafetySource;
+
+/**
+ * An invisible retained worker fragment to track the AsyncWork that saves (and optionally
+ * verifies if a challenge is given) the chosen lock credential (pattern/pin/password).
+ */
+public class SaveAndFinishWorker extends Fragment {
+ private static final String TAG = "SaveAndFinishWorker";
+
+ private Listener mListener;
+ private boolean mFinished;
+ private Intent mResultData;
+
+ private LockPatternUtils mUtils;
+ private boolean mRequestGatekeeperPassword;
+ private boolean mRequestWriteRepairModePassword;
+ private boolean mWasSecureBefore;
+ private int mUserId;
+ private int mUnificationProfileId = UserHandle.USER_NULL;
+ private LockscreenCredential mUnificationProfileCredential;
+ private LockscreenCredential mChosenCredential;
+ private LockscreenCredential mCurrentCredential;
+
+ private boolean mBlocking;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setRetainInstance(true);
+ }
+
+ public SaveAndFinishWorker setListener(Listener listener) {
+ if (mListener == listener) {
+ return this;
+ }
+
+ mListener = listener;
+ if (mFinished && mListener != null) {
+ mListener.onChosenLockSaveFinished(mWasSecureBefore, mResultData);
+ }
+ return this;
+ }
+
+ @VisibleForTesting
+ void prepare(LockPatternUtils utils, LockscreenCredential chosenCredential,
+ LockscreenCredential currentCredential, int userId) {
+ mUtils = utils;
+ mUserId = userId;
+ // This will be a no-op for non managed profiles.
+ mWasSecureBefore = mUtils.isSecure(mUserId);
+ mFinished = false;
+ mResultData = null;
+
+ mChosenCredential = chosenCredential;
+ mCurrentCredential = currentCredential != null ? currentCredential
+ : LockscreenCredential.createNone();
+ }
+
+ public void start(LockPatternUtils utils, LockscreenCredential chosenCredential,
+ LockscreenCredential currentCredential, int userId) {
+ prepare(utils, chosenCredential, currentCredential, userId);
+ if (mBlocking) {
+ finish(saveAndVerifyInBackground().second);
+ } else {
+ new Task().execute();
+ }
+ }
+
+ /**
+ * Executes the save and verify work in background.
+ * @return pair where the first is a boolean confirming whether the change was successful or not
+ * and second is the Intent which has the challenge token or is null.
+ */
+ @VisibleForTesting
+ Pair<Boolean, Intent> saveAndVerifyInBackground() {
+ final int userId = mUserId;
+ try {
+ if (!mUtils.setLockCredential(mChosenCredential, mCurrentCredential, userId)) {
+ return Pair.create(false, null);
+ }
+ } catch (RuntimeException e) {
+ Log.e(TAG, "Failed to set lockscreen credential", e);
+ return Pair.create(false, null);
+ }
+
+ unifyProfileCredentialIfRequested();
+
+ @LockPatternUtils.VerifyFlag int flags = 0;
+ if (mRequestGatekeeperPassword) {
+ // If a Gatekeeper Password was requested, invoke the LockSettingsService code
+ // path to return a Gatekeeper Password based on the credential that the user
+ // chose. This should only be run if the credential was successfully set.
+ flags |= LockPatternUtils.VERIFY_FLAG_REQUEST_GK_PW_HANDLE;
+ }
+ if (mRequestWriteRepairModePassword) {
+ flags |= LockPatternUtils.VERIFY_FLAG_WRITE_REPAIR_MODE_PW;
+ }
+ if (flags == 0) {
+ return Pair.create(true, null);
+ }
+
+ Intent result = new Intent();
+ final VerifyCredentialResponse response = mUtils.verifyCredential(mChosenCredential,
+ userId, flags);
+ if (response.isMatched()) {
+ if (mRequestGatekeeperPassword && response.containsGatekeeperPasswordHandle()) {
+ result.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_GK_PW_HANDLE,
+ response.getGatekeeperPasswordHandle());
+ } else if (mRequestGatekeeperPassword) {
+ Log.e(TAG, "critical: missing GK PW handle for known good credential: " + response);
+ }
+ } else {
+ Log.e(TAG, "critical: bad response for known good credential: " + response);
+ }
+ if (mRequestWriteRepairModePassword) {
+ // Notify the caller if repair mode credential is saved successfully
+ result.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_WROTE_REPAIR_MODE_CREDENTIAL,
+ response.isMatched());
+ }
+
+ return Pair.create(true, result);
+ }
+
+ private void finish(Intent resultData) {
+ mFinished = true;
+ mResultData = resultData;
+ if (mListener != null) {
+ mListener.onChosenLockSaveFinished(mWasSecureBefore, mResultData);
+ }
+ if (mUnificationProfileCredential != null) {
+ mUnificationProfileCredential.zeroize();
+ }
+ LockScreenSafetySource.onLockScreenChange(getContext());
+ }
+
+ public SaveAndFinishWorker setRequestGatekeeperPasswordHandle(boolean value) {
+ mRequestGatekeeperPassword = value;
+ return this;
+ }
+
+ public SaveAndFinishWorker setRequestWriteRepairModePassword(boolean value) {
+ mRequestWriteRepairModePassword = value;
+ return this;
+ }
+
+ public SaveAndFinishWorker setBlocking(boolean blocking) {
+ mBlocking = blocking;
+ return this;
+ }
+
+ public SaveAndFinishWorker setProfileToUnify(
+ int profileId, LockscreenCredential credential) {
+ mUnificationProfileId = profileId;
+ mUnificationProfileCredential = credential.duplicate();
+ return this;
+ }
+
+ private void unifyProfileCredentialIfRequested() {
+ if (mUnificationProfileId != UserHandle.USER_NULL) {
+ mUtils.setSeparateProfileChallengeEnabled(mUnificationProfileId, false,
+ mUnificationProfileCredential);
+ }
+ }
+
+ private class Task extends AsyncTask<Void, Void, Pair<Boolean, Intent>> {
+
+ @Override
+ protected Pair<Boolean, Intent> doInBackground(Void... params){
+ return saveAndVerifyInBackground();
+ }
+
+ @Override
+ protected void onPostExecute(Pair<Boolean, Intent> resultData) {
+ if (!resultData.first) {
+ Toast.makeText(getContext(), R.string.lockpassword_credential_changed,
+ Toast.LENGTH_LONG).show();
+ }
+ finish(resultData.second);
+ }
+ }
+
+ interface Listener {
+ void onChosenLockSaveFinished(boolean wasSecureBefore, Intent resultData);
+ }
+}
diff --git a/src/com/android/settings/password/SaveChosenLockWorkerBase.java b/src/com/android/settings/password/SaveChosenLockWorkerBase.java
deleted file mode 100644
index 4864941..0000000
--- a/src/com/android/settings/password/SaveChosenLockWorkerBase.java
+++ /dev/null
@@ -1,142 +0,0 @@
-/*
- * Copyright (C) 2015 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.settings.password;
-
-import android.content.Intent;
-import android.os.AsyncTask;
-import android.os.Bundle;
-import android.os.UserHandle;
-import android.util.Pair;
-import android.widget.Toast;
-
-import androidx.fragment.app.Fragment;
-
-import com.android.internal.widget.LockPatternUtils;
-import com.android.internal.widget.LockscreenCredential;
-import com.android.settings.R;
-import com.android.settings.safetycenter.LockScreenSafetySource;
-
-/**
- * An invisible retained worker fragment to track the AsyncWork that saves (and optionally
- * verifies if a challenge is given) the chosen lock credential (pattern/pin/password).
- */
-abstract class SaveChosenLockWorkerBase extends Fragment {
-
- private Listener mListener;
- private boolean mFinished;
- private Intent mResultData;
-
- protected LockPatternUtils mUtils;
- protected boolean mRequestGatekeeperPassword;
- protected boolean mWasSecureBefore;
- protected int mUserId;
- protected int mUnificationProfileId = UserHandle.USER_NULL;
- protected LockscreenCredential mUnificationProfileCredential;
-
- private boolean mBlocking;
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setRetainInstance(true);
- }
-
- public void setListener(Listener listener) {
- if (mListener == listener) {
- return;
- }
-
- mListener = listener;
- if (mFinished && mListener != null) {
- mListener.onChosenLockSaveFinished(mWasSecureBefore, mResultData);
- }
- }
-
- protected void prepare(LockPatternUtils utils, boolean requestGatekeeperPassword, int userId) {
- mUtils = utils;
- mUserId = userId;
- mRequestGatekeeperPassword = requestGatekeeperPassword;
- // This will be a no-op for non managed profiles.
- mWasSecureBefore = mUtils.isSecure(mUserId);
- mFinished = false;
- mResultData = null;
- }
-
- protected void start() {
- if (mBlocking) {
- finish(saveAndVerifyInBackground().second);
- } else {
- new Task().execute();
- }
- }
-
- /**
- * Executes the save and verify work in background.
- * @return pair where the first is a boolean confirming whether the change was successful or not
- * and second is the Intent which has the challenge token or is null.
- */
- protected abstract Pair<Boolean, Intent> saveAndVerifyInBackground();
-
- protected void finish(Intent resultData) {
- mFinished = true;
- mResultData = resultData;
- if (mListener != null) {
- mListener.onChosenLockSaveFinished(mWasSecureBefore, mResultData);
- }
- if (mUnificationProfileCredential != null) {
- mUnificationProfileCredential.zeroize();
- }
- LockScreenSafetySource.onLockScreenChange(getContext());
- }
-
- public void setBlocking(boolean blocking) {
- mBlocking = blocking;
- }
-
- public void setProfileToUnify(int profileId, LockscreenCredential credential) {
- mUnificationProfileId = profileId;
- mUnificationProfileCredential = credential.duplicate();
- }
-
- protected void unifyProfileCredentialIfRequested() {
- if (mUnificationProfileId != UserHandle.USER_NULL) {
- mUtils.setSeparateProfileChallengeEnabled(mUnificationProfileId, false,
- mUnificationProfileCredential);
- }
- }
-
- private class Task extends AsyncTask<Void, Void, Pair<Boolean, Intent>> {
-
- @Override
- protected Pair<Boolean, Intent> doInBackground(Void... params){
- return saveAndVerifyInBackground();
- }
-
- @Override
- protected void onPostExecute(Pair<Boolean, Intent> resultData) {
- if (!resultData.first) {
- Toast.makeText(getContext(), R.string.lockpassword_credential_changed,
- Toast.LENGTH_LONG).show();
- }
- finish(resultData.second);
- }
- }
-
- interface Listener {
- void onChosenLockSaveFinished(boolean wasSecureBefore, Intent resultData);
- }
-}
diff --git a/src/com/android/settings/password/SetupChooseLockPassword.java b/src/com/android/settings/password/SetupChooseLockPassword.java
index 0101aa5..d0d7d93 100644
--- a/src/com/android/settings/password/SetupChooseLockPassword.java
+++ b/src/com/android/settings/password/SetupChooseLockPassword.java
@@ -24,6 +24,7 @@
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
+import android.view.ContextThemeWrapper;
import android.view.View;
import android.widget.Button;
@@ -97,7 +98,10 @@
}
if (showOptionsButton && anyOptionsShown) {
- mOptionsButton = view.findViewById(R.id.screen_lock_options);
+ mOptionsButton = new Button(new ContextThemeWrapper(getActivity(),
+ R.style.SudGlifButton_Tertiary));
+ mOptionsButton.setId(R.id.screen_lock_options);
+ PasswordUtils.setupScreenLockOptionsButton(getActivity(), view, mOptionsButton);
mOptionsButton.setVisibility(View.VISIBLE);
mOptionsButton.setOnClickListener((btn) ->
ChooseLockTypeDialogFragment.newInstance(mUserId)
diff --git a/src/com/android/settings/password/SetupChooseLockPattern.java b/src/com/android/settings/password/SetupChooseLockPattern.java
index 2cad181..560906d 100644
--- a/src/com/android/settings/password/SetupChooseLockPattern.java
+++ b/src/com/android/settings/password/SetupChooseLockPattern.java
@@ -23,6 +23,7 @@
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
+import android.view.ContextThemeWrapper;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@@ -83,7 +84,10 @@
LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = super.onCreateView(inflater, container, savedInstanceState);
if (!getResources().getBoolean(R.bool.config_lock_pattern_minimal_ui)) {
- mOptionsButton = view.findViewById(R.id.screen_lock_options);
+ mOptionsButton = new Button(new ContextThemeWrapper(getActivity(),
+ R.style.SudGlifButton_Tertiary));
+ mOptionsButton.setId(R.id.screen_lock_options);
+ PasswordUtils.setupScreenLockOptionsButton(getActivity(), view, mOptionsButton);
mOptionsButton.setOnClickListener((btn) ->
ChooseLockTypeDialogFragment.newInstance(mUserId)
.show(getChildFragmentManager(), TAG_SKIP_SCREEN_LOCK_DIALOG));
diff --git a/src/com/android/settings/print/PrintServiceSettingsFragment.java b/src/com/android/settings/print/PrintServiceSettingsFragment.java
index 39b5ab9..c5316e6 100644
--- a/src/com/android/settings/print/PrintServiceSettingsFragment.java
+++ b/src/com/android/settings/print/PrintServiceSettingsFragment.java
@@ -17,6 +17,7 @@
package com.android.settings.print;
import android.app.Activity;
+import android.app.ActivityOptions;
import android.app.settings.SettingsEnums;
import android.content.ComponentName;
import android.content.Context;
@@ -547,8 +548,13 @@
@Override
public void onClick(View v) {
try {
+ Bundle options = ActivityOptions.makeBasic()
+ .setPendingIntentBackgroundActivityStartMode(
+ ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED)
+ .toBundle();
getActivity().startIntentSender(
- printer.getInfoIntent().getIntentSender(), null, 0, 0, 0);
+ printer.getInfoIntent().getIntentSender(), null, 0, 0, 0,
+ options);
} catch (SendIntentException e) {
Log.e(LOG_TAG, "Could not execute pending info intent: %s", e);
}
diff --git a/src/com/android/settings/regionalpreferences/FirstDayOfWeekItemListController.java b/src/com/android/settings/regionalpreferences/FirstDayOfWeekItemListController.java
index d509d2e..03a59de 100644
--- a/src/com/android/settings/regionalpreferences/FirstDayOfWeekItemListController.java
+++ b/src/com/android/settings/regionalpreferences/FirstDayOfWeekItemListController.java
@@ -16,7 +16,6 @@
package com.android.settings.regionalpreferences;
-import android.app.settings.SettingsEnums;
import android.content.Context;
import com.android.settings.R;
@@ -58,9 +57,4 @@
protected String[] getUnitValues() {
return mContext.getResources().getStringArray(R.array.first_day_of_week);
}
-
- @Override
- protected int getMetricsActionKey() {
- return SettingsEnums.ACTION_SET_FIRST_DAY_OF_WEEK;
- }
}
diff --git a/src/com/android/settings/regionalpreferences/NumberingSystemItemController.java b/src/com/android/settings/regionalpreferences/NumberingSystemItemController.java
index e3a8d23..c1e77ab 100644
--- a/src/com/android/settings/regionalpreferences/NumberingSystemItemController.java
+++ b/src/com/android/settings/regionalpreferences/NumberingSystemItemController.java
@@ -152,8 +152,6 @@
private void handleLanguageSelect(Preference preference) {
String selectedLanguage = preference.getKey();
- mMetricsFeatureProvider.action(mContext,
- SettingsEnums.ACTION_CHOOSE_LANGUAGE_FOR_NUMBERS_PREFERENCES);
final Bundle extra = new Bundle();
extra.putString(RegionalPreferencesEntriesFragment.ARG_KEY_REGIONAL_PREFERENCE,
ARG_VALUE_NUMBERING_SYSTEM_SELECT);
@@ -176,8 +174,6 @@
Locale updatedLocale =
saveNumberingSystemToLocale(Locale.forLanguageTag(mSelectedLanguage),
numberingSystem);
- mMetricsFeatureProvider.action(mContext,
- SettingsEnums.ACTION_SET_NUMBERS_PREFERENCES);
// After updated locale to framework, this fragment will recreate,
// so it needs to update the argument of selected language.
Bundle bundle = new Bundle();
diff --git a/src/com/android/settings/regionalpreferences/RegionalPreferenceListBasePreferenceController.java b/src/com/android/settings/regionalpreferences/RegionalPreferenceListBasePreferenceController.java
index 1e39fff..0608b0c 100644
--- a/src/com/android/settings/regionalpreferences/RegionalPreferenceListBasePreferenceController.java
+++ b/src/com/android/settings/regionalpreferences/RegionalPreferenceListBasePreferenceController.java
@@ -16,7 +16,6 @@
package com.android.settings.regionalpreferences;
-import android.app.settings.SettingsEnums;
import android.content.Context;
import android.util.Log;
@@ -25,20 +24,16 @@
import androidx.preference.PreferenceScreen;
import com.android.settings.core.BasePreferenceController;
-import com.android.settings.overlay.FeatureFactory;
import com.android.settings.widget.TickButtonPreference;
-import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
/** A base controller for handling all regional preferences controllers. */
public abstract class RegionalPreferenceListBasePreferenceController extends
BasePreferenceController {
- private final MetricsFeatureProvider mMetricsFeatureProvider;
private PreferenceCategory mPreferenceCategory;
public RegionalPreferenceListBasePreferenceController(Context context, String preferenceKey) {
super(context, preferenceKey);
- mMetricsFeatureProvider = FeatureFactory.getFactory(context).getMetricsFeatureProvider();
}
@Override
@@ -59,6 +54,8 @@
TickButtonPreference pref = new TickButtonPreference(mContext);
mPreferenceCategory.addPreference(pref);
final String item = unitValues[i];
+ final String value = RegionalPreferencesDataUtils.getDefaultUnicodeExtensionData(
+ mContext, getExtensionTypes());
pref.setTitle(getPreferenceTitle(item));
pref.setKey(item);
pref.setOnPreferenceClickListener(clickedPref -> {
@@ -66,11 +63,8 @@
RegionalPreferencesDataUtils.savePreference(mContext, getExtensionTypes(),
item.equals(RegionalPreferencesDataUtils.DEFAULT_VALUE)
? null : item);
- mMetricsFeatureProvider.action(mContext, getMetricsActionKey());
return true;
});
- String value = RegionalPreferencesDataUtils.getDefaultUnicodeExtensionData(mContext,
- getExtensionTypes());
pref.setSelected(!value.isEmpty() && item.equals(value));
}
}
@@ -98,8 +92,4 @@
protected abstract String getExtensionTypes();
protected abstract String[] getUnitValues();
-
- protected abstract int getMetricsActionKey();
-
-
}
diff --git a/src/com/android/settings/regionalpreferences/TemperatureUnitListController.java b/src/com/android/settings/regionalpreferences/TemperatureUnitListController.java
index 91ab1a2..c51ca71 100644
--- a/src/com/android/settings/regionalpreferences/TemperatureUnitListController.java
+++ b/src/com/android/settings/regionalpreferences/TemperatureUnitListController.java
@@ -16,7 +16,6 @@
package com.android.settings.regionalpreferences;
-import android.app.settings.SettingsEnums;
import android.content.Context;
import com.android.settings.R;
@@ -56,9 +55,4 @@
protected String[] getUnitValues() {
return mContext.getResources().getStringArray(R.array.temperature_units);
}
-
- @Override
- protected int getMetricsActionKey() {
- return SettingsEnums.ACTION_SET_TEMPERATURE_UNIT;
- }
}
diff --git a/src/com/android/settings/search/SettingsSearchIndexablesProvider.java b/src/com/android/settings/search/SettingsSearchIndexablesProvider.java
index d6635a1..b081c7f 100644
--- a/src/com/android/settings/search/SettingsSearchIndexablesProvider.java
+++ b/src/com/android/settings/search/SettingsSearchIndexablesProvider.java
@@ -454,7 +454,7 @@
// Skip Settings injected items because they should be indexed in the sub-pages.
return false;
}
- return true;
+ return tile.isSearchable();
}
private static Object[] createIndexableRawColumnObjects(SearchIndexableRaw raw) {
diff --git a/src/com/android/settings/security/ScreenPinningSettings.java b/src/com/android/settings/security/ScreenPinningSettings.java
index e219b44..8fae6e1 100644
--- a/src/com/android/settings/security/ScreenPinningSettings.java
+++ b/src/com/android/settings/security/ScreenPinningSettings.java
@@ -23,7 +23,6 @@
import android.os.Bundle;
import android.os.UserHandle;
import android.os.UserManager;
-import android.provider.SearchIndexableResource;
import android.provider.Settings;
import android.widget.Switch;
@@ -38,14 +37,12 @@
import com.android.settings.SettingsActivity;
import com.android.settings.SettingsPreferenceFragment;
import com.android.settings.password.ChooseLockGeneric;
+import com.android.settings.password.ChooseLockSettingsHelper;
import com.android.settings.search.BaseSearchIndexProvider;
import com.android.settings.widget.SettingsMainSwitchBar;
import com.android.settingslib.search.SearchIndexable;
import com.android.settingslib.widget.FooterPreference;
import com.android.settingslib.widget.OnMainSwitchChangeListener;
-
-import java.util.Arrays;
-import java.util.List;
/**
* Screen pinning settings.
*/
@@ -56,6 +53,7 @@
private static final String KEY_USE_SCREEN_LOCK = "use_screen_lock";
private static final String KEY_FOOTER = "screen_pinning_settings_screen_footer";
private static final int CHANGE_LOCK_METHOD_REQUEST = 43;
+ private static final int CONFIRM_REQUEST = 1000;
private SettingsMainSwitchBar mSwitchBar;
private SwitchPreference mUseScreenLock;
@@ -129,10 +127,10 @@
}
private boolean setScreenLockUsed(boolean isEnabled) {
+ LockPatternUtils lockPatternUtils = new LockPatternUtils(getActivity());
+ final int passwordQuality = lockPatternUtils
+ .getKeyguardStoredPasswordQuality(UserHandle.myUserId());
if (isEnabled) {
- LockPatternUtils lockPatternUtils = new LockPatternUtils(getActivity());
- int passwordQuality = lockPatternUtils
- .getKeyguardStoredPasswordQuality(UserHandle.myUserId());
if (passwordQuality == DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED) {
Intent chooseLockIntent = new Intent(DevicePolicyManager.ACTION_SET_NEW_PASSWORD);
chooseLockIntent.putExtra(
@@ -141,6 +139,12 @@
startActivityForResult(chooseLockIntent, CHANGE_LOCK_METHOD_REQUEST);
return false;
}
+ } else {
+ if (passwordQuality != DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED) {
+ final ChooseLockSettingsHelper.Builder builder =
+ new ChooseLockSettingsHelper.Builder(getActivity(), this);
+ return builder.setRequestCode(CONFIRM_REQUEST).show();
+ }
}
setScreenLockUsedSetting(isEnabled);
return true;
@@ -162,6 +166,8 @@
setScreenLockUsed(validPassQuality);
// Make sure the screen updates.
mUseScreenLock.setChecked(validPassQuality);
+ } else if (requestCode == CONFIRM_REQUEST) {
+ setScreenLockUsedSetting(false);
}
}
@@ -245,14 +251,5 @@
* For search
*/
public static final BaseSearchIndexProvider SEARCH_INDEX_DATA_PROVIDER =
- new BaseSearchIndexProvider() {
-
- @Override
- public List<SearchIndexableResource> getXmlResourcesToIndex(Context context,
- boolean enabled) {
- final SearchIndexableResource sir = new SearchIndexableResource(context);
- sir.xmlResId = R.xml.screen_pinning_settings;
- return Arrays.asList(sir);
- }
- };
+ new BaseSearchIndexProvider(R.xml.screen_pinning_settings);
}
diff --git a/src/com/android/settings/shortcut/CreateShortcutPreferenceController.java b/src/com/android/settings/shortcut/CreateShortcutPreferenceController.java
index bf95348..8871135 100644
--- a/src/com/android/settings/shortcut/CreateShortcutPreferenceController.java
+++ b/src/com/android/settings/shortcut/CreateShortcutPreferenceController.java
@@ -45,11 +45,14 @@
import com.android.settings.R;
import com.android.settings.Settings;
+import com.android.settings.Settings.DataUsageSummaryActivity;
import com.android.settings.Settings.TetherSettingsActivity;
import com.android.settings.Settings.WifiTetherSettingsActivity;
import com.android.settings.activityembedding.ActivityEmbeddingUtils;
import com.android.settings.core.BasePreferenceController;
import com.android.settings.gestures.OneHandedSettingsUtils;
+import com.android.settings.network.SubscriptionUtil;
+import com.android.settings.network.telephony.MobileNetworkUtils;
import com.android.settings.overlay.FeatureFactory;
import com.android.settings.wifi.WifiUtils;
import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
@@ -204,6 +207,12 @@
Log.d(TAG, "Skipping non-system app: " + info.activityInfo);
continue;
}
+ if (info.activityInfo.name.endsWith(DataUsageSummaryActivity.class.getSimpleName())) {
+ if (!canShowDataUsage()) {
+ Log.d(TAG, "Skipping data usage settings:" + info.activityInfo);
+ continue;
+ }
+ }
shortcuts.add(info);
}
Collections.sort(shortcuts, SHORTCUT_COMPARATOR);
@@ -211,6 +220,12 @@
}
@VisibleForTesting
+ boolean canShowDataUsage() {
+ return SubscriptionUtil.isSimHardwareVisible(mContext)
+ && !MobileNetworkUtils.isMobileNetworkUserRestricted(mContext);
+ }
+
+ @VisibleForTesting
boolean canShowWifiHotspot() {
return WifiUtils.canShowWifiHotspot(mContext);
}
diff --git a/src/com/android/settings/sim/SimDialogActivity.java b/src/com/android/settings/sim/SimDialogActivity.java
index 7d39938..e7b0185 100644
--- a/src/com/android/settings/sim/SimDialogActivity.java
+++ b/src/com/android/settings/sim/SimDialogActivity.java
@@ -280,8 +280,20 @@
public void showEnableAutoDataSwitchDialog() {
final FragmentManager fragmentManager = getSupportFragmentManager();
SimDialogFragment fragment = createFragment(ENABLE_AUTO_DATA_SWITCH);
- fragment.show(fragmentManager, Integer.toString(ENABLE_AUTO_DATA_SWITCH));
+ if (fragmentManager.isStateSaved()) {
+ Log.w(TAG, "Failed to show EnableAutoDataSwitchDialog. The fragmentManager "
+ + "is StateSaved.");
+ forceClose();
+ return;
+ }
+ try {
+ fragment.show(fragmentManager, Integer.toString(ENABLE_AUTO_DATA_SWITCH));
+ } catch (Exception e) {
+ Log.e(TAG, "Failed to show EnableAutoDataSwitchDialog.", e);
+ forceClose();
+ return;
+ }
if (getResources().getBoolean(
R.bool.config_auto_data_switch_enables_cross_sim_calling)) {
// If auto data switch is already enabled on the non-DDS, the dialog for enabling it
diff --git a/src/com/android/settings/slices/RestrictedSliceUtils.java b/src/com/android/settings/slices/RestrictedSliceUtils.java
new file mode 100644
index 0000000..a5b5a14
--- /dev/null
+++ b/src/com/android/settings/slices/RestrictedSliceUtils.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.slices;
+
+import android.content.ContentResolver;
+import android.net.Uri;
+import android.provider.SettingsSlicesContract;
+
+/**
+ * A utility class to check slice Uris for restriction.
+ */
+public class RestrictedSliceUtils {
+
+ /**
+ * Uri for the notifying open networks Slice.
+ */
+ private static final Uri NOTIFY_OPEN_NETWORKS_SLICE_URI = new Uri.Builder()
+ .scheme(ContentResolver.SCHEME_CONTENT)
+ .authority(SettingsSliceProvider.SLICE_AUTHORITY)
+ .appendPath(SettingsSlicesContract.PATH_SETTING_ACTION)
+ .appendPath("notify_open_networks")
+ .build();
+
+ /**
+ * Uri for the auto turning on Wi-Fi Slice.
+ */
+ private static final Uri AUTO_TURN_ON_WIFI_SLICE_URI = new Uri.Builder()
+ .scheme(ContentResolver.SCHEME_CONTENT)
+ .authority(SettingsSliceProvider.SLICE_AUTHORITY)
+ .appendPath(SettingsSlicesContract.PATH_SETTING_ACTION)
+ .appendPath("enable_wifi_wakeup")
+ .build();
+
+ /**
+ * Uri for the usb tethering Slice.
+ */
+ private static final Uri USB_TETHERING_SLICE_URI = new Uri.Builder()
+ .scheme(ContentResolver.SCHEME_CONTENT)
+ .authority(SettingsSliceProvider.SLICE_AUTHORITY)
+ .appendPath(SettingsSlicesContract.PATH_SETTING_ACTION)
+ .appendPath("enable_usb_tethering")
+ .build();
+
+ /**
+ * Uri for the bluetooth tethering Slice.
+ */
+ private static final Uri BLUETOOTH_TETHERING_SLICE_URI = new Uri.Builder()
+ .scheme(ContentResolver.SCHEME_CONTENT)
+ .authority(SettingsSliceProvider.SLICE_AUTHORITY)
+ .appendPath(SettingsSlicesContract.PATH_SETTING_ACTION)
+ .appendPath("enable_bluetooth_tethering_2")
+ .build();
+
+ /**
+ * Returns true if the slice Uri restricts access to guest user.
+ */
+ public static boolean isGuestRestricted(Uri sliceUri) {
+ if (AUTO_TURN_ON_WIFI_SLICE_URI.equals(sliceUri)
+ || NOTIFY_OPEN_NETWORKS_SLICE_URI.equals(sliceUri)
+ || BLUETOOTH_TETHERING_SLICE_URI.equals(sliceUri)
+ || USB_TETHERING_SLICE_URI.equals(sliceUri)
+ || CustomSliceRegistry.MOBILE_DATA_SLICE_URI.equals(sliceUri)) {
+ return true;
+ }
+ return false;
+ }
+}
diff --git a/src/com/android/settings/slices/SettingsSliceProvider.java b/src/com/android/settings/slices/SettingsSliceProvider.java
index 12272a7..5d2bde3 100644
--- a/src/com/android/settings/slices/SettingsSliceProvider.java
+++ b/src/com/android/settings/slices/SettingsSliceProvider.java
@@ -30,6 +30,7 @@
import android.net.Uri;
import android.os.Binder;
import android.os.StrictMode;
+import android.os.UserManager;
import android.provider.Settings;
import android.provider.SettingsSlicesContract;
import android.text.TextUtils;
@@ -233,6 +234,14 @@
getContext().getTheme().rebase();
}
+ // Checking if some semi-sensitive slices are requested by a guest user. If so, will
+ // return an empty slice.
+ final UserManager userManager = getContext().getSystemService(UserManager.class);
+ if (userManager.isGuestUser() && RestrictedSliceUtils.isGuestRestricted(sliceUri)) {
+ Log.i(TAG, "Guest user access denied.");
+ return null;
+ }
+
// Before adding a slice to {@link CustomSliceManager}, please get approval
// from the Settings team.
if (CustomSliceRegistry.isValidUri(sliceUri)) {
diff --git a/src/com/android/settings/spa/SettingsSpaEnvironment.kt b/src/com/android/settings/spa/SettingsSpaEnvironment.kt
index 455fe9f..db88784 100644
--- a/src/com/android/settings/spa/SettingsSpaEnvironment.kt
+++ b/src/com/android/settings/spa/SettingsSpaEnvironment.kt
@@ -20,6 +20,7 @@
import android.util.FeatureFlagUtils
import com.android.settings.spa.app.AllAppListPageProvider
import com.android.settings.spa.app.AppsMainPageProvider
+import com.android.settings.spa.app.appcompat.UserAspectRatioAppsPageProvider
import com.android.settings.spa.app.appinfo.AppInfoSettingsProvider
import com.android.settings.spa.app.appinfo.CloneAppInfoSettingsProvider
import com.android.settings.spa.app.backgroundinstall.BackgroundInstalledAppsPageProvider
@@ -29,12 +30,14 @@
import com.android.settings.spa.app.specialaccess.InstallUnknownAppsListProvider
import com.android.settings.spa.app.specialaccess.MediaManagementAppsAppListProvider
import com.android.settings.spa.app.specialaccess.ModifySystemSettingsAppListProvider
+import com.android.settings.spa.app.specialaccess.NfcTagAppsSettingsProvider
import com.android.settings.spa.app.specialaccess.PictureInPictureListProvider
import com.android.settings.spa.app.specialaccess.SpecialAppAccessPageProvider
import com.android.settings.spa.app.specialaccess.WifiControlAppListProvider
import com.android.settings.spa.app.specialaccess.UseFullScreenIntentAppListProvider
import com.android.settings.spa.core.instrumentation.SpaLogProvider
import com.android.settings.spa.development.UsageStatsPageProvider
+import com.android.settings.spa.development.compat.PlatformCompatAppListPageProvider
import com.android.settings.spa.home.HomePageProvider
import com.android.settings.spa.network.NetworkAndInternetPageProvider
import com.android.settings.spa.notification.AppListNotificationsPageProvider
@@ -61,6 +64,7 @@
InstallUnknownAppsListProvider,
AlarmsAndRemindersAppListProvider,
WifiControlAppListProvider,
+ NfcTagAppsSettingsProvider,
)
}
@@ -81,7 +85,9 @@
LanguageAndInputPageProvider,
AppLanguagesPageProvider,
UsageStatsPageProvider,
+ PlatformCompatAppListPageProvider,
BackgroundInstalledAppsPageProvider,
+ UserAspectRatioAppsPageProvider,
CloneAppInfoSettingsProvider,
NetworkAndInternetPageProvider,
) + togglePermissionAppListTemplate.createPageProviders(),
@@ -93,5 +99,5 @@
override val logger =
if (FeatureFlagUtils.isEnabled(context, FeatureFlagUtils.SETTINGS_ENABLE_SPA_METRICS))
SpaLogProvider
- else object: SpaLogger {}
+ else object : SpaLogger {}
}
diff --git a/src/com/android/settings/spa/app/appcompat/UserAspectRatioAppPreference.kt b/src/com/android/settings/spa/app/appcompat/UserAspectRatioAppPreference.kt
new file mode 100644
index 0000000..61098e8
--- /dev/null
+++ b/src/com/android/settings/spa/app/appcompat/UserAspectRatioAppPreference.kt
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.spa.app.appcompat
+
+import android.content.Context
+import android.content.pm.ApplicationInfo
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.android.settings.R
+import com.android.settings.applications.appcompat.UserAspectRatioDetails
+import com.android.settings.applications.appcompat.UserAspectRatioManager
+import com.android.settings.applications.appinfo.AppInfoDashboardFragment
+import com.android.settings.spa.app.appinfo.AppInfoSettingsProvider
+import com.android.settingslib.spa.widget.preference.Preference
+import com.android.settingslib.spa.widget.preference.PreferenceModel
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.flow
+import kotlinx.coroutines.flow.flowOn
+
+@Composable
+fun UserAspectRatioAppPreference(app: ApplicationInfo) {
+ val context = LocalContext.current
+ val presenter = remember { UserAspectRatioAppPresenter(context, app) }
+ if (!presenter.isAvailableFlow.collectAsStateWithLifecycle(initialValue = false).value) return
+
+ Preference(object : PreferenceModel {
+ override val title = stringResource(R.string.aspect_ratio_experimental_title)
+ override val summary = presenter.summaryFlow.collectAsStateWithLifecycle(
+ initialValue = stringResource(R.string.summary_placeholder),
+ )
+ override val onClick = presenter::startActivity
+ })
+}
+
+class UserAspectRatioAppPresenter(
+ private val context: Context,
+ private val app: ApplicationInfo,
+) {
+ private val manager = UserAspectRatioManager(context)
+
+ val isAvailableFlow = flow {
+ emit(UserAspectRatioManager.isFeatureEnabled(context)
+ && manager.canDisplayAspectRatioUi(app))
+ }.flowOn(Dispatchers.IO)
+
+ fun startActivity() =
+ navigateToAppAspectRatioSettings(context, app)
+
+ val summaryFlow = flow {
+ emit(manager.getUserMinAspectRatioEntry(app.packageName, context.userId))
+ }.flowOn(Dispatchers.IO)
+}
+
+fun navigateToAppAspectRatioSettings(context: Context, app: ApplicationInfo) {
+ AppInfoDashboardFragment.startAppInfoFragment(
+ UserAspectRatioDetails::class.java,
+ app,
+ context,
+ AppInfoSettingsProvider.METRICS_CATEGORY,
+ )
+}
\ No newline at end of file
diff --git a/src/com/android/settings/spa/app/appcompat/UserAspectRatioAppsPageProvider.kt b/src/com/android/settings/spa/app/appcompat/UserAspectRatioAppsPageProvider.kt
new file mode 100644
index 0000000..deea745
--- /dev/null
+++ b/src/com/android/settings/spa/app/appcompat/UserAspectRatioAppsPageProvider.kt
@@ -0,0 +1,220 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.spa.app.appcompat
+
+import android.content.Context
+import android.content.pm.ApplicationInfo
+import android.content.pm.PackageInfo
+import android.content.pm.PackageManager
+import android.content.pm.PackageManager.GET_ACTIVITIES
+import android.content.pm.PackageManager.USER_MIN_ASPECT_RATIO_UNSET
+import android.os.Build
+import android.os.Bundle
+import android.util.Log
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.padding
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.State
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.android.settings.R
+import com.android.settings.applications.appcompat.UserAspectRatioManager
+import com.android.settingslib.spa.framework.common.SettingsEntryBuilder
+import com.android.settingslib.spa.framework.common.SettingsPageProvider
+import com.android.settingslib.spa.framework.common.SpaEnvironmentFactory
+import com.android.settingslib.spa.framework.common.createSettingsPage
+import com.android.settingslib.spa.framework.compose.navigator
+import com.android.settingslib.spa.framework.compose.rememberContext
+import com.android.settingslib.spa.framework.compose.toState
+import com.android.settingslib.spa.framework.theme.SettingsDimension
+import com.android.settingslib.spa.framework.util.asyncMap
+import com.android.settingslib.spa.framework.util.filterItem
+import com.android.settingslib.spa.widget.preference.Preference
+import com.android.settingslib.spa.widget.preference.PreferenceModel
+import com.android.settingslib.spa.widget.illustration.Illustration
+import com.android.settingslib.spa.widget.illustration.IllustrationModel
+import com.android.settingslib.spa.widget.illustration.ResourceType
+import com.android.settingslib.spa.widget.ui.SettingsBody
+import com.android.settingslib.spa.widget.ui.SpinnerOption
+import com.android.settingslib.spaprivileged.model.app.AppListModel
+import com.android.settingslib.spaprivileged.model.app.AppRecord
+import com.android.settingslib.spaprivileged.model.app.userId
+import com.android.settingslib.spaprivileged.template.app.AppList
+import com.android.settingslib.spaprivileged.template.app.AppListInput
+import com.android.settingslib.spaprivileged.template.app.AppListItem
+import com.android.settingslib.spaprivileged.template.app.AppListItemModel
+import com.android.settingslib.spaprivileged.template.app.AppListPage
+import com.google.common.annotations.VisibleForTesting
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.flow
+import kotlinx.coroutines.flow.flowOn
+
+object UserAspectRatioAppsPageProvider : SettingsPageProvider {
+ override val name = "UserAspectRatioAppsPage"
+ private val owner = createSettingsPage()
+
+ override fun isEnabled(arguments: Bundle?): Boolean =
+ UserAspectRatioManager.isFeatureEnabled(SpaEnvironmentFactory.instance.appContext)
+
+ @Composable
+ override fun Page(arguments: Bundle?) =
+ UserAspectRatioAppList()
+
+ @Composable
+ @VisibleForTesting
+ fun EntryItem() =
+ Preference(object : PreferenceModel {
+ override val title = stringResource(R.string.aspect_ratio_experimental_title)
+ override val summary = getSummary().toState()
+ override val onClick = navigator(name)
+ })
+
+ @VisibleForTesting
+ fun buildInjectEntry() = SettingsEntryBuilder
+ .createInject(owner)
+ .setSearchDataFn { null }
+ .setUiLayoutFn { EntryItem() }
+
+ @Composable
+ @VisibleForTesting
+ fun getSummary(): String = stringResource(R.string.aspect_ratio_summary_text, Build.MODEL)
+}
+
+@Composable
+fun UserAspectRatioAppList(
+ appList: @Composable AppListInput<UserAspectRatioAppListItemModel>.() -> Unit
+ = { AppList() },
+) {
+ AppListPage(
+ title = stringResource(R.string.aspect_ratio_experimental_title),
+ listModel = rememberContext(::UserAspectRatioAppListModel),
+ appList = appList,
+ header = {
+ Box(Modifier.padding(SettingsDimension.itemPadding)) {
+ SettingsBody(stringResource(R.string.aspect_ratio_main_summary_text, Build.MODEL))
+ }
+ Illustration(object : IllustrationModel {
+ override val resId = R.raw.user_aspect_ratio_education
+ override val resourceType = ResourceType.LOTTIE
+ })
+ },
+ noMoreOptions = true,
+ )
+}
+
+data class UserAspectRatioAppListItemModel(
+ override val app: ApplicationInfo,
+ val userOverride: Int,
+ val suggested: Boolean,
+ val canDisplay: Boolean,
+) : AppRecord
+
+class UserAspectRatioAppListModel(private val context: Context)
+ : AppListModel<UserAspectRatioAppListItemModel> {
+
+ private val packageManager = context.packageManager
+ private val userAspectRatioManager = UserAspectRatioManager(context)
+
+ override fun getSpinnerOptions(
+ recordList: List<UserAspectRatioAppListItemModel>
+ ): List<SpinnerOption> {
+ val hasSuggested = recordList.any { it.suggested }
+ val hasOverride = recordList.any { it.userOverride != USER_MIN_ASPECT_RATIO_UNSET }
+ val options = mutableListOf(SpinnerItem.All)
+ // Add suggested filter first as default
+ if (hasSuggested) options.add(0, SpinnerItem.Suggested)
+ if (hasOverride) options += SpinnerItem.Overridden
+ return options.map {
+ SpinnerOption(
+ id = it.ordinal,
+ text = context.getString(it.stringResId),
+ )
+ }
+ }
+
+ @Composable
+ override fun AppListItemModel<UserAspectRatioAppListItemModel>.AppItem() {
+ val app = record.app
+ AppListItem(
+ onClick = { navigateToAppAspectRatioSettings(context, app) }
+ )
+ }
+
+ override fun transform(userIdFlow: Flow<Int>, appListFlow: Flow<List<ApplicationInfo>>) =
+ userIdFlow.combine(appListFlow) { uid, appList ->
+ appList.asyncMap { app ->
+ UserAspectRatioAppListItemModel(
+ app = app,
+ suggested = !app.isSystemApp && getPackageAndActivityInfo(
+ app)?.isFixedOrientationOrAspectRatio() == true,
+ userOverride = userAspectRatioManager.getUserMinAspectRatioValue(
+ app.packageName, uid),
+ canDisplay = userAspectRatioManager.canDisplayAspectRatioUi(app),
+ )
+ }
+ }
+
+ override fun filter(
+ userIdFlow: Flow<Int>,
+ option: Int,
+ recordListFlow: Flow<List<UserAspectRatioAppListItemModel>>
+ ): Flow<List<UserAspectRatioAppListItemModel>> = recordListFlow.filterItem(
+ when (SpinnerItem.values().getOrNull(option)) {
+ SpinnerItem.Suggested -> ({ it.canDisplay && it.suggested })
+ SpinnerItem.Overridden -> ({ it.userOverride != USER_MIN_ASPECT_RATIO_UNSET })
+ else -> ({ it.canDisplay })
+ }
+ )
+
+ @Composable
+ override fun getSummary(option: Int, record: UserAspectRatioAppListItemModel) : State<String> =
+ remember(record.userOverride) {
+ flow {
+ emit(userAspectRatioManager.getUserMinAspectRatioEntry(record.userOverride,
+ record.app.packageName))
+ }.flowOn(Dispatchers.IO)
+ }.collectAsStateWithLifecycle(initialValue = stringResource(R.string.summary_placeholder))
+
+ private fun getPackageAndActivityInfo(app: ApplicationInfo): PackageInfo? = try {
+ packageManager.getPackageInfoAsUser(app.packageName, GET_ACTIVITIES_FLAGS, app.userId)
+ } catch (e: Exception) {
+ // Query PackageManager.getPackageInfoAsUser() with GET_ACTIVITIES_FLAGS could cause
+ // exception sometimes. Since we reply on this flag to retrieve the Picture In Picture
+ // packages, we need to catch the exception to alleviate the impact before PackageManager
+ // fixing this issue or provide a better api.
+ Log.e(TAG, "Exception while getPackageInfoAsUser", e)
+ null
+ }
+
+ companion object {
+ private const val TAG = "AspectRatioAppsListModel"
+ private fun PackageInfo.isFixedOrientationOrAspectRatio() =
+ activities?.any { a -> a.isFixedOrientation || a.hasFixedAspectRatio() } ?: false
+ private val GET_ACTIVITIES_FLAGS =
+ PackageManager.PackageInfoFlags.of(GET_ACTIVITIES.toLong())
+ }
+}
+
+private enum class SpinnerItem(val stringResId: Int) {
+ Suggested(R.string.user_aspect_ratio_suggested_apps_label),
+ All(R.string.filter_all_apps),
+ Overridden(R.string.user_aspect_ratio_changed_apps_label)
+}
\ No newline at end of file
diff --git a/src/com/android/settings/spa/app/appinfo/AppInfoSettings.kt b/src/com/android/settings/spa/app/appinfo/AppInfoSettings.kt
index f906c16..a9d16ae 100644
--- a/src/com/android/settings/spa/app/appinfo/AppInfoSettings.kt
+++ b/src/com/android/settings/spa/app/appinfo/AppInfoSettings.kt
@@ -34,6 +34,7 @@
import com.android.settings.applications.AppInfoBase
import com.android.settings.applications.appinfo.AppInfoDashboardFragment
import com.android.settings.spa.SpaActivity.Companion.startSpaActivity
+import com.android.settings.spa.app.appcompat.UserAspectRatioAppPreference
import com.android.settings.spa.app.specialaccess.AlarmsAndRemindersAppListProvider
import com.android.settings.spa.app.specialaccess.DisplayOverOtherAppsAppListProvider
import com.android.settings.spa.app.specialaccess.InstallUnknownAppsListProvider
@@ -148,6 +149,7 @@
}
Category(title = stringResource(R.string.advanced_apps)) {
+ UserAspectRatioAppPreference(app)
DisplayOverOtherAppsAppListProvider.InfoPageEntryItem(app)
ModifySystemSettingsAppListProvider.InfoPageEntryItem(app)
PictureInPictureListProvider.InfoPageEntryItem(app)
diff --git a/src/com/android/settings/spa/app/specialaccess/NfcTagAppsSettings.kt b/src/com/android/settings/spa/app/specialaccess/NfcTagAppsSettings.kt
new file mode 100644
index 0000000..3dede42
--- /dev/null
+++ b/src/com/android/settings/spa/app/specialaccess/NfcTagAppsSettings.kt
@@ -0,0 +1,127 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.spa.app.specialaccess
+
+import android.content.Context
+import android.content.pm.ApplicationInfo
+import android.content.pm.PackageManager.GET_ACTIVITIES
+import android.content.pm.PackageManager.PackageInfoFlags
+import android.nfc.NfcAdapter
+import android.util.Log
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.livedata.observeAsState
+import com.android.settings.R
+import com.android.settingslib.spaprivileged.model.app.AppRecord
+import com.android.settingslib.spaprivileged.model.app.userId
+import com.android.settingslib.spaprivileged.template.app.TogglePermissionAppListModel
+import com.android.settingslib.spaprivileged.template.app.TogglePermissionAppListProvider
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.map
+
+object NfcTagAppsSettingsProvider : TogglePermissionAppListProvider {
+ override val permissionType = "NfcTagAppsSettings"
+ override fun createModel(context: Context) = NfcTagAppsSettingsListModel(context)
+}
+
+data class NfcTagAppsSettingsRecord(
+ override val app: ApplicationInfo,
+ val controller: NfcTagAppsSettingsController,
+ val isSupported: Boolean,
+) : AppRecord
+
+class NfcTagAppsSettingsListModel(private val context: Context) :
+ TogglePermissionAppListModel<NfcTagAppsSettingsRecord> {
+ override val pageTitleResId = R.string.change_nfc_tag_apps_title
+ override val switchTitleResId = R.string.change_nfc_tag_apps_detail_switch
+ override val footerResId = R.string.change_nfc_tag_apps_detail_summary
+
+ private val packageManager = context.packageManager
+
+ override fun transform(
+ userIdFlow: Flow<Int>,
+ appListFlow: Flow<List<ApplicationInfo>>
+ ): Flow<List<NfcTagAppsSettingsRecord>> =
+ userIdFlow.combine(appListFlow) { userId, appList ->
+ // The appListFlow always refreshed on resume, need to update nfcTagAppsSettingsPackages
+ // here to handle status change.
+ val nfcTagAppsSettingsPackages = getNfcTagAppsSettingsPackages(userId)
+ appList.map { app ->
+ createNfcTagAppsSettingsRecord(
+ app = app,
+ isAllowed = nfcTagAppsSettingsPackages[app.packageName],
+ )
+ }
+ }
+
+ private fun getNfcTagAppsSettingsPackages(userId: Int): Map<String, Boolean> {
+ NfcAdapter.getDefaultAdapter(context)?.let { nfcAdapter ->
+ if (nfcAdapter.isTagIntentAppPreferenceSupported) {
+ return nfcAdapter.getTagIntentAppPreferenceForUser(userId)
+ }
+ }
+ return emptyMap()
+ }
+
+ override fun transformItem(app: ApplicationInfo) =
+ createNfcTagAppsSettingsRecord(
+ app = app,
+ isAllowed = getNfcTagAppsSettingsPackages(app.userId)[app.packageName],
+ )
+
+ private fun createNfcTagAppsSettingsRecord(
+ app: ApplicationInfo,
+ isAllowed: Boolean?,
+ ) =
+ NfcTagAppsSettingsRecord(
+ app = app,
+ isSupported = isAllowed != null,
+ controller = NfcTagAppsSettingsController(isAllowed == true),
+ )
+
+ override fun filter(
+ userIdFlow: Flow<Int>,
+ recordListFlow: Flow<List<NfcTagAppsSettingsRecord>>
+ ) = recordListFlow.map { recordList -> recordList.filter { it.isSupported } }
+
+ @Composable
+ override fun isAllowed(record: NfcTagAppsSettingsRecord) =
+ record.controller.isAllowed.observeAsState()
+
+ override fun isChangeable(record: NfcTagAppsSettingsRecord) = true
+
+ override fun setAllowed(record: NfcTagAppsSettingsRecord, newAllowed: Boolean) {
+ NfcAdapter.getDefaultAdapter(context)?.let {
+ if (
+ it.setTagIntentAppPreferenceForUser(
+ record.app.userId,
+ record.app.packageName,
+ newAllowed
+ ) == NfcAdapter.TAG_INTENT_APP_PREF_RESULT_SUCCESS
+ ) {
+ record.controller.setAllowed(newAllowed)
+ } else {
+ Log.e(TAG, "Error updating TagIntentAppPreference")
+ }
+ }
+ }
+
+ private companion object {
+ const val TAG = "NfcTagAppsSettingsListModel"
+ val GET_ACTIVITIES_FLAGS = PackageInfoFlags.of(GET_ACTIVITIES.toLong())
+ }
+}
diff --git a/src/com/android/settings/spa/app/specialaccess/NfcTagAppsSettingsController.kt b/src/com/android/settings/spa/app/specialaccess/NfcTagAppsSettingsController.kt
new file mode 100644
index 0000000..6e1b7b3
--- /dev/null
+++ b/src/com/android/settings/spa/app/specialaccess/NfcTagAppsSettingsController.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.spa.app.specialaccess
+
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+
+class NfcTagAppsSettingsController(initialStatus: Boolean) {
+ val isAllowed: LiveData<Boolean>
+ get() = _allowed
+
+ fun setAllowed(newAllowed: Boolean) {
+ _allowed.postValue(newAllowed)
+ }
+ private val _allowed = MutableLiveData<Boolean>(initialStatus)
+}
diff --git a/src/com/android/settings/spa/development/UsageStats.kt b/src/com/android/settings/spa/development/UsageStats.kt
index b681d75..4d9c455 100644
--- a/src/com/android/settings/spa/development/UsageStats.kt
+++ b/src/com/android/settings/spa/development/UsageStats.kt
@@ -32,7 +32,6 @@
AppListPage(
title = stringResource(R.string.testing_usage_stats),
listModel = rememberContext(::UsageStatsListModel),
- primaryUserOnly = true,
)
}
}
diff --git a/src/com/android/settings/spa/development/compat/PlatformCompatAppList.kt b/src/com/android/settings/spa/development/compat/PlatformCompatAppList.kt
new file mode 100644
index 0000000..5f3b4e7
--- /dev/null
+++ b/src/com/android/settings/spa/development/compat/PlatformCompatAppList.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.spa.development.compat
+
+import android.os.Bundle
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.res.stringResource
+import com.android.settings.R
+import com.android.settingslib.spa.framework.common.SettingsPageProvider
+import com.android.settingslib.spa.framework.compose.rememberContext
+import com.android.settingslib.spaprivileged.template.app.AppListPage
+
+object PlatformCompatAppListPageProvider : SettingsPageProvider {
+ override val name = "PlatformCompatAppList"
+
+ @Composable
+ override fun Page(arguments: Bundle?) {
+ AppListPage(
+ title = stringResource(R.string.platform_compat_dashboard_title),
+ listModel = rememberContext(::PlatformCompatAppListModel),
+ noItemMessage = stringResource(R.string.platform_compat_dialog_text_no_apps),
+ )
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/settings/spa/development/compat/PlatformCompatAppListModel.kt b/src/com/android/settings/spa/development/compat/PlatformCompatAppListModel.kt
new file mode 100644
index 0000000..c6752b9
--- /dev/null
+++ b/src/com/android/settings/spa/development/compat/PlatformCompatAppListModel.kt
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.spa.development.compat
+
+import android.app.settings.SettingsEnums
+import android.content.Context
+import android.content.pm.ApplicationInfo
+import android.os.Build
+import androidx.compose.runtime.Composable
+import androidx.core.os.bundleOf
+import com.android.settings.core.SubSettingLauncher
+import com.android.settings.development.compat.PlatformCompatDashboard
+import com.android.settingslib.spa.framework.compose.stateOf
+import com.android.settingslib.spa.framework.util.filterItem
+import com.android.settingslib.spa.framework.util.mapItem
+import com.android.settingslib.spaprivileged.model.app.AppListModel
+import com.android.settingslib.spaprivileged.model.app.AppRecord
+import com.android.settingslib.spaprivileged.model.app.hasFlag
+import com.android.settingslib.spaprivileged.model.app.userHandle
+import com.android.settingslib.spaprivileged.template.app.AppListItem
+import com.android.settingslib.spaprivileged.template.app.AppListItemModel
+import kotlinx.coroutines.flow.Flow
+
+data class PlatformCompatAppRecord(
+ override val app: ApplicationInfo,
+) : AppRecord
+
+class PlatformCompatAppListModel(
+ private val context: Context,
+) : AppListModel<PlatformCompatAppRecord> {
+
+ override fun transform(userIdFlow: Flow<Int>, appListFlow: Flow<List<ApplicationInfo>>) =
+ appListFlow.mapItem(::PlatformCompatAppRecord)
+
+ override fun filter(
+ userIdFlow: Flow<Int>, option: Int, recordListFlow: Flow<List<PlatformCompatAppRecord>>,
+ ) = recordListFlow.filterItem { record ->
+ Build.IS_DEBUGGABLE || record.app.hasFlag(ApplicationInfo.FLAG_DEBUGGABLE)
+ }
+
+ @Composable
+ override fun getSummary(option: Int, record: PlatformCompatAppRecord) =
+ stateOf(record.app.packageName)
+
+ @Composable
+ override fun AppListItemModel<PlatformCompatAppRecord>.AppItem() {
+ AppListItem { navigateToAppCompat(app = record.app) }
+ }
+
+ private fun navigateToAppCompat(app: ApplicationInfo) {
+ SubSettingLauncher(context)
+ .setDestination(PlatformCompatDashboard::class.qualifiedName)
+ .setSourceMetricsCategory(SettingsEnums.DEVELOPMENT)
+ .setArguments(bundleOf(PlatformCompatDashboard.COMPAT_APP to app.packageName))
+ .setUserHandle(app.userHandle)
+ .launch()
+ }
+}
diff --git a/src/com/android/settings/spa/development/compat/PlatformCompatPreferenceController.kt b/src/com/android/settings/spa/development/compat/PlatformCompatPreferenceController.kt
new file mode 100644
index 0000000..c0a421c
--- /dev/null
+++ b/src/com/android/settings/spa/development/compat/PlatformCompatPreferenceController.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.settings.spa.development.compat
+
+import android.content.Context
+import androidx.preference.Preference
+import com.android.settings.core.BasePreferenceController
+import com.android.settings.spa.SpaActivity.Companion.startSpaActivity
+
+class PlatformCompatPreferenceController(context: Context, preferenceKey: String) :
+ BasePreferenceController(context, preferenceKey) {
+ override fun getAvailabilityStatus() = AVAILABLE
+
+ override fun handlePreferenceTreeClick(preference: Preference): Boolean {
+ if (preference.key == mPreferenceKey) {
+ mContext.startSpaActivity(PlatformCompatAppListPageProvider.name)
+ return true
+ }
+ return false
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/settings/system/FactoryResetDemoUserPreferenceController.java b/src/com/android/settings/system/FactoryResetDemoUserPreferenceController.java
new file mode 100644
index 0000000..f6a9b31
--- /dev/null
+++ b/src/com/android/settings/system/FactoryResetDemoUserPreferenceController.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2017 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.settings.system;
+
+import android.content.Context;
+import com.android.settings.Utils;
+
+public class FactoryResetDemoUserPreferenceController extends FactoryResetPreferenceController {
+
+ public FactoryResetDemoUserPreferenceController(Context context, String preferenceKey) {
+ super(context, preferenceKey);
+ }
+
+ /** Hide demo user specific "Factory reset" settings for non demo users. */
+ @Override
+ public int getAvailabilityStatus() {
+ return Utils.isDemoUser(mContext) ? AVAILABLE : DISABLED_FOR_USER;
+ }
+}
diff --git a/src/com/android/settings/system/FactoryResetPreferenceController.java b/src/com/android/settings/system/FactoryResetPreferenceController.java
index a307171..6e010c1 100644
--- a/src/com/android/settings/system/FactoryResetPreferenceController.java
+++ b/src/com/android/settings/system/FactoryResetPreferenceController.java
@@ -24,35 +24,26 @@
import com.android.settings.R;
import com.android.settings.Settings;
import com.android.settings.Utils;
-import com.android.settings.core.PreferenceControllerMixin;
-import com.android.settingslib.core.AbstractPreferenceController;
+import com.android.settings.core.BasePreferenceController;
-public class FactoryResetPreferenceController extends AbstractPreferenceController
- implements PreferenceControllerMixin {
- /** Key of the "Factory reset" preference in {@link R.xml.reset_dashboard_fragment}. */
- private static final String KEY_FACTORY_RESET = "factory_reset";
+public class FactoryResetPreferenceController extends BasePreferenceController {
private final UserManager mUm;
- public FactoryResetPreferenceController(Context context) {
- super(context);
+ public FactoryResetPreferenceController(Context context, String preferenceKey) {
+ super(context, preferenceKey);
mUm = (UserManager) context.getSystemService(Context.USER_SERVICE);
}
- /** Hide "Factory reset" settings for secondary users, except demo users. */
+ /** Hide "Factory reset" settings for secondary users. */
@Override
- public boolean isAvailable() {
- return mUm.isAdminUser() || Utils.isDemoUser(mContext);
- }
-
- @Override
- public String getPreferenceKey() {
- return KEY_FACTORY_RESET;
+ public int getAvailabilityStatus() {
+ return mUm.isAdminUser() ? AVAILABLE : DISABLED_FOR_USER;
}
@Override
public boolean handlePreferenceTreeClick(Preference preference) {
- if (KEY_FACTORY_RESET.equals(preference.getKey())) {
+ if (mPreferenceKey.equals(preference.getKey())) {
final Intent intent = new Intent(mContext, Settings.FactoryResetActivity.class);
mContext.startActivity(intent);
return true;
diff --git a/src/com/android/settings/system/ResetDashboardFragment.java b/src/com/android/settings/system/ResetDashboardFragment.java
index aea92aa..662edc5 100644
--- a/src/com/android/settings/system/ResetDashboardFragment.java
+++ b/src/com/android/settings/system/ResetDashboardFragment.java
@@ -78,7 +78,6 @@
if (SubscriptionUtil.isSimHardwareVisible(context)) {
controllers.add(new NetworkResetPreferenceController(context));
}
- controllers.add(new FactoryResetPreferenceController(context));
controllers.add(new ResetAppPrefPreferenceController(context, lifecycle));
return controllers;
}
diff --git a/src/com/android/settings/system/ResetPreferenceController.java b/src/com/android/settings/system/ResetPreferenceController.java
index 0740ac9..35f1ff7 100644
--- a/src/com/android/settings/system/ResetPreferenceController.java
+++ b/src/com/android/settings/system/ResetPreferenceController.java
@@ -26,13 +26,11 @@
private final UserManager mUm;
private final NetworkResetPreferenceController mNetworkReset;
- private final FactoryResetPreferenceController mFactpruReset;
public ResetPreferenceController(Context context, String preferenceKey) {
super(context, preferenceKey);
mUm = (UserManager) context.getSystemService(Context.USER_SERVICE);
mNetworkReset = new NetworkResetPreferenceController(context);
- mFactpruReset = new FactoryResetPreferenceController(context);
}
@Override
diff --git a/src/com/android/settings/users/GuestTelephonyPreferenceController.java b/src/com/android/settings/users/GuestTelephonyPreferenceController.java
index a935b8a..83e4bfc 100644
--- a/src/com/android/settings/users/GuestTelephonyPreferenceController.java
+++ b/src/com/android/settings/users/GuestTelephonyPreferenceController.java
@@ -17,6 +17,7 @@
package com.android.settings.users;
import android.content.Context;
+import android.content.pm.PackageManager;
import android.os.Bundle;
import android.os.UserManager;
@@ -33,14 +34,11 @@
private final UserManager mUserManager;
private final UserCapabilities mUserCaps;
- private Bundle mDefaultGuestRestrictions;
public GuestTelephonyPreferenceController(Context context, String preferenceKey) {
super(context, preferenceKey);
mUserManager = context.getSystemService(UserManager.class);
mUserCaps = UserCapabilities.create(context);
- mDefaultGuestRestrictions = mUserManager.getDefaultGuestRestrictions();
- mDefaultGuestRestrictions.putBoolean(UserManager.DISALLOW_SMS, true);
}
@Override
@@ -54,13 +52,16 @@
@Override
public boolean isChecked() {
- return !mDefaultGuestRestrictions.getBoolean(UserManager.DISALLOW_OUTGOING_CALLS, false);
+ return !mUserManager.getDefaultGuestRestrictions()
+ .getBoolean(UserManager.DISALLOW_OUTGOING_CALLS, false);
}
@Override
public boolean setChecked(boolean isChecked) {
- mDefaultGuestRestrictions.putBoolean(UserManager.DISALLOW_OUTGOING_CALLS, !isChecked);
- mUserManager.setDefaultGuestRestrictions(mDefaultGuestRestrictions);
+ Bundle guestRestrictions = mUserManager.getDefaultGuestRestrictions();
+ guestRestrictions.putBoolean(UserManager.DISALLOW_SMS, true);
+ guestRestrictions.putBoolean(UserManager.DISALLOW_OUTGOING_CALLS, !isChecked);
+ mUserManager.setDefaultGuestRestrictions(guestRestrictions);
return true;
}
@@ -73,6 +74,7 @@
public void updateState(Preference preference) {
super.updateState(preference);
mUserCaps.updateAddUserCapabilities(mContext);
- preference.setVisible(isAvailable() && mUserCaps.mUserSwitcherEnabled);
+ preference.setVisible(isAvailable() && mUserCaps.mUserSwitcherEnabled
+ && mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_TELEPHONY));
}
}
diff --git a/src/com/android/settings/users/UserDetailsSettings.java b/src/com/android/settings/users/UserDetailsSettings.java
index 2f9031e..402d4b1 100644
--- a/src/com/android/settings/users/UserDetailsSettings.java
+++ b/src/com/android/settings/users/UserDetailsSettings.java
@@ -79,6 +79,7 @@
/** Whether to enable the app_copying fragment. */
private static final boolean SHOW_APP_COPYING_PREF = false;
+ private static final int MESSAGE_PADDING = 20;
private UserManager mUserManager;
private UserCapabilities mUserCaps;
@@ -274,6 +275,7 @@
context.getDrawable(com.android.settingslib.R.drawable.ic_admin_panel_settings));
dialogHelper.setTitle(R.string.user_revoke_admin_confirm_title);
dialogHelper.setMessage(R.string.user_revoke_admin_confirm_message);
+ dialogHelper.setMessagePadding(MESSAGE_PADDING);
dialogHelper.setPositiveButton(R.string.remove, view -> {
updateUserAdminStatus(false);
dialogHelper.getDialog().dismiss();
@@ -294,6 +296,7 @@
context.getDrawable(com.android.settingslib.R.drawable.ic_admin_panel_settings));
dialogHelper.setTitle(com.android.settingslib.R.string.user_grant_admin_title);
dialogHelper.setMessage(com.android.settingslib.R.string.user_grant_admin_message);
+ dialogHelper.setMessagePadding(MESSAGE_PADDING);
dialogHelper.setPositiveButton(com.android.settingslib.R.string.user_grant_admin_button,
view -> {
updateUserAdminStatus(true);
diff --git a/src/com/android/settings/users/UserSettings.java b/src/com/android/settings/users/UserSettings.java
index 28e02ec..b0816fd 100644
--- a/src/com/android/settings/users/UserSettings.java
+++ b/src/com/android/settings/users/UserSettings.java
@@ -885,7 +885,6 @@
this::startActivityForResult,
userIcon,
user.name,
- getString(com.android.settingslib.R.string.profile_info_settings_title),
(newUserName, newUserIcon) -> {
if (newUserIcon != userIcon) {
ThreadUtils.postOnBackgroundThread(() ->
@@ -978,10 +977,10 @@
return;
}
try {
- getContext().getSystemService(UserManager.class)
- .removeUserWhenPossible(UserHandle.of(UserHandle.myUserId()),
- /* overrideDevicePolicy= */ false);
- ActivityManager.getService().switchUser(UserHandle.USER_SYSTEM);
+ mUserManager.removeUserWhenPossible(
+ UserHandle.of(UserHandle.myUserId()), /* overrideDevicePolicy= */ false);
+ ActivityManager.getService().switchUser(
+ mUserManager.getPreviousForegroundUser().getIdentifier());
} catch (RemoteException re) {
Log.e(TAG, "Unable to remove self user");
}
@@ -1100,7 +1099,7 @@
}
mMetricsFeatureProvider.action(getActivity(),
SettingsEnums.ACTION_USER_GUEST_EXIT_CONFIRMED);
- switchToUserId(UserHandle.USER_SYSTEM);
+ switchToUserId(mUserManager.getPreviousForegroundUser().getIdentifier());
}
private int createGuest() {
@@ -1140,8 +1139,8 @@
// Create a new guest in the foreground, and then immediately switch to it
int newGuestUserId = createGuest();
if (newGuestUserId == UserHandle.USER_NULL) {
- Log.e(TAG, "Could not create new guest, switching back to system user");
- switchToUserId(UserHandle.USER_SYSTEM);
+ Log.e(TAG, "Could not create new guest, switching back to previous user");
+ switchToUserId(mUserManager.getPreviousForegroundUser().getIdentifier());
mUserManager.removeUser(oldGuestUserId);
WindowManagerGlobal.getWindowManagerService().lockNow(/* options= */ null);
return;
@@ -1629,7 +1628,7 @@
mRemovingUserId = -1;
updateUserList();
if (mCreateUserDialogController.isActive()) {
- mCreateUserDialogController.clear();
+ mCreateUserDialogController.finish();
}
}
}
diff --git a/src/com/android/settings/vpn2/VpnSettings.java b/src/com/android/settings/vpn2/VpnSettings.java
index e9b6a4e..0389ecd 100644
--- a/src/com/android/settings/vpn2/VpnSettings.java
+++ b/src/com/android/settings/vpn2/VpnSettings.java
@@ -61,7 +61,7 @@
import com.android.internal.net.VpnConfig;
import com.android.internal.net.VpnProfile;
import com.android.settings.R;
-import com.android.settings.RestrictedSettingsFragment;
+import com.android.settings.dashboard.RestrictedDashboardFragment;
import com.android.settings.overlay.FeatureFactory;
import com.android.settings.widget.GearPreference;
import com.android.settings.widget.GearPreference.OnGearClickListener;
@@ -80,7 +80,7 @@
* Settings screen listing VPNs. Configured VPNs and networks managed by apps
* are shown in the same list.
*/
-public class VpnSettings extends RestrictedSettingsFragment implements
+public class VpnSettings extends RestrictedDashboardFragment implements
Handler.Callback, Preference.OnPreferenceClickListener {
private static final String LOG_TAG = "VpnSettings";
private static final boolean DEBUG = Log.isLoggable(LOG_TAG, Log.DEBUG);
@@ -135,7 +135,6 @@
mUnavailable = isUiRestricted();
setHasOptionsMenu(!mUnavailable);
- addPreferencesFromResource(R.xml.vpn_settings2);
mPreferenceScreen = getPreferenceScreen();
}
@@ -217,6 +216,16 @@
}
@Override
+ protected int getPreferenceScreenResId() {
+ return R.xml.vpn_settings2;
+ }
+
+ @Override
+ protected String getLogTag() {
+ return LOG_TAG;
+ }
+
+ @Override
public void onPause() {
if (mUnavailable) {
super.onPause();
diff --git a/src/com/android/settings/widget/RadioButtonPickerFragment.java b/src/com/android/settings/widget/RadioButtonPickerFragment.java
index 0b59714..9450bb9 100644
--- a/src/com/android/settings/widget/RadioButtonPickerFragment.java
+++ b/src/com/android/settings/widget/RadioButtonPickerFragment.java
@@ -33,8 +33,8 @@
import androidx.preference.PreferenceScreen;
import com.android.settings.R;
+import com.android.settings.SettingsPreferenceFragment;
import com.android.settings.Utils;
-import com.android.settings.core.InstrumentedPreferenceFragment;
import com.android.settings.core.PreferenceXmlParserUtils;
import com.android.settings.core.PreferenceXmlParserUtils.MetadataFlag;
import com.android.settingslib.widget.CandidateInfo;
@@ -46,7 +46,10 @@
import java.util.List;
import java.util.Map;
-public abstract class RadioButtonPickerFragment extends InstrumentedPreferenceFragment implements
+/**
+ * A fragment to handle general radio button picker
+ */
+public abstract class RadioButtonPickerFragment extends SettingsPreferenceFragment implements
SelectorWithWidgetPreference.OnClickListener {
@VisibleForTesting
diff --git a/src/com/android/settings/wifi/LongPressWifiEntryPreference.java b/src/com/android/settings/wifi/LongPressWifiEntryPreference.java
index 6343e06..ec94e74 100644
--- a/src/com/android/settings/wifi/LongPressWifiEntryPreference.java
+++ b/src/com/android/settings/wifi/LongPressWifiEntryPreference.java
@@ -22,6 +22,7 @@
import androidx.fragment.app.Fragment;
import androidx.preference.PreferenceViewHolder;
+import com.android.settingslib.RestrictedLockUtils;
import com.android.wifitrackerlib.WifiEntry;
/**
@@ -34,7 +35,7 @@
public LongPressWifiEntryPreference(Context context, WifiEntry wifiEntry, Fragment fragment) {
super(context, wifiEntry);
mFragment = fragment;
- checkRestrictionAndSetDisabled(UserManager.DISALLOW_ADD_WIFI_CONFIG);
+ checkRestrictionAndSetDisabled();
}
@Override
@@ -65,4 +66,22 @@
}
return enabled;
}
+
+ @VisibleForTesting
+ void checkRestrictionAndSetDisabled() {
+ if (!getWifiEntry().hasAdminRestrictions()) {
+ return;
+ }
+ RestrictedLockUtils.EnforcedAdmin admin = null;
+ Context context = getContext();
+ if (context != null) {
+ admin = RestrictedLockUtils.getProfileOrDeviceOwner(context, context.getUser());
+ }
+ if (admin == null) {
+ // Use UserManager.DISALLOW_ADD_WIFI_CONFIG as default Wi-Fi network restriction.
+ admin = RestrictedLockUtils.EnforcedAdmin.createDefaultEnforcedAdminWithRestriction(
+ UserManager.DISALLOW_ADD_WIFI_CONFIG);
+ }
+ setDisabledByAdmin(admin);
+ }
}
diff --git a/src/com/android/settings/wifi/NetworkRequestDialogFragment.java b/src/com/android/settings/wifi/NetworkRequestDialogFragment.java
index 5639047..93d88e9 100644
--- a/src/com/android/settings/wifi/NetworkRequestDialogFragment.java
+++ b/src/com/android/settings/wifi/NetworkRequestDialogFragment.java
@@ -18,8 +18,6 @@
import static com.android.wifitrackerlib.Utils.getSecurityTypesFromScanResult;
-import static java.util.stream.Collectors.toList;
-
import android.app.Dialog;
import android.content.Context;
import android.content.DialogInterface;
@@ -273,19 +271,31 @@
@VisibleForTesting
void updateWifiEntries() {
final List<WifiEntry> wifiEntries = new ArrayList<>();
- if (mWifiPickerTracker.getConnectedWifiEntry() != null) {
- wifiEntries.add(mWifiPickerTracker.getConnectedWifiEntry());
+ WifiEntry connectedWifiEntry = mWifiPickerTracker.getConnectedWifiEntry();
+ String connectedSsid;
+ if (connectedWifiEntry != null) {
+ connectedSsid = connectedWifiEntry.getSsid();
+ wifiEntries.add(connectedWifiEntry);
+ } else {
+ connectedSsid = null;
}
wifiEntries.addAll(mWifiPickerTracker.getWifiEntries());
mFilteredWifiEntries.clear();
mFilteredWifiEntries.addAll(wifiEntries.stream()
- .filter(entry -> isMatchedWifiEntry(entry))
+ .filter(entry -> isMatchedWifiEntry(entry, connectedSsid))
.limit(mShowLimitedItem ? MAX_NUMBER_LIST_ITEM : Long.MAX_VALUE)
- .collect(toList()));
+ .toList());
}
- private boolean isMatchedWifiEntry(WifiEntry entry) {
+ private boolean isMatchedWifiEntry(WifiEntry entry, String connectedSsid) {
+ if (entry.getConnectedState() == WifiEntry.CONNECTED_STATE_DISCONNECTED
+ && TextUtils.equals(entry.getSsid(), connectedSsid)) {
+ // WifiPickerTracker may return a duplicate unsaved network that is separate from
+ // the connecting app-requested network, so make sure we only show the connected
+ // app-requested one.
+ return false;
+ }
for (MatchWifi wifi : mMatchWifis) {
if (!TextUtils.equals(entry.getSsid(), wifi.mSsid)) {
continue;
diff --git a/src/com/android/settings/wifi/WifiAPITest.java b/src/com/android/settings/wifi/WifiAPITest.java
index 15465ed..c8bcf7f 100644
--- a/src/com/android/settings/wifi/WifiAPITest.java
+++ b/src/com/android/settings/wifi/WifiAPITest.java
@@ -69,7 +69,7 @@
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
- addPreferencesFromResource(R.layout.wifi_api_test);
+ addPreferencesFromResource(R.xml.wifi_api_test);
final PreferenceScreen preferenceScreen = getPreferenceScreen();
diff --git a/src/com/android/settings/wifi/WifiEntryPreference.java b/src/com/android/settings/wifi/WifiEntryPreference.java
index 5b44887..7206666 100644
--- a/src/com/android/settings/wifi/WifiEntryPreference.java
+++ b/src/com/android/settings/wifi/WifiEntryPreference.java
@@ -15,6 +15,8 @@
*/
package com.android.settings.wifi;
+import static com.android.settingslib.wifi.WifiUtils.getHotspotIconResource;
+
import android.annotation.Nullable;
import android.content.Context;
import android.content.res.Resources;
@@ -37,6 +39,7 @@
import com.android.settingslib.Utils;
import com.android.settingslib.wifi.WifiUtils;
import com.android.wifitrackerlib.BaseWifiTracker;
+import com.android.wifitrackerlib.HotspotNetworkEntry;
import com.android.wifitrackerlib.WifiEntry;
/**
@@ -145,13 +148,17 @@
*/
public void refresh() {
setTitle(mWifiEntry.getTitle());
- final int level = mWifiEntry.getLevel();
- final boolean showX = mWifiEntry.shouldShowXLevelIcon();
- if (level != mLevel || showX != mShowX) {
- mLevel = level;
- mShowX = showX;
- updateIcon(mShowX, mLevel);
- notifyChanged();
+ if (mWifiEntry instanceof HotspotNetworkEntry) {
+ updateHotspotIcon(((HotspotNetworkEntry) mWifiEntry).getDeviceType());
+ } else {
+ int level = mWifiEntry.getLevel();
+ boolean showX = mWifiEntry.shouldShowXLevelIcon();
+
+ if (level != mLevel || showX != mShowX) {
+ mLevel = level;
+ mShowX = showX;
+ updateIcon(mShowX, mLevel);
+ }
}
setSummary(mWifiEntry.getSummary(false /* concise */));
@@ -201,14 +208,7 @@
return accent ? android.R.attr.colorAccent : android.R.attr.colorControlNormal;
}
- @VisibleForTesting
- void updateIcon(boolean showX, int level) {
- if (level == -1) {
- setIcon(null);
- return;
- }
-
- final Drawable drawable = mIconInjector.getIcon(showX, level);
+ private void setIconWithTint(Drawable drawable) {
if (drawable != null) {
// Must use Drawable#setTintList() instead of Drawable#setTint() to show the grey
// icon when the preference is disabled.
@@ -219,6 +219,20 @@
}
}
+ @VisibleForTesting
+ void updateIcon(boolean showX, int level) {
+ if (level == -1) {
+ setIcon(null);
+ return;
+ }
+ setIconWithTint(mIconInjector.getIcon(showX, level));
+ }
+
+ @VisibleForTesting
+ void updateHotspotIcon(int deviceType) {
+ setIconWithTint(getContext().getDrawable(getHotspotIconResource(deviceType)));
+ }
+
@Nullable
private StateListDrawable getFrictionStateListDrawable() {
TypedArray frictionSld;
diff --git a/src/com/android/settings/wifi/calling/WifiCallingSettingsForSub.java b/src/com/android/settings/wifi/calling/WifiCallingSettingsForSub.java
index 0c3d769..098787c 100644
--- a/src/com/android/settings/wifi/calling/WifiCallingSettingsForSub.java
+++ b/src/com/android/settings/wifi/calling/WifiCallingSettingsForSub.java
@@ -94,7 +94,6 @@
private ListWithEntrySummaryPreference mButtonWfcRoamingMode;
private Preference mUpdateAddress;
- private boolean mValidListener = false;
private boolean mEditableWfcMode = true;
private boolean mEditableWfcRoamingMode = true;
private boolean mUseWfcHomeModeForRoaming = false;
@@ -104,7 +103,7 @@
private ProvisioningManager mProvisioningManager;
private TelephonyManager mTelephonyManager;
- private final PhoneTelephonyCallback mTelephonyCallback = new PhoneTelephonyCallback();
+ private PhoneTelephonyCallback mTelephonyCallback;
private class PhoneTelephonyCallback extends TelephonyCallback implements
TelephonyCallback.CallStateListener {
@@ -202,8 +201,10 @@
void showAlert(Intent intent) {
final Context context = getActivity();
- final CharSequence title = intent.getCharSequenceExtra(Phone.EXTRA_KEY_ALERT_TITLE);
- final CharSequence message = intent.getCharSequenceExtra(Phone.EXTRA_KEY_ALERT_MESSAGE);
+ final CharSequence title =
+ intent.getCharSequenceExtra(ImsManager.EXTRA_WFC_REGISTRATION_FAILURE_TITLE);
+ final CharSequence message =
+ intent.getCharSequenceExtra(ImsManager.EXTRA_WFC_REGISTRATION_FAILURE_MESSAGE);
final AlertDialog.Builder builder = new AlertDialog.Builder(context);
builder.setMessage(message)
@@ -418,27 +419,20 @@
@Override
public void onResume() {
super.onResume();
-
updateBody();
-
- final Context context = getActivity();
- if (queryImsState(mSubId).isWifiCallingSupported()) {
+ Context context = getActivity();
+ if (mTelephonyCallback == null && queryImsState(mSubId).isWifiCallingSupported()) {
+ mTelephonyCallback = new PhoneTelephonyCallback();
getTelephonyManagerForSub(mSubId).registerTelephonyCallback(
context.getMainExecutor(), mTelephonyCallback);
-
mSwitchBar.addOnSwitchChangeListener(this);
-
- mValidListener = true;
}
-
context.registerReceiver(mIntentReceiver, mIntentFilter,
Context.RECEIVER_EXPORTED_UNAUDITED);
-
final Intent intent = getActivity().getIntent();
if (intent.getBooleanExtra(Phone.EXTRA_KEY_ALERT_SHOW, false)) {
showAlert(intent);
}
-
// Register callback for provisioning changes.
registerProvisioningChangedCallback();
}
@@ -446,19 +440,13 @@
@Override
public void onPause() {
super.onPause();
-
- final Context context = getActivity();
-
- if (mValidListener) {
- mValidListener = false;
-
+ Context context = getActivity();
+ if (mTelephonyCallback != null) {
getTelephonyManagerForSub(mSubId).unregisterTelephonyCallback(mTelephonyCallback);
-
+ mTelephonyCallback = null;
mSwitchBar.removeOnSwitchChangeListener(this);
}
-
context.unregisterReceiver(mIntentReceiver);
-
// Remove callback for provisioning changes.
unregisterProvisioningChangedCallback();
}
diff --git a/src/com/android/settings/wifi/details/WifiNetworkDetailsFragment.java b/src/com/android/settings/wifi/details/WifiNetworkDetailsFragment.java
index dd0c20a..e3812af 100644
--- a/src/com/android/settings/wifi/details/WifiNetworkDetailsFragment.java
+++ b/src/com/android/settings/wifi/details/WifiNetworkDetailsFragment.java
@@ -15,14 +15,18 @@
*/
package com.android.settings.wifi.details;
+import static com.android.settings.network.telephony.MobileNetworkUtils.NO_CELL_DATA_TYPE_ICON;
import static com.android.settings.wifi.WifiSettings.WIFI_DIALOG_ID;
+import static com.android.settingslib.Utils.formatPercentage;
import android.app.Dialog;
import android.app.admin.DevicePolicyManager;
import android.app.settings.SettingsEnums;
import android.content.Context;
+import android.graphics.drawable.Drawable;
import android.net.ConnectivityManager;
import android.net.wifi.WifiManager;
+import android.net.wifi.sharedconnectivity.app.HotspotNetwork;
import android.os.Bundle;
import android.os.Handler;
import android.os.HandlerThread;
@@ -32,19 +36,23 @@
import android.os.SystemClock;
import android.os.UserHandle;
import android.os.UserManager;
+import android.telephony.SignalStrength;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import androidx.annotation.VisibleForTesting;
+import androidx.preference.Preference;
import androidx.preference.PreferenceScreen;
import com.android.settings.R;
import com.android.settings.Utils;
import com.android.settings.dashboard.RestrictedDashboardFragment;
+import com.android.settings.network.telephony.MobileNetworkUtils;
import com.android.settings.overlay.FeatureFactory;
import com.android.settings.wifi.WifiConfigUiBase2;
import com.android.settings.wifi.WifiDialog2;
+import com.android.settings.wifi.WifiUtils;
import com.android.settings.wifi.details2.AddDevicePreferenceController2;
import com.android.settings.wifi.details2.WifiAutoConnectPreferenceController2;
import com.android.settings.wifi.details2.WifiDetailPreferenceController2;
@@ -52,6 +60,7 @@
import com.android.settings.wifi.details2.WifiPrivacyPreferenceController2;
import com.android.settings.wifi.details2.WifiSecondSummaryController2;
import com.android.settings.wifi.details2.WifiSubscriptionDetailPreferenceController2;
+import com.android.settings.wifi.repository.SharedConnectivityRepository;
import com.android.settingslib.RestrictedLockUtils;
import com.android.settingslib.RestrictedLockUtils.EnforcedAdmin;
import com.android.settingslib.RestrictedLockUtilsInternal;
@@ -78,6 +87,12 @@
// Key of a Bundle to save/restore the selected WifiEntry
public static final String KEY_CHOSEN_WIFIENTRY_KEY = "key_chosen_wifientry_key";
+ public static final String KEY_HOTSPOT_DEVICE_CATEGORY = "hotspot_device_details_category";
+ public static final String KEY_HOTSPOT_DEVICE_INTERNET_SOURCE =
+ "hotspot_device_details_internet_source";
+ public static final String KEY_HOTSPOT_DEVICE_BATTERY = "hotspot_device_details_battery";
+ public static final String KEY_HOTSPOT_CONNECTION_CATEGORY = "hotspot_connection_category";
+
// Max age of tracked WifiEntries
private static final long MAX_SCAN_AGE_MILLIS = 15_000;
// Interval between initiating SavedNetworkTracker scans
@@ -88,10 +103,15 @@
@VisibleForTesting
NetworkDetailsTracker mNetworkDetailsTracker;
private HandlerThread mWorkerThread;
- private WifiDetailPreferenceController2 mWifiDetailPreferenceController2;
+ @VisibleForTesting
+ WifiDetailPreferenceController2 mWifiDetailPreferenceController2;
private List<WifiDialog2.WifiDialog2Listener> mWifiDialogListeners = new ArrayList<>();
@VisibleForTesting
List<AbstractPreferenceController> mControllers;
+ private boolean mIsInstantHotspotFeatureEnabled =
+ SharedConnectivityRepository.isDeviceConfigEnabled();
+ @VisibleForTesting
+ WifiNetworkDetailsViewModel mWifiNetworkDetailsViewModel;
public WifiNetworkDetailsFragment() {
super(UserManager.DISALLOW_CONFIG_WIFI);
@@ -207,6 +227,10 @@
setupNetworksDetailTracker();
final WifiEntry wifiEntry = mNetworkDetailsTracker.getWifiEntry();
+ if (mIsInstantHotspotFeatureEnabled) {
+ getWifiNetworkDetailsViewModel().setWifiEntry(wifiEntry);
+ }
+
final WifiSecondSummaryController2 wifiSecondSummaryController2 =
new WifiSecondSummaryController2(context);
wifiSecondSummaryController2.setWifiEntry(wifiEntry);
@@ -335,5 +359,83 @@
}
controller.displayPreference(screen);
}
+ if (mIsInstantHotspotFeatureEnabled) {
+ getWifiNetworkDetailsViewModel().setWifiEntry(mNetworkDetailsTracker.getWifiEntry());
+ }
+ }
+
+ private WifiNetworkDetailsViewModel getWifiNetworkDetailsViewModel() {
+ if (mWifiNetworkDetailsViewModel == null) {
+ mWifiNetworkDetailsViewModel = FeatureFactory.getFactory(getContext())
+ .getWifiFeatureProvider().getWifiNetworkDetailsViewModel(this);
+ mWifiNetworkDetailsViewModel.getHotspotNetworkData()
+ .observe(this, this::onHotspotNetworkChanged);
+ }
+ return mWifiNetworkDetailsViewModel;
+ }
+
+ @VisibleForTesting
+ void onHotspotNetworkChanged(WifiNetworkDetailsViewModel.HotspotNetworkData data) {
+ PreferenceScreen screen = getPreferenceScreen();
+ if (screen == null) {
+ return;
+ }
+ if (data == null) {
+ screen.findPreference(KEY_HOTSPOT_DEVICE_CATEGORY).setVisible(false);
+ screen.findPreference(KEY_HOTSPOT_CONNECTION_CATEGORY).setVisible(false);
+ if (mWifiDetailPreferenceController2 != null) {
+ mWifiDetailPreferenceController2.setSignalStrengthTitle(R.string.wifi_signal);
+ }
+ return;
+ }
+ screen.findPreference(KEY_HOTSPOT_DEVICE_CATEGORY).setVisible(true);
+ updateInternetSource(data.getNetworkType(), data.getUpstreamConnectionStrength());
+ updateBattery(data.isBatteryCharging(), data.getBatteryPercentage());
+
+ screen.findPreference(KEY_HOTSPOT_CONNECTION_CATEGORY).setVisible(true);
+ if (mWifiDetailPreferenceController2 != null) {
+ mWifiDetailPreferenceController2
+ .setSignalStrengthTitle(R.string.hotspot_connection_strength);
+ }
+ }
+
+ @VisibleForTesting
+ void updateInternetSource(int networkType, int upstreamConnectionStrength) {
+ Preference internetSource = getPreferenceScreen()
+ .findPreference(KEY_HOTSPOT_DEVICE_INTERNET_SOURCE);
+ Drawable drawable;
+ if (networkType == HotspotNetwork.NETWORK_TYPE_WIFI) {
+ internetSource.setSummary(R.string.internet_source_wifi);
+ drawable = getContext().getDrawable(
+ WifiUtils.getInternetIconResource(upstreamConnectionStrength, false));
+ } else if (networkType == HotspotNetwork.NETWORK_TYPE_CELLULAR) {
+ internetSource.setSummary(R.string.internet_source_mobile_data);
+ drawable = getMobileDataIcon(upstreamConnectionStrength);
+ } else if (networkType == HotspotNetwork.NETWORK_TYPE_ETHERNET) {
+ internetSource.setSummary(R.string.internet_source_ethernet);
+ drawable = getContext().getDrawable(R.drawable.ic_settings_ethernet);
+ } else {
+ internetSource.setSummary(R.string.summary_placeholder);
+ drawable = null;
+ }
+ if (drawable != null) {
+ drawable.setTintList(
+ Utils.getColorAttr(getContext(), android.R.attr.colorControlNormal));
+ }
+ internetSource.setIcon(drawable);
+ }
+
+ @VisibleForTesting
+ Drawable getMobileDataIcon(int level) {
+ return MobileNetworkUtils.getSignalStrengthIcon(getContext(), level,
+ SignalStrength.NUM_SIGNAL_STRENGTH_BINS, NO_CELL_DATA_TYPE_ICON, false, false);
+ }
+
+ @VisibleForTesting
+ void updateBattery(boolean isChanging, int percentage) {
+ Preference battery = getPreferenceScreen().findPreference(KEY_HOTSPOT_DEVICE_BATTERY);
+ battery.setSummary((isChanging)
+ ? getString(R.string.hotspot_battery_charging_summary, formatPercentage(percentage))
+ : formatPercentage(percentage));
}
}
diff --git a/src/com/android/settings/wifi/details/WifiNetworkDetailsViewModel.java b/src/com/android/settings/wifi/details/WifiNetworkDetailsViewModel.java
new file mode 100644
index 0000000..9c24d66
--- /dev/null
+++ b/src/com/android/settings/wifi/details/WifiNetworkDetailsViewModel.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.wifi.details;
+
+import android.app.Application;
+
+import androidx.annotation.VisibleForTesting;
+import androidx.lifecycle.AndroidViewModel;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.MutableLiveData;
+
+import com.android.settings.overlay.FeatureFactory;
+import com.android.wifitrackerlib.HotspotNetworkEntry;
+import com.android.wifitrackerlib.WifiEntry;
+
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * Wi-Fi Network Details ViewModel
+ */
+public class WifiNetworkDetailsViewModel extends AndroidViewModel {
+ private static final String TAG = "WifiNetworkDetailsViewModel";
+
+ @VisibleForTesting
+ MutableLiveData<HotspotNetworkData> mHotspotNetworkData = new MutableLiveData<>();
+
+ public WifiNetworkDetailsViewModel(@NotNull Application application) {
+ super(application);
+ }
+
+ /** Sets the {@link WifiEntry} class */
+ public void setWifiEntry(WifiEntry wifiEntry) {
+ if (!(wifiEntry instanceof HotspotNetworkEntry)) {
+ log("post HotspotNetworkData:null");
+ mHotspotNetworkData.postValue(null);
+ return;
+ }
+ HotspotNetworkEntry entry = (HotspotNetworkEntry) wifiEntry;
+ HotspotNetworkData data = new HotspotNetworkData(
+ entry.getNetworkType(),
+ entry.getUpstreamConnectionStrength(),
+ entry.getBatteryPercentage(),
+ entry.isBatteryCharging());
+ log("post HotspotNetworkData:" + data);
+ mHotspotNetworkData.postValue(data);
+ }
+
+ /** Gets the {@link HotspotNetworkData} LiveData */
+ public LiveData<HotspotNetworkData> getHotspotNetworkData() {
+ return mHotspotNetworkData;
+ }
+
+ /** The {@link HotspotNetworkData} class */
+ static class HotspotNetworkData {
+ private int mNetworkType;
+ private int mUpstreamConnectionStrength;
+ private int mBatteryPercentage;
+ private boolean mIsBatteryCharging;
+
+ HotspotNetworkData(int networkType, int upstreamConnectionStrength,
+ int batteryPercentage,
+ boolean isBatteryCharging) {
+ mNetworkType = networkType;
+ mUpstreamConnectionStrength = upstreamConnectionStrength;
+ mBatteryPercentage = batteryPercentage;
+ mIsBatteryCharging = isBatteryCharging;
+ }
+
+ /** Gets the network type */
+ public int getNetworkType() {
+ return mNetworkType;
+ }
+
+ /** Gets the upstream connection strength */
+ public int getUpstreamConnectionStrength() {
+ return mUpstreamConnectionStrength;
+ }
+
+ /** Gets the battery percentage */
+ public int getBatteryPercentage() {
+ return mBatteryPercentage;
+ }
+
+ /** Returns true if the battery is charging */
+ public boolean isBatteryCharging() {
+ return mIsBatteryCharging;
+ }
+
+ @Override
+ public String toString() {
+ return getClass().getSimpleName()
+ + ":{networkType:" + mNetworkType
+ + ", upstreamConnectionStrength:" + mUpstreamConnectionStrength
+ + ", batteryPercentage:" + mBatteryPercentage
+ + ", isBatteryCharging:" + mIsBatteryCharging
+ + " }";
+ }
+ }
+
+ private void log(String msg) {
+ FeatureFactory.getFactory(getApplication().getApplicationContext()).getWifiFeatureProvider()
+ .verboseLog(TAG, msg);
+ }
+}
diff --git a/src/com/android/settings/wifi/details2/WifiDetailPreferenceController2.java b/src/com/android/settings/wifi/details2/WifiDetailPreferenceController2.java
index 4c5a4bf..0647a77 100644
--- a/src/com/android/settings/wifi/details2/WifiDetailPreferenceController2.java
+++ b/src/com/android/settings/wifi/details2/WifiDetailPreferenceController2.java
@@ -21,6 +21,8 @@
import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
import static android.telephony.TelephonyManager.UNKNOWN_CARRIER_ID;
+import static com.android.settingslib.wifi.WifiUtils.getHotspotIconResource;
+
import android.app.Activity;
import android.app.AlertDialog;
import android.app.settings.SettingsEnums;
@@ -86,6 +88,7 @@
import com.android.settingslib.utils.StringUtil;
import com.android.settingslib.widget.ActionButtonsPreference;
import com.android.settingslib.widget.LayoutPreference;
+import com.android.wifitrackerlib.HotspotNetworkEntry;
import com.android.wifitrackerlib.WifiEntry;
import com.android.wifitrackerlib.WifiEntry.ConnectCallback;
import com.android.wifitrackerlib.WifiEntry.DisconnectCallback;
@@ -172,7 +175,8 @@
// UI elements - in order of appearance
private ActionButtonsPreference mButtonsPref;
- private EntityHeaderController mEntityHeaderController;
+ @VisibleForTesting
+ EntityHeaderController mEntityHeaderController;
private Preference mSignalStrengthPref;
private Preference mTxLinkSpeedPref;
private Preference mRxLinkSpeedPref;
@@ -447,8 +451,6 @@
ImageView iconView = headerPref.findViewById(R.id.entity_header_icon);
iconView.setScaleType(ImageView.ScaleType.CENTER_INSIDE);
-
- mEntityHeaderController.setLabel(mWifiEntry.getTitle());
}
private String getExpiryTimeSummary() {
@@ -487,6 +489,7 @@
mSummaryHeaderController.updateState(mDataUsageSummaryPref);
} else {
mEntityHeaderController
+ .setLabel(mWifiEntry.getTitle())
.setSummary(mWifiEntry.getSummary())
.setSecondSummary(getExpiryTimeSummary())
.setRecyclerView(mFragment.getListView(), mLifecycle)
@@ -535,6 +538,8 @@
private void refreshPage() {
Log.d(TAG, "Update UI!");
+ // refresh header icon
+ refreshEntryHeaderIcon();
// refresh header
refreshEntityHeader();
@@ -563,9 +568,33 @@
refreshWifiType();
}
- private void refreshRssiViews() {
- final int signalLevel = mWifiEntry.getLevel();
+ @VisibleForTesting
+ void refreshEntryHeaderIcon() {
+ if (mEntityHeaderController == null) {
+ return;
+ }
+ Drawable drawable = getWifiDrawable(mWifiEntry);
+ mEntityHeaderController
+ .setIcon(redrawIconForHeader(drawable))
+ .done(mFragment.getActivity(), true /* rebind */);
+ }
+ /**
+ * Returns a Wi-Fi icon {@link Drawable}.
+ *
+ * @param wifiEntry {@link WifiEntry}
+ */
+ @VisibleForTesting
+ Drawable getWifiDrawable(WifiEntry wifiEntry) {
+ if (wifiEntry instanceof HotspotNetworkEntry) {
+ int deviceType = ((HotspotNetworkEntry) wifiEntry).getDeviceType();
+ return mContext.getDrawable(getHotspotIconResource(deviceType));
+ }
+ return mIconInjector.getIcon(wifiEntry.shouldShowXLevelIcon(), wifiEntry.getLevel());
+ }
+
+ private void refreshRssiViews() {
+ int signalLevel = mWifiEntry.getLevel();
// Disappears signal view if not in range. e.g. for saved networks.
if (signalLevel == WifiEntry.WIFI_LEVEL_UNREACHABLE) {
mSignalStrengthPref.setVisible(false);
@@ -573,21 +602,13 @@
return;
}
- final boolean showX = mWifiEntry.shouldShowXLevelIcon();
-
+ boolean showX = mWifiEntry.shouldShowXLevelIcon();
if (mRssiSignalLevel == signalLevel && mShowX == showX) {
return;
}
mRssiSignalLevel = signalLevel;
mShowX = showX;
Drawable wifiIcon = mIconInjector.getIcon(mShowX, mRssiSignalLevel);
-
- if (mEntityHeaderController != null) {
- mEntityHeaderController
- .setIcon(redrawIconForHeader(wifiIcon)).done(mFragment.getActivity(),
- true /* rebind */);
- }
-
Drawable wifiIconDark = wifiIcon.getConstantState().newDrawable().mutate();
wifiIconDark.setTintList(Utils.getColorAttr(mContext, android.R.attr.colorControlNormal));
mSignalStrengthPref.setIcon(wifiIconDark);
@@ -637,29 +658,23 @@
}
private void refreshTxSpeed() {
- if (mWifiInfo == null
- || mWifiEntry.getConnectedState() != WifiEntry.CONNECTED_STATE_CONNECTED) {
+ String summary = mWifiEntry.getTxSpeedString();
+ if (TextUtils.isEmpty(summary)) {
mTxLinkSpeedPref.setVisible(false);
return;
}
-
- int txLinkSpeedMbps = mWifiInfo.getTxLinkSpeedMbps();
- mTxLinkSpeedPref.setVisible(txLinkSpeedMbps >= 0);
- mTxLinkSpeedPref.setSummary(mContext.getString(
- R.string.tx_link_speed, mWifiInfo.getTxLinkSpeedMbps()));
+ mTxLinkSpeedPref.setVisible(true);
+ mTxLinkSpeedPref.setSummary(summary);
}
private void refreshRxSpeed() {
- if (mWifiInfo == null
- || mWifiEntry.getConnectedState() != WifiEntry.CONNECTED_STATE_CONNECTED) {
+ String summary = mWifiEntry.getRxSpeedString();
+ if (TextUtils.isEmpty(summary)) {
mRxLinkSpeedPref.setVisible(false);
return;
}
-
- int rxLinkSpeedMbps = mWifiInfo.getRxLinkSpeedMbps();
- mRxLinkSpeedPref.setVisible(rxLinkSpeedMbps >= 0);
- mRxLinkSpeedPref.setSummary(mContext.getString(
- R.string.rx_link_speed, mWifiInfo.getRxLinkSpeedMbps()));
+ mRxLinkSpeedPref.setVisible(true);
+ mRxLinkSpeedPref.setSummary(summary);
}
private void refreshSsid() {
@@ -1130,4 +1145,11 @@
public void onSignInResult(@SignInStatus int status) {
refreshPage();
}
+
+ /** Sets signal strength title */
+ public void setSignalStrengthTitle(int titleResId) {
+ if (mSignalStrengthPref != null) {
+ mSignalStrengthPref.setTitle(titleResId);
+ }
+ }
}
diff --git a/src/com/android/settings/wifi/dpp/WifiDppQrCodeGeneratorFragment.java b/src/com/android/settings/wifi/dpp/WifiDppQrCodeGeneratorFragment.java
index d3a4be7..7af8343 100644
--- a/src/com/android/settings/wifi/dpp/WifiDppQrCodeGeneratorFragment.java
+++ b/src/com/android/settings/wifi/dpp/WifiDppQrCodeGeneratorFragment.java
@@ -223,11 +223,9 @@
private Button createActionButton(Drawable icon, CharSequence title, View.OnClickListener r) {
final Button b = (Button) LayoutInflater.from(getContext()).inflate(
- com.android.internal.R.layout.chooser_action_button, null);
+ R.layout.action_button, null);
if (icon != null) {
- final int size = getResources()
- .getDimensionPixelSize(
- com.android.internal.R.dimen.chooser_action_button_icon_size);
+ final int size = getResources().getDimensionPixelSize(R.dimen.action_button_icon_size);
icon.setBounds(0, 0, size, size);
b.setCompoundDrawablesRelative(icon, null, null, null);
}
diff --git a/src/com/android/settings/wifi/dpp/WifiDppUtils.java b/src/com/android/settings/wifi/dpp/WifiDppUtils.java
index 39a5431..c336c62 100644
--- a/src/com/android/settings/wifi/dpp/WifiDppUtils.java
+++ b/src/com/android/settings/wifi/dpp/WifiDppUtils.java
@@ -27,11 +27,13 @@
import android.os.CancellationSignal;
import android.os.Handler;
import android.os.Looper;
+import android.os.UserHandle;
import android.os.VibrationEffect;
import android.os.Vibrator;
import android.text.TextUtils;
import com.android.settings.R;
+import com.android.settings.Utils;
import com.android.settingslib.wifi.AccessPoint;
import com.android.wifitrackerlib.WifiEntry;
@@ -391,11 +393,19 @@
}
};
+ final int userId = UserHandle.myUserId();
+
final BiometricPrompt.Builder builder = new BiometricPrompt.Builder(context)
- .setTitle(context.getText(R.string.wifi_dpp_lockscreen_title));
+ .setTitle(context.getText(R.string.wifi_dpp_lockscreen_title))
+ .setUseDefaultSubtitle();
if (keyguardManager.isDeviceSecure()) {
builder.setDeviceCredentialAllowed(true);
+ builder.setTextForDeviceCredential(
+ null /* title */,
+ Utils.getConfirmCredentialStringForUser(
+ context, userId, Utils.getCredentialType(context, userId)),
+ null /* description */);
}
final BiometricPrompt bp = builder.build();
diff --git a/src/com/android/settings/wifi/dpp/WifiQrCode.java b/src/com/android/settings/wifi/dpp/WifiQrCode.java
index 2b4c3ed..70ac96c 100644
--- a/src/com/android/settings/wifi/dpp/WifiQrCode.java
+++ b/src/com/android/settings/wifi/dpp/WifiQrCode.java
@@ -160,8 +160,9 @@
private String getValueOrNull(List<String> keyValueList, String prefix) {
for (String keyValue : keyValueList) {
- if (keyValue.startsWith(prefix)) {
- return keyValue.substring(prefix.length());
+ String strippedKeyValue = keyValue.stripLeading();
+ if (strippedKeyValue.startsWith(prefix)) {
+ return strippedKeyValue.substring(prefix.length());
}
}
diff --git a/src/com/android/settings/wifi/factory/WifiFeatureProvider.java b/src/com/android/settings/wifi/factory/WifiFeatureProvider.java
index c61cf51..9e7365e 100644
--- a/src/com/android/settings/wifi/factory/WifiFeatureProvider.java
+++ b/src/com/android/settings/wifi/factory/WifiFeatureProvider.java
@@ -26,6 +26,8 @@
import androidx.lifecycle.ViewModelProvider;
import androidx.lifecycle.ViewModelStoreOwner;
+import com.android.settings.wifi.details.WifiNetworkDetailsViewModel;
+import com.android.settings.wifi.repository.SharedConnectivityRepository;
import com.android.settings.wifi.repository.WifiHotspotRepository;
import com.android.settings.wifi.tether.WifiHotspotSecurityViewModel;
import com.android.settings.wifi.tether.WifiHotspotSpeedViewModel;
@@ -44,6 +46,7 @@
private TetheringManager mTetheringManager;
private WifiVerboseLogging mWifiVerboseLogging;
private WifiHotspotRepository mWifiHotspotRepository;
+ private SharedConnectivityRepository mSharedConnectivityRepository;
public WifiFeatureProvider(@NonNull Context appContext) {
mAppContext = appContext;
@@ -93,6 +96,17 @@
}
/**
+ * Gets SharedConnectivityRepository
+ */
+ public SharedConnectivityRepository getSharedConnectivityRepository() {
+ if (mSharedConnectivityRepository == null) {
+ mSharedConnectivityRepository = new SharedConnectivityRepository(mAppContext);
+ verboseLog(TAG, "getSharedConnectivityRepository():" + mSharedConnectivityRepository);
+ }
+ return mSharedConnectivityRepository;
+ }
+
+ /**
* Gets WifiTetherViewModel
*/
public WifiTetherViewModel getWifiTetherViewModel(@NotNull ViewModelStoreOwner owner) {
@@ -122,6 +136,17 @@
}
/**
+ * Gets WifiNetworkDetailsViewModel
+ */
+ public WifiNetworkDetailsViewModel getWifiNetworkDetailsViewModel(
+ @NotNull ViewModelStoreOwner owner) {
+ WifiNetworkDetailsViewModel viewModel =
+ new ViewModelProvider(owner).get(WifiNetworkDetailsViewModel.class);
+ verboseLog(TAG, "getWifiNetworkDetailsViewModel():" + viewModel);
+ return viewModel;
+ }
+
+ /**
* Send a {@link Log#VERBOSE} log message.
*
* @param tag Used to identify the source of a log message. It usually identifies
diff --git a/src/com/android/settings/wifi/p2p/WifiP2pSettings.java b/src/com/android/settings/wifi/p2p/WifiP2pSettings.java
index c2111d6..1a268f5 100644
--- a/src/com/android/settings/wifi/p2p/WifiP2pSettings.java
+++ b/src/com/android/settings/wifi/p2p/WifiP2pSettings.java
@@ -617,6 +617,9 @@
}
private void onDeviceAvailable() {
+ if (mWifiP2pManager == null || sChannel == null) {
+ return;
+ }
mWifiP2pManager.requestNetworkInfo(sChannel, networkInfo -> {
if (sChannel == null) return;
mWifiP2pManager.requestConnectionInfo(sChannel, wifip2pinfo -> {
diff --git a/src/com/android/settings/wifi/repository/SharedConnectivityRepository.java b/src/com/android/settings/wifi/repository/SharedConnectivityRepository.java
new file mode 100644
index 0000000..35ac3f6
--- /dev/null
+++ b/src/com/android/settings/wifi/repository/SharedConnectivityRepository.java
@@ -0,0 +1,190 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.wifi.repository;
+
+import android.app.PendingIntent;
+import android.content.Context;
+import android.net.wifi.sharedconnectivity.app.HotspotNetwork;
+import android.net.wifi.sharedconnectivity.app.HotspotNetworkConnectionStatus;
+import android.net.wifi.sharedconnectivity.app.KnownNetwork;
+import android.net.wifi.sharedconnectivity.app.KnownNetworkConnectionStatus;
+import android.net.wifi.sharedconnectivity.app.SharedConnectivityClientCallback;
+import android.net.wifi.sharedconnectivity.app.SharedConnectivityManager;
+import android.net.wifi.sharedconnectivity.app.SharedConnectivitySettingsState;
+import android.os.HandlerThread;
+import android.provider.DeviceConfig;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.VisibleForTesting;
+import androidx.annotation.WorkerThread;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.MutableLiveData;
+
+import com.android.settings.overlay.FeatureFactory;
+
+import java.util.List;
+import java.util.concurrent.Executor;
+
+/**
+ * Shared Connectivity Repository for {@link SharedConnectivityManager}
+ */
+public class SharedConnectivityRepository {
+ private static final String TAG = "SharedConnectivityRepository";
+ private static final String DEVICE_CONFIG_NAMESPACE = "wifi";
+ private static final String DEVICE_CONFIG_KEY = "shared_connectivity_enabled";
+
+ private Context mAppContext;
+ private SharedConnectivityManager mManager;
+ private ClientCallback mClientCallback = new ClientCallback();
+ private HandlerThread mWorkerThread = new HandlerThread(TAG);
+ private Executor mWorkerExecutor = cmd -> mWorkerThread.getThreadHandler().post(cmd);
+ private Runnable mLaunchSettingsRunnable = () -> handleLaunchSettings();
+ @VisibleForTesting
+ MutableLiveData<SharedConnectivitySettingsState> mSettingsState = new MutableLiveData<>();
+
+ public SharedConnectivityRepository(@NonNull Context appContext) {
+ this(appContext, isDeviceConfigEnabled());
+ }
+
+ @VisibleForTesting
+ SharedConnectivityRepository(@NonNull Context appContext, boolean isConfigEnabled) {
+ mAppContext = appContext;
+ if (!isConfigEnabled) {
+ return;
+ }
+ mManager = mAppContext.getSystemService(SharedConnectivityManager.class);
+ if (mManager == null) {
+ Log.w(TAG, "Failed to get SharedConnectivityManager");
+ return;
+ }
+ mWorkerThread.start();
+ mManager.registerCallback(mWorkerExecutor, mClientCallback);
+ }
+
+ /**
+ * Return whether Wi-Fi Shared Connectivity service is available or not.
+ *
+ * @return {@code true} if Wi-Fi Shared Connectivity service is available
+ */
+ public boolean isServiceAvailable() {
+ return mManager != null;
+ }
+
+ /**
+ * Gets SharedConnectivitySettingsState LiveData
+ */
+ public LiveData<SharedConnectivitySettingsState> getSettingsState() {
+ return mSettingsState;
+ }
+
+ /**
+ * Launch Instant Hotspot Settings
+ */
+ public void launchSettings() {
+ mWorkerExecutor.execute(mLaunchSettingsRunnable);
+ }
+
+ @WorkerThread
+ @VisibleForTesting
+ void handleLaunchSettings() {
+ if (mManager == null) {
+ return;
+ }
+ SharedConnectivitySettingsState state = mManager.getSettingsState();
+ log("handleLaunchSettings(), state:" + state);
+ if (state == null) {
+ Log.e(TAG, "No SettingsState to launch Instant Hotspot settings");
+ return;
+ }
+ PendingIntent intent = state.getInstantTetherSettingsPendingIntent();
+ if (intent == null) {
+ Log.e(TAG, "No PendingIntent to launch Instant Hotspot settings");
+ return;
+ }
+ sendSettingsIntent(intent);
+ }
+
+ @WorkerThread
+ @VisibleForTesting
+ void sendSettingsIntent(@NonNull PendingIntent intent) {
+ try {
+ log("sendSettingsIntent(), sent intent:" + intent);
+ intent.send();
+ } catch (PendingIntent.CanceledException e) {
+ Log.e(TAG, "Failed to launch Instant Hotspot settings", e);
+ }
+ }
+
+ @WorkerThread
+ class ClientCallback implements SharedConnectivityClientCallback {
+
+ @Override
+ public void onHotspotNetworkConnectionStatusChanged(HotspotNetworkConnectionStatus status) {
+ log("onHotspotNetworkConnectionStatusChanged(), status:" + status);
+ }
+
+ @Override
+ public void onHotspotNetworksUpdated(List<HotspotNetwork> networks) {
+ log("onHotspotNetworksUpdated(), networks:" + networks);
+ }
+
+ @Override
+ public void onKnownNetworkConnectionStatusChanged(KnownNetworkConnectionStatus status) {
+ log("onKnownNetworkConnectionStatusChanged(), status:" + status);
+ }
+
+ @Override
+ public void onKnownNetworksUpdated(List<KnownNetwork> networks) {
+ log("onKnownNetworksUpdated(), networks:" + networks);
+ }
+
+ @Override
+ public void onRegisterCallbackFailed(Exception e) {
+ Log.e(TAG, "onRegisterCallbackFailed(), e:" + e);
+ }
+
+ @Override
+ public void onServiceConnected() {
+ SharedConnectivitySettingsState state = mManager.getSettingsState();
+ Log.d(TAG, "onServiceConnected(), Manager#getSettingsState:" + state);
+ mSettingsState.postValue(state);
+ }
+
+ @Override
+ public void onServiceDisconnected() {
+ log("onServiceDisconnected()");
+ }
+
+ @Override
+ public void onSharedConnectivitySettingsChanged(SharedConnectivitySettingsState state) {
+ Log.d(TAG, "onSharedConnectivitySettingsChanged(), state:" + state);
+ mSettingsState.postValue(state);
+ }
+ }
+
+ private void log(String msg) {
+ FeatureFactory.getFactory(mAppContext).getWifiFeatureProvider().verboseLog(TAG, msg);
+ }
+
+ /**
+ * Returns true if Shared Connectivity feature is enabled.
+ */
+ public static boolean isDeviceConfigEnabled() {
+ return DeviceConfig.getBoolean(DEVICE_CONFIG_NAMESPACE, DEVICE_CONFIG_KEY, false);
+ }
+}
diff --git a/src/com/android/settings/wifi/repository/WifiHotspotRepository.java b/src/com/android/settings/wifi/repository/WifiHotspotRepository.java
index 6764214..af8eb47 100644
--- a/src/com/android/settings/wifi/repository/WifiHotspotRepository.java
+++ b/src/com/android/settings/wifi/repository/WifiHotspotRepository.java
@@ -623,9 +623,11 @@
@VisibleForTesting
class SoftApCallback implements WifiManager.SoftApCallback {
+ private static final String TAG = "SoftApCallback";
+
@Override
public void onStateChanged(int state, int failureReason) {
- log("onStateChanged(), state:" + state + ", failureReason:" + failureReason);
+ Log.d(TAG, "onStateChanged(), state:" + state + ", failureReason:" + failureReason);
mWifiApState = state;
if (!mIsRestarting) {
return;
diff --git a/src/com/android/settings/wifi/slice/WifiSlice.java b/src/com/android/settings/wifi/slice/WifiSlice.java
index c06e869..f75fe11 100644
--- a/src/com/android/settings/wifi/slice/WifiSlice.java
+++ b/src/com/android/settings/wifi/slice/WifiSlice.java
@@ -20,6 +20,7 @@
import static android.provider.SettingsSlicesContract.KEY_WIFI;
import static com.android.settings.slices.CustomSliceRegistry.WIFI_SLICE_URI;
+import static com.android.settingslib.wifi.WifiUtils.getHotspotIconResource;
import android.annotation.ColorInt;
import android.app.PendingIntent;
@@ -264,13 +265,19 @@
android.R.attr.colorControlNormal));
}
- final Drawable drawable = mContext.getDrawable(
- WifiUtils.getInternetIconResource(wifiSliceItem.getLevel(),
- wifiSliceItem.shouldShowXLevelIcon()));
+ Drawable drawable = mContext.getDrawable(getWifiIconResId(wifiSliceItem));
drawable.setTint(tint);
return Utils.createIconWithDrawable(drawable);
}
+ @VisibleForTesting
+ int getWifiIconResId(WifiSliceItem wifiSliceItem) {
+ return (wifiSliceItem.isInstantHotspotNetwork())
+ ? getHotspotIconResource(wifiSliceItem.getInstantHotspotDeviceType())
+ : WifiUtils.getInternetIconResource(wifiSliceItem.getLevel(),
+ wifiSliceItem.shouldShowXLevelIcon());
+ }
+
protected IconCompat getEndIcon(WifiSliceItem wifiSliceItem) {
if (wifiSliceItem.getConnectedState() != WifiEntry.CONNECTED_STATE_DISCONNECTED) {
return IconCompat.createWithResource(mContext, R.drawable.ic_settings_24dp);
diff --git a/src/com/android/settings/wifi/slice/WifiSliceItem.java b/src/com/android/settings/wifi/slice/WifiSliceItem.java
index 5028a06..c6f85e5 100644
--- a/src/com/android/settings/wifi/slice/WifiSliceItem.java
+++ b/src/com/android/settings/wifi/slice/WifiSliceItem.java
@@ -20,6 +20,7 @@
import android.text.TextUtils;
import com.android.settingslib.R;
+import com.android.wifitrackerlib.HotspotNetworkEntry;
import com.android.wifitrackerlib.WifiEntry;
/**
@@ -39,6 +40,9 @@
private final boolean mHasInternetAccess;
private final String mSummary;
+ private boolean mIsInstantHotspotNetwork;
+ private int mInstantHotspotDeviceType;
+
// These values must be kept within [WifiEntry.WIFI_LEVEL_MIN, WifiEntry.WIFI_LEVEL_MAX]
private static final int[] WIFI_CONNECTION_STRENGTH = {
R.string.accessibility_no_wifi,
@@ -59,6 +63,10 @@
mShouldEditBeforeConnect = wifiEntry.shouldEditBeforeConnect();
mHasInternetAccess = wifiEntry.hasInternetAccess();
mSummary = wifiEntry.getSummary(false /* concise */);
+ mIsInstantHotspotNetwork = wifiEntry instanceof HotspotNetworkEntry;
+ if (mIsInstantHotspotNetwork) {
+ mInstantHotspotDeviceType = ((HotspotNetworkEntry) wifiEntry).getDeviceType();
+ }
}
@Override
@@ -83,6 +91,12 @@
if (!TextUtils.equals(getSummary(), otherItem.getSummary())) {
return false;
}
+ if (isInstantHotspotNetwork() != otherItem.isInstantHotspotNetwork()) {
+ return false;
+ }
+ if (getInstantHotspotDeviceType() != otherItem.getInstantHotspotDeviceType()) {
+ return false;
+ }
return true;
}
@@ -137,6 +151,20 @@
}
/**
+ * Returns true if this is a Instant Hotspot network.
+ */
+ public boolean isInstantHotspotNetwork() {
+ return mIsInstantHotspotNetwork;
+ }
+
+ /**
+ * Returns DeviceType of Instant Hotspot network.
+ */
+ public int getInstantHotspotDeviceType() {
+ return mInstantHotspotDeviceType;
+ }
+
+ /**
* This method has similar code as WifiEntryPreference#buildContentDescription().
* TODO(b/154191825): Adds WifiEntry#getContentDescription() to replace the duplicate code.
*/
diff --git a/src/com/android/settings/wifi/tether/WifiHotspotSpeedSettings.java b/src/com/android/settings/wifi/tether/WifiHotspotSpeedSettings.java
index f5066bd..a5e12d8 100644
--- a/src/com/android/settings/wifi/tether/WifiHotspotSpeedSettings.java
+++ b/src/com/android/settings/wifi/tether/WifiHotspotSpeedSettings.java
@@ -108,15 +108,17 @@
if (radioButton == null) {
continue;
}
- if (radioButton.isChecked() != speedInfo.mIsChecked) {
- radioButton.setChecked(speedInfo.mIsChecked);
+ if (!speedInfo.mIsVisible) {
+ radioButton.setVisible(false);
+ continue;
}
- if (radioButton.isEnabled() != speedInfo.mIsEnabled) {
- radioButton.setEnabled(speedInfo.mIsEnabled);
+ radioButton.setEnabled(speedInfo.mIsEnabled);
+ radioButton.setChecked(speedInfo.mIsChecked);
+ if (speedInfo.mSummary != null) {
+ radioButton.setSummary(speedInfo.mSummary);
}
- if (radioButton.isVisible() != speedInfo.mIsVisible) {
- radioButton.setVisible(speedInfo.mIsVisible);
- }
+ // setVisible at the end to avoid UI flickering
+ radioButton.setVisible(true);
}
}
diff --git a/src/com/android/settings/wifi/tether/WifiHotspotSpeedViewModel.java b/src/com/android/settings/wifi/tether/WifiHotspotSpeedViewModel.java
index f04669a..e7eb3a6 100644
--- a/src/com/android/settings/wifi/tether/WifiHotspotSpeedViewModel.java
+++ b/src/com/android/settings/wifi/tether/WifiHotspotSpeedViewModel.java
@@ -22,12 +22,15 @@
import static com.android.settings.wifi.repository.WifiHotspotRepository.SPEED_6GHZ;
import android.app.Application;
+import android.util.Log;
+import androidx.annotation.VisibleForTesting;
import androidx.lifecycle.AndroidViewModel;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.Observer;
+import com.android.settings.R;
import com.android.settings.overlay.FeatureFactory;
import com.android.settings.wifi.repository.WifiHotspotRepository;
@@ -41,6 +44,12 @@
*/
public class WifiHotspotSpeedViewModel extends AndroidViewModel {
private static final String TAG = "WifiHotspotSpeedViewModel";
+ @VisibleForTesting
+ static final int RES_SPEED_5G_SUMMARY = R.string.wifi_hotspot_speed_5g_summary;
+ @VisibleForTesting
+ static final int RES_SPEED_6G_SUMMARY = R.string.wifi_hotspot_speed_6g_summary;
+ @VisibleForTesting
+ static final int RES_SUMMARY_UNAVAILABLE = R.string.wifi_hotspot_speed_summary_unavailable;
protected final WifiHotspotRepository mWifiHotspotRepository;
protected Map<Integer, SpeedInfo> mSpeedInfoMap = new HashMap<>();
@@ -75,14 +84,18 @@
}
protected void on6gAvailableChanged(Boolean available) {
- log("on6gAvailableChanged(), available:" + available);
+ Log.d(TAG, "on6gAvailableChanged(), available:" + available);
mSpeedInfo6g.mIsEnabled = available;
+ mSpeedInfo6g.mSummary = getApplication()
+ .getString(available ? RES_SPEED_6G_SUMMARY : RES_SUMMARY_UNAVAILABLE);
updateSpeedInfoMapData();
}
protected void on5gAvailableChanged(Boolean available) {
- log("on5gAvailableChanged(), available:" + available);
+ Log.d(TAG, "on5gAvailableChanged(), available:" + available);
mSpeedInfo5g.mIsEnabled = available;
+ mSpeedInfo5g.mSummary = getApplication()
+ .getString(available ? RES_SPEED_5G_SUMMARY : RES_SUMMARY_UNAVAILABLE);
boolean showDualBand = mWifiHotspotRepository.isDualBand() && available;
log("on5gAvailableChanged(), showDualBand:" + showDualBand);
@@ -144,6 +157,7 @@
Boolean mIsChecked;
boolean mIsEnabled;
boolean mIsVisible;
+ String mSummary;
public SpeedInfo(boolean isChecked, boolean isEnabled, boolean isVisible) {
this.mIsChecked = isChecked;
@@ -157,6 +171,7 @@
.append("isChecked:").append(mIsChecked)
.append(",isEnabled:").append(mIsEnabled)
.append(",isVisible:").append(mIsVisible)
+ .append(",mSummary:").append(mSummary)
.append('}').toString();
}
}
diff --git a/src/com/android/settings/wifi/tether/WifiTetherAutoOffPreferenceController.java b/src/com/android/settings/wifi/tether/WifiTetherAutoOffPreferenceController.java
index 5dc5758..0b6d533 100644
--- a/src/com/android/settings/wifi/tether/WifiTetherAutoOffPreferenceController.java
+++ b/src/com/android/settings/wifi/tether/WifiTetherAutoOffPreferenceController.java
@@ -20,19 +20,29 @@
import android.net.wifi.SoftApConfiguration;
import android.net.wifi.WifiManager;
+import androidx.annotation.VisibleForTesting;
import androidx.preference.Preference;
import androidx.preference.SwitchPreference;
import com.android.settings.core.BasePreferenceController;
+import com.android.settings.overlay.FeatureFactory;
+import com.android.settings.wifi.repository.WifiHotspotRepository;
public class WifiTetherAutoOffPreferenceController extends BasePreferenceController implements
Preference.OnPreferenceChangeListener {
private final WifiManager mWifiManager;
private boolean mSettingsOn;
+ @VisibleForTesting
+ boolean mNeedShutdownSecondarySap;
public WifiTetherAutoOffPreferenceController(Context context, String preferenceKey) {
super(context, preferenceKey);
+ WifiHotspotRepository wifiHotspotRepository = FeatureFactory.getFactory(context)
+ .getWifiFeatureProvider().getWifiHotspotRepository();
+ if (wifiHotspotRepository.isSpeedFeatureAvailable() && wifiHotspotRepository.isDualBand()) {
+ mNeedShutdownSecondarySap = true;
+ }
mWifiManager = context.getSystemService(WifiManager.class);
}
@@ -51,14 +61,15 @@
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
- final boolean settingsOn = (Boolean) newValue;
- SoftApConfiguration softApConfiguration = mWifiManager.getSoftApConfiguration();
- SoftApConfiguration newSoftApConfiguration =
- new SoftApConfiguration.Builder(softApConfiguration)
- .setAutoShutdownEnabled(settingsOn)
- .build();
+ boolean settingsOn = (Boolean) newValue;
+ SoftApConfiguration.Builder configBuilder =
+ new SoftApConfiguration.Builder(mWifiManager.getSoftApConfiguration());
+ configBuilder.setAutoShutdownEnabled(settingsOn);
+ if (mNeedShutdownSecondarySap) {
+ configBuilder.setBridgedModeOpportunisticShutdownEnabled(settingsOn);
+ }
mSettingsOn = settingsOn;
- return mWifiManager.setSoftApConfiguration(newSoftApConfiguration);
+ return mWifiManager.setSoftApConfiguration(configBuilder.build());
}
public boolean isEnabled() {
diff --git a/src/com/android/settings/wifi/tether/WifiTetherSettings.java b/src/com/android/settings/wifi/tether/WifiTetherSettings.java
index 88d601a..fa897b7 100644
--- a/src/com/android/settings/wifi/tether/WifiTetherSettings.java
+++ b/src/com/android/settings/wifi/tether/WifiTetherSettings.java
@@ -76,6 +76,8 @@
static final String KEY_WIFI_HOTSPOT_SECURITY = "wifi_hotspot_security";
@VisibleForTesting
static final String KEY_WIFI_HOTSPOT_SPEED = "wifi_hotspot_speed";
+ @VisibleForTesting
+ static final String KEY_INSTANT_HOTSPOT = "wifi_hotspot_instant";
@VisibleForTesting
SettingsMainSwitchBar mMainSwitchBar;
@@ -91,7 +93,8 @@
@VisibleForTesting
WifiTetherAutoOffPreferenceController mWifiTetherAutoOffPreferenceController;
- private boolean mUnavailable;
+ @VisibleForTesting
+ boolean mUnavailable;
private WifiRestriction mWifiRestriction;
@VisibleForTesting
TetherChangeReceiver mTetherChangeReceiver;
@@ -102,6 +105,8 @@
Preference mWifiHotspotSecurity;
@VisibleForTesting
Preference mWifiHotspotSpeed;
+ @VisibleForTesting
+ Preference mInstantHotspot;
static {
TETHER_STATE_CHANGE_FILTER = new IntentFilter(WIFI_AP_STATE_CHANGED_ACTION);
@@ -139,11 +144,15 @@
setIfOnlyAvailableForAdmins(true);
mUnavailable = isUiRestricted() || !mWifiRestriction.isHotspotAvailable(getContext());
+ if (mUnavailable) {
+ return;
+ }
mWifiTetherViewModel = FeatureFactory.getFactory(getContext()).getWifiFeatureProvider()
.getWifiTetherViewModel(this);
if (mWifiTetherViewModel != null) {
setupSpeedFeature(mWifiTetherViewModel.isSpeedFeatureAvailable());
+ setupInstantHotspot(mWifiTetherViewModel.isInstantHotspotFeatureAvailable());
mWifiTetherViewModel.getRestarting().observe(this, this::onRestartingChanged);
}
}
@@ -163,6 +172,24 @@
}
}
+ @VisibleForTesting
+ void setupInstantHotspot(boolean isFeatureAvailable) {
+ if (!isFeatureAvailable) {
+ return;
+ }
+ mInstantHotspot = findPreference(KEY_INSTANT_HOTSPOT);
+ if (mInstantHotspot == null) {
+ Log.e(TAG, "Failed to find Instant Hotspot preference:" + KEY_INSTANT_HOTSPOT);
+ return;
+ }
+ mWifiTetherViewModel.getInstantHotspotSummary()
+ .observe(this, this::onInstantHotspotChanged);
+ mInstantHotspot.setOnPreferenceClickListener(p -> {
+ mWifiTetherViewModel.launchInstantHotspotSettings();
+ return true;
+ });
+ }
+
@Override
public void onAttach(Context context) {
super.onAttach(context);
@@ -275,6 +302,16 @@
}
@VisibleForTesting
+ void onInstantHotspotChanged(String summary) {
+ if (summary == null) {
+ mInstantHotspot.setVisible(false);
+ return;
+ }
+ mInstantHotspot.setVisible(true);
+ mInstantHotspot.setSummary(summary);
+ }
+
+ @VisibleForTesting
SoftApConfiguration buildNewConfig() {
SoftApConfiguration currentConfig = mWifiTetherViewModel.getSoftApConfiguration();
SoftApConfiguration.Builder configBuilder = new SoftApConfiguration.Builder(currentConfig);
@@ -342,7 +379,16 @@
@Override
protected boolean isPageSearchEnabled(Context context) {
- if (context == null || !WifiUtils.canShowWifiHotspot(context)) return false;
+ if (context == null) {
+ return false;
+ }
+ UserManager userManager = context.getSystemService(UserManager.class);
+ if (userManager == null || !userManager.isAdminUser()) {
+ return false;
+ }
+ if (!WifiUtils.canShowWifiHotspot(context)) {
+ return false;
+ }
return !FeatureFlagUtils.isEnabled(context, FeatureFlags.TETHER_ALL_IN_ONE);
}
diff --git a/src/com/android/settings/wifi/tether/WifiTetherViewModel.java b/src/com/android/settings/wifi/tether/WifiTetherViewModel.java
index fb2160f..b0a18a8 100644
--- a/src/com/android/settings/wifi/tether/WifiTetherViewModel.java
+++ b/src/com/android/settings/wifi/tether/WifiTetherViewModel.java
@@ -28,7 +28,9 @@
import android.app.Application;
import android.net.wifi.SoftApConfiguration;
+import android.net.wifi.sharedconnectivity.app.SharedConnectivitySettingsState;
+import androidx.annotation.VisibleForTesting;
import androidx.lifecycle.AndroidViewModel;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
@@ -36,6 +38,8 @@
import com.android.settings.R;
import com.android.settings.overlay.FeatureFactory;
+import com.android.settings.wifi.factory.WifiFeatureProvider;
+import com.android.settings.wifi.repository.SharedConnectivityRepository;
import com.android.settings.wifi.repository.WifiHotspotRepository;
import org.jetbrains.annotations.NotNull;
@@ -48,6 +52,8 @@
*/
public class WifiTetherViewModel extends AndroidViewModel {
private static final String TAG = "WifiTetherViewModel";
+ static final int RES_INSTANT_HOTSPOT_SUMMARY_ON = R.string.wifi_hotspot_instant_summary_on;
+ static final int RES_INSTANT_HOTSPOT_SUMMARY_OFF = R.string.wifi_hotspot_instant_summary_off;
static Map<Integer, Integer> sSecuritySummaryResMap = new HashMap<>();
@@ -75,10 +81,23 @@
protected final Observer<Integer> mSecurityTypeObserver = st -> onSecurityTypeChanged(st);
protected final Observer<Integer> mSpeedTypeObserver = st -> onSpeedTypeChanged(st);
+ private SharedConnectivityRepository mSharedConnectivityRepository;
+ @VisibleForTesting
+ MutableLiveData<String> mInstantHotspotSummary = new MutableLiveData<>();
+ @VisibleForTesting
+ Observer<SharedConnectivitySettingsState> mInstantHotspotStateObserver =
+ state -> onInstantHotspotStateChanged(state);
+
public WifiTetherViewModel(@NotNull Application application) {
super(application);
- mWifiHotspotRepository = FeatureFactory.getFactory(application).getWifiFeatureProvider()
- .getWifiHotspotRepository();
+ WifiFeatureProvider featureProvider = FeatureFactory.getFactory(application)
+ .getWifiFeatureProvider();
+ mWifiHotspotRepository = featureProvider.getWifiHotspotRepository();
+ mSharedConnectivityRepository = featureProvider.getSharedConnectivityRepository();
+ if (mSharedConnectivityRepository.isServiceAvailable()) {
+ mSharedConnectivityRepository.getSettingsState()
+ .observeForever(mInstantHotspotStateObserver);
+ }
}
@Override
@@ -89,6 +108,10 @@
if (mSpeedSummary != null) {
mWifiHotspotRepository.getSpeedType().removeObserver(mSpeedTypeObserver);
}
+ if (mSharedConnectivityRepository.isServiceAvailable()) {
+ mSharedConnectivityRepository.getSettingsState()
+ .removeObserver(mInstantHotspotStateObserver);
+ }
}
/**
@@ -169,4 +192,47 @@
public LiveData<Boolean> getRestarting() {
return mWifiHotspotRepository.getRestarting();
}
+
+ /**
+ * Return whether Wi-Fi Instant Hotspot feature is available or not.
+ *
+ * @return {@code true} if Wi-Fi Instant Hotspot feature is available
+ */
+ public boolean isInstantHotspotFeatureAvailable() {
+ return mSharedConnectivityRepository.isServiceAvailable();
+ }
+
+ /**
+ * Gets InstantHotspotSummary
+ */
+ public LiveData<String> getInstantHotspotSummary() {
+ return mInstantHotspotSummary;
+ }
+
+ @VisibleForTesting
+ void onInstantHotspotStateChanged(SharedConnectivitySettingsState state) {
+ log("onInstantHotspotStateChanged(), state:" + state);
+ if (state == null) {
+ mInstantHotspotSummary.setValue(null);
+ return;
+ }
+ mInstantHotspotSummary.setValue(getInstantHotspotSummary(state.isInstantTetherEnabled()));
+ }
+
+ private String getInstantHotspotSummary(boolean enabled) {
+ return getApplication().getString(
+ enabled ? RES_INSTANT_HOTSPOT_SUMMARY_ON : RES_INSTANT_HOTSPOT_SUMMARY_OFF);
+ }
+
+ /**
+ * Launch Instant Hotspot Settings
+ */
+ public void launchInstantHotspotSettings() {
+ mSharedConnectivityRepository.launchSettings();
+ }
+
+ private void log(String msg) {
+ FeatureFactory.getFactory(getApplication().getApplicationContext()).getWifiFeatureProvider()
+ .verboseLog(TAG, msg);
+ }
}
diff --git a/tests/robotests/assets/exempt_not_implementing_instrumentable b/tests/robotests/assets/exempt_not_implementing_instrumentable
index 04ef0ef..28e1e73 100644
--- a/tests/robotests/assets/exempt_not_implementing_instrumentable
+++ b/tests/robotests/assets/exempt_not_implementing_instrumentable
@@ -1,8 +1,7 @@
com.android.settings.deletionhelper.ActivationWarningFragment
com.android.settings.applications.appops.AppOpsCategory
com.android.settings.CustomListPreference$CustomListPreferenceDialogFragment
-com.android.settings.password.ChooseLockPassword$SaveAndFinishWorker
-com.android.settings.password.ChooseLockPattern$SaveAndFinishWorker
+com.android.settings.password.SaveAndFinishWorker
com.android.settings.RestrictedListPreference$RestrictedListPreferenceDialogFragment
com.android.settings.password.ConfirmDeviceCredentialBaseFragment$LastTryDialog
com.android.settings.password.CredentialCheckResultTracker
diff --git a/tests/robotests/res/drawable/regulatory_info.png b/tests/robotests/res/drawable/regulatory_info.png
deleted file mode 100644
index 65de26c..0000000
--- a/tests/robotests/res/drawable/regulatory_info.png
+++ /dev/null
Binary files differ
diff --git a/tests/robotests/res/drawable/regulatory_info_sku.png b/tests/robotests/res/drawable/regulatory_info_sku.png
deleted file mode 100644
index 65de26c..0000000
--- a/tests/robotests/res/drawable/regulatory_info_sku.png
+++ /dev/null
Binary files differ
diff --git a/tests/robotests/res/drawable/regulatory_info_sku1_coo.png b/tests/robotests/res/drawable/regulatory_info_sku1_coo.png
deleted file mode 100644
index 65de26c..0000000
--- a/tests/robotests/res/drawable/regulatory_info_sku1_coo.png
+++ /dev/null
Binary files differ
diff --git a/tests/robotests/src/com/android/settings/RegulatoryInfoDisplayActivityTest.java b/tests/robotests/src/com/android/settings/RegulatoryInfoDisplayActivityTest.java
deleted file mode 100644
index d05d5d7..0000000
--- a/tests/robotests/src/com/android/settings/RegulatoryInfoDisplayActivityTest.java
+++ /dev/null
@@ -1,83 +0,0 @@
-/*
- * Copyright (C) 2019 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.settings;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import android.os.SystemProperties;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.robolectric.Robolectric;
-import org.robolectric.RobolectricTestRunner;
-
-
-@RunWith(RobolectricTestRunner.class)
-public class RegulatoryInfoDisplayActivityTest {
-
- private static final String SKU_PROP_KEY = "ro.boot.hardware.sku";
- private static final String COO_PROP_KEY = "ro.boot.hardware.coo";
-
- private RegulatoryInfoDisplayActivity mRegulatoryInfoDisplayActivity;
-
- @Before
- public void setUp() {
- mRegulatoryInfoDisplayActivity = Robolectric.buildActivity(
- RegulatoryInfoDisplayActivity.class).create().get();
- }
-
- @Test
- public void getResourceId_noSkuProperty_shouldReturnDefaultLabel() {
- SystemProperties.set(SKU_PROP_KEY, "");
-
- final int expectedResId = getResourceId("regulatory_info");
- assertThat(mRegulatoryInfoDisplayActivity.getResourceId()).isEqualTo(expectedResId);
- }
-
- @Test
- public void getResourceId_noCooProperty_shouldReturnSkuLabel() {
- SystemProperties.set(SKU_PROP_KEY, "sku");
- SystemProperties.set(COO_PROP_KEY, "");
-
- final int expectedResId = getResourceId("regulatory_info_sku");
- assertThat(mRegulatoryInfoDisplayActivity.getResourceId()).isEqualTo(expectedResId);
- }
-
- @Test
- public void getResourceId_hasSkuAndCooProperties_shouldReturnCooLabel() {
- SystemProperties.set(SKU_PROP_KEY, "sku1");
- SystemProperties.set(COO_PROP_KEY, "coo");
-
- final int expectedResId = getResourceId("regulatory_info_sku1_coo");
- assertThat(mRegulatoryInfoDisplayActivity.getResourceId()).isEqualTo(expectedResId);
- }
-
- @Test
- public void getResourceId_noCorrespondingCooLabel_shouldReturnSkuLabel() {
- SystemProperties.set(SKU_PROP_KEY, "sku");
- SystemProperties.set(COO_PROP_KEY, "unknown");
-
- final int expectedResId = getResourceId("regulatory_info_sku");
- assertThat(mRegulatoryInfoDisplayActivity.getResourceId()).isEqualTo(expectedResId);
- }
-
- private int getResourceId(String resourceName) {
- return mRegulatoryInfoDisplayActivity.getResources().getIdentifier(resourceName, "drawable",
- mRegulatoryInfoDisplayActivity.getPackageName());
- }
-}
diff --git a/tests/robotests/src/com/android/settings/UtilsTest.java b/tests/robotests/src/com/android/settings/UtilsTest.java
index f0a18ec..733a5e6 100644
--- a/tests/robotests/src/com/android/settings/UtilsTest.java
+++ b/tests/robotests/src/com/android/settings/UtilsTest.java
@@ -16,9 +16,15 @@
package com.android.settings;
+import static android.app.admin.DevicePolicyResources.Strings.Settings.WORK_PROFILE_CONFIRM_PASSWORD;
+import static android.app.admin.DevicePolicyResources.Strings.Settings.WORK_PROFILE_CONFIRM_PATTERN;
+import static android.app.admin.DevicePolicyResources.Strings.Settings.WORK_PROFILE_CONFIRM_PIN;
+
import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
@@ -31,6 +37,7 @@
import android.app.ActionBar;
import android.app.admin.DevicePolicyManager;
+import android.app.admin.DevicePolicyResourcesManager;
import android.content.ComponentName;
import android.content.Context;
import android.content.pm.ApplicationInfo;
@@ -60,6 +67,7 @@
import androidx.core.graphics.drawable.IconCompat;
import androidx.fragment.app.FragmentActivity;
+import com.android.internal.widget.LockPatternUtils;
import com.android.settings.testutils.shadow.ShadowLockPatternUtils;
import org.junit.After;
@@ -94,6 +102,8 @@
@Mock
private DevicePolicyManager mDevicePolicyManager;
@Mock
+ private DevicePolicyResourcesManager mDevicePolicyResourcesManager;
+ @Mock
private UserManager mMockUserManager;
@Mock
private PackageManager mPackageManager;
@@ -348,4 +358,103 @@
SecurityException.class,
() -> Utils.checkUserOwnsFrpCredential(mContext, 123));
}
+
+ @Test
+ public void getConfirmCredentialStringForUser_Pin_shouldReturnCorrectString() {
+ setUpForConfirmCredentialString(false /* isEffectiveUserManagedProfile */);
+
+ when(mContext.getString(R.string.lockpassword_confirm_your_pin_generic))
+ .thenReturn("PIN");
+
+ String confirmCredentialString = Utils.getConfirmCredentialStringForUser(mContext,
+ USER_ID, LockPatternUtils.CREDENTIAL_TYPE_PIN);
+
+ assertThat(confirmCredentialString).isEqualTo("PIN");
+ }
+
+ @Test
+ public void getConfirmCredentialStringForUser_Pattern_shouldReturnCorrectString() {
+ setUpForConfirmCredentialString(false /* isEffectiveUserManagedProfile */);
+
+ when(mContext.getString(R.string.lockpassword_confirm_your_pattern_generic))
+ .thenReturn("PATTERN");
+
+ String confirmCredentialString = Utils.getConfirmCredentialStringForUser(mContext,
+ USER_ID, LockPatternUtils.CREDENTIAL_TYPE_PATTERN);
+
+ assertThat(confirmCredentialString).isEqualTo("PATTERN");
+ }
+
+ @Test
+ public void getConfirmCredentialStringForUser_Password_shouldReturnCorrectString() {
+ setUpForConfirmCredentialString(false /* isEffectiveUserManagedProfile */);
+
+ when(mContext.getString(R.string.lockpassword_confirm_your_password_generic))
+ .thenReturn("PASSWORD");
+
+ String confirmCredentialString = Utils.getConfirmCredentialStringForUser(mContext,
+ USER_ID, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD);
+
+ assertThat(confirmCredentialString).isEqualTo("PASSWORD");
+ }
+
+ @Test
+ public void getConfirmCredentialStringForUser_workPin_shouldReturnCorrectString() {
+ setUpForConfirmCredentialString(true /* isEffectiveUserManagedProfile */);
+
+ when(mDevicePolicyResourcesManager
+ .getString(eq(WORK_PROFILE_CONFIRM_PIN), any()))
+ .thenReturn("WORK PIN");
+
+ String confirmCredentialString = Utils.getConfirmCredentialStringForUser(mContext,
+ USER_ID, LockPatternUtils.CREDENTIAL_TYPE_PIN);
+
+ assertThat(confirmCredentialString).isEqualTo("WORK PIN");
+ }
+
+ @Test
+ public void getConfirmCredentialStringForUser_workPattern_shouldReturnCorrectString() {
+ setUpForConfirmCredentialString(true /* isEffectiveUserManagedProfile */);
+
+ when(mDevicePolicyResourcesManager
+ .getString(eq(WORK_PROFILE_CONFIRM_PATTERN), any()))
+ .thenReturn("WORK PATTERN");
+
+ String confirmCredentialString = Utils.getConfirmCredentialStringForUser(mContext,
+ USER_ID, LockPatternUtils.CREDENTIAL_TYPE_PATTERN);
+
+ assertThat(confirmCredentialString).isEqualTo("WORK PATTERN");
+ }
+
+ @Test
+ public void getConfirmCredentialStringForUser_workPassword_shouldReturnCorrectString() {
+ setUpForConfirmCredentialString(true /* isEffectiveUserManagedProfile */);
+
+ when(mDevicePolicyResourcesManager
+ .getString(eq(WORK_PROFILE_CONFIRM_PASSWORD), any()))
+ .thenReturn("WORK PASSWORD");
+
+ String confirmCredentialString = Utils.getConfirmCredentialStringForUser(mContext,
+ USER_ID, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD);
+
+ assertThat(confirmCredentialString).isEqualTo("WORK PASSWORD");
+ }
+
+ @Test
+ public void getConfirmCredentialStringForUser_credentialTypeNone_shouldReturnNull() {
+ setUpForConfirmCredentialString(false /* isEffectiveUserManagedProfile */);
+
+ String confirmCredentialString = Utils.getConfirmCredentialStringForUser(mContext,
+ USER_ID, LockPatternUtils.CREDENTIAL_TYPE_NONE);
+
+ assertNull(confirmCredentialString);
+ }
+
+ private void setUpForConfirmCredentialString(boolean isEffectiveUserManagedProfile) {
+ when(mContext.getSystemService(Context.USER_SERVICE)).thenReturn(mMockUserManager);
+ when(mMockUserManager.getCredentialOwnerProfile(USER_ID)).thenReturn(USER_ID);
+ when(mMockUserManager.isManagedProfile(USER_ID)).thenReturn(isEffectiveUserManagedProfile);
+ when(mContext.getSystemService(DevicePolicyManager.class)).thenReturn(mDevicePolicyManager);
+ when(mDevicePolicyManager.getResources()).thenReturn(mDevicePolicyResourcesManager);
+ }
}
diff --git a/tests/robotests/src/com/android/settings/accessibility/AccessibilitySettingsForSetupWizardTest.java b/tests/robotests/src/com/android/settings/accessibility/AccessibilitySettingsForSetupWizardTest.java
index e14e271..ea2852f 100644
--- a/tests/robotests/src/com/android/settings/accessibility/AccessibilitySettingsForSetupWizardTest.java
+++ b/tests/robotests/src/com/android/settings/accessibility/AccessibilitySettingsForSetupWizardTest.java
@@ -38,6 +38,7 @@
import android.view.accessibility.AccessibilityManager;
import androidx.fragment.app.FragmentActivity;
+import androidx.lifecycle.LifecycleOwner;
import androidx.preference.Preference;
import androidx.preference.PreferenceManager;
import androidx.preference.PreferenceScreen;
@@ -93,6 +94,7 @@
when(mAccessibilityManager.getInstalledAccessibilityServiceList()).thenReturn(
mAccessibilityServices);
doReturn(mActivity).when(mFragment).getActivity();
+ doReturn(mock(LifecycleOwner.class)).when(mFragment).getViewLifecycleOwner();
doReturn(mFooterBarMixin).when(mGlifLayoutView).getMixin(FooterBarMixin.class);
}
diff --git a/tests/robotests/src/com/android/settings/accessibility/AvailableHearingDeviceUpdaterTest.java b/tests/robotests/src/com/android/settings/accessibility/AvailableHearingDeviceUpdaterTest.java
index 6305014..0aab5bb 100644
--- a/tests/robotests/src/com/android/settings/accessibility/AvailableHearingDeviceUpdaterTest.java
+++ b/tests/robotests/src/com/android/settings/accessibility/AvailableHearingDeviceUpdaterTest.java
@@ -18,7 +18,6 @@
import static com.google.common.truth.Truth.assertThat;
-import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.when;
import android.bluetooth.BluetoothDevice;
@@ -80,8 +79,9 @@
@Test
public void isFilterMatch_connectedHearingDevice_returnTrue() {
CachedBluetoothDevice connectedHearingDevice = mCachedBluetoothDevice;
- when(connectedHearingDevice.isConnectedHearingAidDevice()).thenReturn(true);
- doReturn(BluetoothDevice.BOND_BONDED).when(mBluetoothDevice).getBondState();
+ when(connectedHearingDevice.isHearingAidDevice()).thenReturn(true);
+ when(mBluetoothDevice.isConnected()).thenReturn(true);
+ when(mBluetoothDevice.getBondState()).thenReturn(BluetoothDevice.BOND_BONDED);
when(mCachedDeviceManager.getCachedDevicesCopy()).thenReturn(
new ArrayList<>(List.of(connectedHearingDevice)));
@@ -91,8 +91,9 @@
@Test
public void isFilterMatch_nonConnectedHearingDevice_returnFalse() {
CachedBluetoothDevice nonConnectedHearingDevice = mCachedBluetoothDevice;
- when(nonConnectedHearingDevice.isConnectedHearingAidDevice()).thenReturn(false);
- doReturn(BluetoothDevice.BOND_BONDED).when(mBluetoothDevice).getBondState();
+ when(nonConnectedHearingDevice.isHearingAidDevice()).thenReturn(true);
+ when(mBluetoothDevice.isConnected()).thenReturn(false);
+ when(mBluetoothDevice.getBondState()).thenReturn(BluetoothDevice.BOND_BONDED);
when(mCachedDeviceManager.getCachedDevicesCopy()).thenReturn(
new ArrayList<>(List.of(nonConnectedHearingDevice)));
@@ -103,7 +104,8 @@
public void isFilterMatch_connectedBondingHearingDevice_returnFalse() {
CachedBluetoothDevice connectedBondingHearingDevice = mCachedBluetoothDevice;
when(connectedBondingHearingDevice.isHearingAidDevice()).thenReturn(true);
- doReturn(BluetoothDevice.BOND_BONDING).when(mBluetoothDevice).getBondState();
+ when(mBluetoothDevice.isConnected()).thenReturn(true);
+ when(mBluetoothDevice.getBondState()).thenReturn(BluetoothDevice.BOND_BONDING);
when(mCachedDeviceManager.getCachedDevicesCopy()).thenReturn(
new ArrayList<>(List.of(connectedBondingHearingDevice)));
@@ -114,8 +116,8 @@
public void isFilterMatch_hearingDeviceNotInCachedDevicesList_returnFalse() {
CachedBluetoothDevice notInCachedDevicesListDevice = mCachedBluetoothDevice;
when(notInCachedDevicesListDevice.isHearingAidDevice()).thenReturn(true);
- doReturn(BluetoothDevice.BOND_BONDED).when(mBluetoothDevice).getBondState();
- doReturn(false).when(mBluetoothDevice).isConnected();
+ when(mBluetoothDevice.isConnected()).thenReturn(true);
+ when(mBluetoothDevice.getBondState()).thenReturn(BluetoothDevice.BOND_BONDED);
when(mCachedDeviceManager.getCachedDevicesCopy()).thenReturn(new ArrayList<>());
assertThat(mUpdater.isFilterMatched(notInCachedDevicesListDevice)).isEqualTo(false);
diff --git a/tests/robotests/src/com/android/settings/accessibility/HearingAidHelperTest.java b/tests/robotests/src/com/android/settings/accessibility/HearingAidHelperTest.java
index 194b766..3889cf0 100644
--- a/tests/robotests/src/com/android/settings/accessibility/HearingAidHelperTest.java
+++ b/tests/robotests/src/com/android/settings/accessibility/HearingAidHelperTest.java
@@ -95,8 +95,7 @@
}
@Test
- public void isHearingAidSupported_supported_returnTrue() {
- mBluetoothAdapter.enable();
+ public void isHearingAidSupported_ashaSupported_returnTrue() {
mShadowBluetoothAdapter.clearSupportedProfiles();
mShadowBluetoothAdapter.addSupportedProfiles(BluetoothProfile.HEARING_AID);
@@ -104,15 +103,20 @@
}
@Test
- public void isHearingAidSupported_bluetoothOff_returnFalse() {
+ public void isHearingAidSupported_hapSupported_returnTrue() {
mShadowBluetoothAdapter.clearSupportedProfiles();
- mShadowBluetoothAdapter.addSupportedProfiles(BluetoothProfile.HEARING_AID);
- mBluetoothAdapter.disable();
+ mShadowBluetoothAdapter.addSupportedProfiles(BluetoothProfile.HAP_CLIENT);
+
+ assertThat(mHelper.isHearingAidSupported()).isTrue();
+ }
+
+ @Test
+ public void isHearingAidSupported_unsupported_returnFalse() {
+ mShadowBluetoothAdapter.clearSupportedProfiles();
assertThat(mHelper.isHearingAidSupported()).isFalse();
}
-
@Test
public void isAllHearingAidRelatedProfilesReady_allReady_returnTrue() {
when(mHearingAidProfile.isProfileReady()).thenReturn(true);
diff --git a/tests/robotests/src/com/android/settings/accessibility/HearingAidUtilsTest.java b/tests/robotests/src/com/android/settings/accessibility/HearingAidUtilsTest.java
index 09db6e9..56ab082 100644
--- a/tests/robotests/src/com/android/settings/accessibility/HearingAidUtilsTest.java
+++ b/tests/robotests/src/com/android/settings/accessibility/HearingAidUtilsTest.java
@@ -37,8 +37,10 @@
import com.android.settings.utils.ActivityControllerWrapper;
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager;
+import com.android.settingslib.bluetooth.CsipSetCoordinatorProfile;
import com.android.settingslib.bluetooth.HearingAidInfo;
import com.android.settingslib.bluetooth.LocalBluetoothManager;
+import com.android.settingslib.bluetooth.LocalBluetoothProfile;
import org.junit.Before;
import org.junit.Rule;
@@ -52,6 +54,9 @@
import org.robolectric.annotation.Config;
import org.robolectric.shadow.api.Shadow;
+import java.util.ArrayList;
+import java.util.List;
+
/** Tests for {@link HearingAidUtils}. */
@RunWith(RobolectricTestRunner.class)
@Config(shadows = {ShadowAlertDialogCompat.class, ShadowBluetoothAdapter.class,
@@ -72,6 +77,8 @@
private LocalBluetoothManager mLocalBluetoothManager;
@Mock
private CachedBluetoothDeviceManager mCachedDeviceManager;
+ @Mock
+ private CsipSetCoordinatorProfile mCsipSetCoordinatorProfile;
private BluetoothDevice mBluetoothDevice;
private BluetoothAdapter mBluetoothAdapter;
private ShadowBluetoothAdapter mShadowBluetoothAdapter;
@@ -137,6 +144,38 @@
}
@Test
+ public void launchHearingAidPairingDialog_deviceSupportsCsip_csipEnabled_noDialog() {
+ when(mCachedBluetoothDevice.isConnectedAshaHearingAidDevice()).thenReturn(true);
+ when(mCachedBluetoothDevice.getDeviceMode()).thenReturn(
+ HearingAidInfo.DeviceMode.MODE_BINAURAL);
+ when(mCachedBluetoothDevice.getDeviceSide()).thenReturn(
+ HearingAidInfo.DeviceSide.SIDE_LEFT);
+ makeDeviceSupportCsip();
+ makeDeviceEnableCsip(true);
+
+ HearingAidUtils.launchHearingAidPairingDialog(mFragmentManager, mCachedBluetoothDevice);
+
+ final AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
+ assertThat(dialog).isNull();
+ }
+
+ @Test
+ public void launchHearingAidPairingDialog_deviceSupportsCsip_csipDisabled_dialogShown() {
+ when(mCachedBluetoothDevice.isConnectedAshaHearingAidDevice()).thenReturn(true);
+ when(mCachedBluetoothDevice.getDeviceMode()).thenReturn(
+ HearingAidInfo.DeviceMode.MODE_BINAURAL);
+ when(mCachedBluetoothDevice.getDeviceSide()).thenReturn(
+ HearingAidInfo.DeviceSide.SIDE_LEFT);
+ makeDeviceSupportCsip();
+ makeDeviceEnableCsip(false);
+
+ HearingAidUtils.launchHearingAidPairingDialog(mFragmentManager, mCachedBluetoothDevice);
+
+ final AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
+ assertThat(dialog.isShowing()).isTrue();
+ }
+
+ @Test
public void launchHearingAidPairingDialog_dialogShown() {
when(mCachedBluetoothDevice.isConnectedAshaHearingAidDevice()).thenReturn(true);
when(mCachedBluetoothDevice.getDeviceMode()).thenReturn(
@@ -150,6 +189,17 @@
assertThat(dialog.isShowing()).isTrue();
}
+ private void makeDeviceSupportCsip() {
+ List<LocalBluetoothProfile> uuids = new ArrayList<>();
+ uuids.add(mCsipSetCoordinatorProfile);
+ when(mCachedBluetoothDevice.getProfiles()).thenReturn(uuids);
+ }
+
+ private void makeDeviceEnableCsip(boolean enabled) {
+ when(mCsipSetCoordinatorProfile.isEnabled(mCachedBluetoothDevice.getDevice()))
+ .thenReturn(enabled);
+ }
+
private void setupEnvironment() {
ShadowBluetoothUtils.sLocalBluetoothManager = mLocalBluetoothManager;
mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
diff --git a/tests/robotests/src/com/android/settings/accessibility/TextReadingPreferenceFragmentForSetupWizardTest.java b/tests/robotests/src/com/android/settings/accessibility/TextReadingPreferenceFragmentForSetupWizardTest.java
index 1cd301f..4ee2a2d 100644
--- a/tests/robotests/src/com/android/settings/accessibility/TextReadingPreferenceFragmentForSetupWizardTest.java
+++ b/tests/robotests/src/com/android/settings/accessibility/TextReadingPreferenceFragmentForSetupWizardTest.java
@@ -22,6 +22,7 @@
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
@@ -29,6 +30,7 @@
import android.content.Context;
import androidx.fragment.app.FragmentActivity;
+import androidx.lifecycle.LifecycleOwner;
import androidx.test.core.app.ApplicationProvider;
import com.android.settings.R;
@@ -73,6 +75,7 @@
final LayoutPreference resetPreference =
new LayoutPreference(mContext, R.layout.accessibility_text_reading_reset_button);
doReturn(mContext).when(mFragment).getContext();
+ doReturn(mock(LifecycleOwner.class)).when(mFragment).getViewLifecycleOwner();
doReturn(resetPreference).when(mFragment).findPreference(RESET_KEY);
doReturn(mFooterBarMixin).when(mGlifLayoutView).getMixin(FooterBarMixin.class);
}
diff --git a/tests/robotests/src/com/android/settings/accessibility/ToggleScreenMagnificationPreferenceFragmentForSetupWizardTest.java b/tests/robotests/src/com/android/settings/accessibility/ToggleScreenMagnificationPreferenceFragmentForSetupWizardTest.java
index 84783b21..aa622f5 100644
--- a/tests/robotests/src/com/android/settings/accessibility/ToggleScreenMagnificationPreferenceFragmentForSetupWizardTest.java
+++ b/tests/robotests/src/com/android/settings/accessibility/ToggleScreenMagnificationPreferenceFragmentForSetupWizardTest.java
@@ -20,6 +20,7 @@
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -27,6 +28,7 @@
import android.app.settings.SettingsEnums;
import android.content.Context;
+import androidx.lifecycle.LifecycleOwner;
import androidx.preference.Preference;
import androidx.preference.PreferenceManager;
import androidx.preference.PreferenceScreen;
@@ -75,6 +77,7 @@
mFragment =
spy(new TestToggleScreenMagnificationPreferenceFragmentForSetupWizard(mContext));
doReturn(mActivity).when(mFragment).getActivity();
+ doReturn(mock(LifecycleOwner.class)).when(mFragment).getViewLifecycleOwner();
when(mActivity.getSwitchBar()).thenReturn(mSwitchBar);
doReturn(mFooterBarMixin).when(mGlifLayoutView).getMixin(FooterBarMixin.class);
}
diff --git a/tests/robotests/src/com/android/settings/accessibility/ToggleScreenReaderPreferenceFragmentForSetupWizardTest.java b/tests/robotests/src/com/android/settings/accessibility/ToggleScreenReaderPreferenceFragmentForSetupWizardTest.java
index c604652..77e5b1f 100644
--- a/tests/robotests/src/com/android/settings/accessibility/ToggleScreenReaderPreferenceFragmentForSetupWizardTest.java
+++ b/tests/robotests/src/com/android/settings/accessibility/ToggleScreenReaderPreferenceFragmentForSetupWizardTest.java
@@ -20,6 +20,7 @@
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -28,6 +29,7 @@
import android.content.Context;
import android.os.Bundle;
+import androidx.lifecycle.LifecycleOwner;
import androidx.preference.PreferenceManager;
import androidx.preference.PreferenceScreen;
import androidx.test.core.app.ApplicationProvider;
@@ -72,6 +74,7 @@
public void setUp() {
mFragment = spy(new TestToggleScreenReaderPreferenceFragmentForSetupWizard(mContext));
doReturn(mActivity).when(mFragment).getActivity();
+ doReturn(mock(LifecycleOwner.class)).when(mFragment).getViewLifecycleOwner();
when(mActivity.getSwitchBar()).thenReturn(mSwitchBar);
doReturn(mFooterBarMixin).when(mGlifLayoutView).getMixin(FooterBarMixin.class);
}
diff --git a/tests/robotests/src/com/android/settings/accessibility/ToggleSelectToSpeakPreferenceFragmentForSetupWizardTest.java b/tests/robotests/src/com/android/settings/accessibility/ToggleSelectToSpeakPreferenceFragmentForSetupWizardTest.java
index 7893831..8878064 100644
--- a/tests/robotests/src/com/android/settings/accessibility/ToggleSelectToSpeakPreferenceFragmentForSetupWizardTest.java
+++ b/tests/robotests/src/com/android/settings/accessibility/ToggleSelectToSpeakPreferenceFragmentForSetupWizardTest.java
@@ -20,6 +20,7 @@
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -28,6 +29,7 @@
import android.content.Context;
import android.os.Bundle;
+import androidx.lifecycle.LifecycleOwner;
import androidx.preference.PreferenceManager;
import androidx.preference.PreferenceScreen;
import androidx.test.core.app.ApplicationProvider;
@@ -72,6 +74,7 @@
public void setUp() {
mFragment = spy(new TestToggleSelectToSpeakPreferenceFragmentForSetupWizard(mContext));
doReturn(mActivity).when(mFragment).getActivity();
+ doReturn(mock(LifecycleOwner.class)).when(mFragment).getViewLifecycleOwner();
when(mActivity.getSwitchBar()).thenReturn(mSwitchBar);
doReturn(mFooterBarMixin).when(mGlifLayoutView).getMixin(FooterBarMixin.class);
}
diff --git a/tests/robotests/src/com/android/settings/applications/AppDashboardFragmentTest.java b/tests/robotests/src/com/android/settings/applications/AppDashboardFragmentTest.java
index 0eca43c..5137516 100644
--- a/tests/robotests/src/com/android/settings/applications/AppDashboardFragmentTest.java
+++ b/tests/robotests/src/com/android/settings/applications/AppDashboardFragmentTest.java
@@ -16,12 +16,24 @@
package com.android.settings.applications;
+import static com.android.settings.core.BasePreferenceController.AVAILABLE;
+import static com.android.settings.core.BasePreferenceController.CONDITIONALLY_UNAVAILABLE;
+
import static com.google.common.truth.Truth.assertThat;
-import android.content.Context;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.os.Bundle;
+
+import com.android.settings.applications.appcompat.UserAspectRatioAppsPreferenceController;
import com.android.settings.testutils.XmlTestUtils;
import com.android.settings.testutils.shadow.ShadowUserManager;
+import com.android.settings.widget.PreferenceCategoryController;
import com.android.settingslib.core.AbstractPreferenceController;
import com.android.settingslib.drawer.CategoryKey;
@@ -31,6 +43,7 @@
import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Config;
+import org.robolectric.shadows.androidx.fragment.FragmentController;
import java.util.ArrayList;
import java.util.List;
@@ -66,4 +79,22 @@
assertThat(preferenceScreenKeys).containsAtLeastElementsIn(preferenceKeys);
}
+
+ @Test
+ @Config(shadows = ShadowUserManager.class)
+ public void testAdvancedAppsCategory() {
+ AppDashboardFragment fragment = FragmentController.of(new AppDashboardFragment(),
+ new Bundle()).create().get();
+ UserAspectRatioAppsPreferenceController controller =
+ mock(UserAspectRatioAppsPreferenceController.class);
+ final PreferenceCategoryController advancedController =
+ fragment.getAdvancedAppsPreferenceCategoryController();
+ advancedController.setChildren(List.of(controller));
+
+ when(controller.getAvailabilityStatus()).thenReturn(AVAILABLE);
+ assertTrue(advancedController.isAvailable());
+
+ when(controller.getAvailabilityStatus()).thenReturn(CONDITIONALLY_UNAVAILABLE);
+ assertFalse(advancedController.isAvailable());
+ }
}
diff --git a/tests/robotests/src/com/android/settings/applications/SpecialAppAccessPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/applications/SpecialAppAccessPreferenceControllerTest.java
deleted file mode 100644
index da5ada7..0000000
--- a/tests/robotests/src/com/android/settings/applications/SpecialAppAccessPreferenceControllerTest.java
+++ /dev/null
@@ -1,125 +0,0 @@
-/*
- * Copyright (C) 2017 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.settings.applications;
-
-import static com.android.settings.core.BasePreferenceController.AVAILABLE;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.mockito.Mockito.anyInt;
-import static org.mockito.Mockito.doReturn;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.spy;
-import static org.mockito.Mockito.when;
-
-import android.content.Context;
-import android.content.pm.ApplicationInfo;
-import android.content.pm.ModuleInfo;
-import android.content.pm.PackageManager;
-
-import androidx.preference.Preference;
-import androidx.preference.PreferenceScreen;
-
-import com.android.settings.R;
-import com.android.settings.datausage.AppStateDataUsageBridge;
-import com.android.settings.testutils.shadow.ShadowApplicationsState;
-import com.android.settings.testutils.shadow.ShadowUserManager;
-import com.android.settingslib.applications.ApplicationsState;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-import org.robolectric.RobolectricTestRunner;
-import org.robolectric.RuntimeEnvironment;
-import org.robolectric.annotation.Config;
-
-import java.util.ArrayList;
-
-@RunWith(RobolectricTestRunner.class)
-@Config(shadows = {ShadowUserManager.class, ShadowApplicationsState.class})
-public class SpecialAppAccessPreferenceControllerTest {
-
- private Context mContext;
- @Mock
- private ApplicationsState.Session mSession;
- @Mock
- private PreferenceScreen mScreen;
- @Mock
- private PackageManager mPackageManager;
-
- private SpecialAppAccessPreferenceController mController;
- private Preference mPreference;
-
- @Before
- public void setUp() {
- MockitoAnnotations.initMocks(this);
- mContext = spy(RuntimeEnvironment.application);
- when(mContext.getApplicationContext()).thenReturn(mContext);
- ShadowUserManager.getShadow().setProfileIdsWithDisabled(new int[]{0});
- doReturn(mPackageManager).when(mContext).getPackageManager();
- doReturn(new ArrayList<ModuleInfo>()).when(mPackageManager).getInstalledModules(anyInt());
- mController = new SpecialAppAccessPreferenceController(mContext, "test_key");
- mPreference = new Preference(mContext);
- when(mScreen.findPreference(mController.getPreferenceKey())).thenReturn(mPreference);
-
- mController.mSession = mSession;
- }
-
- @Test
- public void getAvailabilityState_unsearchable() {
- assertThat(mController.getAvailabilityStatus()).isEqualTo(AVAILABLE);
- }
-
- @Test
- public void updateState_shouldSetSummary() {
- final ArrayList<ApplicationsState.AppEntry> apps = new ArrayList<>();
- final ApplicationsState.AppEntry entry = mock(ApplicationsState.AppEntry.class);
- entry.hasLauncherEntry = true;
- entry.info = new ApplicationInfo();
- entry.extraInfo = new AppStateDataUsageBridge.DataUsageState(
- true /* allowlisted */, false /* denylisted */);
- apps.add(entry);
- when(mSession.getAllApps()).thenReturn(apps);
-
- mController.displayPreference(mScreen);
- mController.onExtraInfoUpdated();
-
- assertThat(mPreference.getSummary())
- .isEqualTo(mContext.getResources().getQuantityString(
- R.plurals.special_access_summary, 1, 1));
- }
-
- @Test
- public void updateState_wrongExtraInfo_shouldNotIncludeInSummary() {
- final ArrayList<ApplicationsState.AppEntry> apps = new ArrayList<>();
- final ApplicationsState.AppEntry entry = mock(ApplicationsState.AppEntry.class);
- entry.hasLauncherEntry = true;
- entry.info = new ApplicationInfo();
- entry.extraInfo = new AppStateNotificationBridge.NotificationsSentState();
- apps.add(entry);
- when(mSession.getAllApps()).thenReturn(apps);
-
- mController.displayPreference(mScreen);
- mController.onExtraInfoUpdated();
-
- assertThat(mPreference.getSummary())
- .isEqualTo(mContext.getResources().getQuantityString(
- R.plurals.special_access_summary, 0, 0));
- }
-}
diff --git a/tests/robotests/src/com/android/settings/applications/appcompat/UserAspectRatioDetailsTest.java b/tests/robotests/src/com/android/settings/applications/appcompat/UserAspectRatioDetailsTest.java
new file mode 100644
index 0000000..d98b0e7
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/applications/appcompat/UserAspectRatioDetailsTest.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.applications.appcompat;
+
+import static com.android.settings.applications.appcompat.UserAspectRatioDetails.KEY_PREF_3_2;
+import static com.android.settings.applications.appcompat.UserAspectRatioDetails.KEY_PREF_DEFAULT;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.IActivityManager;
+import android.content.Context;
+import android.os.RemoteException;
+
+import androidx.test.core.app.ApplicationProvider;
+
+import com.android.settings.testutils.shadow.ShadowActivityManager;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+
+/**
+ * To run test: atest SettingsRoboTests:UserAspectRatioDetailsTest
+ */
+@RunWith(RobolectricTestRunner.class)
+@Config(shadows = {ShadowActivityManager.class})
+public class UserAspectRatioDetailsTest {
+
+ @Mock
+ private UserAspectRatioManager mUserAspectRatioManager;
+ @Mock
+ private IActivityManager mAm;
+
+ private RadioWithImagePreference mRadioButtonPref;
+ private Context mContext;
+ private UserAspectRatioDetails mFragment;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+
+ mContext = spy(ApplicationProvider.getApplicationContext());
+ mFragment = spy(new UserAspectRatioDetails());
+ when(mFragment.getContext()).thenReturn(mContext);
+ when(mFragment.getAspectRatioManager()).thenReturn(mUserAspectRatioManager);
+ ShadowActivityManager.setService(mAm);
+ mRadioButtonPref = new RadioWithImagePreference(mContext);
+ }
+
+ @Test
+ public void onRadioButtonClicked_prefChange_shouldStopActivity() throws RemoteException {
+ // Default was already selected
+ mRadioButtonPref.setKey(KEY_PREF_DEFAULT);
+ mFragment.onRadioButtonClicked(mRadioButtonPref);
+ // Preference changed
+ mRadioButtonPref.setKey(KEY_PREF_3_2);
+ mFragment.onRadioButtonClicked(mRadioButtonPref);
+ // Only triggered once when preference change
+ verify(mAm).stopAppForUser(any(), anyInt());
+ }
+
+ @Test
+ public void onRadioButtonClicked_prefChange_shouldSetAspectRatio() throws RemoteException {
+ // Default was already selected
+ mRadioButtonPref.setKey(KEY_PREF_DEFAULT);
+ mFragment.onRadioButtonClicked(mRadioButtonPref);
+ // Preference changed
+ mRadioButtonPref.setKey(KEY_PREF_3_2);
+ mFragment.onRadioButtonClicked(mRadioButtonPref);
+ // Only triggered once when preference changes
+ verify(mUserAspectRatioManager).setUserMinAspectRatio(
+ any(), anyInt(), anyInt());
+ }
+}
diff --git a/tests/robotests/src/com/android/settings/applications/specialaccess/DataSaverControllerTest.java b/tests/robotests/src/com/android/settings/applications/specialaccess/DataSaverControllerTest.java
deleted file mode 100644
index f039c97..0000000
--- a/tests/robotests/src/com/android/settings/applications/specialaccess/DataSaverControllerTest.java
+++ /dev/null
@@ -1,74 +0,0 @@
-/*
- * Copyright (C) 2017 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.settings.applications.specialaccess;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.mockito.Mockito.spy;
-import static org.mockito.Mockito.when;
-
-import android.content.Context;
-import android.content.res.Resources;
-
-import com.android.settings.R;
-
-import org.junit.Before;
-import org.junit.Ignore;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.MockitoAnnotations;
-import org.robolectric.RobolectricTestRunner;
-import org.robolectric.RuntimeEnvironment;
-import org.robolectric.annotation.Config;
-
-@RunWith(RobolectricTestRunner.class)
-public class DataSaverControllerTest {
-
- private Context mContext;
- private Resources mResources;
- private DataSaverController mController;
-
- @Before
- public void setUp() {
- MockitoAnnotations.initMocks(this);
- mContext = spy(RuntimeEnvironment.application.getApplicationContext());
-
- mResources = spy(mContext.getResources());
- when(mContext.getResources()).thenReturn(mResources);
-
- mController = new DataSaverController(mContext, "key");
- }
-
- @Test
- public void testDataSaver_byDefault_shouldBeShown() {
- when(mResources.getBoolean(R.bool.config_show_data_saver)).thenReturn(true);
- assertThat(mController.isAvailable()).isTrue();
- }
-
- @Ignore
- @Test
- @Config(qualifiers = "mcc999")
- public void testDataSaver_ifDisabledByCarrier_shouldNotBeShown() {
- assertThat(mController.isAvailable()).isFalse();
- }
-
- @Test
- public void testDataSaver_ifDisabled_shouldNotBeShown() {
- when(mResources.getBoolean(R.bool.config_show_data_saver)).thenReturn(false);
- assertThat(mController.isAvailable()).isFalse();
- }
-}
diff --git a/tests/robotests/src/com/android/settings/biometrics/combination/CombinedBiometricProfileSettingsTest.java b/tests/robotests/src/com/android/settings/biometrics/combination/CombinedBiometricProfileSettingsTest.java
index 2ce0757..4781f56 100644
--- a/tests/robotests/src/com/android/settings/biometrics/combination/CombinedBiometricProfileSettingsTest.java
+++ b/tests/robotests/src/com/android/settings/biometrics/combination/CombinedBiometricProfileSettingsTest.java
@@ -43,6 +43,7 @@
import android.hardware.fingerprint.FingerprintManager;
import android.hardware.fingerprint.FingerprintSensorPropertiesInternal;
import android.os.Bundle;
+import android.util.AndroidRuntimeException;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@@ -58,7 +59,10 @@
import androidx.test.core.app.ApplicationProvider;
import com.android.settings.R;
+import com.android.settings.biometrics.BiometricStatusPreferenceController;
import com.android.settings.biometrics.BiometricsSplitScreenDialog;
+import com.android.settings.biometrics.face.FaceStatusPreferenceController;
+import com.android.settings.biometrics.fingerprint.FingerprintStatusPreferenceController;
import com.android.settings.password.ChooseLockSettingsHelper;
import com.android.settings.testutils.FakeFeatureFactory;
import com.android.settings.testutils.shadow.ShadowFragment;
@@ -68,7 +72,6 @@
import org.junit.After;
import org.junit.Before;
-import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -86,7 +89,6 @@
import java.util.List;
import java.util.Map;
-@Ignore
@RunWith(RobolectricTestRunner.class)
@Config(shadows = {ShadowSettingsPreferenceFragment.class, ShadowUtils.class, ShadowFragment.class})
public class CombinedBiometricProfileSettingsTest {
@@ -104,6 +106,10 @@
@Mock
private BiometricSettingsAppPreferenceController mBiometricSettingsAppPreferenceController;
@Mock
+ private FingerprintStatusPreferenceController mFingerprintStatusPreferenceController;
+ @Mock
+ private FaceStatusPreferenceController mFaceStatusPreferenceController;
+ @Mock
private FaceManager mFaceManager;
@Mock
private FragmentTransaction mFragmentTransaction;
@@ -128,6 +134,29 @@
List<AbstractPreferenceController> controllerList = new ArrayList<>();
controllerList.add(mBiometricSettingsAppPreferenceController);
preferenceControllers.put(BiometricSettingsAppPreferenceController.class, controllerList);
+ controllerList.add(mFingerprintStatusPreferenceController);
+ preferenceControllers.put(FingerprintStatusPreferenceController.class, controllerList);
+ controllerList.add(mFaceStatusPreferenceController);
+ preferenceControllers.put(FaceStatusPreferenceController.class, controllerList);
+
+ doAnswer(invocation -> {
+ final Preference preference = invocation.getArgument(0);
+ return preference.getKey().equals(mFragment.getFingerprintPreferenceKey());
+ }).when(mFingerprintStatusPreferenceController)
+ .setPreferenceTreeClickLauncher(any(), any());
+ doAnswer(invocation -> {
+ final Preference preference = invocation.getArgument(0);
+ return preference.getKey().equals(mFragment.getFingerprintPreferenceKey());
+ }).when(mFingerprintStatusPreferenceController).handlePreferenceTreeClick(any());
+ doAnswer(invocation -> {
+ final Preference preference = invocation.getArgument(0);
+ return preference.getKey().equals(mFragment.getFacePreferenceKey());
+ }).when(mFaceStatusPreferenceController)
+ .setPreferenceTreeClickLauncher(any(), any());
+ doAnswer(invocation -> {
+ final Preference preference = invocation.getArgument(0);
+ return preference.getKey().equals(mFragment.getFacePreferenceKey());
+ }).when(mFaceStatusPreferenceController).handlePreferenceTreeClick(any());
doAnswer(invocation -> {
final CharSequence key = invocation.getArgument(0);
@@ -164,7 +193,7 @@
preference.setKey(mFragment.getFingerprintPreferenceKey());
mFragment.onPreferenceTreeClick(preference);
- verify(mBiometricSettingsAppPreferenceController).handlePreferenceTreeClick(
+ verify(mFingerprintStatusPreferenceController).handlePreferenceTreeClick(
mPreferenceCaptor.capture());
List<Preference> capturedPreferences = mPreferenceCaptor.getAllValues();
@@ -224,7 +253,7 @@
mFragment.onActivityResult(CONFIRM_REQUEST, RESULT_FINISHED,
new Intent().putExtra(ChooseLockSettingsHelper.EXTRA_KEY_GK_PW_HANDLE, 1L));
- verify(mBiometricSettingsAppPreferenceController).handlePreferenceTreeClick(
+ verify(mFingerprintStatusPreferenceController).handlePreferenceTreeClick(
mPreferenceCaptor.capture());
List<Preference> capturedPreferences = mPreferenceCaptor.getAllValues();
assertThat(capturedPreferences.size()).isEqualTo(1);
@@ -254,7 +283,7 @@
preference.setKey(mFragment.getFacePreferenceKey());
mFragment.onPreferenceTreeClick(preference);
- verify(mBiometricSettingsAppPreferenceController).handlePreferenceTreeClick(
+ verify(mFaceStatusPreferenceController).handlePreferenceTreeClick(
mPreferenceCaptor.capture());
List<Preference> capturedPreferences = mPreferenceCaptor.getAllValues();
assertThat(capturedPreferences.size()).isEqualTo(1);
@@ -313,7 +342,7 @@
preference.setKey(mFragment.getFacePreferenceKey());
mFragment.onPreferenceTreeClick(preference);
- verify(mBiometricSettingsAppPreferenceController).handlePreferenceTreeClick(
+ verify(mFaceStatusPreferenceController).handlePreferenceTreeClick(
mPreferenceCaptor.capture());
List<Preference> capturedPreferences = mPreferenceCaptor.getAllValues();
assertThat(capturedPreferences.size()).isEqualTo(1);
@@ -323,7 +352,7 @@
@Test
public void testClickFingerprintUnlock_inMultiWindow_withoutEnrolledFp_showsDialog() {
testClickFingerprintUnlock(true /* isInMultiWindow */, false /* hasEnrolledFingerprint */);
- verifyShowsDialogAfterClickingUnlock();
+ verifyShowsDialogAfterClickingUnlock(mFragment.getFingerprintPreferenceKey());
}
@Test
@@ -380,7 +409,7 @@
@Test
public void testClickFaceUnlock_inMultiWindow_withoutEnrolledFp_showsDialog() {
testClickFaceUnlock(true /* isInMultiWindow */, false /*hasEnrolledFace*/);
- verifyShowsDialogAfterClickingUnlock();
+ verifyShowsDialogAfterClickingUnlock(mFragment.getFacePreferenceKey());
}
@Test
@@ -424,8 +453,11 @@
}
private void verifyNoDialogAfterClickingUnlock(String preferenceKey) {
- verify(mBiometricSettingsAppPreferenceController).handlePreferenceTreeClick(
- mPreferenceCaptor.capture());
+ final BiometricStatusPreferenceController controller =
+ preferenceKey.equals(mFragment.getFacePreferenceKey())
+ ? mFaceStatusPreferenceController
+ : mFingerprintStatusPreferenceController;
+ verify(controller).handlePreferenceTreeClick(mPreferenceCaptor.capture());
List<Preference> capturedPreferences = mPreferenceCaptor.getAllValues();
assertThat(capturedPreferences).hasSize(1);
assertThat(capturedPreferences.get(0).getKey()).isEqualTo(preferenceKey);
@@ -433,12 +465,77 @@
eq(BiometricsSplitScreenDialog.class.getName()));
}
- private void verifyShowsDialogAfterClickingUnlock() {
- verify(mBiometricSettingsAppPreferenceController, never()).handlePreferenceTreeClick(any());
+ private void verifyShowsDialogAfterClickingUnlock(String preferenceKey) {
+ final BiometricStatusPreferenceController controller =
+ preferenceKey.equals(mFragment.getFacePreferenceKey())
+ ? mFaceStatusPreferenceController
+ : mFingerprintStatusPreferenceController;
+ verify(controller, never()).handlePreferenceTreeClick(any());
verify(mFragmentTransaction).add(any(),
eq(BiometricsSplitScreenDialog.class.getName()));
}
+ @Test
+ public void testNoCrashIfDetachActivityDuringGeneratingChallengeThroughFaceManager() {
+ doAnswer(invocation -> {
+ final FaceManager.GenerateChallengeCallback callback =
+ invocation.getArgument(1);
+ mFragment.onPause();
+ mFragment.onStop();
+ mFragment.onDestroy();
+ mFragment.onDetach();
+ doReturn(null).when(mFragment).getActivity();
+ callback.onGenerateChallengeResult(0, 0, 1L);
+ return null;
+ }).when(mFaceManager).generateChallenge(anyInt(), any());
+ doThrow(new IllegalStateException("Test")).when(mFragment).requestGatekeeperHat(
+ any(), anyLong(), anyInt(), anyLong());
+ FragmentManager fragmentManager = mock(FragmentManager.class);
+
+ // Start fragment
+ mFragment.onAttach(mContext);
+ mFragment.onCreate(null);
+ mFragment.onCreateView(LayoutInflater.from(mContext), mock(ViewGroup.class), Bundle.EMPTY);
+ mFragment.onResume();
+
+ // User clicks on "Face Unlock"
+ final Preference preference = new Preference(mContext);
+ preference.setKey(mFragment.getFacePreferenceKey());
+ mFragment.onPreferenceTreeClick(preference);
+
+ verify(mFragment, never()).launchChooseOrConfirmLock();
+ }
+
+ @Test
+ public void testNoCrashIfDetachActivityDuringGeneratingChallengeThroughFingerprintManager() {
+ doAnswer(invocation -> {
+ final FingerprintManager.GenerateChallengeCallback callback =
+ invocation.getArgument(1);
+ mFragment.onPause();
+ mFragment.onStop();
+ mFragment.onDestroy();
+ mFragment.onDetach();
+ doReturn(null).when(mFragment).getActivity();
+ callback.onChallengeGenerated(0, 0, 1L);
+ return null;
+ }).when(mFingerprintManager).generateChallenge(anyInt(), any());
+ doThrow(new IllegalStateException("Test")).when(mFragment).requestGatekeeperHat(
+ any(), anyLong(), anyInt(), anyLong());
+
+ // Start fragment
+ mFragment.onAttach(mContext);
+ mFragment.onCreate(null);
+ mFragment.onCreateView(LayoutInflater.from(mContext), mock(ViewGroup.class), Bundle.EMPTY);
+ mFragment.onResume();
+
+ // User clicks on "Fingerprint Unlock"
+ final Preference preference = new Preference(mContext);
+ preference.setKey(mFragment.getFingerprintPreferenceKey());
+ mFragment.onPreferenceTreeClick(preference);
+
+ verify(mFragment, never()).launchChooseOrConfirmLock();
+ }
+
/**
* a test fragment that initializes PreferenceScreen for testing.
*/
@@ -492,7 +589,9 @@
@Override
protected void launchChooseOrConfirmLock() {
- // do nothing
+ if (getActivity() == null) {
+ throw new AndroidRuntimeException("TestFailed");
+ }
}
}
}
diff --git a/tests/robotests/src/com/android/settings/biometrics/face/FaceEnrollIntroductionTest.java b/tests/robotests/src/com/android/settings/biometrics/face/FaceEnrollIntroductionTest.java
index c4da133..df15e5c 100644
--- a/tests/robotests/src/com/android/settings/biometrics/face/FaceEnrollIntroductionTest.java
+++ b/tests/robotests/src/com/android/settings/biometrics/face/FaceEnrollIntroductionTest.java
@@ -40,6 +40,7 @@
import android.content.Context;
import android.content.Intent;
import android.content.res.Configuration;
+import android.content.res.Resources;
import android.hardware.face.Face;
import android.hardware.face.FaceManager;
import android.hardware.face.FaceSensorProperties;
@@ -116,6 +117,7 @@
private FaceEnrollIntroduction mSpyActivity;
private FakeFeatureFactory mFakeFeatureFactory;
private ShadowUserManager mUserManager;
+ private Resources mResources;
enum GateKeeperAction {CALL_SUPER, RETURN_BYTE_ARRAY, THROW_CREDENTIAL_NOT_MATCH}
@@ -245,6 +247,14 @@
when(mFaceManager.getEnrolledFaces(anyInt())).thenReturn(faces);
}
+ private void setFaceManagerToHaveWithUserId(int numEnrollments, int userId) {
+ List<Face> faces = new ArrayList<>();
+ for (int i = 0; i < numEnrollments; i++) {
+ faces.add(new Face("Face " + i /* name */, 1 /*faceId */, 1 /* deviceId */));
+ }
+ when(mFaceManager.getEnrolledFaces(userId)).thenReturn(faces);
+ }
+
@Test
public void intro_CheckCanEnroll() {
setFaceManagerToHave(0 /* numEnrollments */);
@@ -546,4 +556,40 @@
assertThat(mActivity.getPostureCallback()).isNull();
}
+ @Test
+ public void testFaceEnrollIntroduction_maxFacesNotEnrolled_addUserProfile() {
+ // Enroll a face for one user
+ setFaceManagerToHaveWithUserId(1, 0);
+
+ mContext = spy(ApplicationProvider.getApplicationContext());
+ mResources = spy(mContext.getResources());
+ when(mResources.getInteger(R.integer.suw_max_faces_enrollable)).thenReturn(1);
+
+ mController = Robolectric.buildActivity(TestFaceEnrollIntroduction.class, new Intent());
+ mActivity = (TestFaceEnrollIntroduction) mController.get();
+
+ mController.create();
+
+ // The maximum number of faces is already enrolled
+ int result = mActivity.checkMaxEnrolled();
+ assertThat(result).isEqualTo(R.string.face_intro_error_max);
+
+ // Add another user profile
+ mUserManager.addUser(10, "", 0);
+ final Intent intent = new Intent();
+ intent.putExtra(Intent.EXTRA_USER_ID, 10);
+
+ when(mResources.getInteger(R.integer.suw_max_faces_enrollable)).thenReturn(2);
+
+ mController = Robolectric.buildActivity(TestFaceEnrollIntroduction.class, intent);
+ mActivity = (TestFaceEnrollIntroduction) mController.get();
+
+ mController.create();
+
+ // The maximum number of faces hasn't been enrolled, so a new face
+ // can be enrolled for the added user profile
+ result = mActivity.checkMaxEnrolled();
+ assertThat(result).isEqualTo(0);
+ }
+
}
diff --git a/tests/robotests/src/com/android/settings/biometrics/face/FaceSettingsActivityTest.java b/tests/robotests/src/com/android/settings/biometrics/face/FaceSettingsActivityTest.java
deleted file mode 100644
index 2a95f4c..0000000
--- a/tests/robotests/src/com/android/settings/biometrics/face/FaceSettingsActivityTest.java
+++ /dev/null
@@ -1,70 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.settings.biometrics.face;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.mockito.Mockito.spy;
-
-import android.content.res.Resources;
-import android.os.Bundle;
-
-import com.android.settings.Settings;
-import com.android.settings.testutils.FakeFeatureFactory;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.robolectric.RobolectricTestRunner;
-import org.robolectric.android.controller.ActivityController;
-
-@RunWith(RobolectricTestRunner.class)
-public class FaceSettingsActivityTest {
-
- private static final String APPLIED_SETUP_WIZARD_THEME = "SettingsPreferenceTheme.SetupWizard";
-
- private Settings.FaceSettingsActivity mActivity;
- private Resources.Theme mTheme;
-
- @Before
- public void setUp() {
- FakeFeatureFactory.setupForTest();
- mActivity = spy(Settings.FaceSettingsActivity.class);
- }
-
- @Test
- public void verifyFaceSettingsActivity_shouldAppliedSetupWizardTheme() {
- createActivity();
-
- assertThat(isThemeApplied(APPLIED_SETUP_WIZARD_THEME)).isTrue();
- }
-
- private boolean isThemeApplied(String themeName) {
- final String [] appliedThemes = mTheme.getTheme();
- for (String theme : appliedThemes) {
- if (theme.contains(themeName)) {
- return true;
- }
- }
- return false;
- }
-
- private void createActivity() {
- ActivityController.of(mActivity).create(new Bundle());
- mTheme = mActivity.getTheme();
- }
-}
diff --git a/tests/robotests/src/com/android/settings/biometrics/face/FaceSettingsRemoveButtonPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/biometrics/face/FaceSettingsRemoveButtonPreferenceControllerTest.java
new file mode 100644
index 0000000..5f56fa7
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/biometrics/face/FaceSettingsRemoveButtonPreferenceControllerTest.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.biometrics.face;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.hardware.face.FaceManager;
+
+import com.android.settings.R;
+import com.android.settings.SettingsActivity;
+import com.android.settings.biometrics.face.FaceSettingsRemoveButtonPreferenceController.ConfirmRemoveDialog;
+import com.android.settings.testutils.shadow.ShadowUserManager;
+import com.android.settingslib.widget.LayoutPreference;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+import org.robolectric.Robolectric;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Config;
+import org.robolectric.shadows.ShadowApplication;
+
+@RunWith(RobolectricTestRunner.class)
+@Config(shadows = {ShadowUserManager.class})
+public class FaceSettingsRemoveButtonPreferenceControllerTest {
+ @Rule
+ public final MockitoRule mMockitoRule = MockitoJUnit.rule();
+
+ private static final String TEST_PREF_KEY = "baz";
+
+ @Mock
+ private FaceManager mFaceManager;
+ @Mock
+ private PackageManager mPackageManager;
+ private SettingsActivity mActivity;
+ private Context mContext;
+ private FaceSettingsRemoveButtonPreferenceController mController;
+ private LayoutPreference mPreference;
+
+ @Before
+ public void setUp() {
+ mContext = spy(RuntimeEnvironment.application);
+ when(mContext.getPackageManager()).thenReturn(mPackageManager);
+ when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_FACE)).thenReturn(true);
+ ShadowApplication.getInstance().setSystemService(Context.FACE_SERVICE, mFaceManager);
+
+ mPreference = new LayoutPreference(mContext, R.layout.face_remove_button);
+ mController = new FaceSettingsRemoveButtonPreferenceController(mContext, TEST_PREF_KEY);
+
+ mActivity = spy(Robolectric.buildActivity(SettingsActivity.class).create().get());
+ mController.setActivity(mActivity);
+ }
+
+ @Test
+ public void testRotationConfirmRemoveDialog() {
+ // mController calls onClick(), the dialog is created.
+ mController.updateState(mPreference);
+ assertThat(mController.mRemoving).isFalse();
+
+ mController.onClick(
+ mPreference.findViewById(R.id.security_settings_face_settings_remove_button));
+
+ ConfirmRemoveDialog removeDialog =
+ (ConfirmRemoveDialog) mActivity.getSupportFragmentManager()
+ .findFragmentByTag(ConfirmRemoveDialog.class.getName());
+ assertThat(removeDialog).isNotNull();
+ assertThat(mController.mRemoving).isTrue();
+
+
+ // Simulate rotation, a new controller mController2 is created and updateState() is called.
+ // Since the dialog hasn't been dismissed, so mController2.mRemoving should be true
+ FaceSettingsRemoveButtonPreferenceController controller2 =
+ new FaceSettingsRemoveButtonPreferenceController(mContext, TEST_PREF_KEY);
+ controller2.setActivity(mActivity);
+ assertThat(controller2.mRemoving).isFalse();
+ controller2.updateState(mPreference);
+ assertThat(controller2.mRemoving).isTrue();
+ }
+}
diff --git a/tests/robotests/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollEnrollingTest.java b/tests/robotests/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollEnrollingTest.java
index 959c642..a23eded 100644
--- a/tests/robotests/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollEnrollingTest.java
+++ b/tests/robotests/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollEnrollingTest.java
@@ -56,6 +56,7 @@
import android.view.Display;
import android.view.Surface;
import android.view.View;
+import android.view.accessibility.AccessibilityManager;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.TextView;
@@ -147,6 +148,23 @@
}
@Test
+ public void fingerprintUdfpsEnrollInitStage_afterOnEnrollmentHelp_shouldVibrate() {
+ initializeActivityFor(TYPE_UDFPS_OPTICAL);
+
+ assertThat(getLayout().getDescriptionText()).isNotEqualTo("");
+
+ mActivity.configureEnrollmentStage(0 /* lottie */);
+ mActivity.onEnrollmentHelp(1/* FINGERPRINT_ACQUIRED_PARTIAL */, mContext.getString(
+ com.android.internal.R.string.fingerprint_acquired_partial));
+
+ verify(mVibrator, never()).vibrate(anyInt(), anyString(), any(), anyString(), any());
+
+ mActivity.onEnrollmentProgressChange(1, 1);
+ verify(mVibrator).vibrate(anyInt(), anyString(), any(), anyString(), any());
+
+ }
+
+ @Test
public void fingerprintUdfpsOverlayEnrollment_gainFocus_shouldNotCancel() {
initializeActivityFor(TYPE_UDFPS_OPTICAL);
@@ -314,11 +332,17 @@
@Test
public void fingerprintUdfpsOverlayEnrollment_descriptionViewGoneWithOverlap() {
initializeActivityWithoutCreate(TYPE_UDFPS_OPTICAL);
- doReturn(true).when(mActivity).hasOverlap(any(), any());
when(mMockDisplay.getRotation()).thenReturn(Surface.ROTATION_0);
createActivity();
- final GlifLayout defaultLayout = spy(mActivity.findViewById(R.id.setup_wizard_layout));
+ final UdfpsEnrollEnrollingView defaultLayout = spy(
+ mActivity.findViewById(R.id.setup_wizard_layout));
+ doReturn(true).when(defaultLayout).hasOverlap(any(), any());
+
+ // Somehow spy doesn't work, and we need to call initView manually.
+ defaultLayout.initView(mFingerprintManager.getSensorPropertiesInternal().get(0),
+ mActivity.mUdfpsEnrollHelper,
+ mActivity.getSystemService(AccessibilityManager.class));
final TextView descriptionTextView = defaultLayout.getDescriptionTextView();
defaultLayout.getViewTreeObserver().dispatchOnDraw();
@@ -328,11 +352,17 @@
@Test
public void fingerprintUdfpsOverlayEnrollment_descriptionViewVisibleWithoutOverlap() {
initializeActivityWithoutCreate(TYPE_UDFPS_OPTICAL);
- doReturn(false).when(mActivity).hasOverlap(any(), any());
when(mMockDisplay.getRotation()).thenReturn(Surface.ROTATION_0);
createActivity();
- final GlifLayout defaultLayout = spy(mActivity.findViewById(R.id.setup_wizard_layout));
+ final UdfpsEnrollEnrollingView defaultLayout = spy(
+ mActivity.findViewById(R.id.setup_wizard_layout));
+ doReturn(false).when(defaultLayout).hasOverlap(any(), any());
+
+ // Somehow spy doesn't work, and we need to call initView manually.
+ defaultLayout.initView(mFingerprintManager.getSensorPropertiesInternal().get(0),
+ mActivity.mUdfpsEnrollHelper,
+ mActivity.getSystemService(AccessibilityManager.class));
final TextView descriptionTextView = defaultLayout.getDescriptionTextView();
defaultLayout.getViewTreeObserver().dispatchOnDraw();
@@ -340,6 +370,19 @@
}
@Test
+ public void fingerprintUdfpsOverlayEnrollment_udfpsAnimationViewVisibility() {
+ initializeActivityWithoutCreate(TYPE_UDFPS_OPTICAL);
+ when(mMockDisplay.getRotation()).thenReturn(Surface.ROTATION_0);
+ createActivity();
+
+ final UdfpsEnrollView enrollView = mActivity.findViewById(R.id.udfps_animation_view);
+ assertThat(enrollView.getVisibility()).isEqualTo(View.GONE);
+
+ mActivity.onUdfpsOverlayShown();
+ assertThat(enrollView.getVisibility()).isEqualTo(View.VISIBLE);
+ }
+
+ @Test
public void forwardEnrollProgressEvents() {
initializeActivityFor(TYPE_UDFPS_OPTICAL);
@@ -380,11 +423,11 @@
}
@Test
- public void forwardEnrollPointerDownEvents() {
+ public void forwardUdfpsEnrollPointerDownEvents() {
initializeActivityFor(TYPE_UDFPS_OPTICAL);
EnrollListener listener = new EnrollListener(mActivity);
- mActivity.onPointerDown(0);
+ mActivity.onUdfpsPointerDown(0);
assertThat(listener.mProgress).isFalse();
assertThat(listener.mHelp).isFalse();
assertThat(listener.mAcquired).isFalse();
@@ -393,11 +436,11 @@
}
@Test
- public void forwardEnrollPointerUpEvents() {
+ public void forwardUdfpsEnrollPointerUpEvents() {
initializeActivityFor(TYPE_UDFPS_OPTICAL);
EnrollListener listener = new EnrollListener(mActivity);
- mActivity.onPointerUp(0);
+ mActivity.onUdfpsPointerUp(0);
assertThat(listener.mProgress).isFalse();
assertThat(listener.mHelp).isFalse();
assertThat(listener.mAcquired).isFalse();
@@ -578,7 +621,6 @@
mContext = spy(RuntimeEnvironment.application);
mActivity = spy(FingerprintEnrollEnrolling.class);
- when(mFingerprintManager.getSensorPropertiesInternal()).thenReturn(props);
when(mContext.getDisplay()).thenReturn(mMockDisplay);
when(mMockDisplay.getRotation()).thenReturn(Surface.ROTATION_0);
diff --git a/tests/robotests/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollIntroductionTest.java b/tests/robotests/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollIntroductionTest.java
index 69f10d6..3eba91c 100644
--- a/tests/robotests/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollIntroductionTest.java
+++ b/tests/robotests/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollIntroductionTest.java
@@ -85,6 +85,7 @@
private Context mContext;
private TestFingerprintEnrollIntroduction mFingerprintEnrollIntroduction;
+ private ActivityController<TestFingerprintEnrollIntroduction> mController;
private static final int MAX_ENROLLMENTS = 5;
private static final byte[] EXPECTED_TOKEN = new byte[] { 10, 20, 30, 40 };
@@ -121,9 +122,8 @@
void setupFingerprintEnrollIntroWith(@NonNull Intent intent) {
- final ActivityController<TestFingerprintEnrollIntroduction> controller =
- Robolectric.buildActivity(TestFingerprintEnrollIntroduction.class, intent);
- mFingerprintEnrollIntroduction = controller.get();
+ mController = Robolectric.buildActivity(TestFingerprintEnrollIntroduction.class, intent);
+ mFingerprintEnrollIntroduction = mController.get();
mFingerprintEnrollIntroduction.mMockedFingerprintManager = mFingerprintManager;
mFingerprintEnrollIntroduction.mMockedGatekeeperPasswordProvider =
mGatekeeperPasswordProvider;
@@ -137,7 +137,7 @@
when(mLockPatternUtils.getActivePasswordQuality(userId))
.thenReturn(PASSWORD_QUALITY_SOMETHING);
- controller.create();
+ mController.create();
}
void setFingerprintManagerToHave(int numEnrollments) {
@@ -277,6 +277,18 @@
}
}
+ @Test
+ public void clickNext_onActivityResult_pause_shouldFinish() {
+ setupFingerprintEnrollIntroWith(newTokenOnlyIntent());
+ mController.resume();
+ mFingerprintEnrollIntroduction.clickNextBtn();
+ mController.pause().stop();
+ assertThat(mFingerprintEnrollIntroduction.shouldFinishWhenBackgrounded()).isEqualTo(false);
+
+ mController.resume().pause().stop();
+ assertThat(mFingerprintEnrollIntroduction.shouldFinishWhenBackgrounded()).isEqualTo(true);
+ }
+
private Intent newTokenOnlyIntent() {
return new Intent()
.putExtra(EXTRA_KEY_CHALLENGE_TOKEN, new byte[] { 1 });
@@ -362,5 +374,16 @@
protected void getChallenge(GenerateChallengeCallback callback) {
callback.onChallengeGenerated(mNewSensorId, mUserId, mNewChallenge);
}
+
+ @Override
+ protected boolean shouldFinishWhenBackgrounded() {
+ return super.shouldFinishWhenBackgrounded();
+ }
+
+ //mock click next btn
+ public void clickNextBtn() {
+ super.onNextButtonClick(null);
+ }
+
}
}
diff --git a/tests/robotests/src/com/android/settings/biometrics/fingerprint/FingerprintSettingsFragmentTest.java b/tests/robotests/src/com/android/settings/biometrics/fingerprint/FingerprintSettingsFragmentTest.java
index 18b05ad..8b70550 100644
--- a/tests/robotests/src/com/android/settings/biometrics/fingerprint/FingerprintSettingsFragmentTest.java
+++ b/tests/robotests/src/com/android/settings/biometrics/fingerprint/FingerprintSettingsFragmentTest.java
@@ -16,12 +16,14 @@
package com.android.settings.biometrics.fingerprint;
+import static android.hardware.fingerprint.FingerprintSensorProperties.TYPE_POWER_BUTTON;
import static android.hardware.fingerprint.FingerprintSensorProperties.TYPE_UDFPS_OPTICAL;
import static com.android.settings.biometrics.fingerprint.FingerprintSettings.FingerprintSettingsFragment;
import static com.android.settings.biometrics.fingerprint.FingerprintSettings.FingerprintSettingsFragment.ADD_FINGERPRINT_REQUEST;
import static com.android.settings.biometrics.fingerprint.FingerprintSettings.FingerprintSettingsFragment.CHOOSE_LOCK_GENERIC_REQUEST;
import static com.android.settings.biometrics.fingerprint.FingerprintSettings.FingerprintSettingsFragment.KEY_FINGERPRINT_ADD;
+import static com.android.settings.biometrics.fingerprint.FingerprintSettings.FingerprintSettingsFragment.KEY_REQUIRE_SCREEN_ON_TO_AUTH;
import static com.google.common.truth.Truth.assertThat;
@@ -39,11 +41,16 @@
import android.content.Context;
import android.content.Intent;
+import android.content.pm.UserInfo;
import android.hardware.biometrics.ComponentInfoInternal;
import android.hardware.biometrics.SensorProperties;
import android.hardware.fingerprint.FingerprintManager;
+import android.hardware.fingerprint.FingerprintSensorProperties;
import android.hardware.fingerprint.FingerprintSensorPropertiesInternal;
import android.os.Bundle;
+import android.os.CancellationSignal;
+import android.os.UserHandle;
+import android.provider.Settings;
import android.view.LayoutInflater;
import android.view.ViewGroup;
@@ -61,6 +68,7 @@
import com.android.settings.testutils.shadow.ShadowSettingsPreferenceFragment;
import com.android.settings.testutils.shadow.ShadowUserManager;
import com.android.settings.testutils.shadow.ShadowUtils;
+import com.android.settingslib.RestrictedSwitchPreference;
import org.junit.After;
import org.junit.Before;
@@ -68,6 +76,7 @@
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
@@ -81,6 +90,9 @@
@Config(shadows = {ShadowSettingsPreferenceFragment.class, ShadowUtils.class, ShadowFragment.class,
ShadowUserManager.class, ShadowLockPatternUtils.class})
public class FingerprintSettingsFragmentTest {
+ private static final int PRIMARY_USER_ID = 0;
+ private static final int GUEST_USER_ID = 10;
+
private FingerprintSettingsFragment mFragment;
private Context mContext;
private FragmentActivity mActivity;
@@ -92,11 +104,26 @@
@Mock
private FragmentTransaction mFragmentTransaction;
+ @Captor
+ private ArgumentCaptor<CancellationSignal> mCancellationSignalArgumentCaptor =
+ ArgumentCaptor.forClass(CancellationSignal.class);
+ @Captor
+ private ArgumentCaptor<FingerprintManager.AuthenticationCallback>
+ mAuthenticationCallbackArgumentCaptor = ArgumentCaptor.forClass(
+ FingerprintManager.AuthenticationCallback.class);
+
+ private FingerprintAuthenticateSidecar mFingerprintAuthenticateSidecar;
+
@Before
public void setUp() {
- doReturn(true).when(mFingerprintManager).isHardwareDetected();
ShadowUtils.setFingerprintManager(mFingerprintManager);
FakeFeatureFactory.setupForTest();
+
+ mContext = spy(ApplicationProvider.getApplicationContext());
+ mFragment = spy(new FingerprintSettingsFragment());
+ doReturn(mContext).when(mFragment).getContext();
+
+ doReturn(true).when(mFingerprintManager).isHardwareDetected();
}
@After
@@ -146,19 +173,71 @@
false)).isTrue();
}
+ // Test the case when FingerprintAuthenticateSidecar receives an error callback from the
+ // framework or from another authentication client. The cancellation signal should not be set
+ // to null because there may exist a running authentication client.
+ // The signal can only be cancelled from the caller in FingerprintSettings.
+ @Test
+ public void testCancellationSignalLifeCycle() {
+ setUpFragment(false);
+
+ mFingerprintAuthenticateSidecar.setFingerprintManager(mFingerprintManager);
+
+ doNothing().when(mFingerprintManager).authenticate(any(),
+ mCancellationSignalArgumentCaptor.capture(),
+ mAuthenticationCallbackArgumentCaptor.capture(), any(), anyInt());
+
+ mFingerprintAuthenticateSidecar.startAuthentication(1);
+
+ assertThat(mAuthenticationCallbackArgumentCaptor.getValue()).isNotNull();
+ assertThat(mCancellationSignalArgumentCaptor.getValue()).isNotNull();
+
+ // Authentication error callback should not cancel the signal.
+ mAuthenticationCallbackArgumentCaptor.getValue().onAuthenticationError(0, "");
+ assertThat(mFingerprintAuthenticateSidecar.isCancelled()).isFalse();
+
+ // The signal should be cancelled when caller stops the authentication.
+ mFingerprintAuthenticateSidecar.stopAuthentication();
+ assertThat(mFingerprintAuthenticateSidecar.isCancelled()).isTrue();
+ }
+
+ @Test
+ public void testGuestUserRequireScreenOnToAuth() {
+ Settings.Secure.putIntForUser(
+ mContext.getContentResolver(),
+ Settings.Secure.SFPS_PERFORMANT_AUTH_ENABLED,
+ 0,
+ UserHandle.of(PRIMARY_USER_ID).getIdentifier());
+
+ Settings.Secure.putIntForUser(
+ mContext.getContentResolver(),
+ Settings.Secure.SFPS_PERFORMANT_AUTH_ENABLED,
+ 1,
+ UserHandle.of(GUEST_USER_ID).getIdentifier());
+
+ setUpFragment(false, GUEST_USER_ID, TYPE_POWER_BUTTON);
+
+ final RestrictedSwitchPreference requireScreenOnToAuthPreference = mFragment.findPreference(
+ KEY_REQUIRE_SCREEN_ON_TO_AUTH);
+ assertThat(requireScreenOnToAuthPreference.isChecked()).isTrue();
+ }
+
private void setUpFragment(boolean showChooseLock) {
+ setUpFragment(showChooseLock, PRIMARY_USER_ID, TYPE_UDFPS_OPTICAL);
+ }
+
+ private void setUpFragment(boolean showChooseLock, int userId,
+ @FingerprintSensorProperties.SensorType int sensorType) {
+ ShadowUserManager.getShadow().addProfile(new UserInfo(userId, "", 0));
+
Intent intent = new Intent();
if (!showChooseLock) {
intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN, new byte[0]);
intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_GK_PW_HANDLE, 1L);
}
-
+ intent.putExtra(Intent.EXTRA_USER_ID, userId);
mActivity = spy(Robolectric.buildActivity(FragmentActivity.class, intent).get());
- mContext = spy(ApplicationProvider.getApplicationContext());
-
- mFragment = spy(new FingerprintSettingsFragment());
doReturn(mActivity).when(mFragment).getActivity();
- doReturn(mContext).when(mFragment).getContext();
FragmentManager fragmentManager = mock(FragmentManager.class);
doReturn(mFragmentTransaction).when(fragmentManager).beginTransaction();
@@ -166,9 +245,13 @@
doReturn(fragmentManager).when(mFragment).getFragmentManager();
doReturn(fragmentManager).when(mActivity).getSupportFragmentManager();
+ mFingerprintAuthenticateSidecar = new FingerprintAuthenticateSidecar();
+ doReturn(mFingerprintAuthenticateSidecar).when(fragmentManager).findFragmentByTag(
+ "authenticate_sidecar");
+
doNothing().when(mFragment).startActivityForResult(any(Intent.class), anyInt());
- setSensor();
+ setSensor(sensorType);
// Start fragment
mFragment.onAttach(mContext);
@@ -177,14 +260,14 @@
mFragment.onResume();
}
- private void setSensor() {
+ private void setSensor(@FingerprintSensorProperties.SensorType int sensorType) {
final ArrayList<FingerprintSensorPropertiesInternal> props = new ArrayList<>();
props.add(new FingerprintSensorPropertiesInternal(
0 /* sensorId */,
SensorProperties.STRENGTH_STRONG,
1 /* maxEnrollmentsPerUser */,
new ArrayList<ComponentInfoInternal>(),
- TYPE_UDFPS_OPTICAL,
+ sensorType,
true /* resetLockoutRequiresHardwareAuthToken */));
doReturn(props).when(mFingerprintManager).getSensorPropertiesInternal();
}
diff --git a/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsAudioDeviceTypeControllerTest.java b/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsAudioDeviceTypeControllerTest.java
new file mode 100644
index 0000000..0fc0647
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsAudioDeviceTypeControllerTest.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.bluetooth;
+
+import static android.bluetooth.BluetoothDevice.DEVICE_TYPE_LE;
+import static android.media.AudioManager.AUDIO_DEVICE_CATEGORY_SPEAKER;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.bluetooth.BluetoothDevice;
+import android.media.AudioManager;
+
+import androidx.preference.ListPreference;
+import androidx.preference.PreferenceCategory;
+
+import com.android.settingslib.bluetooth.LeAudioProfile;
+import com.android.settingslib.bluetooth.LocalBluetoothManager;
+import com.android.settingslib.bluetooth.LocalBluetoothProfileManager;
+import com.android.settingslib.core.lifecycle.Lifecycle;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+
+@RunWith(RobolectricTestRunner.class)
+public class BluetoothDetailsAudioDeviceTypeControllerTest extends
+ BluetoothDetailsControllerTestBase {
+
+ private static final String MAC_ADDRESS = "04:52:C7:0B:D8:3C";
+ private static final String KEY_BT_AUDIO_DEVICE_TYPE = "bluetooth_audio_device_type";
+
+ @Mock
+ private AudioManager mAudioManager;
+ @Mock
+ private Lifecycle mAudioDeviceTypeLifecycle;
+ @Mock
+ private PreferenceCategory mProfilesContainer;
+ @Mock
+ private BluetoothDevice mBluetoothDevice;
+ @Mock
+ private LocalBluetoothManager mManager;
+ @Mock
+ private LocalBluetoothProfileManager mProfileManager;
+ @Mock
+ private LeAudioProfile mLeAudioProfile;
+ private BluetoothDetailsAudioDeviceTypeController mController;
+ private ListPreference mAudioDeviceTypePref;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+
+ mContext = spy(RuntimeEnvironment.application);
+ when(mContext.getSystemService(AudioManager.class)).thenReturn(mAudioManager);
+ when(mCachedDevice.getAddress()).thenReturn(MAC_ADDRESS);
+ when(mCachedDevice.getDevice()).thenReturn(mBluetoothDevice);
+ when(mBluetoothDevice.getAnonymizedAddress()).thenReturn(MAC_ADDRESS);
+ when(mBluetoothDevice.getType()).thenReturn(DEVICE_TYPE_LE);
+ when(mManager.getProfileManager()).thenReturn(mProfileManager);
+ when(mProfileManager.getLeAudioProfile()).thenReturn(mLeAudioProfile);
+ when(mLeAudioProfile.isEnabled(mCachedDevice.getDevice())).thenReturn(true);
+
+ mController = new BluetoothDetailsAudioDeviceTypeController(mContext, mFragment, mManager,
+ mCachedDevice, mAudioDeviceTypeLifecycle);
+ mController.mProfilesContainer = mProfilesContainer;
+
+ mController.createAudioDeviceTypePreference(mContext);
+ mAudioDeviceTypePref = mController.getAudioDeviceTypePreference();
+
+ when(mProfilesContainer.findPreference(KEY_BT_AUDIO_DEVICE_TYPE)).thenReturn(
+ mAudioDeviceTypePref);
+ }
+
+ @Test
+ public void createAudioDeviceTypePreference_btDeviceIsCategorized_checkSelection() {
+ int deviceType = AUDIO_DEVICE_CATEGORY_SPEAKER;
+ when(mAudioManager.getBluetoothAudioDeviceCategory(MAC_ADDRESS, /*isBle=*/true)).thenReturn(
+ deviceType);
+
+ mController.createAudioDeviceTypePreference(mContext);
+ mAudioDeviceTypePref = mController.getAudioDeviceTypePreference();
+
+ assertThat(mAudioDeviceTypePref.getValue()).isEqualTo(Integer.toString(deviceType));
+ }
+
+ @Test
+ public void selectDeviceTypeSpeaker_invokeSetBluetoothAudioDeviceType() {
+ int deviceType = AUDIO_DEVICE_CATEGORY_SPEAKER;
+ mAudioDeviceTypePref.setValue(Integer.toString(deviceType));
+
+ mController.onPreferenceChange(mAudioDeviceTypePref, Integer.toString(deviceType));
+
+ verify(mAudioManager).setBluetoothAudioDeviceCategory(eq(MAC_ADDRESS), eq(true),
+ eq(AUDIO_DEVICE_CATEGORY_SPEAKER));
+ }
+}
diff --git a/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsDataSyncControllerTest.java b/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsDataSyncControllerTest.java
new file mode 100644
index 0000000..dbede8e
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsDataSyncControllerTest.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.bluetooth;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.when;
+
+import android.companion.CompanionDeviceManager;
+import android.companion.datatransfer.PermissionSyncRequest;
+
+import androidx.preference.PreferenceCategory;
+import androidx.preference.SwitchPreference;
+
+import com.android.settingslib.core.lifecycle.Lifecycle;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+
+import java.util.Collections;
+
+@RunWith(RobolectricTestRunner.class)
+public class BluetoothDetailsDataSyncControllerTest extends BluetoothDetailsControllerTestBase {
+
+ private static final String MAC_ADDRESS = "AA:BB:CC:DD:EE:FF";
+ private static final int DUMMY_ASSOCIATION_ID = -1;
+ private static final int ASSOCIATION_ID = 1;
+ private static final String KEY_PERM_SYNC = "perm_sync";
+
+ private BluetoothDetailsDataSyncController mController;
+ @Mock
+ private Lifecycle mLifecycle;
+ @Mock
+ private PreferenceCategory mPreferenceCategory;
+ @Mock
+ private CompanionDeviceManager mCompanionDeviceManager;
+
+ private PermissionSyncRequest mPermissionSyncRequest;
+ private SwitchPreference mPermSyncPreference;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+
+ mContext = spy(RuntimeEnvironment.application);
+ when(mContext.getSystemService(CompanionDeviceManager.class)).thenReturn(
+ mCompanionDeviceManager);
+ when(mCachedDevice.getAddress()).thenReturn(MAC_ADDRESS);
+ when(mCompanionDeviceManager.getAllAssociations()).thenReturn(Collections.emptyList());
+ mPermissionSyncRequest = new PermissionSyncRequest(ASSOCIATION_ID);
+ when(mCompanionDeviceManager.getPermissionSyncRequest(ASSOCIATION_ID)).thenReturn(
+ mPermissionSyncRequest);
+
+ mController = new BluetoothDetailsDataSyncController(mContext, mFragment,
+ mCachedDevice, mLifecycle);
+ mController.mAssociationId = ASSOCIATION_ID;
+ mController.mPreferenceCategory = mPreferenceCategory;
+
+ mPermSyncPreference = mController.createPermSyncPreference(mContext);
+ when(mPreferenceCategory.findPreference(KEY_PERM_SYNC)).thenReturn(mPermSyncPreference);
+ }
+
+ @Test
+ public void isAvailable_noAssociations_returnsFalse() {
+ mController.mAssociationId = DUMMY_ASSOCIATION_ID;
+ assertThat(mController.isAvailable()).isFalse();
+ }
+
+ @Test
+ public void isAvailable_hasAssociations_returnsTrue() {
+ assertThat(mController.isAvailable()).isTrue();
+ }
+
+ @Test
+ public void refresh_permSyncNull_preferenceVisibleFalse() {
+ mPermissionSyncRequest = null;
+ when(mCompanionDeviceManager.getPermissionSyncRequest(ASSOCIATION_ID)).thenReturn(
+ mPermissionSyncRequest);
+ mController.refresh();
+
+ assertThat(mPermSyncPreference.isVisible()).isFalse();
+ }
+
+ @Test
+ public void refresh_permSyncEnabled_preferenceCheckedTrue() {
+ mPermissionSyncRequest.setUserConsented(true);
+ mController.refresh();
+
+ assertThat(mPermSyncPreference.isVisible()).isTrue();
+ assertThat(mPermSyncPreference.isChecked()).isTrue();
+ }
+
+ @Test
+ public void refresh_permSyncDisabled_preferenceCheckedFalse() {
+ mPermissionSyncRequest.setUserConsented(false);
+ mController.refresh();
+
+ assertThat(mPermSyncPreference.isVisible()).isTrue();
+ assertThat(mPermSyncPreference.isChecked()).isFalse();
+ }
+}
diff --git a/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsSpatialAudioControllerTest.java b/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsSpatialAudioControllerTest.java
index 1f0adcf..ce5631f 100644
--- a/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsSpatialAudioControllerTest.java
+++ b/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsSpatialAudioControllerTest.java
@@ -16,6 +16,9 @@
package com.android.settings.bluetooth;
+import static android.media.Spatializer.SPATIALIZER_IMMERSIVE_LEVEL_MULTICHANNEL;
+import static android.media.Spatializer.SPATIALIZER_IMMERSIVE_LEVEL_NONE;
+
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Mockito.spy;
@@ -62,6 +65,8 @@
@Mock
private BluetoothDevice mBluetoothDevice;
+ private AudioDeviceAttributes mAvailableDevice;
+
private BluetoothDetailsSpatialAudioController mController;
private SwitchPreference mSpatialAudioPref;
private SwitchPreference mHeadTrackingPref;
@@ -86,94 +91,32 @@
when(mProfilesContainer.findPreference(KEY_SPATIAL_AUDIO)).thenReturn(mSpatialAudioPref);
when(mProfilesContainer.findPreference(KEY_HEAD_TRACKING)).thenReturn(mHeadTrackingPref);
- }
- @Test
- public void isAvailable_spatialAudioSupportA2dpDevice_returnsTrue() {
- AudioDeviceAttributes a2dpDevice = new AudioDeviceAttributes(
+ mAvailableDevice = new AudioDeviceAttributes(
AudioDeviceAttributes.ROLE_OUTPUT,
AudioDeviceInfo.TYPE_BLUETOOTH_A2DP,
MAC_ADDRESS);
- when(mSpatializer.isAvailableForDevice(a2dpDevice)).thenReturn(true);
-
- mController.setAvailableDevice(a2dpDevice);
-
- assertThat(mController.isAvailable()).isTrue();
- assertThat(mController.mAudioDevice.getType())
- .isEqualTo(AudioDeviceInfo.TYPE_BLUETOOTH_A2DP);
}
@Test
- public void isAvailable_spatialAudioSupportBleHeadsetDevice_returnsTrue() {
- AudioDeviceAttributes bleHeadsetDevice = new AudioDeviceAttributes(
- AudioDeviceAttributes.ROLE_OUTPUT,
- AudioDeviceInfo.TYPE_BLE_HEADSET,
- MAC_ADDRESS);
- when(mSpatializer.isAvailableForDevice(bleHeadsetDevice)).thenReturn(true);
-
- mController.setAvailableDevice(bleHeadsetDevice);
-
- assertThat(mController.isAvailable()).isTrue();
- assertThat(mController.mAudioDevice.getType())
- .isEqualTo(AudioDeviceInfo.TYPE_BLE_HEADSET);
- }
-
- @Test
- public void isAvailable_spatialAudioSupportBleSpeakerDevice_returnsTrue() {
- AudioDeviceAttributes bleSpeakerDevice = new AudioDeviceAttributes(
- AudioDeviceAttributes.ROLE_OUTPUT,
- AudioDeviceInfo.TYPE_BLE_SPEAKER,
- MAC_ADDRESS);
- when(mSpatializer.isAvailableForDevice(bleSpeakerDevice)).thenReturn(true);
-
- mController.setAvailableDevice(bleSpeakerDevice);
-
- assertThat(mController.isAvailable()).isTrue();
- assertThat(mController.mAudioDevice.getType())
- .isEqualTo(AudioDeviceInfo.TYPE_BLE_SPEAKER);
- }
-
- @Test
- public void isAvailable_spatialAudioSupportBleBroadcastDevice_returnsTrue() {
- AudioDeviceAttributes bleBroadcastDevice = new AudioDeviceAttributes(
- AudioDeviceAttributes.ROLE_OUTPUT,
- AudioDeviceInfo.TYPE_BLE_BROADCAST,
- MAC_ADDRESS);
- when(mSpatializer.isAvailableForDevice(bleBroadcastDevice)).thenReturn(true);
-
- mController.setAvailableDevice(bleBroadcastDevice);
-
- assertThat(mController.isAvailable()).isTrue();
- assertThat(mController.mAudioDevice.getType())
- .isEqualTo(AudioDeviceInfo.TYPE_BLE_BROADCAST);
- }
-
- @Test
- public void isAvailable_spatialAudioSupportHearingAidDevice_returnsTrue() {
- AudioDeviceAttributes hearingAidDevice = new AudioDeviceAttributes(
- AudioDeviceAttributes.ROLE_OUTPUT,
- AudioDeviceInfo.TYPE_HEARING_AID,
- MAC_ADDRESS);
- when(mSpatializer.isAvailableForDevice(hearingAidDevice)).thenReturn(true);
-
- mController.setAvailableDevice(hearingAidDevice);
-
- assertThat(mController.isAvailable()).isTrue();
- assertThat(mController.mAudioDevice.getType())
- .isEqualTo(AudioDeviceInfo.TYPE_HEARING_AID);
- }
-
- @Test
- public void isAvailable_spatialAudioNotSupported_returnsFalse() {
+ public void isAvailable_forSpatializerWithLevelNone_returnsFalse() {
+ when(mSpatializer.getImmersiveAudioLevel()).thenReturn(SPATIALIZER_IMMERSIVE_LEVEL_NONE);
assertThat(mController.isAvailable()).isFalse();
- assertThat(mController.mAudioDevice.getType())
- .isEqualTo(AudioDeviceInfo.TYPE_HEARING_AID);
+ }
+
+ @Test
+ public void isAvailable_forSpatializerWithLevelNotNone_returnsTrue() {
+ when(mSpatializer.getImmersiveAudioLevel()).thenReturn(
+ SPATIALIZER_IMMERSIVE_LEVEL_MULTICHANNEL);
+ assertThat(mController.isAvailable()).isTrue();
}
@Test
public void refresh_spatialAudioIsTurnedOn_checksSpatialAudioPreference() {
List<AudioDeviceAttributes> compatibleAudioDevices = new ArrayList<>();
+ mController.setAvailableDevice(mAvailableDevice);
compatibleAudioDevices.add(mController.mAudioDevice);
+ when(mSpatializer.isAvailableForDevice(mController.mAudioDevice)).thenReturn(true);
when(mSpatializer.getCompatibleAudioDevices()).thenReturn(compatibleAudioDevices);
mController.refresh();
@@ -207,13 +150,14 @@
public void
refresh_spatialAudioOnAndHeadTrackingIsNotAvailable_hidesHeadTrackingPreference() {
List<AudioDeviceAttributes> compatibleAudioDevices = new ArrayList<>();
+ mController.setAvailableDevice(mAvailableDevice);
compatibleAudioDevices.add(mController.mAudioDevice);
when(mSpatializer.getCompatibleAudioDevices()).thenReturn(compatibleAudioDevices);
when(mSpatializer.hasHeadTracker(mController.mAudioDevice)).thenReturn(false);
mController.refresh();
- assertThat(mHeadTrackingPref.isVisible()).isFalse();
+ verify(mProfilesContainer).removePreference(mHeadTrackingPref);
}
@Test
@@ -223,14 +167,16 @@
mController.refresh();
- assertThat(mHeadTrackingPref.isVisible()).isFalse();
+ verify(mProfilesContainer).removePreference(mHeadTrackingPref);
}
@Test
public void refresh_headTrackingIsTurnedOn_checksHeadTrackingPreference() {
List<AudioDeviceAttributes> compatibleAudioDevices = new ArrayList<>();
+ mController.setAvailableDevice(mAvailableDevice);
compatibleAudioDevices.add(mController.mAudioDevice);
when(mSpatializer.getCompatibleAudioDevices()).thenReturn(compatibleAudioDevices);
+ when(mSpatializer.isAvailableForDevice(mController.mAudioDevice)).thenReturn(true);
when(mSpatializer.hasHeadTracker(mController.mAudioDevice)).thenReturn(true);
when(mSpatializer.isHeadTrackerEnabled(mController.mAudioDevice)).thenReturn(true);
@@ -242,8 +188,10 @@
@Test
public void refresh_headTrackingIsTurnedOff_unchecksHeadTrackingPreference() {
List<AudioDeviceAttributes> compatibleAudioDevices = new ArrayList<>();
+ mController.setAvailableDevice(mAvailableDevice);
compatibleAudioDevices.add(mController.mAudioDevice);
when(mSpatializer.getCompatibleAudioDevices()).thenReturn(compatibleAudioDevices);
+ when(mSpatializer.isAvailableForDevice(mController.mAudioDevice)).thenReturn(true);
when(mSpatializer.hasHeadTracker(mController.mAudioDevice)).thenReturn(true);
when(mSpatializer.isHeadTrackerEnabled(mController.mAudioDevice)).thenReturn(false);
@@ -254,6 +202,7 @@
@Test
public void turnedOnSpatialAudio_invokesAddCompatibleAudioDevice() {
+ mController.setAvailableDevice(mAvailableDevice);
mSpatialAudioPref.setChecked(true);
mController.onPreferenceClick(mSpatialAudioPref);
verify(mSpatializer).addCompatibleAudioDevice(mController.mAudioDevice);
@@ -261,6 +210,7 @@
@Test
public void turnedOffSpatialAudio_invokesRemoveCompatibleAudioDevice() {
+ mController.setAvailableDevice(mAvailableDevice);
mSpatialAudioPref.setChecked(false);
mController.onPreferenceClick(mSpatialAudioPref);
verify(mSpatializer).removeCompatibleAudioDevice(mController.mAudioDevice);
@@ -268,6 +218,7 @@
@Test
public void turnedOnHeadTracking_invokesSetHeadTrackerEnabled_setsTrue() {
+ mController.setAvailableDevice(mAvailableDevice);
mHeadTrackingPref.setChecked(true);
mController.onPreferenceClick(mHeadTrackingPref);
verify(mSpatializer).setHeadTrackerEnabled(true, mController.mAudioDevice);
@@ -275,6 +226,7 @@
@Test
public void turnedOffHeadTracking_invokesSetHeadTrackerEnabled_setsFalse() {
+ mController.setAvailableDevice(mAvailableDevice);
mHeadTrackingPref.setChecked(false);
mController.onPreferenceClick(mHeadTrackingPref);
verify(mSpatializer).setHeadTrackerEnabled(false, mController.mAudioDevice);
diff --git a/tests/robotests/src/com/android/settings/bluetooth/BluetoothDevicePairingDetailBaseTest.java b/tests/robotests/src/com/android/settings/bluetooth/BluetoothDevicePairingDetailBaseTest.java
index 184f521..7c598e0 100644
--- a/tests/robotests/src/com/android/settings/bluetooth/BluetoothDevicePairingDetailBaseTest.java
+++ b/tests/robotests/src/com/android/settings/bluetooth/BluetoothDevicePairingDetailBaseTest.java
@@ -202,7 +202,7 @@
new BluetoothDevicePreference(mContext, mCachedBluetoothDevice,
true, BluetoothDevicePreference.SortType.TYPE_FIFO);
final BluetoothDevice device = mBluetoothAdapter.getRemoteDevice(TEST_DEVICE_ADDRESS);
- mFragment.mDevicePreferenceMap.put(mCachedBluetoothDevice, preference);
+ mFragment.getDevicePreferenceMap().put(mCachedBluetoothDevice, preference);
when(mCachedBluetoothDevice.isConnected()).thenReturn(true);
when(mCachedBluetoothDevice.getDevice()).thenReturn(device);
@@ -210,7 +210,7 @@
mFragment.onProfileConnectionStateChanged(mCachedBluetoothDevice,
BluetoothProfile.A2DP, BluetoothAdapter.STATE_CONNECTED);
- assertThat(mFragment.mDevicePreferenceMap.size()).isEqualTo(0);
+ assertThat(mFragment.getDevicePreferenceMap().size()).isEqualTo(0);
}
@Test
@@ -221,7 +221,7 @@
true, BluetoothDevicePreference.SortType.TYPE_FIFO);
final BluetoothDevice device = mBluetoothAdapter.getRemoteDevice(TEST_DEVICE_ADDRESS);
final BluetoothDevice device2 = mBluetoothAdapter.getRemoteDevice(TEST_DEVICE_ADDRESS_B);
- mFragment.mDevicePreferenceMap.put(mCachedBluetoothDevice, preference);
+ mFragment.getDevicePreferenceMap().put(mCachedBluetoothDevice, preference);
when(mCachedBluetoothDevice.isConnected()).thenReturn(true);
when(mCachedBluetoothDevice.getDevice()).thenReturn(device);
diff --git a/tests/robotests/src/com/android/settings/bluetooth/BluetoothPairingDetailTest.java b/tests/robotests/src/com/android/settings/bluetooth/BluetoothPairingDetailTest.java
index 5fbfee8..ce67051 100644
--- a/tests/robotests/src/com/android/settings/bluetooth/BluetoothPairingDetailTest.java
+++ b/tests/robotests/src/com/android/settings/bluetooth/BluetoothPairingDetailTest.java
@@ -27,7 +27,12 @@
import android.bluetooth.BluetoothAdapter;
import android.content.Context;
import android.os.Bundle;
+import android.view.View;
+import androidx.annotation.NonNull;
+import androidx.lifecycle.Lifecycle;
+import androidx.lifecycle.LifecycleObserver;
+import androidx.lifecycle.LifecycleOwner;
import androidx.test.core.app.ApplicationProvider;
import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager;
@@ -53,6 +58,20 @@
private final Context mContext = ApplicationProvider.getApplicationContext();
+ private final Lifecycle mFakeLifecycle = new Lifecycle() {
+ @Override
+ public void addObserver(@NonNull LifecycleObserver observer) {}
+
+ @Override
+ public void removeObserver(@NonNull LifecycleObserver observer) {}
+
+ @NonNull
+ @Override
+ public State getCurrentState() {
+ return State.CREATED;
+ }
+ };
+
@Mock(answer = Answers.RETURNS_DEEP_STUBS)
private LocalBluetoothManager mLocalManager;
@Mock(answer = Answers.RETURNS_DEEP_STUBS)
@@ -74,6 +93,8 @@
.findPreference(BluetoothPairingDetail.KEY_AVAIL_DEVICES);
doReturn(mFooterPreference).when(mFragment)
.findPreference(BluetoothPairingDetail.KEY_FOOTER_PREF);
+ doReturn(new View(mContext)).when(mFragment).getView();
+ doReturn((LifecycleOwner) () -> mFakeLifecycle).when(mFragment).getViewLifecycleOwner();
doReturn(Collections.emptyList()).when(mDeviceManager).getCachedDevicesCopy();
mFragment.mBluetoothAdapter = mBluetoothAdapter;
@@ -82,7 +103,7 @@
mFragment.mDeviceListGroup = mAvailableDevicesCategory;
mFragment.onViewCreated(mFragment.getView(), Bundle.EMPTY);
}
-//
+
@Test
public void initPreferencesFromPreferenceScreen_findPreferences() {
mFragment.initPreferencesFromPreferenceScreen();
diff --git a/tests/robotests/src/com/android/settings/bluetooth/DeviceListPreferenceFragmentTest.java b/tests/robotests/src/com/android/settings/bluetooth/DeviceListPreferenceFragmentTest.java
deleted file mode 100644
index 4f46ce9..0000000
--- a/tests/robotests/src/com/android/settings/bluetooth/DeviceListPreferenceFragmentTest.java
+++ /dev/null
@@ -1,237 +0,0 @@
-/*
- * Copyright (C) 2017 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.settings.bluetooth;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.doReturn;
-import static org.mockito.Mockito.spy;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-
-import android.bluetooth.BluetoothAdapter;
-import android.bluetooth.BluetoothUuid;
-import android.bluetooth.le.BluetoothLeScanner;
-import android.bluetooth.le.ScanCallback;
-import android.bluetooth.le.ScanFilter;
-import android.bluetooth.le.ScanSettings;
-import android.content.Context;
-import android.content.res.Resources;
-
-import androidx.preference.Preference;
-
-import com.android.settings.R;
-import com.android.settings.testutils.shadow.ShadowBluetoothAdapter;
-import com.android.settingslib.bluetooth.CachedBluetoothDevice;
-import com.android.settingslib.core.AbstractPreferenceController;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-import org.robolectric.RobolectricTestRunner;
-import org.robolectric.RuntimeEnvironment;
-import org.robolectric.annotation.Config;
-
-import java.util.Collections;
-import java.util.List;
-
-@RunWith(RobolectricTestRunner.class)
-@Config(shadows = {ShadowBluetoothAdapter.class})
-public class DeviceListPreferenceFragmentTest {
-
- private static final String FOOTAGE_MAC_STRING = "Bluetooth mac: xxxx";
-
- @Mock
- private Resources mResource;
- @Mock
- private Context mContext;
- @Mock
- private BluetoothLeScanner mBluetoothLeScanner;
-
- private TestFragment mFragment;
- private Preference mMyDevicePreference;
-
-
- private BluetoothAdapter mBluetoothAdapter;
- @Before
- public void setUp() {
- MockitoAnnotations.initMocks(this);
-
- mFragment = spy(new TestFragment());
- doReturn(mContext).when(mFragment).getContext();
- doReturn(mResource).when(mFragment).getResources();
- mBluetoothAdapter = spy(BluetoothAdapter.getDefaultAdapter());
- mFragment.mBluetoothAdapter = mBluetoothAdapter;
-
- mMyDevicePreference = new Preference(RuntimeEnvironment.application);
- }
-
- @Test
- public void setUpdateMyDevicePreference_setTitleCorrectly() {
- doReturn(FOOTAGE_MAC_STRING).when(mFragment)
- .getString(eq(R.string.bluetooth_footer_mac_message), any());
-
- mFragment.updateFooterPreference(mMyDevicePreference);
-
- assertThat(mMyDevicePreference.getTitle()).isEqualTo(FOOTAGE_MAC_STRING);
- }
-
- @Test
- public void testEnableDisableScanning_testStateAfterEanbleDisable() {
- mFragment.enableScanning();
- verify(mFragment).startScanning();
- assertThat(mFragment.mScanEnabled).isTrue();
-
- mFragment.disableScanning();
- verify(mFragment).stopScanning();
- assertThat(mFragment.mScanEnabled).isFalse();
- }
-
- @Test
- public void testScanningStateChanged_testScanStarted() {
- mFragment.enableScanning();
- assertThat(mFragment.mScanEnabled).isTrue();
- verify(mFragment).startScanning();
-
- mFragment.onScanningStateChanged(true);
- verify(mFragment, times(1)).startScanning();
- }
-
- @Test
- public void testScanningStateChanged_testScanFinished() {
- // Could happen when last scanning not done while current scan gets enabled
- mFragment.enableScanning();
- verify(mFragment).startScanning();
- assertThat(mFragment.mScanEnabled).isTrue();
-
- mFragment.onScanningStateChanged(false);
- verify(mFragment, times(2)).startScanning();
- }
-
- @Test
- public void testScanningStateChanged_testScanStateMultiple() {
- // Could happen when last scanning not done while current scan gets enabled
- mFragment.enableScanning();
- assertThat(mFragment.mScanEnabled).isTrue();
- verify(mFragment).startScanning();
-
- mFragment.onScanningStateChanged(true);
- verify(mFragment, times(1)).startScanning();
-
- mFragment.onScanningStateChanged(false);
- verify(mFragment, times(2)).startScanning();
-
- mFragment.onScanningStateChanged(true);
- verify(mFragment, times(2)).startScanning();
-
- mFragment.disableScanning();
- verify(mFragment).stopScanning();
-
- mFragment.onScanningStateChanged(false);
- verify(mFragment, times(2)).startScanning();
-
- mFragment.onScanningStateChanged(true);
- verify(mFragment, times(2)).startScanning();
- }
-
- @Test
- public void testScanningStateChanged_testScanFinishedAfterDisable() {
- mFragment.enableScanning();
- verify(mFragment).startScanning();
- assertThat(mFragment.mScanEnabled).isTrue();
-
- mFragment.disableScanning();
- verify(mFragment).stopScanning();
- assertThat(mFragment.mScanEnabled).isFalse();
-
- mFragment.onScanningStateChanged(false);
- verify(mFragment, times(1)).startScanning();
- }
-
- @Test
- public void testScanningStateChanged_testScanStartedAfterDisable() {
- mFragment.enableScanning();
- verify(mFragment).startScanning();
- assertThat(mFragment.mScanEnabled).isTrue();
-
- mFragment.disableScanning();
- verify(mFragment).stopScanning();
- assertThat(mFragment.mScanEnabled).isFalse();
-
- mFragment.onScanningStateChanged(true);
- verify(mFragment, times(1)).startScanning();
- }
-
- @Test
- public void startScanning_setLeScanFilter_shouldStartLeScan() {
- final ScanFilter leScanFilter = new ScanFilter.Builder()
- .setServiceData(BluetoothUuid.HEARING_AID, new byte[]{0}, new byte[]{0})
- .build();
- doReturn(mBluetoothLeScanner).when(mBluetoothAdapter).getBluetoothLeScanner();
-
- mFragment.setFilter(Collections.singletonList(leScanFilter));
- mFragment.startScanning();
-
- verify(mBluetoothLeScanner).startScan(eq(Collections.singletonList(leScanFilter)),
- any(ScanSettings.class), any(ScanCallback.class));
- }
-
- /**
- * Fragment to test since {@code DeviceListPreferenceFragment} is abstract
- */
- public static class TestFragment extends DeviceListPreferenceFragment {
-
- public TestFragment() {
- super("");
- }
-
- @Override
- public int getMetricsCategory() {
- return 0;
- }
-
- @Override
- public void onDeviceBondStateChanged(CachedBluetoothDevice cachedDevice, int bondState) {}
-
- @Override
- protected void initPreferencesFromPreferenceScreen() {}
-
- @Override
- public String getDeviceListKey() {
- return null;
- }
-
- @Override
- protected String getLogTag() {
- return null;
- }
-
- @Override
- protected int getPreferenceScreenResId() {
- return 0;
- }
-
- @Override
- protected List<AbstractPreferenceController> createPreferenceControllers(Context context) {
- return null;
- }
- }
-}
diff --git a/tests/robotests/src/com/android/settings/bluetooth/DeviceListPreferenceFragmentTest.kt b/tests/robotests/src/com/android/settings/bluetooth/DeviceListPreferenceFragmentTest.kt
new file mode 100644
index 0000000..5a21aff
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/bluetooth/DeviceListPreferenceFragmentTest.kt
@@ -0,0 +1,260 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.bluetooth
+
+import android.bluetooth.BluetoothAdapter
+import android.bluetooth.BluetoothDevice
+import android.bluetooth.BluetoothUuid
+import android.bluetooth.le.BluetoothLeScanner
+import android.bluetooth.le.ScanCallback
+import android.bluetooth.le.ScanFilter
+import android.content.Context
+import android.content.res.Resources
+import androidx.preference.Preference
+import com.android.settings.R
+import com.android.settings.testutils.shadow.ShadowBluetoothAdapter
+import com.android.settingslib.bluetooth.BluetoothDeviceFilter
+import com.android.settingslib.bluetooth.CachedBluetoothDevice
+import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.runBlocking
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.any
+import org.mockito.ArgumentMatchers.eq
+import org.mockito.Mock
+import org.mockito.Mockito.doNothing
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import org.mockito.Spy
+import org.mockito.junit.MockitoJUnit
+import org.mockito.junit.MockitoRule
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.RuntimeEnvironment
+import org.robolectric.annotation.Config
+import org.mockito.Mockito.`when` as whenever
+
+@RunWith(RobolectricTestRunner::class)
+@Config(shadows = [ShadowBluetoothAdapter::class])
+class DeviceListPreferenceFragmentTest {
+ @get:Rule
+ val mockito: MockitoRule = MockitoJUnit.rule()
+
+ @Mock
+ private lateinit var resource: Resources
+
+ @Mock
+ private lateinit var context: Context
+
+ @Mock
+ private lateinit var bluetoothLeScanner: BluetoothLeScanner
+
+ @Mock
+ private lateinit var cachedDeviceManager: CachedBluetoothDeviceManager
+
+ @Mock
+ private lateinit var cachedDevice: CachedBluetoothDevice
+
+ @Spy
+ private var fragment = TestFragment()
+
+ private lateinit var myDevicePreference: Preference
+ private lateinit var bluetoothAdapter: BluetoothAdapter
+
+ @Before
+ fun setUp() {
+ doReturn(context).`when`(fragment).context
+ doReturn(resource).`when`(fragment).resources
+ doNothing().`when`(fragment).onDeviceAdded(cachedDevice)
+ bluetoothAdapter = spy(BluetoothAdapter.getDefaultAdapter())
+ fragment.mBluetoothAdapter = bluetoothAdapter
+ fragment.mCachedDeviceManager = cachedDeviceManager
+
+ myDevicePreference = Preference(RuntimeEnvironment.application)
+ }
+
+ @Test
+ fun setUpdateMyDevicePreference_setTitleCorrectly() {
+ doReturn(FOOTAGE_MAC_STRING).`when`(fragment)
+ .getString(eq(R.string.bluetooth_footer_mac_message), any())
+
+ fragment.updateFooterPreference(myDevicePreference)
+
+ assertThat(myDevicePreference.title).isEqualTo(FOOTAGE_MAC_STRING)
+ }
+
+ @Test
+ fun testEnableDisableScanning_testStateAfterEnableDisable() {
+ fragment.enableScanning()
+ verify(fragment).startScanning()
+ assertThat(fragment.mScanEnabled).isTrue()
+
+ fragment.disableScanning()
+ verify(fragment).stopScanning()
+ assertThat(fragment.mScanEnabled).isFalse()
+ }
+
+ @Test
+ fun testScanningStateChanged_testScanStarted() {
+ fragment.enableScanning()
+ assertThat(fragment.mScanEnabled).isTrue()
+ verify(fragment).startScanning()
+
+ fragment.onScanningStateChanged(true)
+ verify(fragment, times(1)).startScanning()
+ }
+
+ @Test
+ fun testScanningStateChanged_testScanFinished() {
+ // Could happen when last scanning not done while current scan gets enabled
+ fragment.enableScanning()
+ verify(fragment).startScanning()
+ assertThat(fragment.mScanEnabled).isTrue()
+
+ fragment.onScanningStateChanged(false)
+ verify(fragment, times(2)).startScanning()
+ }
+
+ @Test
+ fun testScanningStateChanged_testScanStateMultiple() {
+ // Could happen when last scanning not done while current scan gets enabled
+ fragment.enableScanning()
+ assertThat(fragment.mScanEnabled).isTrue()
+ verify(fragment).startScanning()
+
+ fragment.onScanningStateChanged(true)
+ verify(fragment, times(1)).startScanning()
+
+ fragment.onScanningStateChanged(false)
+ verify(fragment, times(2)).startScanning()
+
+ fragment.onScanningStateChanged(true)
+ verify(fragment, times(2)).startScanning()
+
+ fragment.disableScanning()
+ verify(fragment).stopScanning()
+
+ fragment.onScanningStateChanged(false)
+ verify(fragment, times(2)).startScanning()
+
+ fragment.onScanningStateChanged(true)
+ verify(fragment, times(2)).startScanning()
+ }
+
+ @Test
+ fun testScanningStateChanged_testScanFinishedAfterDisable() {
+ fragment.enableScanning()
+ verify(fragment).startScanning()
+ assertThat(fragment.mScanEnabled).isTrue()
+
+ fragment.disableScanning()
+ verify(fragment).stopScanning()
+ assertThat(fragment.mScanEnabled).isFalse()
+
+ fragment.onScanningStateChanged(false)
+ verify(fragment, times(1)).startScanning()
+ }
+
+ @Test
+ fun testScanningStateChanged_testScanStartedAfterDisable() {
+ fragment.enableScanning()
+ verify(fragment).startScanning()
+ assertThat(fragment.mScanEnabled).isTrue()
+
+ fragment.disableScanning()
+ verify(fragment).stopScanning()
+ assertThat(fragment.mScanEnabled).isFalse()
+
+ fragment.onScanningStateChanged(true)
+ verify(fragment, times(1)).startScanning()
+ }
+
+ @Test
+ fun startScanning_setLeScanFilter_shouldStartLeScan() {
+ val leScanFilter = ScanFilter.Builder()
+ .setServiceData(BluetoothUuid.HEARING_AID, byteArrayOf(0), byteArrayOf(0))
+ .build()
+ doReturn(bluetoothLeScanner).`when`(bluetoothAdapter).bluetoothLeScanner
+
+ fragment.setFilter(listOf(leScanFilter))
+ fragment.startScanning()
+
+ verify(bluetoothLeScanner).startScan(eq(listOf(leScanFilter)), any(), any<ScanCallback>())
+ }
+
+ @Test
+ fun addCachedDevices_whenFilterIsNull_onDeviceAddedIsCalled() = runBlocking {
+ val mockCachedDevice = mock(CachedBluetoothDevice::class.java)
+ whenever(cachedDeviceManager.cachedDevicesCopy).thenReturn(listOf(mockCachedDevice))
+ fragment.lifecycleScope = this
+
+ fragment.addCachedDevices(filterForCachedDevices = null)
+ delay(100)
+
+ verify(fragment).onDeviceAdded(mockCachedDevice)
+ }
+
+ @Test
+ fun addCachedDevices_whenFilterMatched_onDeviceAddedIsCalled() = runBlocking {
+ val mockBluetoothDevice = mock(BluetoothDevice::class.java)
+ whenever(mockBluetoothDevice.bondState).thenReturn(BluetoothDevice.BOND_NONE)
+ whenever(cachedDevice.device).thenReturn(mockBluetoothDevice)
+ whenever(cachedDeviceManager.cachedDevicesCopy).thenReturn(listOf(cachedDevice))
+ fragment.lifecycleScope = this
+
+ fragment.addCachedDevices(BluetoothDeviceFilter.UNBONDED_DEVICE_FILTER)
+ delay(100)
+
+ verify(fragment).onDeviceAdded(cachedDevice)
+ }
+
+ @Test
+ fun addCachedDevices_whenFilterNoMatch_onDeviceAddedNotCalled() = runBlocking {
+ val mockBluetoothDevice = mock(BluetoothDevice::class.java)
+ whenever(mockBluetoothDevice.bondState).thenReturn(BluetoothDevice.BOND_BONDED)
+ whenever(cachedDevice.device).thenReturn(mockBluetoothDevice)
+ whenever(cachedDeviceManager.cachedDevicesCopy).thenReturn(listOf(cachedDevice))
+ fragment.lifecycleScope = this
+
+ fragment.addCachedDevices(BluetoothDeviceFilter.UNBONDED_DEVICE_FILTER)
+ delay(100)
+
+ verify(fragment, never()).onDeviceAdded(cachedDevice)
+ }
+
+ /**
+ * Fragment to test since `DeviceListPreferenceFragment` is abstract
+ */
+ open class TestFragment : DeviceListPreferenceFragment(null) {
+ override fun getMetricsCategory() = 0
+ override fun initPreferencesFromPreferenceScreen() {}
+ override val deviceListKey = "device_list"
+ override fun getLogTag() = null
+ override fun getPreferenceScreenResId() = 0
+ }
+
+ private companion object {
+ const val FOOTAGE_MAC_STRING = "Bluetooth mac: xxxx"
+ }
+}
diff --git a/tests/robotests/src/com/android/settings/connecteddevice/stylus/StylusDevicesControllerTest.java b/tests/robotests/src/com/android/settings/connecteddevice/stylus/StylusDevicesControllerTest.java
index f4fa397..9538092 100644
--- a/tests/robotests/src/com/android/settings/connecteddevice/stylus/StylusDevicesControllerTest.java
+++ b/tests/robotests/src/com/android/settings/connecteddevice/stylus/StylusDevicesControllerTest.java
@@ -18,6 +18,10 @@
import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.doNothing;
@@ -27,13 +31,17 @@
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
+import android.app.Dialog;
import android.app.role.RoleManager;
import android.bluetooth.BluetoothDevice;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
+import android.content.pm.UserInfo;
+import android.os.Process;
import android.os.UserHandle;
+import android.os.UserManager;
import android.provider.Settings;
import android.provider.Settings.Secure;
import android.view.InputDevice;
@@ -48,6 +56,8 @@
import androidx.test.core.app.ApplicationProvider;
import com.android.settings.R;
+import com.android.settings.dashboard.profileselector.UserAdapter;
+import com.android.settingslib.PrimarySwitchPreference;
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
import com.android.settingslib.core.lifecycle.Lifecycle;
@@ -59,7 +69,9 @@
import org.mockito.MockitoAnnotations;
import org.robolectric.RobolectricTestRunner;
+import java.util.Arrays;
import java.util.Collections;
+import java.util.List;
@RunWith(RobolectricTestRunner.class)
public class StylusDevicesControllerTest {
@@ -79,6 +91,8 @@
@Mock
private PackageManager mPm;
@Mock
+ private UserManager mUserManager;
+ @Mock
private RoleManager mRm;
@Mock
private Lifecycle mLifecycle;
@@ -87,7 +101,6 @@
@Mock
private BluetoothDevice mBluetoothDevice;
-
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
@@ -101,6 +114,7 @@
when(mContext.getSystemService(InputMethodManager.class)).thenReturn(mImm);
when(mContext.getSystemService(RoleManager.class)).thenReturn(mRm);
+ when(mContext.getSystemService(UserManager.class)).thenReturn(mUserManager);
doNothing().when(mContext).startActivity(any());
when(mImm.getCurrentInputMethodInfo()).thenReturn(mInputMethodInfo);
@@ -115,6 +129,8 @@
when(mPm.getApplicationInfo(eq(NOTES_PACKAGE_NAME),
any(PackageManager.ApplicationInfoFlags.class))).thenReturn(new ApplicationInfo());
when(mPm.getApplicationLabel(any(ApplicationInfo.class))).thenReturn(NOTES_APP_LABEL);
+ when(mUserManager.getUsers()).thenReturn(Arrays.asList(new UserInfo(0, "default", 0)));
+ when(mUserManager.isManagedProfile(anyInt())).thenReturn(false);
when(mCachedBluetoothDevice.getDevice()).thenReturn(mBluetoothDevice);
@@ -228,22 +244,50 @@
when(mInputMethodInfo.supportsStylusHandwriting()).thenReturn(false);
showScreen(mController);
- Preference handwritingPref = mPreferenceContainer.getPreference(1);
+ Preference handwritingPref = mPreferenceContainer.getPreference(1);
assertThat(handwritingPref.isVisible()).isFalse();
}
@Test
- public void defaultNotesPreference_showsNotesRoleApp() {
+ public void defaultNotesPreference_singleUser_showsNotesRoleApp() {
showScreen(mController);
- Preference defaultNotesPref = mPreferenceContainer.getPreference(0);
+ Preference defaultNotesPref = mPreferenceContainer.getPreference(0);
assertThat(defaultNotesPref.getTitle().toString()).isEqualTo(
mContext.getString(R.string.stylus_default_notes_app));
assertThat(defaultNotesPref.getSummary().toString()).isEqualTo(NOTES_APP_LABEL.toString());
}
@Test
+ public void defaultNotesPreference_workProfileUser_showsWorkNotesRoleApp() {
+ when(mUserManager.isManagedProfile(0)).thenReturn(true);
+
+ showScreen(mController);
+
+ Preference defaultNotesPref = mPreferenceContainer.getPreference(0);
+ assertThat(defaultNotesPref.getTitle().toString()).isEqualTo(
+ mContext.getString(R.string.stylus_default_notes_app));
+ assertThat(defaultNotesPref.getSummary().toString()).isEqualTo(
+ mContext.getString(R.string.stylus_default_notes_summary_work,
+ NOTES_APP_LABEL.toString()));
+ }
+
+ @Test
+ public void defaultNotesPreference_noApplicationInfo_showsBlankSummary()
+ throws PackageManager.NameNotFoundException {
+ when(mPm.getApplicationInfo(eq(NOTES_PACKAGE_NAME),
+ any(PackageManager.ApplicationInfoFlags.class))).thenReturn(null);
+
+ showScreen(mController);
+
+ Preference defaultNotesPref = mPreferenceContainer.getPreference(0);
+ assertThat(defaultNotesPref.getTitle().toString()).isEqualTo(
+ mContext.getString(R.string.stylus_default_notes_app));
+ assertThat(defaultNotesPref.getSummary().toString()).isEqualTo("");
+ }
+
+ @Test
public void defaultNotesPreference_roleHolderChanges_updatesPreference() {
showScreen(mController);
Preference defaultNotesPref = mPreferenceContainer.getPreference(0);
@@ -267,7 +311,7 @@
}
@Test
- public void defaultNotesPreferenceClick_sendsManageDefaultRoleIntent() {
+ public void defaultNotesPreferenceClick_singleUser_sendsManageDefaultRoleIntent() {
final String permissionPackageName = "permissions.package";
when(mPm.getPermissionControllerPackageName()).thenReturn(permissionPackageName);
final ArgumentCaptor<Intent> captor = ArgumentCaptor.forClass(Intent.class);
@@ -282,6 +326,76 @@
assertThat(intent.getPackage()).isEqualTo(permissionPackageName);
assertThat(intent.getStringExtra(Intent.EXTRA_ROLE_NAME)).isEqualTo(
RoleManager.ROLE_NOTES);
+ assertNull(mController.mDialog);
+ }
+
+ @Test
+ public void defaultNotesPreferenceClick_multiUserManagedProfile_showsProfileSelectorDialog() {
+ mContext.setTheme(R.style.Theme_AppCompat);
+ final String permissionPackageName = "permissions.package";
+ final UserHandle currentUser = Process.myUserHandle();
+ List<UserInfo> userInfos = Arrays.asList(
+ new UserInfo(currentUser.getIdentifier(), "current", 0),
+ new UserInfo(1, "profile", UserInfo.FLAG_PROFILE)
+ );
+ when(mUserManager.getUsers()).thenReturn(userInfos);
+ when(mUserManager.isManagedProfile(1)).thenReturn(true);
+ when(mUserManager.getUserInfo(currentUser.getIdentifier())).thenReturn(userInfos.get(0));
+ when(mUserManager.getUserInfo(1)).thenReturn(userInfos.get(1));
+ when(mUserManager.getProfileParent(1)).thenReturn(userInfos.get(0));
+ when(mPm.getPermissionControllerPackageName()).thenReturn(permissionPackageName);
+
+ showScreen(mController);
+ Preference defaultNotesPref = mPreferenceContainer.getPreference(0);
+ mController.onPreferenceClick(defaultNotesPref);
+
+ assertTrue(mController.mDialog.isShowing());
+ }
+
+ @Test
+ public void defaultNotesPreferenceClick_noManagedProfile_sendsManageDefaultRoleIntent() {
+ final ArgumentCaptor<Intent> captor = ArgumentCaptor.forClass(Intent.class);
+ mContext.setTheme(R.style.Theme_AppCompat);
+ final String permissionPackageName = "permissions.package";
+ final UserHandle currentUser = Process.myUserHandle();
+ List<UserInfo> userInfos = Arrays.asList(
+ new UserInfo(currentUser.getIdentifier(), "current", 0),
+ new UserInfo(1, "other", UserInfo.FLAG_FULL)
+ );
+ when(mUserManager.getUsers()).thenReturn(userInfos);
+ when(mUserManager.isManagedProfile(1)).thenReturn(false);
+ when(mUserManager.getUserInfo(currentUser.getIdentifier())).thenReturn(userInfos.get(0));
+ when(mUserManager.getUserInfo(1)).thenReturn(userInfos.get(1));
+ when(mUserManager.getProfileParent(any())).thenReturn(null);
+ when(mPm.getPermissionControllerPackageName()).thenReturn(permissionPackageName);
+
+ showScreen(mController);
+ Preference defaultNotesPref = mPreferenceContainer.getPreference(0);
+ mController.onPreferenceClick(defaultNotesPref);
+
+ verify(mContext).startActivity(captor.capture());
+ Intent intent = captor.getValue();
+ assertThat(intent.getAction()).isEqualTo(Intent.ACTION_MANAGE_DEFAULT_APP);
+ assertThat(intent.getPackage()).isEqualTo(permissionPackageName);
+ assertThat(intent.getStringExtra(Intent.EXTRA_ROLE_NAME)).isEqualTo(
+ RoleManager.ROLE_NOTES);
+ assertNull(mController.mDialog);
+ }
+
+ @Test
+ public void profileSelectDialogClickCallback_onClick_sendsIntent() {
+ Intent intent = new Intent();
+ UserHandle user1 = mock(UserHandle.class);
+ UserHandle user2 = mock(UserHandle.class);
+ List<UserHandle> users = Arrays.asList(user1, user2);
+ mController.mDialog = new Dialog(mContext);
+ UserAdapter.OnClickListener callback = mController
+ .createProfileDialogClickCallback(intent, users);
+
+ callback.onClick(1);
+
+ assertEquals(intent.getExtra(Intent.EXTRA_USER), user2);
+ verify(mContext).startActivity(intent);
}
@Test
@@ -290,9 +404,10 @@
Settings.Secure.STYLUS_HANDWRITING_ENABLED, 1);
showScreen(mController);
- SwitchPreference handwritingPref = (SwitchPreference) mPreferenceContainer.getPreference(1);
+ PrimarySwitchPreference handwritingPref =
+ (PrimarySwitchPreference) mPreferenceContainer.getPreference(1);
- assertThat(handwritingPref.isChecked()).isEqualTo(true);
+ assertThat(handwritingPref.getCheckedState()).isEqualTo(true);
}
@Test
@@ -301,9 +416,10 @@
Settings.Secure.STYLUS_HANDWRITING_ENABLED, 0);
showScreen(mController);
- SwitchPreference handwritingPref = (SwitchPreference) mPreferenceContainer.getPreference(1);
+ PrimarySwitchPreference handwritingPref =
+ (PrimarySwitchPreference) mPreferenceContainer.getPreference(1);
- assertThat(handwritingPref.isChecked()).isEqualTo(false);
+ assertThat(handwritingPref.getCheckedState()).isEqualTo(false);
}
@Test
@@ -311,21 +427,20 @@
Settings.Secure.putInt(mContext.getContentResolver(),
Settings.Secure.STYLUS_HANDWRITING_ENABLED, 0);
showScreen(mController);
- SwitchPreference handwritingPref = (SwitchPreference) mPreferenceContainer.getPreference(1);
+ PrimarySwitchPreference handwritingPref =
+ (PrimarySwitchPreference) mPreferenceContainer.getPreference(1);
- handwritingPref.performClick();
+ handwritingPref.callChangeListener(true);
- assertThat(handwritingPref.isChecked()).isEqualTo(true);
assertThat(Settings.Secure.getInt(mContext.getContentResolver(),
Settings.Secure.STYLUS_HANDWRITING_ENABLED, -1)).isEqualTo(1);
}
@Test
- public void handwritingPreference_startsHandwritingSettingsOnClickIfChecked() {
- Settings.Secure.putInt(mContext.getContentResolver(),
- Settings.Secure.STYLUS_HANDWRITING_ENABLED, 0);
+ public void handwritingPreference_startsHandwritingSettingsOnClick() {
showScreen(mController);
- SwitchPreference handwritingPref = (SwitchPreference) mPreferenceContainer.getPreference(1);
+ PrimarySwitchPreference handwritingPref =
+ (PrimarySwitchPreference) mPreferenceContainer.getPreference(1);
handwritingPref.performClick();
@@ -334,11 +449,23 @@
}
@Test
- public void handwritingPreference_doesNotStartHandwritingSettingsOnClickIfNotChecked() {
- Settings.Secure.putInt(mContext.getContentResolver(),
- Settings.Secure.STYLUS_HANDWRITING_ENABLED, 1);
+ public void handwritingPreference_doesNotStartHandwritingSettingsOnChange() {
showScreen(mController);
- SwitchPreference handwritingPref = (SwitchPreference) mPreferenceContainer.getPreference(1);
+ PrimarySwitchPreference handwritingPref =
+ (PrimarySwitchPreference) mPreferenceContainer.getPreference(1);
+
+ handwritingPref.callChangeListener(true);
+
+ verify(mInputMethodInfo, times(0)).createStylusHandwritingSettingsActivityIntent();
+ verify(mContext, times(0)).startActivity(any());
+ }
+
+ @Test
+ public void handwritingPreference_doesNotCreateIntentIfNoInputMethod() {
+ when(mImm.getCurrentInputMethodInfo()).thenReturn(null);
+ showScreen(mController);
+ PrimarySwitchPreference handwritingPref =
+ (PrimarySwitchPreference) mPreferenceContainer.getPreference(1);
handwritingPref.performClick();
@@ -350,14 +477,12 @@
public void handwritingPreference_doesNotStartHandwritingSettingsIfNoIntent() {
when(mInputMethodInfo.createStylusHandwritingSettingsActivityIntent())
.thenReturn(null);
- Settings.Secure.putInt(mContext.getContentResolver(),
- Settings.Secure.STYLUS_HANDWRITING_ENABLED, 1);
showScreen(mController);
- SwitchPreference handwritingPref = (SwitchPreference) mPreferenceContainer.getPreference(1);
+ PrimarySwitchPreference handwritingPref =
+ (PrimarySwitchPreference) mPreferenceContainer.getPreference(1);
handwritingPref.performClick();
- verify(mInputMethodInfo, times(0)).createStylusHandwritingSettingsActivityIntent();
verify(mContext, times(0)).startActivity(any());
}
diff --git a/tests/robotests/src/com/android/settings/connecteddevice/stylus/StylusUsbFirmwareControllerTest.java b/tests/robotests/src/com/android/settings/connecteddevice/stylus/StylusUsbFirmwareControllerTest.java
new file mode 100644
index 0000000..2ba655a
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/connecteddevice/stylus/StylusUsbFirmwareControllerTest.java
@@ -0,0 +1,163 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.connecteddevice.stylus;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.hardware.usb.UsbDevice;
+import android.hardware.usb.UsbManager;
+
+import androidx.preference.Preference;
+import androidx.preference.PreferenceCategory;
+import androidx.preference.PreferenceManager;
+import androidx.preference.PreferenceScreen;
+
+import com.android.settings.testutils.FakeFeatureFactory;
+import com.android.settingslib.core.lifecycle.Lifecycle;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+
+import java.util.Collections;
+import java.util.HashMap;
+
+@RunWith(RobolectricTestRunner.class)
+public class StylusUsbFirmwareControllerTest {
+
+ private Context mContext;
+ private FakeFeatureFactory mFeatureFactory;
+ private Lifecycle mLifecycle;
+ private PreferenceScreen mScreen;
+
+ private StylusUsbFirmwareController mController;
+ @Mock
+ private StylusUsiDetailsFragment mFragment;
+ @Mock
+ private UsbManager mUsbManager;
+ private PreferenceCategory mPreferenceCategory;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+
+ mContext = spy(RuntimeEnvironment.application);
+ mLifecycle = new Lifecycle(() -> mLifecycle);
+
+ when(mFragment.getContext()).thenReturn(mContext);
+
+ mFeatureFactory = FakeFeatureFactory.setupForTest();
+ mController = new StylusUsbFirmwareController(mContext, "stylus_usb_firmware");
+
+ PreferenceManager preferenceManager = new PreferenceManager(mContext);
+ mScreen = preferenceManager.createPreferenceScreen(mContext);
+
+ mPreferenceCategory = new PreferenceCategory(mContext);
+ mPreferenceCategory.setKey(mController.getPreferenceKey());
+ }
+
+ @Test
+ public void displayPreference_featurePresentUsbStylusAttached_preferenceAdded() {
+ attachUsbDevice();
+ enableFullStylusFeature();
+
+ mController.displayPreference(mScreen);
+
+ assertNotNull(mScreen.findPreference("stylus_usb_firmware"));
+ }
+
+ @Test
+ public void displayPreference_featureAbsentUsbStylusAttached_preferenceNotAdded() {
+ attachUsbDevice();
+ mController.mUsbConnectionListener.onUsbStylusConnectionChanged(
+ mock(UsbDevice.class), true);
+
+ mController.displayPreference(mScreen);
+
+ assertNull(mScreen.findPreference(mController.getPreferenceKey()));
+ }
+
+ @Test
+ public void onUsbStylusConnectionChanged_featurePresentUsbStylusAttached_preferenceAdded() {
+ mController.displayPreference(mScreen);
+
+ attachUsbDevice();
+ enableFullStylusFeature();
+ mController.mUsbConnectionListener.onUsbStylusConnectionChanged(
+ mock(UsbDevice.class), true);
+
+ assertNotNull(mScreen.findPreference(mController.getPreferenceKey()));
+ }
+
+ @Test
+ public void onUsbStylusConnectionChanged_featureAbsentUsbStylusAttached_preferenceRemoved() {
+ mController.displayPreference(mScreen);
+
+ attachUsbDevice();
+ mController.mUsbConnectionListener.onUsbStylusConnectionChanged(
+ mock(UsbDevice.class), true);
+
+ assertNull(mScreen.findPreference(mController.getPreferenceKey()));
+ }
+
+ @Test
+ public void hasUsbStylusFirmwareUpdateFeature_featurePresent_true() {
+ when(mFeatureFactory.getStylusFeatureProvider()
+ .isUsbFirmwareUpdateEnabled(any())).thenReturn(true);
+ attachUsbDevice();
+
+ assertTrue(StylusUsbFirmwareController
+ .hasUsbStylusFirmwareUpdateFeature(mock(UsbDevice.class)));
+ }
+
+ @Test
+ public void hasUsbStylusFirmwareUpdateFeature_featureNotPresent_false() {
+ when(mFeatureFactory.getStylusFeatureProvider()
+ .isUsbFirmwareUpdateEnabled(any())).thenReturn(false);
+ attachUsbDevice();
+
+ assertFalse(StylusUsbFirmwareController
+ .hasUsbStylusFirmwareUpdateFeature(mock(UsbDevice.class)));
+ }
+
+ private void attachUsbDevice() {
+ when(mContext.getSystemService(UsbManager.class)).thenReturn(mUsbManager);
+ HashMap<String, UsbDevice> deviceList = new HashMap<>();
+ deviceList.put("0", mock(UsbDevice.class));
+ when(mUsbManager.getDeviceList()).thenReturn(deviceList);
+ }
+
+ private void enableFullStylusFeature() {
+ when(mFeatureFactory.getStylusFeatureProvider()
+ .isUsbFirmwareUpdateEnabled(any())).thenReturn(true);
+ when(mFeatureFactory.getStylusFeatureProvider()
+ .getUsbFirmwareUpdatePreferences(any(), any()))
+ .thenReturn(Collections.singletonList(mock(Preference.class)));
+ }
+}
diff --git a/tests/robotests/src/com/android/settings/connecteddevice/stylus/UsbStylusBroadcastReceiverTest.java b/tests/robotests/src/com/android/settings/connecteddevice/stylus/UsbStylusBroadcastReceiverTest.java
new file mode 100644
index 0000000..03279ee
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/connecteddevice/stylus/UsbStylusBroadcastReceiverTest.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.connecteddevice.stylus;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.content.Intent;
+import android.hardware.usb.UsbDevice;
+import android.hardware.usb.UsbManager;
+
+import com.android.settings.testutils.FakeFeatureFactory;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+
+@RunWith(RobolectricTestRunner.class)
+public class UsbStylusBroadcastReceiverTest {
+ private Context mContext;
+ private UsbStylusBroadcastReceiver mReceiver;
+ private FakeFeatureFactory mFeatureFactory;
+ @Mock
+ private UsbStylusBroadcastReceiver.UsbStylusConnectionListener mListener;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+
+ mContext = RuntimeEnvironment.application;
+ mReceiver = new UsbStylusBroadcastReceiver(mContext, mListener);
+ mFeatureFactory = FakeFeatureFactory.setupForTest();
+ }
+
+ @Test
+ public void onReceive_usbDeviceAttachedStylus_invokeCallback() {
+ when(mFeatureFactory.mStylusFeatureProvider.isUsbFirmwareUpdateEnabled(any()))
+ .thenReturn(true);
+ final UsbDevice usbDevice = mock(UsbDevice.class);
+ final Intent intent = new Intent();
+ intent.setAction(UsbManager.ACTION_USB_DEVICE_ATTACHED);
+ intent.putExtra(UsbManager.EXTRA_DEVICE, usbDevice);
+
+ mReceiver.onReceive(mContext, intent);
+
+ verify(mListener).onUsbStylusConnectionChanged(usbDevice, true);
+ }
+
+ @Test
+ public void onReceive_usbDeviceDetachedStylus_invokeCallback() {
+ when(mFeatureFactory.mStylusFeatureProvider.isUsbFirmwareUpdateEnabled(any()))
+ .thenReturn(true);
+ final UsbDevice usbDevice = mock(UsbDevice.class);
+ final Intent intent = new Intent();
+ intent.setAction(UsbManager.ACTION_USB_DEVICE_DETACHED);
+ intent.putExtra(UsbManager.EXTRA_DEVICE, usbDevice);
+
+ mReceiver.onReceive(mContext, intent);
+
+ verify(mListener).onUsbStylusConnectionChanged(usbDevice, false);
+ }
+
+ @Test
+ public void onReceive_usbDeviceAttachedNotStylus_doesNotInvokeCallback() {
+ when(mFeatureFactory.mStylusFeatureProvider.isUsbFirmwareUpdateEnabled(any()))
+ .thenReturn(false);
+ final UsbDevice usbDevice = mock(UsbDevice.class);
+ final Intent intent = new Intent();
+ intent.setAction(UsbManager.ACTION_USB_DEVICE_ATTACHED);
+ intent.putExtra(UsbManager.EXTRA_DEVICE, usbDevice);
+
+ mReceiver.onReceive(mContext, intent);
+
+ verifyNoMoreInteractions(mListener);
+ }
+
+ @Test
+ public void onReceive_usbDeviceStateStylus_invokeCallback() {
+ when(mFeatureFactory.mStylusFeatureProvider.isUsbFirmwareUpdateEnabled(any()))
+ .thenReturn(true);
+ final UsbDevice usbDevice = mock(UsbDevice.class);
+ final Intent intent = new Intent();
+ intent.setAction(UsbManager.ACTION_USB_STATE);
+ intent.putExtra(UsbManager.EXTRA_DEVICE, usbDevice);
+
+ mReceiver.onReceive(mContext, intent);
+
+ verify(mListener).onUsbStylusConnectionChanged(usbDevice, false);
+ }
+
+ @Test
+ public void onReceive_usbDeviceStateNotStylus_doesNotInvokeCallback() {
+ when(mFeatureFactory.mStylusFeatureProvider.isUsbFirmwareUpdateEnabled(any()))
+ .thenReturn(false);
+ final UsbDevice usbDevice = mock(UsbDevice.class);
+ final Intent intent = new Intent();
+ intent.setAction(UsbManager.ACTION_USB_STATE);
+ intent.putExtra(UsbManager.EXTRA_DEVICE, usbDevice);
+
+ mReceiver.onReceive(mContext, intent);
+
+ verifyNoMoreInteractions(mListener);
+ }
+}
diff --git a/tests/robotests/src/com/android/settings/dashboard/DashboardFeatureProviderImplTest.java b/tests/robotests/src/com/android/settings/dashboard/DashboardFeatureProviderImplTest.java
index 4ba6eae..f7a940f 100644
--- a/tests/robotests/src/com/android/settings/dashboard/DashboardFeatureProviderImplTest.java
+++ b/tests/robotests/src/com/android/settings/dashboard/DashboardFeatureProviderImplTest.java
@@ -43,6 +43,7 @@
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;
+import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ActivityInfo;
@@ -57,6 +58,7 @@
import android.os.UserManager;
import android.util.Pair;
+import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import androidx.preference.Preference;
import androidx.preference.SwitchPreference;
@@ -200,6 +202,27 @@
}
@Test
+ public void bindPreference_providerTileWithPendingIntent_shouldBindIntent() {
+ final Preference preference = new SwitchPreference(RuntimeEnvironment.application);
+ Bundle metaData = new Bundle();
+ metaData.putInt(META_DATA_PREFERENCE_TITLE, R.string.settings_label);
+ metaData.putInt(META_DATA_PREFERENCE_SUMMARY, R.string.about_settings_summary);
+ metaData.putInt(META_DATA_KEY_ORDER, 10);
+ metaData.putString(META_DATA_PREFERENCE_KEYHINT, KEY);
+ final Tile tile = new ProviderTile(mProviderInfo, CategoryKey.CATEGORY_HOMEPAGE, metaData);
+ PendingIntent pendingIntent =
+ PendingIntent.getActivity(RuntimeEnvironment.application, 0, new Intent("test"), 0);
+ tile.pendingIntentMap.put(UserHandle.CURRENT, pendingIntent);
+
+ mImpl.bindPreferenceToTileAndGetObservers(mActivity, mFragment, mForceRoundedIcon,
+ preference, tile, "123", Preference.DEFAULT_ORDER);
+
+ assertThat(preference.getFragment()).isNull();
+ assertThat(preference.getOnPreferenceClickListener()).isNotNull();
+ assertThat(preference.getOrder()).isEqualTo(tile.getOrder());
+ }
+
+ @Test
public void bindPreference_noFragmentMetadata_shouldBindIntent() {
final Preference preference = new Preference(RuntimeEnvironment.application);
mActivityInfo.metaData.putInt(META_DATA_KEY_ORDER, 10);
@@ -631,6 +654,55 @@
}
@Test
+ public void clickPreference_providerTileWithPendingIntent_singleUser_executesPendingIntent() {
+ final Preference preference = new SwitchPreference(RuntimeEnvironment.application);
+ Bundle metaData = new Bundle();
+ metaData.putInt(META_DATA_PREFERENCE_TITLE, R.string.settings_label);
+ metaData.putInt(META_DATA_PREFERENCE_SUMMARY, R.string.about_settings_summary);
+ metaData.putInt(META_DATA_KEY_ORDER, 10);
+ metaData.putString(META_DATA_PREFERENCE_KEYHINT, KEY);
+ final Tile tile = new ProviderTile(mProviderInfo, CategoryKey.CATEGORY_HOMEPAGE, metaData);
+ PendingIntent pendingIntent =
+ PendingIntent.getActivity(RuntimeEnvironment.application, 0, new Intent("test"), 0);
+ tile.pendingIntentMap.put(UserHandle.CURRENT, pendingIntent);
+
+ mImpl.bindPreferenceToTileAndGetObservers(mActivity, mFragment, mForceRoundedIcon,
+ preference, tile, "123", Preference.DEFAULT_ORDER);
+ preference.performClick();
+
+ Intent nextStartedActivity =
+ Shadows.shadowOf(RuntimeEnvironment.application).peekNextStartedActivity();
+ assertThat(nextStartedActivity).isNotNull();
+ assertThat(nextStartedActivity.getAction()).isEqualTo("test");
+ }
+
+ @Test
+ public void clickPreference_providerTileWithPendingIntent_multiUser_showsProfileDialog() {
+ final Preference preference = new SwitchPreference(RuntimeEnvironment.application);
+ Bundle metaData = new Bundle();
+ metaData.putInt(META_DATA_PREFERENCE_TITLE, R.string.settings_label);
+ metaData.putInt(META_DATA_PREFERENCE_SUMMARY, R.string.about_settings_summary);
+ metaData.putInt(META_DATA_KEY_ORDER, 10);
+ metaData.putString(META_DATA_PREFERENCE_KEYHINT, KEY);
+ final Tile tile = new ProviderTile(mProviderInfo, CategoryKey.CATEGORY_HOMEPAGE, metaData);
+ PendingIntent pendingIntent =
+ PendingIntent.getActivity(RuntimeEnvironment.application, 0, new Intent("test"), 0);
+ tile.pendingIntentMap.put(UserHandle.CURRENT, pendingIntent);
+ tile.pendingIntentMap.put(new UserHandle(10), pendingIntent);
+
+ mImpl.bindPreferenceToTileAndGetObservers(mActivity, mFragment, mForceRoundedIcon,
+ preference, tile, "123", Preference.DEFAULT_ORDER);
+ preference.performClick();
+
+ Fragment dialogFragment =
+ mActivity.getSupportFragmentManager().findFragmentByTag("select_profile");
+ assertThat(dialogFragment).isNotNull();
+ Intent nextStartedActivity =
+ Shadows.shadowOf(RuntimeEnvironment.application).peekNextStartedActivity();
+ assertThat(nextStartedActivity).isNull();
+ }
+
+ @Test
public void openTileIntent_profileSelectionDialog_shouldShow() {
ShadowUserManager.getShadow().addUser(10, "Someone", 0);
diff --git a/tests/robotests/src/com/android/settings/dashboard/DashboardFragmentTest.java b/tests/robotests/src/com/android/settings/dashboard/DashboardFragmentTest.java
index 0739294..ecaf36f 100644
--- a/tests/robotests/src/com/android/settings/dashboard/DashboardFragmentTest.java
+++ b/tests/robotests/src/com/android/settings/dashboard/DashboardFragmentTest.java
@@ -16,7 +16,9 @@
package com.android.settings.dashboard;
import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.DASHBOARD_CONTAINER;
+import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_GROUP_KEY;
import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_KEYHINT;
+import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_PENDING_INTENT;
import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_SWITCH_URI;
import static com.google.common.truth.Truth.assertThat;
@@ -30,6 +32,7 @@
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
+import android.app.PendingIntent;
import android.app.settings.SettingsEnums;
import android.content.ContentResolver;
import android.content.Context;
@@ -38,15 +41,18 @@
import android.content.pm.ProviderInfo;
import android.net.Uri;
import android.os.Bundle;
+import android.os.UserHandle;
import android.preference.PreferenceManager.OnActivityResultListener;
import androidx.preference.Preference;
+import androidx.preference.PreferenceCategory;
import androidx.preference.PreferenceFragmentCompat;
import androidx.preference.PreferenceManager;
import androidx.preference.PreferenceScreen;
import androidx.preference.SwitchPreference;
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
+import com.android.settings.R;
import com.android.settings.core.PreferenceControllerMixin;
import com.android.settings.slices.BlockingSlicePrefController;
import com.android.settings.testutils.FakeFeatureFactory;
@@ -57,6 +63,7 @@
import com.android.settingslib.drawer.ActivityTile;
import com.android.settingslib.drawer.DashboardCategory;
import com.android.settingslib.drawer.ProviderTile;
+import com.android.settingslib.drawer.Tile;
import org.junit.Before;
import org.junit.Ignore;
@@ -178,6 +185,43 @@
}
@Test
+ public void displayTilesAsPreference_withGroup_shouldAddTilesIntoGroup() {
+ final ProviderInfo providerInfo = new ProviderInfo();
+ providerInfo.packageName = "pkg";
+ providerInfo.name = "provider";
+ providerInfo.authority = "authority";
+ final Bundle groupTileMetaData = new Bundle();
+ groupTileMetaData.putString(META_DATA_PREFERENCE_KEYHINT, "injected_tile_group_key");
+ ProviderTile groupTile = new ProviderTile(providerInfo, mDashboardCategory.key,
+ groupTileMetaData);
+ mDashboardCategory.addTile(groupTile);
+
+ final Bundle subTileMetaData = new Bundle();
+ subTileMetaData.putString(META_DATA_PREFERENCE_KEYHINT, "injected_tile_key3");
+ subTileMetaData.putString(META_DATA_PREFERENCE_GROUP_KEY, "injected_tile_group_key");
+ subTileMetaData.putParcelable(
+ META_DATA_PREFERENCE_PENDING_INTENT,
+ PendingIntent.getActivity(mContext, 0, new Intent(), 0));
+ ProviderTile subTile = new ProviderTile(providerInfo, mDashboardCategory.key,
+ subTileMetaData);
+ mDashboardCategory.addTile(subTile);
+
+ PreferenceCategory groupPreference = mock(PreferenceCategory.class);
+ when(mFakeFeatureFactory.dashboardFeatureProvider
+ .getTilesForCategory(nullable(String.class)))
+ .thenReturn(mDashboardCategory);
+ when(mFakeFeatureFactory.dashboardFeatureProvider
+ .getDashboardKeyForTile(any(Tile.class)))
+ .then(invocation -> ((Tile) invocation.getArgument(0)).getKey(mContext));
+ when(mTestFragment.mScreen.findPreference("injected_tile_group_key"))
+ .thenReturn(groupPreference);
+ mTestFragment.onCreatePreferences(new Bundle(), "rootKey");
+
+ verify(mTestFragment.mScreen, times(3)).addPreference(nullable(Preference.class));
+ verify(groupPreference).addPreference(nullable(Preference.class));
+ }
+
+ @Test
public void displayTilesAsPreference_shouldNotAddTilesWithoutIntent() {
mTestFragment.onCreatePreferences(new Bundle(), "rootKey");
@@ -352,6 +396,16 @@
}
@Test
+ public void createPreference_isActivityTile_returnPreference() {
+ final Preference pref = mTestFragment.createPreference(mActivityTile);
+
+ assertThat(pref).isInstanceOf(Preference.class);
+ assertThat(pref).isNotInstanceOf(PrimarySwitchPreference.class);
+ assertThat(pref).isNotInstanceOf(SwitchPreference.class);
+ assertThat(pref.getWidgetLayoutResource()).isEqualTo(0);
+ }
+
+ @Test
public void createPreference_isActivityTileAndHasSwitch_returnPrimarySwitchPreference() {
mActivityTile.getMetaData().putString(META_DATA_PREFERENCE_SWITCH_URI, "uri");
@@ -361,6 +415,64 @@
}
@Test
+ public void createPreference_isProviderTileWithPendingIntent_returnPreferenceWithIcon() {
+ final ProviderInfo providerInfo = new ProviderInfo();
+ providerInfo.packageName = "pkg";
+ providerInfo.name = "provider";
+ providerInfo.authority = "authority";
+ final Bundle metaData = new Bundle();
+ metaData.putString(META_DATA_PREFERENCE_KEYHINT, "injected_tile_key2");
+ ProviderTile providerTile = new ProviderTile(providerInfo, mDashboardCategory.key,
+ metaData);
+ providerTile.pendingIntentMap.put(
+ UserHandle.CURRENT, PendingIntent.getActivity(mContext, 0, new Intent(), 0));
+
+ final Preference pref = mTestFragment.createPreference(providerTile);
+
+ assertThat(pref).isInstanceOf(Preference.class);
+ assertThat(pref).isNotInstanceOf(PrimarySwitchPreference.class);
+ assertThat(pref).isNotInstanceOf(SwitchPreference.class);
+ assertThat(pref.getWidgetLayoutResource())
+ .isEqualTo(R.layout.preference_external_action_icon);
+ }
+
+ @Test
+ public void createPreference_isProviderTileWithPendingIntentAndSwitch_returnPrimarySwitch() {
+ mProviderTile.pendingIntentMap.put(
+ UserHandle.CURRENT, PendingIntent.getActivity(mContext, 0, new Intent(), 0));
+
+ final Preference pref = mTestFragment.createPreference(mProviderTile);
+
+ assertThat(pref).isInstanceOf(PrimarySwitchPreference.class);
+ }
+
+ @Test
+ public void createPreference_isGroupTile_returnPreferenceCategory_logTileAdded() {
+ final ProviderInfo providerInfo = new ProviderInfo();
+ providerInfo.packageName = "pkg";
+ providerInfo.name = "provider";
+ providerInfo.authority = "authority";
+ final Bundle metaData = new Bundle();
+ metaData.putString(META_DATA_PREFERENCE_KEYHINT, "injected_tile_key2");
+ ProviderTile providerTile =
+ new ProviderTile(providerInfo, mDashboardCategory.key, metaData);
+ MetricsFeatureProvider metricsFeatureProvider =
+ mFakeFeatureFactory.getMetricsFeatureProvider();
+ when(metricsFeatureProvider.getAttribution(any())).thenReturn(123);
+
+ final Preference pref = mTestFragment.createPreference(providerTile);
+
+ assertThat(pref).isInstanceOf(PreferenceCategory.class);
+ verify(metricsFeatureProvider)
+ .action(
+ 123,
+ SettingsEnums.ACTION_SETTINGS_GROUP_TILE_ADDED_TO_SCREEN,
+ mTestFragment.getMetricsCategory(),
+ "injected_tile_key2",
+ 0);
+ }
+
+ @Test
public void onActivityResult_test() {
final int requestCode = 10;
final int resultCode = 1;
diff --git a/tests/robotests/src/com/android/settings/dashboard/profileselector/ProfileSelectDialogTest.java b/tests/robotests/src/com/android/settings/dashboard/profileselector/ProfileSelectDialogTest.java
index 4e81cee..9a13961 100644
--- a/tests/robotests/src/com/android/settings/dashboard/profileselector/ProfileSelectDialogTest.java
+++ b/tests/robotests/src/com/android/settings/dashboard/profileselector/ProfileSelectDialogTest.java
@@ -24,7 +24,9 @@
import static org.mockito.Mockito.when;
import android.app.Dialog;
+import android.app.PendingIntent;
import android.content.Context;
+import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.UserInfo;
import android.os.UserHandle;
@@ -119,6 +121,28 @@
}
@Test
+ public void updatePendingIntentsIfNeeded_removesUsersWithNoPendingIntentsAndCloneProfile() {
+ final UserInfo userInfo = new UserInfo(CLONE_USER.getIdentifier(), "clone_user", null,
+ UserInfo.FLAG_PROFILE, UserManager.USER_TYPE_PROFILE_CLONE);
+ when(mUserManager.getUserInfo(CLONE_USER.getIdentifier())).thenReturn(userInfo);
+ final Tile tile = new ActivityTile(mActivityInfo, CategoryKey.CATEGORY_HOMEPAGE);
+ tile.userHandle.add(CLONE_USER);
+ tile.userHandle.add(NORMAL_USER);
+ tile.userHandle.add(new UserHandle(10));
+ PendingIntent pendingIntent = PendingIntent.getActivity(mContext, 0, new Intent(), 0);
+ tile.pendingIntentMap.put(CLONE_USER, pendingIntent);
+ tile.pendingIntentMap.put(NORMAL_USER, pendingIntent);
+
+ ProfileSelectDialog.updatePendingIntentsIfNeeded(mContext, tile);
+
+ assertThat(tile.userHandle).hasSize(1);
+ assertThat(tile.userHandle).containsExactly(NORMAL_USER);
+ assertThat(tile.pendingIntentMap).hasSize(1);
+ assertThat(tile.pendingIntentMap).containsKey(NORMAL_USER);
+ verify(mUserManager, times(1)).getUserInfo(CLONE_USER.getIdentifier());
+ }
+
+ @Test
public void createDialog_showsCorrectTitle() {
mContext.setTheme(R.style.Theme_AppCompat);
diff --git a/tests/robotests/src/com/android/settings/development/BluetoothLeAudioDeviceDetailsPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/development/BluetoothLeAudioDeviceDetailsPreferenceControllerTest.java
index b405f9e..13bc6a4 100644
--- a/tests/robotests/src/com/android/settings/development/BluetoothLeAudioDeviceDetailsPreferenceControllerTest.java
+++ b/tests/robotests/src/com/android/settings/development/BluetoothLeAudioDeviceDetailsPreferenceControllerTest.java
@@ -16,6 +16,9 @@
package com.android.settings.development;
+import static com.android.settings.development.BluetoothLeAudioDeviceDetailsPreferenceController
+ .LE_AUDIO_TOGGLE_VISIBLE_PROPERTY;
+
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Mockito.spy;
@@ -25,12 +28,11 @@
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothStatusCodes;
import android.content.Context;
-import android.provider.DeviceConfig;
+import android.os.SystemProperties;
import androidx.preference.PreferenceScreen;
import androidx.preference.SwitchPreference;
-import com.android.settings.core.SettingsUIDeviceConfig;
import com.android.settings.testutils.shadow.ShadowDeviceConfig;
import org.junit.After;
@@ -77,9 +79,8 @@
public void onPreferenceChanged_settingEnabled_shouldTurnOnLeAudioDeviceDetailSetting() {
mController.sLeAudioSupportedStateCache = BluetoothStatusCodes.FEATURE_SUPPORTED;
mController.onPreferenceChange(mPreference, true /* new value */);
- final boolean isEnabled = DeviceConfig.getBoolean(
- DeviceConfig.NAMESPACE_SETTINGS_UI,
- SettingsUIDeviceConfig.BT_LE_AUDIO_DEVICE_DETAIL_ENABLED, false);
+ final boolean isEnabled = SystemProperties.getBoolean(
+ LE_AUDIO_TOGGLE_VISIBLE_PROPERTY, false);
assertThat(isEnabled).isTrue();
}
@@ -88,9 +89,8 @@
public void onPreferenceChanged_settingDisabled_shouldTurnOffLeAudioDeviceDetailSetting() {
mController.sLeAudioSupportedStateCache = BluetoothStatusCodes.FEATURE_SUPPORTED;
mController.onPreferenceChange(mPreference, false /* new value */);
- final boolean isEnabled = DeviceConfig.getBoolean(
- DeviceConfig.NAMESPACE_SETTINGS_UI,
- SettingsUIDeviceConfig.BT_LE_AUDIO_DEVICE_DETAIL_ENABLED, false);
+ final boolean isEnabled = SystemProperties.getBoolean(
+ LE_AUDIO_TOGGLE_VISIBLE_PROPERTY, true);
assertThat(isEnabled).isFalse();
}
@@ -98,18 +98,15 @@
@Test
public void updateState_settingEnabled_preferenceShouldBeChecked() {
mController.sLeAudioSupportedStateCache = BluetoothStatusCodes.FEATURE_SUPPORTED;
- DeviceConfig.setProperty(DeviceConfig.NAMESPACE_SETTINGS_UI,
- SettingsUIDeviceConfig.BT_LE_AUDIO_DEVICE_DETAIL_ENABLED, "true", false);
+ SystemProperties.set(LE_AUDIO_TOGGLE_VISIBLE_PROPERTY, "true");
mController.updateState(mPreference);
-
verify(mPreference).setChecked(true);
}
@Test
public void updateState_settingDisabled_preferenceShouldNotBeChecked() {
mController.sLeAudioSupportedStateCache = BluetoothStatusCodes.FEATURE_SUPPORTED;
- DeviceConfig.setProperty(DeviceConfig.NAMESPACE_SETTINGS_UI,
- SettingsUIDeviceConfig.BT_LE_AUDIO_DEVICE_DETAIL_ENABLED, "false", false);
+ SystemProperties.set(LE_AUDIO_TOGGLE_VISIBLE_PROPERTY, "false");
mController.updateState(mPreference);
verify(mPreference).setChecked(false);
diff --git a/tests/robotests/src/com/android/settings/development/ShowKeyPressesPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/development/ShowKeyPressesPreferenceControllerTest.java
new file mode 100644
index 0000000..b7fb902
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/development/ShowKeyPressesPreferenceControllerTest.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.development;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.provider.Settings;
+
+import androidx.preference.PreferenceScreen;
+import androidx.preference.SwitchPreference;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+
+@RunWith(RobolectricTestRunner.class)
+public class ShowKeyPressesPreferenceControllerTest {
+
+ @Mock
+ private PreferenceScreen mScreen;
+ @Mock
+ private SwitchPreference mPreference;
+
+ private Context mContext;
+
+ private ShowKeyPressesPreferenceController mController;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ mContext = RuntimeEnvironment.application;
+ mController = new ShowKeyPressesPreferenceController(mContext);
+ when(mScreen.findPreference(mController.getPreferenceKey())).thenReturn(mPreference);
+ mController.displayPreference(mScreen);
+ }
+
+ @Test
+ public void updateState_showKeyPressesEnabled_shouldCheckedPreference() {
+ Settings.System.putInt(mContext.getContentResolver(),
+ Settings.System.SHOW_KEY_PRESSES, ShowTapsPreferenceController.SETTING_VALUE_ON);
+
+ mController.updateState(mPreference);
+
+ verify(mPreference).setChecked(true);
+ }
+
+ @Test
+ public void updateState_showKeyPressesDisabled_shouldUncheckedPreference() {
+ Settings.System.putInt(mContext.getContentResolver(),
+ Settings.System.SHOW_KEY_PRESSES, ShowTapsPreferenceController.SETTING_VALUE_OFF);
+
+ mController.updateState(mPreference);
+
+ verify(mPreference).setChecked(false);
+ }
+
+ @Test
+ public void onPreferenceChange_preferenceChecked_shouldEnableShowKeyPresses() {
+ mController.onPreferenceChange(mPreference, true /* new value */);
+
+ final int showKeyPresses = Settings.System.getInt(mContext.getContentResolver(),
+ Settings.System.SHOW_KEY_PRESSES, -1 /* default */);
+
+ assertThat(showKeyPresses).isEqualTo(ShowTapsPreferenceController.SETTING_VALUE_ON);
+ }
+
+ @Test
+ public void onPreferenceChange_preferenceUnchecked_shouldDisableShowKeyPresses() {
+ mController.onPreferenceChange(mPreference, false /* new value */);
+
+ final int showTapsMode = Settings.System.getInt(mContext.getContentResolver(),
+ Settings.System.SHOW_KEY_PRESSES, -1 /* default */);
+
+ assertThat(showTapsMode).isEqualTo(ShowTapsPreferenceController.SETTING_VALUE_OFF);
+ }
+
+ @Test
+ public void onDeveloperOptionsSwitchDisabled_preferenceShouldBeEnabled() {
+ mController.onDeveloperOptionsSwitchDisabled();
+
+ final int showTapsMode = Settings.System.getInt(mContext.getContentResolver(),
+ Settings.System.SHOW_KEY_PRESSES, -1 /* default */);
+
+ assertThat(showTapsMode).isEqualTo(ShowTapsPreferenceController.SETTING_VALUE_OFF);
+ verify(mPreference).setEnabled(false);
+ verify(mPreference).setChecked(false);
+ }
+}
diff --git a/tests/robotests/src/com/android/settings/deviceinfo/batteryinfo/BatteryCycleCountPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/deviceinfo/batteryinfo/BatteryCycleCountPreferenceControllerTest.java
new file mode 100644
index 0000000..4d1b4d0
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/deviceinfo/batteryinfo/BatteryCycleCountPreferenceControllerTest.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+package com.android.settings.deviceinfo.batteryinfo;
+
+
+import static com.android.settings.core.BasePreferenceController.AVAILABLE;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.spy;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.BatteryManager;
+
+import androidx.test.core.app.ApplicationProvider;
+
+import com.android.settings.R;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+@RunWith(RobolectricTestRunner.class)
+public class BatteryCycleCountPreferenceControllerTest {
+ private BatteryCycleCountPreferenceController mController;
+ private Context mContext;
+
+ @Before
+ public void setUp() {
+ mContext = spy(ApplicationProvider.getApplicationContext());
+ mController = new BatteryCycleCountPreferenceController(mContext,
+ "battery_info_cycle_count");
+ }
+
+ @Test
+ public void getAvailabilityStatus_returnAvailable() {
+ assertThat(mController.getAvailabilityStatus()).isEqualTo(AVAILABLE);
+ }
+
+ @Test
+ public void getSummary_returnExpectedResult() {
+ final Intent batteryIntent = new Intent();
+ batteryIntent.putExtra(BatteryManager.EXTRA_CYCLE_COUNT, 10);
+ doReturn(batteryIntent).when(mContext).registerReceiver(any(), any());
+
+ assertThat(mController.getSummary()).isEqualTo("10");
+ }
+
+ @Test
+ public void getSummary_noValue_returnUnavailable() {
+ final Intent batteryIntent = new Intent();
+ doReturn(batteryIntent).when(mContext).registerReceiver(any(), any());
+
+ assertThat(mController.getSummary()).isEqualTo(
+ mContext.getText(R.string.battery_cycle_count_not_available));
+ }
+}
diff --git a/tests/robotests/src/com/android/settings/deviceinfo/batteryinfo/BatteryFirstUseDatePreferenceControllerTest.java b/tests/robotests/src/com/android/settings/deviceinfo/batteryinfo/BatteryFirstUseDatePreferenceControllerTest.java
new file mode 100644
index 0000000..ff8ea62
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/deviceinfo/batteryinfo/BatteryFirstUseDatePreferenceControllerTest.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.deviceinfo.batteryinfo;
+
+import static com.android.settings.core.BasePreferenceController.AVAILABLE;
+import static com.android.settings.core.BasePreferenceController.CONDITIONALLY_UNAVAILABLE;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.when;
+import static org.robolectric.Shadows.shadowOf;
+
+import android.content.Context;
+import android.os.BatteryManager;
+
+import androidx.test.core.app.ApplicationProvider;
+
+import com.android.settings.testutils.FakeFeatureFactory;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+import org.robolectric.shadows.ShadowBatteryManager;
+
+@RunWith(RobolectricTestRunner.class)
+@Config(shadows = {ShadowBatteryManager.class})
+public class BatteryFirstUseDatePreferenceControllerTest {
+ private BatteryFirstUseDatePreferenceController mController;
+ private Context mContext;
+ private BatteryManager mBatteryManager;
+ private ShadowBatteryManager mShadowBatteryManager;
+ private FakeFeatureFactory mFactory;
+
+ @Before
+ public void setUp() {
+ mContext = ApplicationProvider.getApplicationContext();
+ mBatteryManager = mContext.getSystemService(BatteryManager.class);
+ mShadowBatteryManager = shadowOf(mBatteryManager);
+ mFactory = FakeFeatureFactory.setupForTest();
+ mController = new BatteryFirstUseDatePreferenceController(mContext,
+ "battery_info_first_use_date");
+ }
+
+ @Test
+ public void getAvailabilityStatus_dateAvailable_returnAvailable() {
+ when(mFactory.batterySettingsFeatureProvider.isFirstUseDateAvailable(eq(mContext),
+ anyLong())).thenReturn(true);
+
+ assertThat(mController.getAvailabilityStatus()).isEqualTo(AVAILABLE);
+ }
+
+ @Test
+ public void getAvailabilityStatus_dateUnavailable_returnNotAvailable() {
+ when(mFactory.batterySettingsFeatureProvider.isFirstUseDateAvailable(eq(mContext),
+ anyLong())).thenReturn(false);
+
+ assertThat(mController.getAvailabilityStatus()).isEqualTo(CONDITIONALLY_UNAVAILABLE);
+ }
+
+ @Test
+ public void getSummary_available_returnExpectedDate() {
+ when(mFactory.batterySettingsFeatureProvider.isFirstUseDateAvailable(eq(mContext),
+ anyLong())).thenReturn(true);
+ mShadowBatteryManager.setLongProperty(BatteryManager.BATTERY_PROPERTY_FIRST_USAGE_DATE,
+ 1669680000L);
+
+ final CharSequence result = mController.getSummary();
+
+ assertThat(result.toString()).isEqualTo("November 29, 2022");
+ }
+
+ @Test
+ public void getSummary_unavailable_returnNull() {
+ when(mFactory.batterySettingsFeatureProvider.isFirstUseDateAvailable(eq(mContext),
+ anyLong())).thenReturn(false);
+
+ assertThat(mController.getSummary()).isNull();
+ }
+}
diff --git a/tests/robotests/src/com/android/settings/deviceinfo/batteryinfo/BatteryManufactureDatePreferenceControllerTest.java b/tests/robotests/src/com/android/settings/deviceinfo/batteryinfo/BatteryManufactureDatePreferenceControllerTest.java
new file mode 100644
index 0000000..608ce00
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/deviceinfo/batteryinfo/BatteryManufactureDatePreferenceControllerTest.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.deviceinfo.batteryinfo;
+
+import static com.android.settings.core.BasePreferenceController.AVAILABLE;
+import static com.android.settings.core.BasePreferenceController.CONDITIONALLY_UNAVAILABLE;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.when;
+import static org.robolectric.Shadows.shadowOf;
+
+import android.content.Context;
+import android.os.BatteryManager;
+
+import androidx.test.core.app.ApplicationProvider;
+
+import com.android.settings.testutils.FakeFeatureFactory;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+import org.robolectric.shadows.ShadowBatteryManager;
+
+@RunWith(RobolectricTestRunner.class)
+@Config(shadows = {ShadowBatteryManager.class})
+public class BatteryManufactureDatePreferenceControllerTest {
+
+ private BatteryManufactureDatePreferenceController mController;
+ private Context mContext;
+ private BatteryManager mBatteryManager;
+ private ShadowBatteryManager mShadowBatteryManager;
+ private FakeFeatureFactory mFactory;
+
+ @Before
+ public void setUp() {
+ mContext = ApplicationProvider.getApplicationContext();
+ mBatteryManager = mContext.getSystemService(BatteryManager.class);
+ mShadowBatteryManager = shadowOf(mBatteryManager);
+ mFactory = FakeFeatureFactory.setupForTest();
+ mController = new BatteryManufactureDatePreferenceController(mContext,
+ "battery_info_manufacture_date");
+ }
+
+ @Test
+ public void getAvailabilityStatus_dateAvailable_returnAvailable() {
+ when(mFactory.batterySettingsFeatureProvider.isManufactureDateAvailable(eq(mContext),
+ anyLong())).thenReturn(true);
+
+ assertThat(mController.getAvailabilityStatus()).isEqualTo(AVAILABLE);
+ }
+
+ @Test
+ public void getAvailabilityStatus_dateUnavailable_returnNotAvailable() {
+ when(mFactory.batterySettingsFeatureProvider.isManufactureDateAvailable(eq(mContext),
+ anyLong())).thenReturn(false);
+
+ assertThat(mController.getAvailabilityStatus()).isEqualTo(CONDITIONALLY_UNAVAILABLE);
+ }
+
+ @Test
+ public void getSummary_available_returnExpectedDate() {
+ when(mFactory.batterySettingsFeatureProvider.isManufactureDateAvailable(eq(mContext),
+ anyLong())).thenReturn(true);
+ mShadowBatteryManager.setLongProperty(BatteryManager.BATTERY_PROPERTY_MANUFACTURING_DATE,
+ 1669680000L);
+
+ final CharSequence result = mController.getSummary();
+
+ assertThat(result.toString()).isEqualTo("November 29, 2022");
+ }
+
+ @Test
+ public void getSummary_unavailable_returnNull() {
+ when(mFactory.batterySettingsFeatureProvider.isManufactureDateAvailable(eq(mContext),
+ anyLong())).thenReturn(false);
+
+ assertThat(mController.getSummary()).isNull();
+ }
+}
diff --git a/tests/robotests/src/com/android/settings/display/FoldLockBehaviorPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/display/FoldLockBehaviorPreferenceControllerTest.java
new file mode 100644
index 0000000..2510bf1
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/display/FoldLockBehaviorPreferenceControllerTest.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.display;
+
+
+import static com.android.settings.core.BasePreferenceController.AVAILABLE;
+import static com.android.settings.core.BasePreferenceController.UNSUPPORTED_ON_DEVICE;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.content.res.Resources;
+
+import com.android.internal.R;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+
+@RunWith(RobolectricTestRunner.class)
+public class FoldLockBehaviorPreferenceControllerTest {
+
+ @Mock
+ private Resources mResources;
+ private Context mContext;
+ private FoldLockBehaviorPreferenceController mController;
+
+ @Before
+ public void setUp() {
+ mContext = RuntimeEnvironment.application;
+ mResources = Mockito.mock(Resources.class);
+ mController = new FoldLockBehaviorPreferenceController(mContext, "key", mResources);
+ }
+
+ @Test
+ public void getAvailabilityStatus_withConfigNoShow_returnUnsupported() {
+ when(mResources.getBoolean(R.bool.config_fold_lock_behavior)).thenReturn(false);
+
+ assertThat(mController.getAvailabilityStatus()).isEqualTo(UNSUPPORTED_ON_DEVICE);
+ }
+
+ @Test
+ public void getAvailabilityStatus_withConfigNoShow_returnAvailable() {
+ when(mResources.getBoolean(R.bool.config_fold_lock_behavior)).thenReturn(true);
+
+ assertThat(mController.getAvailabilityStatus()).isEqualTo(AVAILABLE);
+ }
+}
diff --git a/tests/robotests/src/com/android/settings/display/FoldLockBehaviorSettingsTest.java b/tests/robotests/src/com/android/settings/display/FoldLockBehaviorSettingsTest.java
new file mode 100644
index 0000000..37b9391
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/display/FoldLockBehaviorSettingsTest.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.display;
+
+import static android.provider.Settings.System.FOLD_LOCK_BEHAVIOR;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.Context;
+import android.os.UserHandle;
+import android.provider.Settings;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+
+@RunWith(RobolectricTestRunner.class)
+public class FoldLockBehaviorSettingsTest {
+
+ private Context mContext;
+ private FoldLockBehaviorSettings mSetting;
+
+ @Before
+ public void setUp() {
+ mContext = RuntimeEnvironment.application;
+ mSetting = new FoldLockBehaviorSettings();
+ mSetting.onAttach(mContext);
+ }
+
+ @Test
+ public void getDefaultKey_returnFoldSetting() {
+ setFoldSetting("stay_awake_on_fold_key");
+
+ String key = mSetting.getDefaultKey();
+
+ assertThat(key).isEqualTo("stay_awake_on_fold_key");
+ }
+
+ @Test
+ public void setDefaultKey_returnFoldSetting() {
+ mSetting.setDefaultKey("stay_awake_on_fold_key");
+
+ String key = getFoldSettingValue();
+
+ assertThat(key).isEqualTo("stay_awake_on_fold_key");
+ }
+
+ @Test
+ public void setInvalidDefaultKey_returnDefaultFoldSetting() {
+ setFoldSetting("invalid_fold_lock_behavior_key");
+
+ String key = mSetting.getDefaultKey();
+
+ assertThat(key).isEqualTo("selective_stay_awake_key");
+ }
+
+ private void setFoldSetting(String selectedSetting) {
+ Settings.System.putStringForUser(mContext.getContentResolver(),
+ FOLD_LOCK_BEHAVIOR, selectedSetting, UserHandle.USER_CURRENT);
+ }
+
+ private String getFoldSettingValue() {
+ return Settings.System.getStringForUser(mContext.getContentResolver(),
+ FOLD_LOCK_BEHAVIOR, UserHandle.USER_CURRENT);
+ }
+}
diff --git a/tests/robotests/src/com/android/settings/fuelgauge/BatteryHistoricalLogUtilTest.java b/tests/robotests/src/com/android/settings/fuelgauge/BatteryOptimizeLogUtilsTest.java
similarity index 60%
rename from tests/robotests/src/com/android/settings/fuelgauge/BatteryHistoricalLogUtilTest.java
rename to tests/robotests/src/com/android/settings/fuelgauge/BatteryOptimizeLogUtilsTest.java
index cb5de7d..87de62f 100644
--- a/tests/robotests/src/com/android/settings/fuelgauge/BatteryHistoricalLogUtilTest.java
+++ b/tests/robotests/src/com/android/settings/fuelgauge/BatteryOptimizeLogUtilsTest.java
@@ -33,7 +33,7 @@
import java.io.StringWriter;
@RunWith(RobolectricTestRunner.class)
-public final class BatteryHistoricalLogUtilTest {
+public final class BatteryOptimizeLogUtilsTest {
private final StringWriter mTestStringWriter = new StringWriter();
private final PrintWriter mTestPrintWriter = new PrintWriter(mTestStringWriter);
@@ -43,19 +43,19 @@
@Before
public void setUp() {
mContext = ApplicationProvider.getApplicationContext();
- BatteryHistoricalLogUtil.getSharedPreferences(mContext).edit().clear().commit();
+ BatteryOptimizeLogUtils.getSharedPreferences(mContext).edit().clear().commit();
}
@Test
public void printHistoricalLog_withDefaultLogs() {
- BatteryHistoricalLogUtil.printBatteryOptimizeHistoricalLog(mContext, mTestPrintWriter);
+ BatteryOptimizeLogUtils.printBatteryOptimizeHistoricalLog(mContext, mTestPrintWriter);
assertThat(mTestStringWriter.toString()).contains("nothing to dump");
}
@Test
public void writeLog_withExpectedLogs() {
- BatteryHistoricalLogUtil.writeLog(mContext, Action.APPLY, "pkg1", "logs");
- BatteryHistoricalLogUtil.printBatteryOptimizeHistoricalLog(mContext, mTestPrintWriter);
+ BatteryOptimizeLogUtils.writeLog(mContext, Action.APPLY, "pkg1", "logs");
+ BatteryOptimizeLogUtils.printBatteryOptimizeHistoricalLog(mContext, mTestPrintWriter);
assertThat(mTestStringWriter.toString()).contains(
"pkg1\taction:APPLY\tevent:logs");
@@ -63,21 +63,27 @@
@Test
public void writeLog_multipleLogs_withCorrectCounts() {
- for (int i = 0; i < BatteryHistoricalLogUtil.MAX_ENTRIES; i++) {
- BatteryHistoricalLogUtil.writeLog(mContext, Action.LEAVE, "pkg" + i, "logs");
+ final int expectedCount = 10;
+ for (int i = 0; i < expectedCount; i++) {
+ BatteryOptimizeLogUtils.writeLog(mContext, Action.LEAVE, "pkg" + i, "logs");
}
- BatteryHistoricalLogUtil.printBatteryOptimizeHistoricalLog(mContext, mTestPrintWriter);
+ BatteryOptimizeLogUtils.printBatteryOptimizeHistoricalLog(mContext, mTestPrintWriter);
- assertThat(mTestStringWriter.toString().split("LEAVE").length).isEqualTo(41);
+ assertActionCount("LEAVE", expectedCount);
}
@Test
public void writeLog_overMaxEntriesLogs_withCorrectCounts() {
- for (int i = 0; i < BatteryHistoricalLogUtil.MAX_ENTRIES + 10; i++) {
- BatteryHistoricalLogUtil.writeLog(mContext, Action.RESET, "pkg" + i, "logs");
+ for (int i = 0; i < BatteryOptimizeLogUtils.MAX_ENTRIES + 10; i++) {
+ BatteryOptimizeLogUtils.writeLog(mContext, Action.RESET, "pkg" + i, "logs");
}
- BatteryHistoricalLogUtil.printBatteryOptimizeHistoricalLog(mContext, mTestPrintWriter);
+ BatteryOptimizeLogUtils.printBatteryOptimizeHistoricalLog(mContext, mTestPrintWriter);
- assertThat(mTestStringWriter.toString().split("RESET").length).isEqualTo(41);
+ assertActionCount("RESET", BatteryOptimizeLogUtils.MAX_ENTRIES);
+ }
+
+ private void assertActionCount(String token, int count) {
+ final String dumpResults = mTestStringWriter.toString();
+ assertThat(dumpResults.split(token).length).isEqualTo(count + 1);
}
}
diff --git a/tests/robotests/src/com/android/settings/fuelgauge/BatterySettingsFeatureProviderImplTest.java b/tests/robotests/src/com/android/settings/fuelgauge/BatterySettingsFeatureProviderImplTest.java
new file mode 100644
index 0000000..66050a0
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/fuelgauge/BatterySettingsFeatureProviderImplTest.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.fuelgauge;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.Context;
+
+import androidx.test.core.app.ApplicationProvider;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+@RunWith(RobolectricTestRunner.class)
+public class BatterySettingsFeatureProviderImplTest {
+ private BatterySettingsFeatureProviderImpl mImpl;
+ private Context mContext;
+
+ @Before
+ public void setUp() {
+ mImpl = new BatterySettingsFeatureProviderImpl();
+ mContext = ApplicationProvider.getApplicationContext();
+ }
+
+ @Test
+ public void isManufactureDateAvailable_returnFalse() {
+ assertThat(mImpl.isManufactureDateAvailable(mContext, 1000L)).isFalse();
+ }
+
+ @Test
+ public void isFirstUseDateAvailable_returnFalse() {
+ assertThat(mImpl.isFirstUseDateAvailable(mContext, 1000L)).isFalse();
+ }
+}
diff --git a/tests/robotests/src/com/android/settings/fuelgauge/PowerUsageFeatureProviderImplTest.java b/tests/robotests/src/com/android/settings/fuelgauge/PowerUsageFeatureProviderImplTest.java
index 1a43dbb..a0b449a 100644
--- a/tests/robotests/src/com/android/settings/fuelgauge/PowerUsageFeatureProviderImplTest.java
+++ b/tests/robotests/src/com/android/settings/fuelgauge/PowerUsageFeatureProviderImplTest.java
@@ -68,6 +68,11 @@
}
@Test
+ public void testIsBatteryTipsEnabled_returnFalse() {
+ assertThat(mPowerFeatureProvider.isBatteryTipsEnabled()).isFalse();
+ }
+
+ @Test
public void testGetBatteryUsageListConsumePowerThreshold_return0() {
assertThat(mPowerFeatureProvider.getBatteryUsageListConsumePowerThreshold()).isEqualTo(0.0);
}
diff --git a/tests/robotests/src/com/android/settings/fuelgauge/TopLevelBatteryPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/fuelgauge/TopLevelBatteryPreferenceControllerTest.java
index b444309..f6bc297 100644
--- a/tests/robotests/src/com/android/settings/fuelgauge/TopLevelBatteryPreferenceControllerTest.java
+++ b/tests/robotests/src/com/android/settings/fuelgauge/TopLevelBatteryPreferenceControllerTest.java
@@ -31,6 +31,7 @@
import android.hardware.usb.UsbManager;
import android.hardware.usb.UsbPort;
import android.hardware.usb.UsbPortStatus;
+import android.os.BatteryManager;
import androidx.preference.Preference;
import androidx.test.core.app.ApplicationProvider;
@@ -146,6 +147,17 @@
}
@Test
+ public void getDashboardLabel_notChargingState_returnsCorrectLabel() {
+ mController.mPreference = new Preference(mContext);
+ BatteryInfo info = new BatteryInfo();
+ info.batteryStatus = BatteryManager.BATTERY_STATUS_NOT_CHARGING;
+ info.statusLabel = "expected returned label";
+
+ assertThat(mController.getDashboardLabel(mContext, info, true))
+ .isEqualTo(info.statusLabel);
+ }
+
+ @Test
public void getSummary_batteryNotPresent_shouldShowWarningMessage() {
mController.mIsBatteryPresent = false;
assertThat(mController.getSummary())
diff --git a/tests/robotests/src/com/android/settings/fuelgauge/batterytip/detectors/LowBatteryDetectorTest.java b/tests/robotests/src/com/android/settings/fuelgauge/batterytip/detectors/LowBatteryDetectorTest.java
index 7104206..c9e201b 100644
--- a/tests/robotests/src/com/android/settings/fuelgauge/batterytip/detectors/LowBatteryDetectorTest.java
+++ b/tests/robotests/src/com/android/settings/fuelgauge/batterytip/detectors/LowBatteryDetectorTest.java
@@ -53,7 +53,6 @@
mPolicy = spy(new BatteryTipPolicy(RuntimeEnvironment.application));
mContext = RuntimeEnvironment.application;
ReflectionHelpers.setField(mPolicy, "lowBatteryEnabled", true);
- ReflectionHelpers.setField(mPolicy, "lowBatteryHour", 3);
mBatteryInfo.discharging = true;
mLowBatteryDetector = new LowBatteryDetector(mContext, mPolicy, mBatteryInfo,
@@ -78,13 +77,9 @@
@Test
public void testDetect_lowBattery_tipNew() {
- mBatteryInfo.batteryLevel = 3;
+ mBatteryInfo.batteryLevel = 20;
mBatteryInfo.remainingTimeUs = TimeUnit.DAYS.toMillis(1);
assertThat(mLowBatteryDetector.detect().getState()).isEqualTo(BatteryTip.StateType.NEW);
-
- mBatteryInfo.batteryLevel = 50;
- mBatteryInfo.remainingTimeUs = TimeUnit.MINUTES.toMillis(1);
- assertThat(mLowBatteryDetector.detect().getState()).isEqualTo(BatteryTip.StateType.NEW);
}
@Test
@@ -104,9 +99,9 @@
}
@Test
- public void testDetect_timeEstimationZero_tipInvisible() {
+ public void testDetect_lowTimeEstimation_tipInvisible() {
mBatteryInfo.batteryLevel = 50;
- mBatteryInfo.remainingTimeUs = 0;
+ mBatteryInfo.remainingTimeUs = TimeUnit.MINUTES.toMillis(1);
assertThat(mLowBatteryDetector.detect().isVisible()).isFalse();
}
diff --git a/tests/robotests/src/com/android/settings/fuelgauge/batterytip/tips/BatteryTipTest.java b/tests/robotests/src/com/android/settings/fuelgauge/batterytip/tips/BatteryTipTest.java
index 3513168..ecac4f9 100644
--- a/tests/robotests/src/com/android/settings/fuelgauge/batterytip/tips/BatteryTipTest.java
+++ b/tests/robotests/src/com/android/settings/fuelgauge/batterytip/tips/BatteryTipTest.java
@@ -18,11 +18,11 @@
import static com.google.common.truth.Truth.assertThat;
import android.content.Context;
-import android.view.View;
import android.os.Parcel;
import android.os.Parcelable;
+import android.view.View;
-import androidx.annotation.IdRes;
+import androidx.annotation.DrawableRes;
import androidx.preference.Preference;
import androidx.preference.PreferenceViewHolder;
@@ -45,7 +45,7 @@
private static final String TITLE = "title";
private static final String SUMMARY = "summary";
- @IdRes
+ @DrawableRes
private static final int ICON_ID = R.drawable.ic_fingerprint;
private Context mContext;
diff --git a/tests/robotests/src/com/android/settings/fuelgauge/batterytip/tips/IncompatibleChargerTipTest.java b/tests/robotests/src/com/android/settings/fuelgauge/batterytip/tips/IncompatibleChargerTipTest.java
index a5f1ab3..9f6e4e3 100644
--- a/tests/robotests/src/com/android/settings/fuelgauge/batterytip/tips/IncompatibleChargerTipTest.java
+++ b/tests/robotests/src/com/android/settings/fuelgauge/batterytip/tips/IncompatibleChargerTipTest.java
@@ -85,7 +85,7 @@
@Test
public void getIcon_showIcon() {
assertThat(mIncompatibleChargerTip.getIconId())
- .isEqualTo(R.drawable.ic_battery_alert_theme);
+ .isEqualTo(R.drawable.ic_battery_charger);
}
@Test
diff --git a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/AnomalyEventWrapperTest.java b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/AnomalyEventWrapperTest.java
new file mode 100644
index 0000000..60e0af0
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/AnomalyEventWrapperTest.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.fuelgauge.batteryusage;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.spy;
+
+import android.content.Context;
+
+import com.android.settings.testutils.BatteryTestUtils;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+
+import java.util.TimeZone;
+
+@RunWith(RobolectricTestRunner.class)
+public class AnomalyEventWrapperTest {
+ private AnomalyEventWrapper mAnomalyEventWrapper;
+ private Context mContext;
+
+ @Before
+ public void setUp() {
+ TimeZone.setDefault(TimeZone.getTimeZone("GMT+8"));
+ mContext = spy(RuntimeEnvironment.application);
+ }
+
+ @Test
+ public void getDismissRecordKey_returnExpectedResult() {
+ mAnomalyEventWrapper = new AnomalyEventWrapper(mContext,
+ BatteryTestUtils.createAdaptiveBrightnessAnomalyEvent());
+ assertThat(mAnomalyEventWrapper.getDismissRecordKey())
+ .isEqualTo("KEY_BRIGHTNESS");
+
+ mAnomalyEventWrapper = new AnomalyEventWrapper(mContext,
+ BatteryTestUtils.createScreenTimeoutAnomalyEvent());
+ assertThat(mAnomalyEventWrapper.getDismissRecordKey())
+ .isEqualTo("KEY_SCREEN_TIMEOUT");
+
+ mAnomalyEventWrapper = new AnomalyEventWrapper(mContext,
+ BatteryTestUtils.createAppAnomalyEvent());
+ assertThat(mAnomalyEventWrapper.getDismissRecordKey())
+ .isEqualTo("KEY_APP_1");
+ }
+}
diff --git a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/AppUsageDataLoaderTest.java b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/AppUsageDataLoaderTest.java
deleted file mode 100644
index 4b250a3..0000000
--- a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/AppUsageDataLoaderTest.java
+++ /dev/null
@@ -1,102 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.settings.fuelgauge.batteryusage;
-
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Mockito.doReturn;
-import static org.mockito.Mockito.spy;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.verifyNoMoreInteractions;
-
-import android.content.ContentResolver;
-import android.content.Context;
-import android.content.Intent;
-import android.os.UserManager;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-import org.robolectric.RobolectricTestRunner;
-import org.robolectric.RuntimeEnvironment;
-
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-
-@RunWith(RobolectricTestRunner.class)
-public final class AppUsageDataLoaderTest {
- private Context mContext;
- @Mock
- private ContentResolver mMockContentResolver;
- @Mock
- private UserManager mUserManager;
-
- @Before
- public void setUp() {
- MockitoAnnotations.initMocks(this);
- mContext = spy(RuntimeEnvironment.application);
- doReturn(mContext).when(mContext).getApplicationContext();
- doReturn(mMockContentResolver).when(mContext).getContentResolver();
- doReturn(mUserManager).when(mContext).getSystemService(UserManager.class);
- doReturn(new Intent()).when(mContext).registerReceiver(any(), any());
- }
-
- @Test
- public void loadAppUsageData_withData_insertFakeDataIntoProvider() {
- final List<AppUsageEvent> AppUsageEventList = new ArrayList<>();
- final AppUsageEvent appUsageEvent = AppUsageEvent.newBuilder().setUid(0).build();
- AppUsageEventList.add(appUsageEvent);
- AppUsageDataLoader.sFakeAppUsageEventsSupplier = () -> new HashMap<>();
- AppUsageDataLoader.sFakeUsageEventsListSupplier = () -> AppUsageEventList;
-
- AppUsageDataLoader.loadAppUsageData(mContext);
-
- verify(mMockContentResolver).bulkInsert(any(), any());
- verify(mMockContentResolver).notifyChange(any(), any());
- }
-
- @Test
- public void loadAppUsageData_nullAppUsageEvents_notInsertDataIntoProvider() {
- AppUsageDataLoader.sFakeAppUsageEventsSupplier = () -> null;
-
- AppUsageDataLoader.loadAppUsageData(mContext);
-
- verifyNoMoreInteractions(mMockContentResolver);
- }
-
- @Test
- public void loadAppUsageData_nullUsageEventsList_notInsertDataIntoProvider() {
- AppUsageDataLoader.sFakeAppUsageEventsSupplier = () -> new HashMap<>();
- AppUsageDataLoader.sFakeUsageEventsListSupplier = () -> null;
-
- AppUsageDataLoader.loadAppUsageData(mContext);
-
- verifyNoMoreInteractions(mMockContentResolver);
- }
-
- @Test
- public void loadAppUsageData_emptyUsageEventsList_notInsertDataIntoProvider() {
- AppUsageDataLoader.sFakeAppUsageEventsSupplier = () -> new HashMap<>();
- AppUsageDataLoader.sFakeUsageEventsListSupplier = () -> new ArrayList<>();
-
- AppUsageDataLoader.loadAppUsageData(mContext);
-
- verifyNoMoreInteractions(mMockContentResolver);
- }
-}
diff --git a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryChartPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryChartPreferenceControllerTest.java
index e1c193c..cd4e599 100644
--- a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryChartPreferenceControllerTest.java
+++ b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryChartPreferenceControllerTest.java
@@ -16,6 +16,8 @@
package com.android.settings.fuelgauge.batteryusage;
+import static com.android.settings.fuelgauge.batteryusage.BatteryChartViewModel.SELECTED_INDEX_ALL;
+
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.anyFloat;
@@ -26,11 +28,11 @@
import static org.mockito.Mockito.atLeast;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
-import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
@@ -39,9 +41,11 @@
import android.os.LocaleList;
import android.os.UserManager;
import android.text.format.DateUtils;
+import android.util.ArrayMap;
import android.view.View;
import android.view.ViewPropertyAnimator;
import android.widget.LinearLayout;
+import android.widget.TextView;
import com.android.settings.SettingsActivity;
import com.android.settings.testutils.FakeFeatureFactory;
@@ -54,7 +58,6 @@
import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment;
-import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
@@ -70,6 +73,8 @@
@Mock
private SettingsActivity mSettingsActivity;
@Mock
+ private TextView mChartSummaryTextView;
+ @Mock
private BatteryChartView mDailyChartView;
@Mock
private BatteryChartView mHourlyChartView;
@@ -110,8 +115,10 @@
setupHourlyChartViewAnimationMock();
mBatteryChartPreferenceController = createController();
mBatteryChartPreferenceController.mPrefContext = mContext;
+ mBatteryChartPreferenceController.mChartSummaryTextView = mChartSummaryTextView;
mBatteryChartPreferenceController.mDailyChartView = mDailyChartView;
mBatteryChartPreferenceController.mHourlyChartView = mHourlyChartView;
+ BatteryDiffEntry.clearCache();
// Adds fake testing data.
BatteryDiffEntry.sResourceCache.put(
"fakeBatteryDiffEntryKey",
@@ -144,7 +151,7 @@
reset(mHourlyChartView);
setupHourlyChartViewAnimationMock();
- mBatteryChartPreferenceController.setBatteryHistoryMap(createBatteryHistoryMap(6));
+ mBatteryChartPreferenceController.onBatteryLevelDataUpdate(createBatteryLevelData(6));
verify(mDailyChartView, atLeastOnce()).setVisibility(View.GONE);
// Ignore fast refresh ui from the data processor callback.
@@ -176,16 +183,17 @@
BatteryChartViewModel.AxisLabelPosition.CENTER_OF_TRAPEZOIDS,
mBatteryChartPreferenceController.mDailyChartLabelTextGenerator);
- mBatteryChartPreferenceController.setBatteryHistoryMap(createBatteryHistoryMap(60));
+ mBatteryChartPreferenceController.onBatteryLevelDataUpdate(createBatteryLevelData(60));
verify(mDailyChartView, atLeastOnce()).setVisibility(View.VISIBLE);
verify(mViewPropertyAnimator, atLeastOnce()).alpha(0f);
- verify(mDailyChartView).setViewModel(expectedDailyViewModel);
+ verify(mDailyChartView, atLeastOnce()).setViewModel(expectedDailyViewModel);
reset(mDailyChartView);
reset(mHourlyChartView);
setupHourlyChartViewAnimationMock();
doReturn(mLayoutParams).when(mDailyChartView).getLayoutParams();
+ doReturn(View.GONE).when(mHourlyChartView).getVisibility();
mBatteryChartPreferenceController.mDailyChartIndex = 0;
mBatteryChartPreferenceController.refreshUi();
verify(mDailyChartView).setVisibility(View.VISIBLE);
@@ -245,8 +253,7 @@
setupHourlyChartViewAnimationMock();
doReturn(mLayoutParams).when(mDailyChartView).getLayoutParams();
mBatteryChartPreferenceController.mDailyChartIndex = 2;
- mBatteryChartPreferenceController.mHourlyChartIndex =
- BatteryChartViewModel.SELECTED_INDEX_ALL;
+ mBatteryChartPreferenceController.mHourlyChartIndex = SELECTED_INDEX_ALL;
mBatteryChartPreferenceController.refreshUi();
verify(mDailyChartView).setVisibility(View.VISIBLE);
verify(mViewPropertyAnimator, atLeastOnce()).alpha(1f);
@@ -271,63 +278,110 @@
}
@Test
- public void refreshUi_normalCase_returnTrue() {
- mBatteryChartPreferenceController.setBatteryHistoryMap(createBatteryHistoryMap(6));
- assertThat(mBatteryChartPreferenceController.refreshUi()).isTrue();
+ public void onBatteryLevelDataUpdate_oneDay_showHourlyChartOnly() {
+ doReturn(View.GONE).when(mHourlyChartView).getVisibility();
+
+ mBatteryChartPreferenceController.onBatteryLevelDataUpdate(createBatteryLevelData(6));
+
+ verify(mChartSummaryTextView).setVisibility(View.VISIBLE);
+ verify(mDailyChartView).setVisibility(View.GONE);
+ verify(mHourlyChartView).setVisibility(View.VISIBLE);
}
@Test
- public void refreshUi_batteryIndexedMapIsNull_returnTrue() {
- mBatteryChartPreferenceController.setBatteryHistoryMap(null);
- assertThat(mBatteryChartPreferenceController.refreshUi()).isTrue();
+ public void onBatteryLevelDataUpdate_selectAllForMultipleDays_showDailyChartOnly() {
+ doReturn(View.GONE).when(mHourlyChartView).getVisibility();
+
+ mBatteryChartPreferenceController.mDailyChartIndex = SELECTED_INDEX_ALL;
+ mBatteryChartPreferenceController.onBatteryLevelDataUpdate(createBatteryLevelData(60));
+
+ verify(mChartSummaryTextView).setVisibility(View.VISIBLE);
+ verify(mDailyChartView).setVisibility(View.VISIBLE);
+ verify(mHourlyChartView, never()).setVisibility(View.VISIBLE);
+ }
+
+ @Test
+ public void onBatteryLevelDataUpdate_selectOneDayForMultipleDays_showBothCharts() {
+ doReturn(View.GONE).when(mHourlyChartView).getVisibility();
+
+ mBatteryChartPreferenceController.mDailyChartIndex = 0;
+ mBatteryChartPreferenceController.onBatteryLevelDataUpdate(createBatteryLevelData(60));
+
+ verify(mChartSummaryTextView).setVisibility(View.VISIBLE);
+ verify(mDailyChartView).setVisibility(View.VISIBLE);
+ verify(mHourlyChartView).setVisibility(View.VISIBLE);
+ }
+
+ @Test
+ public void onBatteryLevelDataUpdate_batteryLevelDataIsNull_showNoChart() {
+ doReturn(View.GONE).when(mHourlyChartView).getVisibility();
+
+ mBatteryChartPreferenceController.onBatteryLevelDataUpdate(null);
+
+ verify(mChartSummaryTextView).setVisibility(View.GONE);
+ verify(mDailyChartView).setVisibility(View.GONE);
+ verify(mHourlyChartView).setVisibility(View.GONE);
+ }
+
+ @Test
+ public void showEmptyChart_normalCase_showEmptyChart() {
+ doReturn(View.GONE).when(mHourlyChartView).getVisibility();
+
+ mBatteryChartPreferenceController.showEmptyChart();
+
+ verify(mChartSummaryTextView).setVisibility(View.VISIBLE);
+ verify(mDailyChartView).setVisibility(View.GONE);
+ verify(mHourlyChartView).setVisibility(View.VISIBLE);
}
@Test
public void refreshUi_dailyChartViewIsNull_ignoreRefresh() {
mBatteryChartPreferenceController.mDailyChartView = null;
- assertThat(mBatteryChartPreferenceController.refreshUi()).isFalse();
+
+ mBatteryChartPreferenceController.refreshUi();
+
+ verify(mChartSummaryTextView, never()).setVisibility(anyInt());
}
@Test
public void refreshUi_hourlyChartViewIsNull_ignoreRefresh() {
mBatteryChartPreferenceController.mHourlyChartView = null;
- assertThat(mBatteryChartPreferenceController.refreshUi()).isFalse();
+
+ mBatteryChartPreferenceController.refreshUi();
+
+ verify(mChartSummaryTextView, never()).setVisibility(anyInt());
}
@Test
public void selectedSlotText_selectAllDaysAllHours_returnNull() {
- mBatteryChartPreferenceController.setBatteryHistoryMap(createBatteryHistoryMap(60));
- mBatteryChartPreferenceController.mDailyChartIndex =
- BatteryChartViewModel.SELECTED_INDEX_ALL;
- mBatteryChartPreferenceController.mHourlyChartIndex =
- BatteryChartViewModel.SELECTED_INDEX_ALL;
+ mBatteryChartPreferenceController.onBatteryLevelDataUpdate(createBatteryLevelData(60));
+ mBatteryChartPreferenceController.mDailyChartIndex = SELECTED_INDEX_ALL;
+ mBatteryChartPreferenceController.mHourlyChartIndex = SELECTED_INDEX_ALL;
assertThat(mBatteryChartPreferenceController.getSlotInformation()).isEqualTo(null);
}
@Test
public void selectedSlotText_onlyOneDayDataSelectAllHours_returnNull() {
- mBatteryChartPreferenceController.setBatteryHistoryMap(createBatteryHistoryMap(6));
+ mBatteryChartPreferenceController.onBatteryLevelDataUpdate(createBatteryLevelData(6));
mBatteryChartPreferenceController.mDailyChartIndex = 0;
- mBatteryChartPreferenceController.mHourlyChartIndex =
- BatteryChartViewModel.SELECTED_INDEX_ALL;
+ mBatteryChartPreferenceController.mHourlyChartIndex = SELECTED_INDEX_ALL;
assertThat(mBatteryChartPreferenceController.getSlotInformation()).isEqualTo(null);
}
@Test
public void selectedSlotText_selectADayAllHours_onlyDayText() {
- mBatteryChartPreferenceController.setBatteryHistoryMap(createBatteryHistoryMap(60));
+ mBatteryChartPreferenceController.onBatteryLevelDataUpdate(createBatteryLevelData(60));
mBatteryChartPreferenceController.mDailyChartIndex = 1;
- mBatteryChartPreferenceController.mHourlyChartIndex =
- BatteryChartViewModel.SELECTED_INDEX_ALL;
+ mBatteryChartPreferenceController.mHourlyChartIndex = SELECTED_INDEX_ALL;
assertThat(mBatteryChartPreferenceController.getSlotInformation()).isEqualTo("Sunday");
}
@Test
public void selectedSlotText_onlyOneDayDataSelectAnHour_onlyHourText() {
- mBatteryChartPreferenceController.setBatteryHistoryMap(createBatteryHistoryMap(6));
+ mBatteryChartPreferenceController.onBatteryLevelDataUpdate(createBatteryLevelData(6));
mBatteryChartPreferenceController.mDailyChartIndex = 0;
mBatteryChartPreferenceController.mHourlyChartIndex = 2;
@@ -337,7 +391,7 @@
@Test
public void selectedSlotText_SelectADayAnHour_dayAndHourText() {
- mBatteryChartPreferenceController.setBatteryHistoryMap(createBatteryHistoryMap(60));
+ mBatteryChartPreferenceController.onBatteryLevelDataUpdate(createBatteryLevelData(60));
mBatteryChartPreferenceController.mDailyChartIndex = 1;
mBatteryChartPreferenceController.mHourlyChartIndex = 8;
@@ -347,7 +401,7 @@
@Test
public void selectedSlotText_selectFirstSlot_withMinuteText() {
- mBatteryChartPreferenceController.setBatteryHistoryMap(createBatteryHistoryMap(6));
+ mBatteryChartPreferenceController.onBatteryLevelDataUpdate(createBatteryLevelData(6));
mBatteryChartPreferenceController.mDailyChartIndex = 0;
mBatteryChartPreferenceController.mHourlyChartIndex = 0;
@@ -357,7 +411,7 @@
@Test
public void selectedSlotText_selectLastSlot_withNowText() {
- mBatteryChartPreferenceController.setBatteryHistoryMap(createBatteryHistoryMap(6));
+ mBatteryChartPreferenceController.onBatteryLevelDataUpdate(createBatteryLevelData(6));
mBatteryChartPreferenceController.mDailyChartIndex = 0;
mBatteryChartPreferenceController.mHourlyChartIndex = 3;
@@ -367,7 +421,7 @@
@Test
public void selectedSlotText_selectOnlySlot_withMinuteAndNowText() {
- mBatteryChartPreferenceController.setBatteryHistoryMap(createBatteryHistoryMap(1));
+ mBatteryChartPreferenceController.onBatteryLevelDataUpdate(createBatteryLevelData(1));
mBatteryChartPreferenceController.mDailyChartIndex = 0;
mBatteryChartPreferenceController.mHourlyChartIndex = 0;
@@ -388,7 +442,7 @@
mBatteryChartPreferenceController.mHourlyChartIndex = -1;
mBatteryChartPreferenceController.onCreate(bundle);
- mBatteryChartPreferenceController.setBatteryHistoryMap(createBatteryHistoryMap(25));
+ mBatteryChartPreferenceController.onBatteryLevelDataUpdate(createBatteryLevelData(25));
assertThat(mBatteryChartPreferenceController.mDailyChartIndex)
.isEqualTo(expectedDailyIndex);
@@ -398,9 +452,7 @@
@Test
public void getTotalHours_getExpectedResult() {
- Map<Long, Map<String, BatteryHistEntry>> batteryHistoryMap = createBatteryHistoryMap(60);
- BatteryLevelData batteryLevelData =
- DataProcessManager.getBatteryLevelData(mContext, null, batteryHistoryMap, null);
+ BatteryLevelData batteryLevelData = createBatteryLevelData(60);
final int totalHour = BatteryChartPreferenceController.getTotalHours(batteryLevelData);
@@ -413,37 +465,21 @@
return 1619247600000L + index * DateUtils.HOUR_IN_MILLIS;
}
- private static Map<Long, Map<String, BatteryHistEntry>> createBatteryHistoryMap(
- int numOfHours) {
- final Map<Long, Map<String, BatteryHistEntry>> batteryHistoryMap = new HashMap<>();
- for (int index = 0; index < numOfHours; index++) {
- final ContentValues values = new ContentValues();
- final DeviceBatteryState deviceBatteryState =
- DeviceBatteryState
- .newBuilder()
- .setBatteryLevel(100 - index)
- .build();
- final BatteryInformation batteryInformation =
- BatteryInformation
- .newBuilder()
- .setDeviceBatteryState(deviceBatteryState)
- .setConsumePower(100 - index)
- .build();
- values.put(BatteryHistEntry.KEY_BATTERY_INFORMATION,
- ConvertUtils.convertBatteryInformationToString(batteryInformation));
- values.put(BatteryHistEntry.KEY_PACKAGE_NAME, "package" + index);
- final BatteryHistEntry entry = new BatteryHistEntry(values);
- final Map<String, BatteryHistEntry> entryMap = new HashMap<>();
- entryMap.put("fake_entry_key" + index, entry);
- long timestamp = generateTimestamp(index);
+ private static BatteryLevelData createBatteryLevelData(int numOfHours) {
+ Map<Long, Integer> batteryLevelMap = new ArrayMap<>();
+ for (int index = 0; index < numOfHours; index += 2) {
+ final Integer level = 100 - index;
+ Long timestamp = generateTimestamp(index);
if (index == 0) {
timestamp += DateUtils.MINUTE_IN_MILLIS;
+ index--;
}
- batteryHistoryMap.put(timestamp, entryMap);
+ batteryLevelMap.put(timestamp, level);
}
- DataProcessor.sTestCurrentTimeMillis =
- generateTimestamp(numOfHours - 1) + DateUtils.MINUTE_IN_MILLIS * 2;
- return batteryHistoryMap;
+ long current = generateTimestamp(numOfHours - 1) + DateUtils.MINUTE_IN_MILLIS * 2;
+ batteryLevelMap.put(current, 66);
+ DataProcessor.sTestCurrentTimeMillis = current;
+ return new BatteryLevelData(batteryLevelMap);
}
private BatteryChartPreferenceController createController() {
diff --git a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryDiffDataTest.java b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryDiffDataTest.java
index 27539a5..d4bae29 100644
--- a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryDiffDataTest.java
+++ b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryDiffDataTest.java
@@ -147,6 +147,14 @@
/*foregroundUsageTimeInMs=*/ 0L, /*backgroundUsageTimeInMs=*/ 0L, isHidden);
return new BatteryDiffEntry(
context,
+ batteryHistEntry.mUid,
+ batteryHistEntry.mUserId,
+ batteryHistEntry.getKey(),
+ batteryHistEntry.mIsHidden,
+ batteryHistEntry.mDrainType,
+ batteryHistEntry.mPackageName,
+ batteryHistEntry.mAppLabel,
+ batteryHistEntry.mConsumerType,
/*foregroundUsageTimeInMs=*/ 0,
/*backgroundUsageTimeInMs=*/ 0,
/*screenOnTimeInMs=*/ 0,
@@ -154,8 +162,7 @@
/*foregroundUsageConsumePower=*/ 0,
/*foregroundServiceUsageConsumePower=*/ 0,
/*backgroundUsageConsumePower=*/ 0,
- /*cachedUsageConsumePower=*/ 0,
- batteryHistEntry);
+ /*cachedUsageConsumePower=*/ 0);
}
private static BatteryHistEntry createBatteryHistEntry(
diff --git a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryDiffEntryTest.java b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryDiffEntryTest.java
index 655f1e4..9bb4b73 100644
--- a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryDiffEntryTest.java
+++ b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryDiffEntryTest.java
@@ -95,6 +95,14 @@
final BatteryDiffEntry entry =
new BatteryDiffEntry(
mContext,
+ /*uid=*/ 0,
+ /*userId=*/ 0,
+ /*key=*/ "key",
+ /*isHidden=*/ false,
+ /*componentId=*/ -1,
+ /*legacyPackageName=*/ null,
+ /*legacyLabel=*/ null,
+ /*consumerType*/ ConvertUtils.CONSUMER_TYPE_UID_BATTERY,
/*foregroundUsageTimeInMs=*/ 10001L,
/*backgroundUsageTimeInMs=*/ 20002L,
/*screenOnTimeInMs=*/ 30003L,
@@ -102,8 +110,7 @@
/*foregroundUsageConsumePower=*/ 10.0,
/*foregroundServiceUsageConsumePower=*/ 10.0,
/*backgroundUsageConsumePower=*/ 1.0,
- /*cachedUsageConsumePower=*/ 1.0,
- /*batteryHistEntry=*/ null);
+ /*cachedUsageConsumePower=*/ 1.0);
entry.setTotalConsumePower(100.0);
assertThat(entry.getPercentage()).isEqualTo(22.0);
@@ -114,6 +121,14 @@
final BatteryDiffEntry entry =
new BatteryDiffEntry(
mContext,
+ /*uid=*/ 0,
+ /*userId=*/ 0,
+ /*key=*/ "key",
+ /*isHidden=*/ false,
+ /*componentId=*/ -1,
+ /*legacyPackageName=*/ null,
+ /*legacyLabel=*/ null,
+ /*consumerType*/ ConvertUtils.CONSUMER_TYPE_UID_BATTERY,
/*foregroundUsageTimeInMs=*/ 10001L,
/*backgroundUsageTimeInMs=*/ 20002L,
/*screenOnTimeInMs=*/ 30003L,
@@ -121,8 +136,7 @@
/*foregroundUsageConsumePower=*/ 10.0,
/*foregroundServiceUsageConsumePower=*/ 10.0,
/*backgroundUsageConsumePower=*/ 1.0,
- /*cachedUsageConsumePower=*/ 1.0,
- /*batteryHistEntry=*/ null);
+ /*cachedUsageConsumePower=*/ 1.0);
entry.setTotalConsumePower(0);
assertThat(entry.getPercentage()).isEqualTo(0);
@@ -133,7 +147,24 @@
final List<BatteryDiffEntry> entryList = new ArrayList<>();
// Generates fake testing data.
BatteryDiffEntry systemAppsBatteryDiffEntry =
- new BatteryDiffEntry.SystemAppsBatteryDiffEntry(mContext);
+ new BatteryDiffEntry(
+ mContext,
+ /*uid=*/ 0,
+ /*userId=*/ 0,
+ /*key=*/ BatteryDiffEntry.SYSTEM_APPS_KEY,
+ /*isHidden=*/ false,
+ /*componentId=*/ -1,
+ /*legacyPackageName=*/ null,
+ /*legacyLabel=*/ BatteryDiffEntry.SYSTEM_APPS_KEY,
+ /*consumerType*/ ConvertUtils.CONSUMER_TYPE_UID_BATTERY,
+ /*foregroundUsageTimeInMs=*/ 0,
+ /*backgroundUsageTimeInMs=*/ 0,
+ /*screenOnTimeInMs=*/ 0,
+ /*consumePower=*/ 0,
+ /*foregroundUsageConsumePower=*/ 0,
+ /*foregroundServiceUsageConsumePower=*/ 0,
+ /*backgroundUsageConsumePower=*/ 0,
+ /*cachedUsageConsumePower=*/ 0);
systemAppsBatteryDiffEntry.mConsumePower = 16;
systemAppsBatteryDiffEntry.setTotalConsumePower(100);
entryList.add(systemAppsBatteryDiffEntry);
@@ -448,17 +479,16 @@
private BatteryDiffEntry createBatteryDiffEntry(
int consumerType, long uid, boolean isHidden) {
- final ContentValues values = getContentValuesWithType(consumerType);
- final BatteryInformation batteryInformation =
- BatteryInformation
- .newBuilder()
- .setIsHidden(isHidden)
- .build();
- values.put(BatteryHistEntry.KEY_BATTERY_INFORMATION,
- ConvertUtils.convertBatteryInformationToString(batteryInformation));
- values.put(BatteryHistEntry.KEY_UID, uid);
return new BatteryDiffEntry(
mContext,
+ /*uid=*/ uid,
+ /*userId=*/ 0,
+ /*key=*/ "key",
+ /*isHidden=*/ isHidden,
+ /*componentId=*/ -1,
+ /*legacyPackageName=*/ null,
+ /*legacyLabel=*/ null,
+ /*consumerType*/ consumerType,
/*foregroundUsageTimeInMs=*/ 0,
/*backgroundUsageTimeInMs=*/ 0,
/*screenOnTimeInMs=*/ 0,
@@ -466,14 +496,21 @@
/*foregroundUsageConsumePower=*/ 0,
/*foregroundServiceUsageConsumePower=*/ 0,
/*backgroundUsageConsumePower=*/ 0,
- /*cachedUsageConsumePower=*/ 0,
- new BatteryHistEntry(values));
+ /*cachedUsageConsumePower=*/ 0);
}
private BatteryDiffEntry createBatteryDiffEntry(
double consumePower, BatteryHistEntry batteryHistEntry) {
final BatteryDiffEntry entry = new BatteryDiffEntry(
mContext,
+ batteryHistEntry.mUid,
+ batteryHistEntry.mUserId,
+ batteryHistEntry.getKey(),
+ batteryHistEntry.mIsHidden,
+ batteryHistEntry.mDrainType,
+ batteryHistEntry.mPackageName,
+ batteryHistEntry.mAppLabel,
+ batteryHistEntry.mConsumerType,
/*foregroundUsageTimeInMs=*/ 0,
/*backgroundUsageTimeInMs=*/ 0,
/*screenOnTimeInMs=*/ 0,
@@ -481,8 +518,7 @@
/*foregroundUsageConsumePower=*/ 0,
/*foregroundServiceUsageConsumePower=*/ 0,
/*backgroundUsageConsumePower=*/ 0,
- /*cachedUsageConsumePower=*/ 0,
- batteryHistEntry);
+ /*cachedUsageConsumePower=*/ 0);
entry.setTotalConsumePower(100.0);
return entry;
}
diff --git a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryEntryTest.java b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryEntryTest.java
index dec5d7d..108d6e2 100644
--- a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryEntryTest.java
+++ b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryEntryTest.java
@@ -40,6 +40,7 @@
import com.android.settings.fuelgauge.batteryusage.BatteryEntry.NameAndIcon;
import org.junit.Before;
+import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -50,8 +51,6 @@
import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment;
-import java.util.Locale;
-
@RunWith(RobolectricTestRunner.class)
public class BatteryEntryTest {
@@ -232,17 +231,7 @@
assertThat(entry.getTimeInBackgroundMs()).isEqualTo(0);
}
- @Test
- public void testUidCache_switchLocale_shouldCleanCache() {
- Locale.setDefault(new Locale("en_US"));
- BatteryEntry.sUidCache.put(Integer.toString(APP_UID), null);
- assertThat(BatteryEntry.sUidCache).isNotEmpty();
-
- Locale.setDefault(new Locale("zh_TW"));
- createBatteryEntryForApp(null, null, HIGH_DRAIN_PACKAGE);
- assertThat(BatteryEntry.sUidCache).isEmpty(); // check if cache is clear
- }
-
+ @Ignore
@Test
public void getKey_UidBatteryConsumer() {
final BatteryEntry entry = createBatteryEntryForApp(null, null, null);
diff --git a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryHistEntryTest.java b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryHistEntryTest.java
index 9667760..609f2fc 100644
--- a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryHistEntryTest.java
+++ b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryHistEntryTest.java
@@ -15,6 +15,10 @@
*/
package com.android.settings.fuelgauge.batteryusage;
+import static com.android.settings.fuelgauge.batteryusage.ConvertUtils.isSystemConsumer;
+import static com.android.settings.fuelgauge.batteryusage.ConvertUtils.isUidConsumer;
+import static com.android.settings.fuelgauge.batteryusage.ConvertUtils.isUserConsumer;
+
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Mockito.when;
@@ -147,32 +151,32 @@
@Test
public void testIsAppEntry_returnExpectedResult() {
- assertThat(createEntry(ConvertUtils.CONSUMER_TYPE_SYSTEM_BATTERY).isAppEntry())
- .isFalse();
- assertThat(createEntry(ConvertUtils.CONSUMER_TYPE_USER_BATTERY).isAppEntry())
- .isFalse();
- assertThat(createEntry(ConvertUtils.CONSUMER_TYPE_UID_BATTERY).isAppEntry())
- .isTrue();
+ assertThat(isUidConsumer(
+ createEntry(ConvertUtils.CONSUMER_TYPE_SYSTEM_BATTERY).mConsumerType)).isFalse();
+ assertThat(isUidConsumer(
+ createEntry(ConvertUtils.CONSUMER_TYPE_USER_BATTERY).mConsumerType)).isFalse();
+ assertThat(isUidConsumer(
+ createEntry(ConvertUtils.CONSUMER_TYPE_UID_BATTERY).mConsumerType)).isTrue();
}
@Test
public void testIsUserEntry_returnExpectedResult() {
- assertThat(createEntry(ConvertUtils.CONSUMER_TYPE_SYSTEM_BATTERY).isUserEntry())
- .isFalse();
- assertThat(createEntry(ConvertUtils.CONSUMER_TYPE_USER_BATTERY).isUserEntry())
- .isTrue();
- assertThat(createEntry(ConvertUtils.CONSUMER_TYPE_UID_BATTERY).isUserEntry())
- .isFalse();
+ assertThat(isUserConsumer(
+ createEntry(ConvertUtils.CONSUMER_TYPE_SYSTEM_BATTERY).mConsumerType)).isFalse();
+ assertThat(isUserConsumer(
+ createEntry(ConvertUtils.CONSUMER_TYPE_USER_BATTERY).mConsumerType)).isTrue();
+ assertThat(isUserConsumer(
+ createEntry(ConvertUtils.CONSUMER_TYPE_UID_BATTERY).mConsumerType)).isFalse();
}
@Test
public void testIsSystemEntry_returnExpectedResult() {
- assertThat(createEntry(ConvertUtils.CONSUMER_TYPE_SYSTEM_BATTERY).isSystemEntry())
- .isTrue();
- assertThat(createEntry(ConvertUtils.CONSUMER_TYPE_USER_BATTERY).isSystemEntry())
- .isFalse();
- assertThat(createEntry(ConvertUtils.CONSUMER_TYPE_UID_BATTERY).isSystemEntry())
- .isFalse();
+ assertThat(isSystemConsumer(
+ createEntry(ConvertUtils.CONSUMER_TYPE_SYSTEM_BATTERY).mConsumerType)).isTrue();
+ assertThat(isSystemConsumer(
+ createEntry(ConvertUtils.CONSUMER_TYPE_USER_BATTERY).mConsumerType)).isFalse();
+ assertThat(isSystemConsumer(
+ createEntry(ConvertUtils.CONSUMER_TYPE_UID_BATTERY).mConsumerType)).isFalse();
}
@Test
diff --git a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryHistoryPreferenceTest.java b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryHistoryPreferenceTest.java
index e14ead5..9155c66 100644
--- a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryHistoryPreferenceTest.java
+++ b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryHistoryPreferenceTest.java
@@ -27,7 +27,6 @@
import androidx.preference.PreferenceViewHolder;
import com.android.settings.R;
-import com.android.settings.fuelgauge.BatteryInfo;
import org.junit.Before;
import org.junit.Test;
@@ -42,8 +41,6 @@
@Mock
private PreferenceViewHolder mViewHolder;
@Mock
- private BatteryInfo mBatteryInfo;
- @Mock
private TextView mTextView;
@Mock
private BatteryChartView mDailyChartView;
@@ -59,7 +56,6 @@
LayoutInflater.from(context).inflate(R.layout.battery_chart_graph, null);
mBatteryHistoryPreference = new BatteryHistoryPreference(context, null);
- mBatteryHistoryPreference.mBatteryInfo = mBatteryInfo;
mViewHolder = spy(PreferenceViewHolder.createInstanceForTests(itemView));
when(mViewHolder.findViewById(R.id.daily_battery_chart)).thenReturn(mDailyChartView);
when(mViewHolder.findViewById(R.id.hourly_battery_chart)).thenReturn(mHourlyChartView);
diff --git a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryLevelDataTest.java b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryLevelDataTest.java
new file mode 100644
index 0000000..7dc4eab
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryLevelDataTest.java
@@ -0,0 +1,243 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.fuelgauge.batteryusage;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.util.Pair;
+
+import com.android.settings.testutils.BatteryTestUtils;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+import java.util.List;
+import java.util.Map;
+import java.util.TimeZone;
+
+@RunWith(RobolectricTestRunner.class)
+public class BatteryLevelDataTest {
+
+ @Before
+ public void setUp() {
+ TimeZone.setDefault(TimeZone.getTimeZone("GMT+8"));
+ }
+
+ @Test
+ public void getDailyTimestamps_allDataInOneHour_returnExpectedList() {
+ // Timezone GMT+8
+ final List<Long> timestamps = List.of(
+ 1640970006000L, // 2022-01-01 01:00:06
+ 1640973608000L // 2022-01-01 01:00:08
+ );
+
+ final List<Long> expectedTimestamps = List.of(
+ 1640970006000L, // 2022-01-01 01:00:06
+ 1640973608000L // 2022-01-01 01:00:08
+ );
+ assertThat(BatteryLevelData.getDailyTimestamps(timestamps)).isEqualTo(expectedTimestamps);
+ }
+
+ @Test
+ public void getDailyTimestamps_OneHourDataPerDay_returnExpectedList() {
+ // Timezone GMT+8
+ final List<Long> timestamps = List.of(
+ 1641049200000L, // 2022-01-01 23:00:00
+ 1641052800000L, // 2022-01-02 00:00:00
+ 1641056400000L // 2022-01-02 01:00:00
+ );
+
+ final List<Long> expectedTimestamps = List.of(
+ 1641049200000L, // 2022-01-01 23:00:00
+ 1641052800000L, // 2022-01-02 00:00:00
+ 1641056400000L // 2022-01-02 01:00:00
+ );
+ assertThat(BatteryLevelData.getDailyTimestamps(timestamps)).isEqualTo(expectedTimestamps);
+ }
+
+ @Test
+ public void getDailyTimestamps_OneDayData_returnExpectedList() {
+ // Timezone GMT+8
+ final List<Long> timestamps = List.of(
+ 1640966400000L, // 2022-01-01 00:00:00
+ 1640970000000L, // 2022-01-01 01:00:00
+ 1640973600000L, // 2022-01-01 02:00:00
+ 1640977200000L, // 2022-01-01 03:00:00
+ 1640980800000L // 2022-01-01 04:00:00
+ );
+
+ final List<Long> expectedTimestamps = List.of(
+ 1640966400000L, // 2022-01-01 00:00:00
+ 1640980800000L // 2022-01-01 04:00:00
+ );
+ assertThat(BatteryLevelData.getDailyTimestamps(timestamps)).isEqualTo(expectedTimestamps);
+ }
+
+ @Test
+ public void getDailyTimestamps_MultipleDaysData_returnExpectedList() {
+ // Timezone GMT+8
+ final List<Long> timestamps = List.of(
+ 1641045600000L, // 2022-01-01 22:00:00
+ 1641060000000L, // 2022-01-02 02:00:00
+ 1641160800000L, // 2022-01-03 06:00:00
+ 1641232800000L // 2022-01-04 02:00:00
+ );
+
+ final List<Long> expectedTimestamps = List.of(
+ 1641045600000L, // 2022-01-01 22:00:00
+ 1641052800000L, // 2022-01-02 00:00:00
+ 1641139200000L, // 2022-01-03 00:00:00
+ 1641225600000L, // 2022-01-04 00:00:00
+ 1641232800000L // 2022-01-04 02:00:00
+ );
+ assertThat(BatteryLevelData.getDailyTimestamps(timestamps)).isEqualTo(expectedTimestamps);
+ }
+
+ @Test
+ public void getDailyTimestamps_FirstDayOneHourData_returnExpectedList() {
+ // Timezone GMT+8
+ final List<Long> timestamps = List.of(
+ 1641049200000L, // 2022-01-01 23:00:00
+ 1641060000000L, // 2022-01-02 02:00:00
+ 1641160800000L, // 2022-01-03 06:00:00
+ 1641254400000L // 2022-01-04 08:00:00
+ );
+
+ final List<Long> expectedTimestamps = List.of(
+ 1641049200000L, // 2022-01-01 23:00:00
+ 1641052800000L, // 2022-01-02 00:00:00
+ 1641139200000L, // 2022-01-03 00:00:00
+ 1641225600000L, // 2022-01-04 00:00:00
+ 1641254400000L // 2022-01-04 08:00:00
+ );
+ assertThat(BatteryLevelData.getDailyTimestamps(timestamps)).isEqualTo(expectedTimestamps);
+ }
+
+ @Test
+ public void getDailyTimestamps_LastDayNoData_returnExpectedList() {
+ // Timezone GMT+8
+ final List<Long> timestamps = List.of(
+ 1640988000000L, // 2022-01-01 06:00:00
+ 1641060000000L, // 2022-01-02 02:00:00
+ 1641160800000L, // 2022-01-03 06:00:00
+ 1641225600000L // 2022-01-04 00:00:00
+ );
+
+ final List<Long> expectedTimestamps = List.of(
+ 1640988000000L, // 2022-01-01 06:00:00
+ 1641052800000L, // 2022-01-02 00:00:00
+ 1641139200000L, // 2022-01-03 00:00:00
+ 1641225600000L // 2022-01-04 00:00:00
+ );
+ assertThat(BatteryLevelData.getDailyTimestamps(timestamps)).isEqualTo(expectedTimestamps);
+ }
+
+ @Test
+ public void getDailyTimestamps_LastDayOneHourData_returnExpectedList() {
+ // Timezone GMT+8
+ final List<Long> timestamps = List.of(
+ 1640988000000L, // 2022-01-01 06:00:00
+ 1641060000000L, // 2022-01-02 02:00:00
+ 1641160800000L, // 2022-01-03 06:00:00
+ 1641229200000L // 2022-01-04 01:00:00
+ );
+
+ final List<Long> expectedTimestamps = List.of(
+ 1640988000000L, // 2022-01-01 06:00:00
+ 1641052800000L, // 2022-01-02 00:00:00
+ 1641139200000L, // 2022-01-03 00:00:00
+ 1641225600000L, // 2022-01-04 00:00:00
+ 1641229200000L // 2022-01-04 01:00:00
+ );
+ assertThat(BatteryLevelData.getDailyTimestamps(timestamps)).isEqualTo(expectedTimestamps);
+ }
+
+ @Test
+ public void combine_normalFlow_returnExpectedResult() {
+ final BatteryLevelData batteryLevelData =
+ new BatteryLevelData(Map.of(1691596800000L, 90, 1691604000000L, 80));
+ final List<BatteryEvent> batteryLevelRecordEvents = List.of(
+ BatteryEvent.newBuilder().setTimestamp(1691586000166L).setBatteryLevel(100)
+ .setType(BatteryEventType.FULL_CHARGED).build(),
+ BatteryEvent.newBuilder().setTimestamp(1691589600000L).setBatteryLevel(98)
+ .setType(BatteryEventType.EVEN_HOUR).build());
+
+ BatteryLevelData result =
+ BatteryLevelData.combine(batteryLevelData, batteryLevelRecordEvents);
+
+ assertThat(result.getDailyBatteryLevels().getTimestamps())
+ .isEqualTo(List.of(1691586000166L, 1691596800000L, 1691604000000L));
+ assertThat(result.getDailyBatteryLevels().getLevels())
+ .isEqualTo(List.of(100, 90, 80));
+ assertThat(result.getHourlyBatteryLevelsPerDay())
+ .hasSize(2);
+ assertThat(result.getHourlyBatteryLevelsPerDay().get(0).getTimestamps())
+ .isEqualTo(List.of(1691586000166L, 1691589600000L, 1691596800000L));
+ assertThat(result.getHourlyBatteryLevelsPerDay().get(0).getLevels())
+ .isEqualTo(List.of(100, 98, 90));
+ assertThat(result.getHourlyBatteryLevelsPerDay().get(1).getTimestamps())
+ .isEqualTo(List.of(1691596800000L, 1691604000000L));
+ assertThat(result.getHourlyBatteryLevelsPerDay().get(1).getLevels())
+ .isEqualTo(List.of(90, 80));
+ }
+
+ @Test
+ public void combine_existingBatteryLevelDataIsNull_returnExpectedResult() {
+ final List<BatteryEvent> batteryLevelRecordEvents = List.of(
+ BatteryEvent.newBuilder().setTimestamp(1691586000166L).setBatteryLevel(100)
+ .setType(BatteryEventType.FULL_CHARGED).build(),
+ BatteryEvent.newBuilder().setTimestamp(1691589600000L).setBatteryLevel(98)
+ .setType(BatteryEventType.EVEN_HOUR).build());
+
+ BatteryLevelData result =
+ BatteryLevelData.combine(null, batteryLevelRecordEvents);
+
+ assertThat(result.getHourlyBatteryLevelsPerDay())
+ .hasSize(1);
+ assertThat(result.getHourlyBatteryLevelsPerDay().get(0).getTimestamps())
+ .isEqualTo(List.of(1691586000166L, 1691589600000L));
+ assertThat(result.getHourlyBatteryLevelsPerDay().get(0).getLevels())
+ .isEqualTo(List.of(100, 98));
+ }
+
+ @Test
+ public void getIndexByTimestamps_returnExpectedResult() {
+ final BatteryLevelData batteryLevelData =
+ new BatteryLevelData(Map.of(
+ 1694354400000L, 1, // 2023-09-10 22:00:00
+ 1694361600000L, 2, // 2023-09-11 00:00:00
+ 1694368800000L, 3)); // 2023-09-11 02:00:00
+ final PowerAnomalyEvent event = BatteryTestUtils.createAppAnomalyEvent();
+
+ assertThat(batteryLevelData.getIndexByTimestamps(0L, 0L))
+ .isEqualTo(Pair.create(BatteryChartViewModel.SELECTED_INDEX_INVALID,
+ BatteryChartViewModel.SELECTED_INDEX_INVALID));
+ assertThat(batteryLevelData.getIndexByTimestamps(1694361600000L + 1L, 1694368800000L + 1L))
+ .isEqualTo(Pair.create(BatteryChartViewModel.SELECTED_INDEX_INVALID,
+ BatteryChartViewModel.SELECTED_INDEX_INVALID));
+ assertThat(batteryLevelData.getIndexByTimestamps(1694361600000L, 1694368800000L))
+ .isEqualTo(Pair.create(1, 0));
+ assertThat(batteryLevelData.getIndexByTimestamps(1694361600000L + 1L, 1694368800000L - 1L))
+ .isEqualTo(Pair.create(1, 0));
+ assertThat(batteryLevelData.getIndexByTimestamps(
+ event.getWarningItemInfo().getStartTimestamp(),
+ event.getWarningItemInfo().getEndTimestamp()))
+ .isEqualTo(Pair.create(1, 0));
+ }
+}
diff --git a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryTipsCardPreferenceTest.java b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryTipsCardPreferenceTest.java
new file mode 100644
index 0000000..63cb1b3
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryTipsCardPreferenceTest.java
@@ -0,0 +1,191 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.fuelgauge.batteryusage;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.settings.SettingsEnums;
+import android.content.Context;
+import android.content.Intent;
+import android.view.View;
+
+import com.android.settings.DisplaySettings;
+import com.android.settings.R;
+import com.android.settings.SettingsActivity;
+import com.android.settings.testutils.BatteryTestUtils;
+import com.android.settings.testutils.FakeFeatureFactory;
+import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+
+import java.util.Map;
+import java.util.Optional;
+
+@RunWith(RobolectricTestRunner.class)
+public final class BatteryTipsCardPreferenceTest {
+
+ private Context mContext;
+ private FakeFeatureFactory mFeatureFactory;
+ private BatteryTipsCardPreference mBatteryTipsCardPreference;
+ private PowerUsageAdvanced mPowerUsageAdvanced;
+ private BatteryTipsController mBatteryTipsController;
+ private BatteryChartPreferenceController mBatteryChartPreferenceController;
+
+ @Mock
+ private View mFakeView;
+ @Mock
+ private BatteryUsageBreakdownController mBatteryUsageBreakdownController;
+ @Mock
+ private BatteryDiffEntry mFakeEntry;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ mContext = spy(RuntimeEnvironment.application);
+ mFeatureFactory = FakeFeatureFactory.setupForTest();
+ mBatteryTipsCardPreference = new BatteryTipsCardPreference(mContext, /*attrs=*/ null);
+ mBatteryTipsController = new BatteryTipsController(mContext);
+ mBatteryChartPreferenceController =
+ spy(new BatteryChartPreferenceController(mContext, null, null));
+ mBatteryChartPreferenceController.mPrefContext = mContext;
+ mBatteryTipsController.mCardPreference = mBatteryTipsCardPreference;
+
+ mPowerUsageAdvanced = spy(new PowerUsageAdvanced());
+ doReturn(mContext).when(mPowerUsageAdvanced).getContext();
+ mPowerUsageAdvanced.mBatteryTipsController = mBatteryTipsController;
+ mPowerUsageAdvanced.mBatteryChartPreferenceController = mBatteryChartPreferenceController;
+ mPowerUsageAdvanced.mBatteryUsageBreakdownController = mBatteryUsageBreakdownController;
+ mPowerUsageAdvanced.mBatteryLevelData = Optional.of(new BatteryLevelData(Map.of(
+ 1694354400000L, 1, // 2023-09-10 22:00:00
+ 1694361600000L, 2, // 2023-09-11 00:00:00
+ 1694368800000L, 3))); // 2023-09-11 02:00:00
+ doReturn("TestEntriesKey").when(mFakeEntry).getKey();
+ }
+
+ @Test
+ public void constructor_returnExpectedResult() {
+ assertThat(mBatteryTipsCardPreference.getLayoutResource()).isEqualTo(
+ R.layout.battery_tips_card);
+ }
+
+ @Test
+ public void onClick_mainBtnOfSettingsAnomaly_getAdaptiveBrightnessLauncher() {
+ final ArgumentCaptor<Intent> captor = ArgumentCaptor.forClass(Intent.class);
+ PowerAnomalyEvent adaptiveBrightnessAnomaly =
+ BatteryTestUtils.createAdaptiveBrightnessAnomalyEvent();
+ when(mFeatureFactory.powerUsageFeatureProvider.isBatteryTipsEnabled()).thenReturn(true);
+ when(mFakeView.getId()).thenReturn(R.id.main_button);
+ doNothing().when(mContext).startActivity(captor.capture());
+
+ mPowerUsageAdvanced.onDisplayAnomalyEventUpdated(
+ adaptiveBrightnessAnomaly, adaptiveBrightnessAnomaly);
+ mBatteryTipsCardPreference.onClick(mFakeView);
+
+ assertThat(mBatteryTipsCardPreference.isVisible()).isFalse();
+ verify(mContext).startActivity(any(Intent.class));
+ final Intent intent = captor.getValue();
+ assertThat(intent.getStringExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT))
+ .isEqualTo(DisplaySettings.class.getName());
+ assertThat(intent.getIntExtra(MetricsFeatureProvider.EXTRA_SOURCE_METRICS_CATEGORY, -1))
+ .isEqualTo(SettingsEnums.DISPLAY);
+ verify(mFeatureFactory.metricsFeatureProvider).action(
+ mContext, SettingsEnums.ACTION_BATTERY_TIPS_CARD_SHOW, "BrightnessAnomaly");
+ verify(mFeatureFactory.metricsFeatureProvider).action(
+ mContext, SettingsEnums.ACTION_BATTERY_TIPS_CARD_ACCEPT, "BrightnessAnomaly");
+ }
+
+ @Test
+ public void onClick_dismissBtnOfSettingsAnomaly_cardDismissAndLogged() {
+ final PowerAnomalyEvent screenTimeoutAnomaly =
+ BatteryTestUtils.createScreenTimeoutAnomalyEvent();
+ DatabaseUtils.removeDismissedPowerAnomalyKeys(mContext);
+ when(mFeatureFactory.powerUsageFeatureProvider.isBatteryTipsEnabled()).thenReturn(true);
+ when(mFakeView.getId()).thenReturn(R.id.dismiss_button);
+
+ mPowerUsageAdvanced.onDisplayAnomalyEventUpdated(
+ screenTimeoutAnomaly, screenTimeoutAnomaly);
+ mBatteryTipsCardPreference.onClick(mFakeView);
+
+ assertThat(mBatteryTipsCardPreference.isVisible()).isFalse();
+ assertThat(DatabaseUtils.getDismissedPowerAnomalyKeys(mContext)).hasSize(1);
+ assertThat(DatabaseUtils.getDismissedPowerAnomalyKeys(mContext))
+ .contains(PowerAnomalyKey.KEY_SCREEN_TIMEOUT.name());
+ verify(mFeatureFactory.metricsFeatureProvider).action(
+ mContext, SettingsEnums.ACTION_BATTERY_TIPS_CARD_SHOW, "ScreenTimeoutAnomaly");
+ verify(mFeatureFactory.metricsFeatureProvider).action(
+ mContext, SettingsEnums.ACTION_BATTERY_TIPS_CARD_DISMISS, "ScreenTimeoutAnomaly");
+ }
+
+ @Test
+ public void onClick_mainBtnOfAppsAnomaly_selectHighlightSlot() {
+ final PowerAnomalyEvent appsAnomaly = BatteryTestUtils.createAppAnomalyEvent();
+ when(mFeatureFactory.powerUsageFeatureProvider.isBatteryTipsEnabled()).thenReturn(true);
+ when(mFakeView.getId()).thenReturn(R.id.main_button);
+ doNothing().when(mBatteryChartPreferenceController).selectHighlightSlotIndex();
+ when(mPowerUsageAdvanced.findRelatedBatteryDiffEntry(any())).thenReturn(mFakeEntry);
+
+ mPowerUsageAdvanced.onDisplayAnomalyEventUpdated(appsAnomaly, appsAnomaly);
+ mBatteryTipsCardPreference.onClick(mFakeView);
+
+ assertThat(mBatteryTipsCardPreference.isVisible()).isFalse();
+ verify(mContext, never()).startActivity(any(Intent.class));
+ verify(mBatteryChartPreferenceController).onHighlightSlotIndexUpdate(
+ eq(1), eq(0));
+ verify(mBatteryChartPreferenceController).selectHighlightSlotIndex();
+ verify(mFeatureFactory.metricsFeatureProvider).action(
+ mContext, SettingsEnums.ACTION_BATTERY_TIPS_CARD_SHOW, "AppAnomaly");
+ verify(mFeatureFactory.metricsFeatureProvider).action(
+ mContext, SettingsEnums.ACTION_BATTERY_TIPS_CARD_ACCEPT, "AppAnomaly");
+ }
+
+ @Test
+ public void onClick_dismissBtnOfAppsAnomaly_keepHighlightSlotIndex() {
+ final PowerAnomalyEvent appsAnomaly = BatteryTestUtils.createAppAnomalyEvent();
+ when(mFeatureFactory.powerUsageFeatureProvider.isBatteryTipsEnabled()).thenReturn(true);
+ when(mFakeView.getId()).thenReturn(R.id.dismiss_button);
+ when(mPowerUsageAdvanced.findRelatedBatteryDiffEntry(any())).thenReturn(mFakeEntry);
+
+ mPowerUsageAdvanced.onDisplayAnomalyEventUpdated(appsAnomaly, appsAnomaly);
+ mBatteryTipsCardPreference.onClick(mFakeView);
+
+ assertThat(mBatteryTipsCardPreference.isVisible()).isFalse();
+ verify(mContext, never()).startActivity(any(Intent.class));
+ verify(mBatteryChartPreferenceController).onHighlightSlotIndexUpdate(
+ eq(1), eq(0));
+ verify(mBatteryChartPreferenceController, never()).selectHighlightSlotIndex();
+ verify(mFeatureFactory.metricsFeatureProvider).action(
+ mContext, SettingsEnums.ACTION_BATTERY_TIPS_CARD_SHOW, "AppAnomaly");
+ verify(mFeatureFactory.metricsFeatureProvider).action(
+ mContext, SettingsEnums.ACTION_BATTERY_TIPS_CARD_DISMISS, "AppAnomaly");
+ }
+}
diff --git a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryTipsControllerTest.java b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryTipsControllerTest.java
new file mode 100644
index 0000000..b8afe98
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryTipsControllerTest.java
@@ -0,0 +1,165 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.fuelgauge.batteryusage;
+
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.settings.SettingsEnums;
+import android.content.Context;
+import android.content.res.Resources;
+import android.os.LocaleList;
+
+import com.android.settings.R;
+import com.android.settings.testutils.BatteryTestUtils;
+import com.android.settings.testutils.FakeFeatureFactory;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+
+import java.util.Locale;
+import java.util.TimeZone;
+
+@RunWith(RobolectricTestRunner.class)
+public final class BatteryTipsControllerTest {
+
+ private Context mContext;
+ private FakeFeatureFactory mFeatureFactory;
+ private BatteryTipsController mBatteryTipsController;
+
+ @Mock
+ private BatteryTipsCardPreference mBatteryTipsCardPreference;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ Locale.setDefault(new Locale("en_US"));
+ org.robolectric.shadows.ShadowSettings.set24HourTimeFormat(false);
+ TimeZone.setDefault(TimeZone.getTimeZone("UTC"));
+ mContext = spy(RuntimeEnvironment.application);
+ final Resources resources = spy(mContext.getResources());
+ resources.getConfiguration().setLocales(new LocaleList(new Locale("en_US")));
+ doReturn(resources).when(mContext).getResources();
+ mFeatureFactory = FakeFeatureFactory.setupForTest();
+ mBatteryTipsController = new BatteryTipsController(mContext);
+ mBatteryTipsController.mCardPreference = mBatteryTipsCardPreference;
+ }
+
+ @Test
+ public void handleBatteryTipsCardUpdated_null_hidePreference() {
+ mBatteryTipsController.handleBatteryTipsCardUpdated(/* powerAnomalyEvents= */ null, false);
+
+ verify(mBatteryTipsCardPreference).setVisible(false);
+ }
+
+ @Test
+ public void handleBatteryTipsCardUpdated_adaptiveBrightnessAnomaly_showAnomaly() {
+ PowerAnomalyEvent event = BatteryTestUtils.createAdaptiveBrightnessAnomalyEvent();
+ when(mFeatureFactory.powerUsageFeatureProvider.isBatteryTipsEnabled()).thenReturn(true);
+
+ mBatteryTipsController.handleBatteryTipsCardUpdated(
+ new AnomalyEventWrapper(mContext, event), false);
+
+ // Check pre-defined string
+ verify(mBatteryTipsCardPreference).setTitle(
+ "Turn on adaptive brightness to extend battery life");
+ verify(mBatteryTipsCardPreference).setIconResourceId(R.drawable.ic_battery_tips_lightbulb);
+ verify(mBatteryTipsCardPreference).setMainButtonStrokeColorResourceId(
+ R.color.color_accent_selector);
+ verify(mBatteryTipsCardPreference).setMainButtonLabel("View Settings");
+ verify(mBatteryTipsCardPreference).setDismissButtonLabel("Got it");
+ // Check proto info
+ verify(mBatteryTipsCardPreference).setVisible(true);
+ verify(mFeatureFactory.metricsFeatureProvider).action(
+ mContext, SettingsEnums.ACTION_BATTERY_TIPS_CARD_SHOW, "BrightnessAnomaly");
+ }
+
+ @Test
+ public void handleBatteryTipsCardUpdated_screenTimeoutAnomaly_showAnomaly() {
+ PowerAnomalyEvent event = BatteryTestUtils.createScreenTimeoutAnomalyEvent();
+ when(mFeatureFactory.powerUsageFeatureProvider.isBatteryTipsEnabled()).thenReturn(true);
+
+ mBatteryTipsController.handleBatteryTipsCardUpdated(
+ new AnomalyEventWrapper(mContext, event), false);
+
+ verify(mBatteryTipsCardPreference).setTitle("Reduce screen timeout to extend battery life");
+ verify(mBatteryTipsCardPreference).setIconResourceId(R.drawable.ic_battery_tips_lightbulb);
+ verify(mBatteryTipsCardPreference).setMainButtonStrokeColorResourceId(
+ R.color.color_accent_selector);
+ verify(mBatteryTipsCardPreference).setMainButtonLabel("View Settings");
+ verify(mBatteryTipsCardPreference).setDismissButtonLabel("Got it");
+ verify(mBatteryTipsCardPreference).setVisible(true);
+ verify(mFeatureFactory.metricsFeatureProvider).action(
+ mContext, SettingsEnums.ACTION_BATTERY_TIPS_CARD_SHOW, "ScreenTimeoutAnomaly");
+ }
+
+ @Test
+ public void handleBatteryTipsCardUpdated_screenTimeoutAnomalyHasTitle_showAnomaly() {
+ PowerAnomalyEvent event = BatteryTestUtils.createScreenTimeoutAnomalyEvent();
+ String testTitle = "TestTitle";
+ event = event.toBuilder()
+ .setWarningBannerInfo(
+ event.getWarningBannerInfo().toBuilder()
+ .setTitleString(testTitle)
+ .build())
+ .build();
+ when(mFeatureFactory.powerUsageFeatureProvider.isBatteryTipsEnabled()).thenReturn(true);
+
+ mBatteryTipsController.handleBatteryTipsCardUpdated(
+ new AnomalyEventWrapper(mContext, event), false);
+
+ verify(mBatteryTipsCardPreference).setTitle(testTitle);
+ verify(mBatteryTipsCardPreference).setIconResourceId(R.drawable.ic_battery_tips_lightbulb);
+ verify(mBatteryTipsCardPreference).setMainButtonStrokeColorResourceId(
+ R.color.color_accent_selector);
+ verify(mBatteryTipsCardPreference).setMainButtonLabel("View Settings");
+ verify(mBatteryTipsCardPreference).setDismissButtonLabel("Got it");
+ verify(mBatteryTipsCardPreference).setVisible(true);
+ verify(mFeatureFactory.metricsFeatureProvider).action(
+ mContext, SettingsEnums.ACTION_BATTERY_TIPS_CARD_SHOW, "ScreenTimeoutAnomaly");
+ }
+
+ @Test
+ public void handleBatteryTipsCardUpdated_appAnomaly_showAnomaly() {
+ PowerAnomalyEvent event = BatteryTestUtils.createAppAnomalyEvent();
+ when(mFeatureFactory.powerUsageFeatureProvider.isBatteryTipsEnabled()).thenReturn(true);
+
+ AnomalyEventWrapper eventWrapper = new AnomalyEventWrapper(mContext, event);
+ eventWrapper.setRelatedBatteryDiffEntry(
+ new BatteryDiffEntry(mContext, "", "Chrome", 0));
+ mBatteryTipsController.handleBatteryTipsCardUpdated(eventWrapper, false);
+
+ verify(mBatteryTipsCardPreference).setTitle(
+ "Chrome used more battery than usual");
+ verify(mBatteryTipsCardPreference).setIconResourceId(
+ R.drawable.ic_battery_tips_warning_icon);
+ verify(mBatteryTipsCardPreference).setMainButtonStrokeColorResourceId(
+ R.color.color_battery_anomaly_yellow_selector);
+ verify(mBatteryTipsCardPreference).setMainButtonLabel("Check");
+ verify(mBatteryTipsCardPreference).setDismissButtonLabel("Got it");
+ verify(mBatteryTipsCardPreference).setVisible(true);
+ verify(mFeatureFactory.metricsFeatureProvider).action(
+ mContext, SettingsEnums.ACTION_BATTERY_TIPS_CARD_SHOW, "AppAnomaly");
+ }
+}
diff --git a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryUsageBreakdownControllerTest.java b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryUsageBreakdownControllerTest.java
index 3a9ce2b..a721ad4 100644
--- a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryUsageBreakdownControllerTest.java
+++ b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryUsageBreakdownControllerTest.java
@@ -69,7 +69,7 @@
@Mock
private BatteryHistEntry mBatteryHistEntry;
@Mock
- private PowerGaugePreference mPowerGaugePreference;
+ private AnomalyAppItemPreference mAnomalyAppItemPreference;
private Context mContext;
private FakeFeatureFactory mFeatureFactory;
@@ -96,6 +96,14 @@
mBatteryUsageBreakdownController.mAppListPreferenceGroup = mAppListPreferenceGroup;
mBatteryDiffEntry = new BatteryDiffEntry(
mContext,
+ /*uid=*/ 0L,
+ /*userId=*/ 0L,
+ /*key=*/ "key",
+ /*isHidden=*/ false,
+ /*componentId=*/ -1,
+ /*legacyPackageName=*/ null,
+ /*legacyLabel=*/ null,
+ /*consumerType=*/ ConvertUtils.CONSUMER_TYPE_UID_BATTERY,
/*foregroundUsageTimeInMs=*/ 1,
/*backgroundUsageTimeInMs=*/ 2,
/*screenOnTimeInMs=*/ 0,
@@ -103,24 +111,26 @@
/*foregroundUsageConsumePower=*/ 0,
/*foregroundServiceUsageConsumePower=*/ 1,
/*backgroundUsageConsumePower=*/ 2,
- /*cachedUsageConsumePower=*/ 0,
- mBatteryHistEntry);
+ /*cachedUsageConsumePower=*/ 0);
mBatteryDiffEntry = spy(mBatteryDiffEntry);
mBatteryUsageBreakdownController.mBatteryDiffData =
- new BatteryDiffData(mContext, /* screenOnTime= */ 0L,
- Arrays.asList(mBatteryDiffEntry), Arrays.asList(), Set.of(), Set.of(),
- /* isAccumulated= */ false);
+ new BatteryDiffData(mContext, /* startTimestamp= */ 0L, /* endTimestamp= */ 0L,
+ /* startBatteryLevel= */ 0, /* endBatteryLevel= */ 0,
+ /* screenOnTime= */ 0L, Arrays.asList(mBatteryDiffEntry), Arrays.asList(),
+ Set.of(), Set.of(), /* isAccumulated= */ false);
+ BatteryDiffEntry.clearCache();
// Adds fake testing data.
BatteryDiffEntry.sResourceCache.put(
"fakeBatteryDiffEntryKey",
new BatteryEntry.NameAndIcon("fakeName", /*icon=*/ null, /*iconId=*/ 1));
+ doReturn(mAnomalyAppItemPreference).when(mAppListPreferenceGroup).findPreference(PREF_KEY);
}
@Test
public void onDestroy_clearPreferenceCacheAndPreferenceGroupRemoveAll() {
// Ensures the testing environment is correct.
mBatteryUsageBreakdownController.mPreferenceCache.put(
- PREF_KEY, mPowerGaugePreference);
+ PREF_KEY, mAnomalyAppItemPreference);
assertThat(mBatteryUsageBreakdownController.mPreferenceCache).hasSize(1);
mBatteryUsageBreakdownController.onDestroy();
@@ -140,7 +150,7 @@
doReturn(1).when(mAppListPreferenceGroup).getPreferenceCount();
doReturn(mDrawable).when(mBatteryDiffEntry).getAppIcon();
doReturn(appLabel).when(mBatteryDiffEntry).getAppLabel();
- doReturn(PREF_KEY).when(mBatteryHistEntry).getKey();
+ doReturn(PREF_KEY).when(mBatteryDiffEntry).getKey();
doReturn(null).when(mAppListPreferenceGroup).findPreference(PREF_KEY);
doReturn(false).when(mBatteryDiffEntry).validForRestriction();
@@ -168,8 +178,7 @@
doReturn(1).when(mAppListPreferenceGroup).getPreferenceCount();
doReturn(mDrawable).when(mBatteryDiffEntry).getAppIcon();
doReturn(appLabel).when(mBatteryDiffEntry).getAppLabel();
- doReturn(PREF_KEY).when(mBatteryHistEntry).getKey();
- doReturn(mPowerGaugePreference).when(mAppListPreferenceGroup).findPreference(PREF_KEY);
+ doReturn(PREF_KEY).when(mBatteryDiffEntry).getKey();
mBatteryUsageBreakdownController.addAllPreferences();
@@ -179,27 +188,25 @@
@Test
public void removeAndCacheAllUnusedPreferences_removePref_buildCacheAndRemoveAllPreference() {
doReturn(1).when(mAppListPreferenceGroup).getPreferenceCount();
- doReturn(mPowerGaugePreference).when(mAppListPreferenceGroup).getPreference(0);
+ doReturn(mAnomalyAppItemPreference).when(mAppListPreferenceGroup).getPreference(0);
doReturn(PREF_KEY2).when(mBatteryHistEntry).getKey();
- doReturn(PREF_KEY).when(mPowerGaugePreference).getKey();
- doReturn(mPowerGaugePreference).when(mAppListPreferenceGroup).findPreference(PREF_KEY);
+ doReturn(PREF_KEY).when(mAnomalyAppItemPreference).getKey();
// Ensures the testing data is correct.
assertThat(mBatteryUsageBreakdownController.mPreferenceCache).isEmpty();
mBatteryUsageBreakdownController.removeAndCacheAllUnusedPreferences();
assertThat(mBatteryUsageBreakdownController.mPreferenceCache.get(PREF_KEY))
- .isEqualTo(mPowerGaugePreference);
- verify(mAppListPreferenceGroup).removePreference(mPowerGaugePreference);
+ .isEqualTo(mAnomalyAppItemPreference);
+ verify(mAppListPreferenceGroup).removePreference(mAnomalyAppItemPreference);
}
@Test
public void removeAndCacheAllUnusedPreferences_keepPref_KeepAllPreference() {
doReturn(1).when(mAppListPreferenceGroup).getPreferenceCount();
- doReturn(mPowerGaugePreference).when(mAppListPreferenceGroup).getPreference(0);
- doReturn(PREF_KEY).when(mBatteryHistEntry).getKey();
- doReturn(PREF_KEY).when(mPowerGaugePreference).getKey();
- doReturn(mPowerGaugePreference).when(mAppListPreferenceGroup).findPreference(PREF_KEY);
+ doReturn(mAnomalyAppItemPreference).when(mAppListPreferenceGroup).getPreference(0);
+ doReturn(PREF_KEY).when(mBatteryDiffEntry).getKey();
+ doReturn(PREF_KEY).when(mAnomalyAppItemPreference).getKey();
// Ensures the testing data is correct.
assertThat(mBatteryUsageBreakdownController.mPreferenceCache).isEmpty();
@@ -222,11 +229,11 @@
@Test
public void handlePreferenceTreeClick_forAppEntry_returnTrue() {
- doReturn(false).when(mBatteryHistEntry).isAppEntry();
- doReturn(mBatteryDiffEntry).when(mPowerGaugePreference).getBatteryDiffEntry();
+ mBatteryDiffEntry.mConsumerType = ConvertUtils.CONSUMER_TYPE_SYSTEM_BATTERY;
+ doReturn(mBatteryDiffEntry).when(mAnomalyAppItemPreference).getBatteryDiffEntry();
assertThat(mBatteryUsageBreakdownController.handlePreferenceTreeClick(
- mPowerGaugePreference)).isTrue();
+ mAnomalyAppItemPreference)).isTrue();
verify(mMetricsFeatureProvider)
.action(
SettingsEnums.OPEN_BATTERY_USAGE,
@@ -238,11 +245,11 @@
@Test
public void handlePreferenceTreeClick_forSystemEntry_returnTrue() {
- doReturn(true).when(mBatteryHistEntry).isAppEntry();
- doReturn(mBatteryDiffEntry).when(mPowerGaugePreference).getBatteryDiffEntry();
+ mBatteryDiffEntry.mConsumerType = ConvertUtils.CONSUMER_TYPE_UID_BATTERY;
+ doReturn(mBatteryDiffEntry).when(mAnomalyAppItemPreference).getBatteryDiffEntry();
assertThat(mBatteryUsageBreakdownController.handlePreferenceTreeClick(
- mPowerGaugePreference)).isTrue();
+ mAnomalyAppItemPreference)).isTrue();
verify(mMetricsFeatureProvider)
.action(
SettingsEnums.OPEN_BATTERY_USAGE,
@@ -394,10 +401,23 @@
contentValues.put(BatteryHistEntry.KEY_USER_ID, Integer.valueOf(1001));
final BatteryHistEntry batteryHistEntry = new BatteryHistEntry(contentValues);
return new BatteryDiffEntry(
- mContext, foregroundUsageTimeInMs, backgroundUsageTimeInMs, screenOnTimeInMs,
- /*consumePower=*/ 0, /*foregroundUsageConsumePower=*/ 0,
- /*foregroundServiceUsageConsumePower=*/ 0, /*backgroundUsageConsumePower=*/ 0,
- /*cachedUsageConsumePower=*/ 0, batteryHistEntry);
+ mContext,
+ batteryHistEntry.mUid,
+ batteryHistEntry.mUserId,
+ batteryHistEntry.getKey(),
+ batteryHistEntry.mIsHidden,
+ batteryHistEntry.mDrainType,
+ batteryHistEntry.mPackageName,
+ batteryHistEntry.mAppLabel,
+ batteryHistEntry.mConsumerType,
+ foregroundUsageTimeInMs,
+ backgroundUsageTimeInMs,
+ screenOnTimeInMs,
+ /*consumePower=*/ 0,
+ /*foregroundUsageConsumePower=*/ 0,
+ /*foregroundServiceUsageConsumePower=*/ 0,
+ /*backgroundUsageConsumePower=*/ 0,
+ /*cachedUsageConsumePower=*/ 0);
}
private BatteryUsageBreakdownController createController() {
diff --git a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryUsageContentProviderTest.java b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryUsageContentProviderTest.java
index 05a6f2b..999a921 100644
--- a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryUsageContentProviderTest.java
+++ b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryUsageContentProviderTest.java
@@ -20,11 +20,9 @@
import static org.junit.Assert.assertThrows;
-import android.app.Application;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
-import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
@@ -34,6 +32,7 @@
import com.android.settings.fuelgauge.batteryusage.db.BatteryEventEntity;
import com.android.settings.fuelgauge.batteryusage.db.BatteryState;
import com.android.settings.fuelgauge.batteryusage.db.BatteryStateDatabase;
+import com.android.settings.fuelgauge.batteryusage.db.BatteryUsageSlotEntity;
import com.android.settings.testutils.BatteryTestUtils;
import com.android.settings.testutils.FakeClock;
@@ -41,12 +40,10 @@
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
-import org.robolectric.Shadows;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
-import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
/** Tests for {@link BatteryUsageContentProvider}. */
@@ -127,10 +124,28 @@
}
@Test
+ public void query_getLastFullChargeTimestamp_returnsExpectedResult() throws Exception {
+ mProvider.onCreate();
+ ContentValues values = new ContentValues();
+ values.put(BatteryEventEntity.KEY_TIMESTAMP, 10001L);
+ values.put(BatteryEventEntity.KEY_BATTERY_EVENT_TYPE,
+ BatteryEventType.FULL_CHARGED.getNumber());
+ values.put(BatteryEventEntity.KEY_BATTERY_LEVEL, 100);
+ mProvider.insert(DatabaseUtils.BATTERY_EVENT_URI, values);
+
+ final Cursor cursor = getCursorOfLastFullChargeTimestamp();
+
+ assertThat(cursor.getCount()).isEqualTo(1);
+ cursor.moveToFirst();
+ final long lastFullChargeTimestamp = cursor.getLong(0);
+ assertThat(lastFullChargeTimestamp).isEqualTo(10001L);
+ }
+
+ @Test
public void query_batteryState_returnsExpectedResult() throws Exception {
mProvider.onCreate();
final Duration currentTime = Duration.ofHours(52);
- final long expiredTimeCutoff = currentTime.toMillis() - 3;
+ final long expiredTimeCutoff = currentTime.toMillis() - 8;
final Cursor cursor = insertBatteryState(currentTime, Long.toString(expiredTimeCutoff));
@@ -150,19 +165,13 @@
final String actualPackageName3 = cursor.getString(packageNameIndex);
assertThat(actualPackageName3).isEqualTo(PACKAGE_NAME3);
cursor.close();
- // Verifies the broadcast intent.
- TimeUnit.SECONDS.sleep(1);
- final List<Intent> intents = Shadows.shadowOf((Application) mContext).getBroadcastIntents();
- assertThat(intents).hasSize(1);
- assertThat(intents.get(0).getAction()).isEqualTo(
- BootBroadcastReceiver.ACTION_PERIODIC_JOB_RECHECK);
}
@Test
public void query_batteryStateTimestamp_returnsExpectedResult() throws Exception {
mProvider.onCreate();
final Duration currentTime = Duration.ofHours(52);
- final long expiredTimeCutoff = currentTime.toMillis() - 1;
+ final long expiredTimeCutoff = currentTime.toMillis() - 2;
final Cursor cursor = insertBatteryState(currentTime, Long.toString(expiredTimeCutoff));
@@ -178,12 +187,25 @@
final String actualPackageName2 = cursor.getString(packageNameIndex);
assertThat(actualPackageName2).isEqualTo(PACKAGE_NAME3);
cursor.close();
- // Verifies the broadcast intent.
- TimeUnit.SECONDS.sleep(1);
- final List<Intent> intents = Shadows.shadowOf((Application) mContext).getBroadcastIntents();
- assertThat(intents).hasSize(1);
- assertThat(intents.get(0).getAction()).isEqualTo(
- BootBroadcastReceiver.ACTION_PERIODIC_JOB_RECHECK);
+ }
+
+ @Test
+ public void query_getBatteryStateLatestTimestamp_returnsExpectedResult() throws Exception {
+ mProvider.onCreate();
+ final Duration currentTime = Duration.ofHours(52);
+ insertBatteryState(currentTime, Long.toString(currentTime.toMillis()));
+
+ final Cursor cursor1 = getCursorOfBatteryStateLatestTimestamp(currentTime.toMillis() - 5);
+ assertThat(cursor1.getCount()).isEqualTo(1);
+ cursor1.moveToFirst();
+ final long latestTimestamp1 = cursor1.getLong(0);
+ assertThat(latestTimestamp1).isEqualTo(currentTime.toMillis() - 6);
+
+ final Cursor cursor2 = getCursorOfBatteryStateLatestTimestamp(currentTime.toMillis() - 2);
+ assertThat(cursor2.getCount()).isEqualTo(1);
+ cursor2.moveToFirst();
+ final long latestTimestamp2 = cursor2.getLong(0);
+ assertThat(latestTimestamp2).isEqualTo(currentTime.toMillis() - 2);
}
@Test
@@ -355,7 +377,7 @@
}
@Test
- public void insert_batteryEvent_returnsExpectedResult() {
+ public void insertAndQuery_batteryEvent_returnsExpectedResult() {
mProvider.onCreate();
ContentValues values = new ContentValues();
values.put(BatteryEventEntity.KEY_TIMESTAMP, 10001L);
@@ -366,7 +388,7 @@
final Uri uri = mProvider.insert(DatabaseUtils.BATTERY_EVENT_URI, values);
assertThat(uri).isEqualTo(DatabaseUtils.BATTERY_EVENT_URI);
- // Verifies the AppUsageEventEntity content.
+ // Verifies the BatteryEventEntity content.
final List<BatteryEventEntity> entities =
BatteryStateDatabase.getInstance(mContext).batteryEventDao().getAll();
assertThat(entities).hasSize(1);
@@ -374,6 +396,50 @@
assertThat(entities.get(0).batteryEventType).isEqualTo(
BatteryEventType.POWER_CONNECTED.getNumber());
assertThat(entities.get(0).batteryLevel).isEqualTo(66);
+
+ final Cursor cursor1 = getCursorOfBatteryEvents(
+ 0L, List.of(BatteryEventType.POWER_CONNECTED.getNumber()));
+ assertThat(cursor1.getCount()).isEqualTo(1);
+ cursor1.moveToFirst();
+ assertThat(cursor1.getLong(cursor1.getColumnIndex(BatteryEventEntity.KEY_TIMESTAMP)))
+ .isEqualTo(10001L);
+ assertThat(
+ cursor1.getInt(cursor1.getColumnIndex(BatteryEventEntity.KEY_BATTERY_EVENT_TYPE)))
+ .isEqualTo(BatteryEventType.POWER_CONNECTED.getNumber());
+ assertThat(cursor1.getInt(cursor1.getColumnIndex(BatteryEventEntity.KEY_BATTERY_LEVEL)))
+ .isEqualTo(66);
+
+ final Cursor cursor2 = getCursorOfBatteryEvents(
+ 0L, List.of(BatteryEventType.POWER_DISCONNECTED.getNumber()));
+ assertThat(cursor2.getCount()).isEqualTo(0);
+ }
+
+ @Test
+ public void insertAndQuery_batteryUsageSlot_returnsExpectedResult() {
+ mProvider.onCreate();
+ ContentValues values = new ContentValues();
+ values.put(BatteryUsageSlotEntity.KEY_TIMESTAMP, 10001L);
+ values.put(BatteryUsageSlotEntity.KEY_BATTERY_USAGE_SLOT, "TEST_STRING");
+
+ final Uri uri = mProvider.insert(DatabaseUtils.BATTERY_USAGE_SLOT_URI, values);
+ // Verifies the BatteryUsageSlotEntity content.
+ assertThat(uri).isEqualTo(DatabaseUtils.BATTERY_USAGE_SLOT_URI);
+ final List<BatteryUsageSlotEntity> entities =
+ BatteryStateDatabase.getInstance(mContext).batteryUsageSlotDao().getAll();
+ assertThat(entities).hasSize(1);
+ assertThat(entities.get(0).timestamp).isEqualTo(10001L);
+ assertThat(entities.get(0).batteryUsageSlot).isEqualTo("TEST_STRING");
+
+ final Cursor cursor1 = getCursorOfBatteryUsageSlots(10001L);
+ assertThat(cursor1.getCount()).isEqualTo(1);
+ cursor1.moveToFirst();
+ assertThat(cursor1.getLong(cursor1.getColumnIndex(BatteryUsageSlotEntity.KEY_TIMESTAMP)))
+ .isEqualTo(10001L);
+ assertThat(cursor1.getString(cursor1.getColumnIndex(
+ BatteryUsageSlotEntity.KEY_BATTERY_USAGE_SLOT))).isEqualTo("TEST_STRING");
+
+ final Cursor cursor2 = getCursorOfBatteryUsageSlots(10002L);
+ assertThat(cursor2.getCount()).isEqualTo(0);
}
@Test
@@ -404,10 +470,10 @@
final long currentTimestamp = currentTime.toMillis();
// Inserts some valid testing data.
BatteryTestUtils.insertDataToBatteryStateTable(
- mContext, currentTimestamp - 2, PACKAGE_NAME1,
+ mContext, currentTimestamp - 6, PACKAGE_NAME1,
/*isFullChargeStart=*/ true);
BatteryTestUtils.insertDataToBatteryStateTable(
- mContext, currentTimestamp - 1, PACKAGE_NAME2);
+ mContext, currentTimestamp - 2, PACKAGE_NAME2);
BatteryTestUtils.insertDataToBatteryStateTable(
mContext, currentTimestamp, PACKAGE_NAME3);
@@ -420,17 +486,35 @@
DatabaseUtils.QUERY_KEY_TIMESTAMP, queryTimestamp)
.build();
- final Cursor cursor =
- mProvider.query(
- batteryStateQueryContentUri,
- /*strings=*/ null,
- /*s=*/ null,
- /*strings1=*/ null,
- /*s1=*/ null);
+ final Cursor cursor = query(batteryStateQueryContentUri);
return cursor;
}
+ private Cursor getCursorOfLastFullChargeTimestamp() {
+ final Uri lastFullChargeTimestampContentUri =
+ new Uri.Builder()
+ .scheme(ContentResolver.SCHEME_CONTENT)
+ .authority(DatabaseUtils.AUTHORITY)
+ .appendPath(DatabaseUtils.LAST_FULL_CHARGE_TIMESTAMP_PATH)
+ .build();
+
+ return query(lastFullChargeTimestampContentUri);
+ }
+
+ private Cursor getCursorOfBatteryStateLatestTimestamp(final long queryTimestamp) {
+ final Uri batteryStateLatestTimestampUri =
+ new Uri.Builder()
+ .scheme(ContentResolver.SCHEME_CONTENT)
+ .authority(DatabaseUtils.AUTHORITY)
+ .appendPath(DatabaseUtils.BATTERY_STATE_LATEST_TIMESTAMP_PATH)
+ .appendQueryParameter(
+ DatabaseUtils.QUERY_KEY_TIMESTAMP, Long.toString(queryTimestamp))
+ .build();
+
+ return query(batteryStateLatestTimestampUri);
+ }
+
private void insertAppUsageEvent() {
mProvider.onCreate();
// Inserts some valid testing data.
@@ -452,12 +536,7 @@
DatabaseUtils.QUERY_KEY_USERID, Long.toString(userId))
.build();
- return mProvider.query(
- appUsageLatestTimestampQueryContentUri,
- /*strings=*/ null,
- /*s=*/ null,
- /*strings1=*/ null,
- /*s1=*/ null);
+ return query(appUsageLatestTimestampQueryContentUri);
}
private Cursor getCursorOfAppUsage(final List<Long> userIds, final long queryTimestamp) {
@@ -474,7 +553,43 @@
.appendQueryParameter(DatabaseUtils.QUERY_KEY_USERID, queryUserIdString)
.build();
+ return query(appUsageEventUri);
+ }
+
+ private Cursor getCursorOfBatteryEvents(
+ final long queryTimestamp, final List<Integer> batteryEventTypes) {
+ final String batteryEventTypesString = batteryEventTypes.stream()
+ .map(type -> String.valueOf(type))
+ .collect(Collectors.joining(","));
+ final Uri batteryEventUri =
+ new Uri.Builder()
+ .scheme(ContentResolver.SCHEME_CONTENT)
+ .authority(DatabaseUtils.AUTHORITY)
+ .appendPath(DatabaseUtils.BATTERY_EVENT_TABLE)
+ .appendQueryParameter(
+ DatabaseUtils.QUERY_KEY_TIMESTAMP, Long.toString(queryTimestamp))
+ .appendQueryParameter(
+ DatabaseUtils.QUERY_BATTERY_EVENT_TYPE, batteryEventTypesString)
+ .build();
+
+ return query(batteryEventUri);
+ }
+
+ private Cursor getCursorOfBatteryUsageSlots(final long queryTimestamp) {
+ final Uri batteryUsageSlotUri =
+ new Uri.Builder()
+ .scheme(ContentResolver.SCHEME_CONTENT)
+ .authority(DatabaseUtils.AUTHORITY)
+ .appendPath(DatabaseUtils.BATTERY_USAGE_SLOT_TABLE)
+ .appendQueryParameter(
+ DatabaseUtils.QUERY_KEY_TIMESTAMP, Long.toString(queryTimestamp))
+ .build();
+
+ return query(batteryUsageSlotUri);
+ }
+
+ private Cursor query(Uri uri) {
return mProvider.query(
- appUsageEventUri, /*strings=*/ null, /*s=*/ null, /*strings1=*/ null, /*s1=*/ null);
+ uri, /*strings=*/ null, /*s=*/ null, /*strings1=*/ null, /*s1=*/ null);
}
}
diff --git a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryUsageDataLoaderTest.java b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryUsageDataLoaderTest.java
index 9aeff79..f3965fd 100644
--- a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryUsageDataLoaderTest.java
+++ b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryUsageDataLoaderTest.java
@@ -22,6 +22,7 @@
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
import android.content.ContentResolver;
@@ -31,6 +32,7 @@
import android.os.BatteryStatsManager;
import android.os.BatteryUsageStats;
import android.os.BatteryUsageStatsQuery;
+import android.os.UserManager;
import org.junit.Before;
import org.junit.Test;
@@ -43,6 +45,7 @@
import org.robolectric.RuntimeEnvironment;
import java.util.ArrayList;
+import java.util.HashMap;
import java.util.List;
@RunWith(RobolectricTestRunner.class)
@@ -56,6 +59,8 @@
@Mock
private PackageManager mPackageManager;
@Mock
+ private UserManager mUserManager;
+ @Mock
private BatteryUsageStats mBatteryUsageStats;
@Mock
private BatteryEntry mMockBatteryEntry;
@@ -70,6 +75,7 @@
doReturn(mBatteryStatsManager).when(mContext).getSystemService(
Context.BATTERY_STATS_SERVICE);
doReturn(mPackageManager).when(mContext).getPackageManager();
+ doReturn(mUserManager).when(mContext).getSystemService(UserManager.class);
doReturn(mMockContentResolver).when(mContext).getContentResolver();
doReturn(new Intent()).when(mContext).registerReceiver(any(), any());
}
@@ -82,7 +88,7 @@
.thenReturn(mBatteryUsageStats);
BatteryUsageDataLoader.sFakeBatteryEntryListSupplier = () -> batteryEntryList;
- BatteryUsageDataLoader.loadUsageData(mContext, /*isFullChargeStart=*/ false);
+ BatteryUsageDataLoader.loadBatteryStatsData(mContext, /*isFullChargeStart=*/ false);
final int queryFlags = mStatsQueryCaptor.getValue().getFlags();
assertThat(queryFlags
@@ -97,7 +103,7 @@
.thenReturn(mBatteryUsageStats);
BatteryUsageDataLoader.sFakeBatteryEntryListSupplier = () -> null;
- BatteryUsageDataLoader.loadUsageData(mContext, /*isFullChargeStart=*/ false);
+ BatteryUsageDataLoader.loadBatteryStatsData(mContext, /*isFullChargeStart=*/ false);
verify(mMockContentResolver).insert(any(), any());
}
@@ -108,8 +114,51 @@
.thenReturn(mBatteryUsageStats);
BatteryUsageDataLoader.sFakeBatteryEntryListSupplier = () -> new ArrayList<>();
- BatteryUsageDataLoader.loadUsageData(mContext, /*isFullChargeStart=*/ false);
+ BatteryUsageDataLoader.loadBatteryStatsData(mContext, /*isFullChargeStart=*/ false);
verify(mMockContentResolver).insert(any(), any());
}
+
+ @Test
+ public void loadAppUsageData_withData_insertFakeDataIntoProvider() {
+ final List<AppUsageEvent> AppUsageEventList = new ArrayList<>();
+ final AppUsageEvent appUsageEvent = AppUsageEvent.newBuilder().setUid(0).build();
+ AppUsageEventList.add(appUsageEvent);
+ BatteryUsageDataLoader.sFakeAppUsageEventsSupplier = () -> new HashMap<>();
+ BatteryUsageDataLoader.sFakeUsageEventsListSupplier = () -> AppUsageEventList;
+
+ BatteryUsageDataLoader.loadAppUsageData(mContext);
+
+ verify(mMockContentResolver).bulkInsert(any(), any());
+ verify(mMockContentResolver).notifyChange(any(), any());
+ }
+
+ @Test
+ public void loadAppUsageData_nullAppUsageEvents_notInsertDataIntoProvider() {
+ BatteryUsageDataLoader.sFakeAppUsageEventsSupplier = () -> null;
+
+ BatteryUsageDataLoader.loadAppUsageData(mContext);
+
+ verifyNoMoreInteractions(mMockContentResolver);
+ }
+
+ @Test
+ public void loadAppUsageData_nullUsageEventsList_notInsertDataIntoProvider() {
+ BatteryUsageDataLoader.sFakeAppUsageEventsSupplier = () -> new HashMap<>();
+ BatteryUsageDataLoader.sFakeUsageEventsListSupplier = () -> null;
+
+ BatteryUsageDataLoader.loadAppUsageData(mContext);
+
+ verifyNoMoreInteractions(mMockContentResolver);
+ }
+
+ @Test
+ public void loadAppUsageData_emptyUsageEventsList_notInsertDataIntoProvider() {
+ BatteryUsageDataLoader.sFakeAppUsageEventsSupplier = () -> new HashMap<>();
+ BatteryUsageDataLoader.sFakeUsageEventsListSupplier = () -> new ArrayList<>();
+
+ BatteryUsageDataLoader.loadAppUsageData(mContext);
+
+ verifyNoMoreInteractions(mMockContentResolver);
+ }
}
diff --git a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BootBroadcastReceiverTest.java b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BootBroadcastReceiverTest.java
index aa1ebd7..566df52 100644
--- a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BootBroadcastReceiverTest.java
+++ b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BootBroadcastReceiverTest.java
@@ -22,8 +22,10 @@
import android.app.AlarmManager;
import android.app.Application;
+import android.app.usage.UsageStatsManager;
import android.content.Context;
import android.content.Intent;
+import android.content.SharedPreferences;
import androidx.test.core.app.ApplicationProvider;
@@ -40,7 +42,6 @@
import org.robolectric.shadows.ShadowAlarmManager;
import java.time.Clock;
-import java.time.Duration;
import java.util.List;
import java.util.concurrent.TimeUnit;
@@ -65,10 +66,12 @@
BatteryTestUtils.insertDataToBatteryStateTable(
mContext, Clock.systemUTC().millis(), "com.android.systemui");
mDao = database.batteryStateDao();
+ clearSharedPreferences();
}
@After
public void tearDown() {
+ clearSharedPreferences();
mPeriodicJobManager.reset();
}
@@ -82,8 +85,21 @@
@Test
public void onReceive_withBootCompletedIntent_refreshesJob() {
+ final SharedPreferences sharedPreferences = DatabaseUtils.getSharedPreferences(mContext);
+ sharedPreferences
+ .edit()
+ .putInt(DatabaseUtils.KEY_LAST_USAGE_SOURCE,
+ UsageStatsManager.USAGE_SOURCE_CURRENT_ACTIVITY)
+ .apply();
+
mReceiver.onReceive(mContext, new Intent(Intent.ACTION_BOOT_COMPLETED));
+
assertThat(mShadowAlarmManager.peekNextScheduledAlarm()).isNotNull();
+ assertThat(
+ DatabaseUtils
+ .getSharedPreferences(mContext)
+ .contains(DatabaseUtils.KEY_LAST_USAGE_SOURCE))
+ .isFalse();
}
@Test
@@ -133,15 +149,7 @@
BootBroadcastReceiver.ACTION_PERIODIC_JOB_RECHECK);
}
- private void insertExpiredData(int shiftDay) {
- final long expiredTimeInMs =
- Clock.systemUTC().millis() - Duration.ofDays(shiftDay).toMillis();
- BatteryTestUtils.insertDataToBatteryStateTable(
- mContext, expiredTimeInMs - 1, "com.android.systemui");
- BatteryTestUtils.insertDataToBatteryStateTable(
- mContext, expiredTimeInMs, "com.android.systemui");
- // Ensures the testing environment is correct.
- assertThat(mDao.getAllAfter(0)).hasSize(3);
+ private void clearSharedPreferences() {
+ DatabaseUtils.getSharedPreferences(mContext).edit().clear().apply();
}
-
}
diff --git a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/ConvertUtilsTest.java b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/ConvertUtilsTest.java
index 6b8073b..cd594d3 100644
--- a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/ConvertUtilsTest.java
+++ b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/ConvertUtilsTest.java
@@ -35,11 +35,11 @@
import android.os.BatteryManager;
import android.os.BatteryUsageStats;
import android.os.LocaleList;
-import android.os.RemoteException;
import android.os.UserHandle;
import com.android.settings.fuelgauge.batteryusage.db.AppUsageEventEntity;
import com.android.settings.fuelgauge.batteryusage.db.BatteryEventEntity;
+import com.android.settings.fuelgauge.batteryusage.db.BatteryUsageSlotEntity;
import org.junit.Before;
import org.junit.Test;
@@ -49,7 +49,10 @@
import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment;
+import java.util.List;
import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
import java.util.TimeZone;
@RunWith(RobolectricTestRunner.class)
@@ -62,15 +65,18 @@
@Mock
private BatteryUsageStats mBatteryUsageStats;
@Mock
- private IUsageStatsManager mUsageStatsManager;
- @Mock
private BatteryEntry mMockBatteryEntry;
+ @Mock
+ private IUsageStatsManager mUsageStatsManager;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
+ TimeZone.setDefault(TimeZone.getTimeZone("UTC"));
mContext = spy(RuntimeEnvironment.application);
+ ConvertUtils.sUsageSource = ConvertUtils.EMPTY_USAGE_SOURCE;
when(mContext.getPackageManager()).thenReturn(mMockPackageManager);
+ DataProcessor.sUsageStatsManager = mUsageStatsManager;
}
@Test
@@ -212,6 +218,22 @@
}
@Test
+ public void convertBatteryUsageSlotToContentValues_normalCase_returnsExpectedContentValues() {
+ final BatteryUsageSlot batteryUsageSlot =
+ BatteryUsageSlot.newBuilder()
+ .setStartTimestamp(10001L)
+ .setEndTimestamp(30003L)
+ .setStartBatteryLevel(88)
+ .setEndBatteryLevel(66)
+ .setScreenOnTime(123L)
+ .build();
+ final ContentValues values =
+ ConvertUtils.convertBatteryUsageSlotToContentValues(batteryUsageSlot);
+ assertThat(values.getAsLong(BatteryUsageSlotEntity.KEY_TIMESTAMP)).isEqualTo(10001L);
+ assertThat(BatteryUsageSlotEntity.KEY_BATTERY_USAGE_SLOT).isNotEmpty();
+ }
+
+ @Test
public void convertToBatteryHistEntry_returnsExpectedResult() {
final int expectedType = 3;
when(mMockBatteryEntry.getUid()).thenReturn(1001);
@@ -322,8 +344,8 @@
when(mMockPackageManager.getPackageUidAsUser(any(), anyInt())).thenReturn(1001);
final long userId = 1;
- final AppUsageEvent appUsageEvent = ConvertUtils.convertToAppUsageEvent(
- mContext, mUsageStatsManager, event, userId);
+ final AppUsageEvent appUsageEvent =
+ ConvertUtils.convertToAppUsageEvent(mContext, mUsageStatsManager, event, userId);
assertThat(appUsageEvent.getTimestamp()).isEqualTo(101L);
assertThat(appUsageEvent.getType()).isEqualTo(AppUsageEventType.DEVICE_SHUTDOWN);
assertThat(appUsageEvent.getPackageName()).isEqualTo("com.android.settings1");
@@ -338,8 +360,9 @@
final Event event = new Event();
event.mPackage = null;
- final AppUsageEvent appUsageEvent = ConvertUtils.convertToAppUsageEvent(
- mContext, mUsageStatsManager, event, /*userId=*/ 0);
+ final AppUsageEvent appUsageEvent =
+ ConvertUtils.convertToAppUsageEvent(
+ mContext, mUsageStatsManager, event, /*userId=*/ 0);
assertThat(appUsageEvent).isNull();
}
@@ -354,14 +377,14 @@
.thenThrow(new PackageManager.NameNotFoundException());
final long userId = 1;
- final AppUsageEvent appUsageEvent = ConvertUtils.convertToAppUsageEvent(
- mContext, mUsageStatsManager, event, userId);
+ final AppUsageEvent appUsageEvent =
+ ConvertUtils.convertToAppUsageEvent(mContext, mUsageStatsManager, event, userId);
assertThat(appUsageEvent).isNull();
}
@Test
- public void convertToAppUsageEventFromCursor_returnExpectedResult() {
+ public void convertToAppUsageEvent_returnExpectedResult() {
final MatrixCursor cursor = new MatrixCursor(
new String[]{
AppUsageEventEntity.KEY_UID,
@@ -382,7 +405,7 @@
100001L});
cursor.moveToFirst();
- final AppUsageEvent appUsageEvent = ConvertUtils.convertToAppUsageEventFromCursor(cursor);
+ final AppUsageEvent appUsageEvent = ConvertUtils.convertToAppUsageEvent(cursor);
assertThat(appUsageEvent.getUid()).isEqualTo(101L);
assertThat(appUsageEvent.getUserId()).isEqualTo(1001L);
@@ -394,7 +417,7 @@
}
@Test
- public void convertToAppUsageEventFromCursor_emptyInstanceIdAndRootName_returnExpectedResult() {
+ public void convertToAppUsageEvent_emptyInstanceIdAndRootName_returnExpectedResult() {
final MatrixCursor cursor = new MatrixCursor(
new String[]{
AppUsageEventEntity.KEY_UID,
@@ -411,7 +434,7 @@
AppUsageEventType.DEVICE_SHUTDOWN.getNumber()});
cursor.moveToFirst();
- final AppUsageEvent appUsageEvent = ConvertUtils.convertToAppUsageEventFromCursor(cursor);
+ final AppUsageEvent appUsageEvent = ConvertUtils.convertToAppUsageEvent(cursor);
assertThat(appUsageEvent.getUid()).isEqualTo(101L);
assertThat(appUsageEvent.getUserId()).isEqualTo(1001L);
@@ -432,6 +455,42 @@
}
@Test
+ public void convertToBatteryEventList_normalCase_returnsExpectedResult() {
+ final BatteryLevelData batteryLevelData = new BatteryLevelData(Map.of(
+ 1691589600000L, 98, 1691596800000L, 90, 1691596812345L, 80));
+
+ final List<BatteryEvent> batteryEventList =
+ ConvertUtils.convertToBatteryEventList(batteryLevelData);
+
+ assertThat(batteryEventList).hasSize(2);
+ assertThat(batteryEventList.get(0).getTimestamp()).isEqualTo(1691589600000L);
+ assertThat(batteryEventList.get(0).getType()).isEqualTo(BatteryEventType.EVEN_HOUR);
+ assertThat(batteryEventList.get(0).getBatteryLevel()).isEqualTo(98);
+ assertThat(batteryEventList.get(1).getTimestamp()).isEqualTo(1691596800000L);
+ assertThat(batteryEventList.get(1).getType()).isEqualTo(BatteryEventType.EVEN_HOUR);
+ assertThat(batteryEventList.get(1).getBatteryLevel()).isEqualTo(90);
+ }
+
+ @Test
+ public void convertToBatteryUsageSlotList_normalCase_returnsExpectedResult() {
+ BatteryDiffData batteryDiffData1 = new BatteryDiffData(
+ mContext, 11L, 12L, 13, 14, 15, List.of(), List.of(), Set.of(), Set.of(), false);
+ BatteryDiffData batteryDiffData2 = new BatteryDiffData(
+ mContext, 21L, 22L, 23, 24, 25, List.of(), List.of(), Set.of(), Set.of(), false);
+ BatteryDiffData batteryDiffData3 = new BatteryDiffData(
+ mContext, 31L, 32L, 33, 34, 35, List.of(), List.of(), Set.of(), Set.of(), false);
+ final Map<Long, BatteryDiffData> batteryDiffDataMap = Map.of(
+ 11L, batteryDiffData1, 21L, batteryDiffData2, 31L, batteryDiffData3);
+
+ final List<BatteryUsageSlot> batteryUsageSlotList =
+ ConvertUtils.convertToBatteryUsageSlotList(batteryDiffDataMap);
+
+ assertThat(batteryUsageSlotList).hasSize(3);
+ assertThat(batteryUsageSlotList.stream().map((s) -> s.getScreenOnTime()).sorted().toList())
+ .isEqualTo(List.of(15L, 25L, 35L));
+ }
+
+ @Test
public void getLocale_nullContext_returnDefaultLocale() {
assertThat(ConvertUtils.getLocale(/*context=*/ null))
.isEqualTo(Locale.getDefault());
@@ -450,51 +509,47 @@
}
@Test
- public void getEffectivePackageName_currentActivity_returnPackageName() throws RemoteException {
- when(mUsageStatsManager.getUsageSource()).thenReturn(USAGE_SOURCE_CURRENT_ACTIVITY);
+ public void getEffectivePackageName_currentActivity_returnPackageName() {
+ ConvertUtils.sUsageSource = USAGE_SOURCE_CURRENT_ACTIVITY;
final String packageName = "com.android.settings1";
final String taskRootPackageName = "com.android.settings2";
assertThat(ConvertUtils.getEffectivePackageName(
- mUsageStatsManager, packageName, taskRootPackageName))
+ mContext, mUsageStatsManager, packageName, taskRootPackageName))
.isEqualTo(packageName);
}
@Test
- public void getEffectivePackageName_usageSourceThrowException_returnPackageName()
- throws RemoteException {
- when(mUsageStatsManager.getUsageSource()).thenThrow(new RemoteException());
+ public void getEffectivePackageName_emptyUsageSource_returnPackageName() {
final String packageName = "com.android.settings1";
final String taskRootPackageName = "com.android.settings2";
assertThat(ConvertUtils.getEffectivePackageName(
- mUsageStatsManager, packageName, taskRootPackageName))
+ mContext, mUsageStatsManager, packageName, taskRootPackageName))
.isEqualTo(packageName);
}
@Test
- public void getEffectivePackageName_rootActivity_returnTaskRootPackageName()
- throws RemoteException {
- when(mUsageStatsManager.getUsageSource()).thenReturn(USAGE_SOURCE_TASK_ROOT_ACTIVITY);
+ public void getEffectivePackageName_rootActivity_returnTaskRootPackageName() {
+ ConvertUtils.sUsageSource = USAGE_SOURCE_TASK_ROOT_ACTIVITY;
final String packageName = "com.android.settings1";
final String taskRootPackageName = "com.android.settings2";
assertThat(ConvertUtils.getEffectivePackageName(
- mUsageStatsManager, packageName, taskRootPackageName))
+ mContext, mUsageStatsManager, packageName, taskRootPackageName))
.isEqualTo(taskRootPackageName);
}
@Test
- public void getEffectivePackageName_nullOrEmptyTaskRoot_returnPackageName()
- throws RemoteException {
- when(mUsageStatsManager.getUsageSource()).thenReturn(USAGE_SOURCE_TASK_ROOT_ACTIVITY);
+ public void getEffectivePackageName_nullOrEmptyTaskRoot_returnPackageName() {
+ ConvertUtils.sUsageSource = USAGE_SOURCE_TASK_ROOT_ACTIVITY;
final String packageName = "com.android.settings1";
assertThat(ConvertUtils.getEffectivePackageName(
- mUsageStatsManager, packageName, /*taskRootPackageName=*/ null))
+ mContext, mUsageStatsManager, packageName, /*taskRootPackageName=*/ null))
.isEqualTo(packageName);
assertThat(ConvertUtils.getEffectivePackageName(
- mUsageStatsManager, packageName, /*taskRootPackageName=*/ ""))
+ mContext, mUsageStatsManager, packageName, /*taskRootPackageName=*/ ""))
.isEqualTo(packageName);
}
}
diff --git a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/DataProcessManagerTest.java b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/DataProcessManagerTest.java
index b610cfb..94fa00f 100644
--- a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/DataProcessManagerTest.java
+++ b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/DataProcessManagerTest.java
@@ -30,8 +30,12 @@
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
+import android.database.Cursor;
import android.database.MatrixCursor;
import android.os.BatteryManager;
+import android.os.BatteryStatsManager;
+import android.os.BatteryUsageStats;
+import android.os.BatteryUsageStatsQuery;
import android.os.Parcel;
import android.os.RemoteException;
import android.os.UserManager;
@@ -39,9 +43,12 @@
import com.android.settings.fuelgauge.batteryusage.db.AppUsageEventEntity;
+import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.robolectric.RobolectricTestRunner;
@@ -52,6 +59,7 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.function.Supplier;
@RunWith(RobolectricTestRunner.class)
public final class DataProcessManagerTest {
@@ -65,7 +73,13 @@
@Mock
private UserManager mUserManager;
@Mock
+ private BatteryStatsManager mBatteryStatsManager;
+ @Mock
+ private BatteryUsageStats mBatteryUsageStats;
+ @Mock
private Intent mIntent;
+ @Captor
+ private ArgumentCaptor<BatteryUsageStatsQuery> mBatteryUsageStatsQueryCaptor;
@Before
public void setUp() {
@@ -77,22 +91,32 @@
doReturn(mUserManager)
.when(mContext)
.getSystemService(UserManager.class);
+ doReturn(mBatteryStatsManager).when(mContext).getSystemService(
+ Context.BATTERY_STATS_SERVICE);
+ doReturn(mBatteryUsageStats).when(
+ mBatteryStatsManager).getBatteryUsageStats(mBatteryUsageStatsQueryCaptor.capture());
doReturn(mIntent).when(mContext).registerReceiver(any(), any());
doReturn(100).when(mIntent).getIntExtra(eq(BatteryManager.EXTRA_SCALE), anyInt());
doReturn(66).when(mIntent).getIntExtra(eq(BatteryManager.EXTRA_LEVEL), anyInt());
mDataProcessManager = new DataProcessManager(
mContext, /*handler=*/ null, /*rawStartTimestamp=*/ 0L,
- /*callbackFunction=*/ null, /*hourlyBatteryLevelsPerDay=*/ new ArrayList<>(),
+ /*lastFullChargeTimestamp=*/ 0L, /*callbackFunction=*/ null,
+ /*hourlyBatteryLevelsPerDay=*/ new ArrayList<>(),
/*batteryHistoryMap=*/ new HashMap<>());
}
+ @After
+ public void cleanUp() {
+ DatabaseUtils.sFakeSupplier = null;
+ DataProcessManager.sFakeBatteryHistoryMap = null;
+ }
+
@Test
public void constructor_noLevelData() {
final DataProcessManager dataProcessManager =
new DataProcessManager(mContext, /*handler=*/ null, /*callbackFunction=*/ null);
assertThat(dataProcessManager.getShowScreenOnTime()).isFalse();
- assertThat(dataProcessManager.getShowBatteryLevel()).isFalse();
}
@Test
@@ -122,16 +146,18 @@
final String packageName = "package";
// Adds the day 1 data.
final List<Long> timestamps1 = List.of(2L, 3L, 4L);
- final List<Integer> levels1 = List.of(100, 100, 100);
+ final Map<Long, Integer> batteryLevelMap1 =
+ Map.of(timestamps1.get(0), 100, timestamps1.get(1), 100, timestamps1.get(2), 100);
hourlyBatteryLevelsPerDay.add(
- new BatteryLevelData.PeriodBatteryLevelData(timestamps1, levels1));
+ new BatteryLevelData.PeriodBatteryLevelData(batteryLevelMap1, timestamps1));
// Adds the day 2 data.
hourlyBatteryLevelsPerDay.add(null);
// Adds the day 3 data.
final List<Long> timestamps2 = List.of(5L, 6L);
- final List<Integer> levels2 = List.of(100, 100);
+ final Map<Long, Integer> batteryLevelMap2 =
+ Map.of(timestamps2.get(0), 100, timestamps2.get(1), 100);
hourlyBatteryLevelsPerDay.add(
- new BatteryLevelData.PeriodBatteryLevelData(timestamps2, levels2));
+ new BatteryLevelData.PeriodBatteryLevelData(batteryLevelMap2, timestamps2));
// Fake current usage data.
final UsageEvents.Event event1 =
getUsageEvent(UsageEvents.Event.ACTIVITY_RESUMED, /*timestamp=*/ 1, packageName);
@@ -171,10 +197,18 @@
cursor.addRow(new Object[] {
AppUsageEventType.ACTIVITY_STOPPED.getNumber(), /*timestamp=*/ 6, /*userId=*/ 1,
/*instanceId=*/ 2, packageName});
- DatabaseUtils.sFakeSupplier = () -> cursor;
+ DatabaseUtils.sFakeSupplier = new Supplier<>() {
+ private int mTimes = 0;
+ @Override
+ public Cursor get() {
+ mTimes++;
+ return mTimes <= 2 ? null : cursor;
+ }
+ };
final DataProcessManager dataProcessManager = new DataProcessManager(
- mContext, /*handler=*/ null, /*rawStartTimestamp=*/ 2L, /*callbackFunction=*/ null,
+ mContext, /*handler=*/ null, /*rawStartTimestamp=*/ 2L,
+ /*lastFullChargeTimestamp=*/ 1L, /*callbackFunction=*/ null,
hourlyBatteryLevelsPerDay, /*batteryHistoryMap=*/ new HashMap<>());
dataProcessManager.start();
@@ -254,12 +288,13 @@
assertThat(DataProcessManager.getBatteryLevelData(
mContext,
/*handler=*/ null,
- /*batteryHistoryMap=*/ null,
- /*asyncResponseDelegate=*/ null))
- .isNull();
+ /*isFromPeriodJob=*/ false,
+ /*asyncResponseDelegate=*/ null)).isNull();
assertThat(DataProcessManager.getBatteryLevelData(
- mContext, /*handler=*/ null, new HashMap<>(), /*asyncResponseDelegate=*/ null))
- .isNull();
+ mContext,
+ /*handler=*/ null,
+ /*isFromPeriodJob=*/ true,
+ /*asyncResponseDelegate=*/ null)).isNull();
}
@Test
@@ -270,18 +305,16 @@
DateUtils.HOUR_IN_MILLIS * 2 - 200L,
DateUtils.HOUR_IN_MILLIS * 2 - 100L};
final int[] levels = {100, 99, 98};
- final Map<Long, Map<String, BatteryHistEntry>> batteryHistoryMap =
- createHistoryMap(timestamps, levels);
+ DataProcessManager.sFakeBatteryHistoryMap = createHistoryMap(timestamps, levels);
DataProcessor.sTestCurrentTimeMillis = timestamps[timestamps.length - 1];
final BatteryLevelData resultData =
DataProcessManager.getBatteryLevelData(
mContext,
/*handler=*/ null,
- batteryHistoryMap,
+ /*isFromPeriodJob=*/ false,
/*asyncResponseDelegate=*/ null);
-
final List<Long> expectedDailyTimestamps = List.of(
DateUtils.HOUR_IN_MILLIS * 2 - 300L,
DateUtils.HOUR_IN_MILLIS * 2 - 100L);
@@ -301,15 +334,14 @@
// Timezone GMT+8: 2022-01-01 00:00:00, 2022-01-01 01:00:00
final long[] timestamps = {1640966400000L, 1640970000000L};
final int[] levels = {100, 99};
- final Map<Long, Map<String, BatteryHistEntry>> batteryHistoryMap =
- createHistoryMap(timestamps, levels);
+ DataProcessManager.sFakeBatteryHistoryMap = createHistoryMap(timestamps, levels);
DataProcessor.sTestCurrentTimeMillis = timestamps[timestamps.length - 1];
final BatteryLevelData resultData =
DataProcessManager.getBatteryLevelData(
mContext,
/*handler=*/ null,
- batteryHistoryMap,
+ /*isFromPeriodJob=*/ false,
/*asyncResponseDelegate=*/ null);
final List<Long> expectedDailyTimestamps = List.of(
diff --git a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/DataProcessorTest.java b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/DataProcessorTest.java
index e2274e2..c4394f7 100644
--- a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/DataProcessorTest.java
+++ b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/DataProcessorTest.java
@@ -16,6 +16,9 @@
package com.android.settings.fuelgauge.batteryusage;
+import static com.android.settings.fuelgauge.batteryusage.ConvertUtils.FAKE_PACKAGE_NAME;
+import static com.android.settingslib.fuelgauge.BatteryStatus.BATTERY_LEVEL_UNKNOWN;
+
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Mockito.anyInt;
@@ -42,6 +45,7 @@
import android.os.Parcel;
import android.os.RemoteException;
import android.os.UserManager;
+import android.util.ArrayMap;
import com.android.settings.fuelgauge.PowerUsageFeatureProvider;
import com.android.settings.testutils.FakeFeatureFactory;
@@ -188,16 +192,18 @@
final String packageName = "com.android.settings";
// Adds the day 1 data.
final List<Long> timestamps1 = List.of(14400000L, 18000000L, 21600000L);
- final List<Integer> levels1 = List.of(100, 100, 100);
+ final Map<Long, Integer> batteryLevelMap1 =
+ Map.of(timestamps1.get(0), 100, timestamps1.get(1), 100, timestamps1.get(2), 100);
hourlyBatteryLevelsPerDay.add(
- new BatteryLevelData.PeriodBatteryLevelData(timestamps1, levels1));
+ new BatteryLevelData.PeriodBatteryLevelData(batteryLevelMap1, timestamps1));
// Adds the day 2 data.
hourlyBatteryLevelsPerDay.add(null);
// Adds the day 3 data.
final List<Long> timestamps2 = List.of(45200000L, 48800000L);
- final List<Integer> levels2 = List.of(100, 100);
+ final Map<Long, Integer> batteryLevelMap2 =
+ Map.of(timestamps2.get(0), 100, timestamps2.get(1), 100);
hourlyBatteryLevelsPerDay.add(
- new BatteryLevelData.PeriodBatteryLevelData(timestamps2, levels2));
+ new BatteryLevelData.PeriodBatteryLevelData(batteryLevelMap2, timestamps2));
final List<AppUsageEvent> appUsageEventList = new ArrayList<>();
// Adds some events before the start timestamp.
appUsageEventList.add(buildAppUsageEvent(
@@ -249,7 +255,7 @@
final Map<Integer, Map<Integer, Map<Long, Map<String, List<AppUsagePeriod>>>>> periodMap =
DataProcessor.generateAppUsagePeriodMap(
- 14400000L, hourlyBatteryLevelsPerDay, appUsageEventList, new ArrayList<>());
+ mContext, hourlyBatteryLevelsPerDay, appUsageEventList, new ArrayList<>());
assertThat(periodMap).hasSize(3);
// Day 1
@@ -285,9 +291,10 @@
final List<BatteryLevelData.PeriodBatteryLevelData> hourlyBatteryLevelsPerDay =
new ArrayList<>();
hourlyBatteryLevelsPerDay.add(
- new BatteryLevelData.PeriodBatteryLevelData(new ArrayList<>(), new ArrayList<>()));
+ new BatteryLevelData.PeriodBatteryLevelData(new ArrayMap<>(), new ArrayList<>()));
assertThat(DataProcessor.generateAppUsagePeriodMap(
- 0L, hourlyBatteryLevelsPerDay, new ArrayList<>(), new ArrayList<>())).isNull();
+ mContext, hourlyBatteryLevelsPerDay, new ArrayList<>(), new ArrayList<>()))
+ .isNull();
}
@Test
@@ -370,19 +377,6 @@
}
@Test
- public void getLevelDataThroughProcessedHistoryMap_notEnoughData_returnNull() {
- final long[] timestamps = {100L};
- final int[] levels = {100};
- final Map<Long, Map<String, BatteryHistEntry>> batteryHistoryMap =
- createHistoryMap(timestamps, levels);
- DataProcessor.sTestCurrentTimeMillis = timestamps[timestamps.length - 1];
-
- assertThat(
- DataProcessor.getLevelDataThroughProcessedHistoryMap(mContext, batteryHistoryMap))
- .isNull();
- }
-
- @Test
public void getLevelDataThroughProcessedHistoryMap_OneDayData_returnExpectedResult() {
// Timezone GMT+8
final long[] timestamps = {
@@ -440,7 +434,7 @@
);
final List<Integer> expectedDailyLevels = new ArrayList<>();
expectedDailyLevels.add(100);
- expectedDailyLevels.add(null);
+ expectedDailyLevels.add(BATTERY_LEVEL_UNKNOWN);
expectedDailyLevels.add(82);
final List<List<Long>> expectedHourlyTimestamps = List.of(
List.of(
@@ -458,13 +452,13 @@
);
final List<Integer> expectedHourlyLevels1 = new ArrayList<>();
expectedHourlyLevels1.add(100);
- expectedHourlyLevels1.add(null);
- expectedHourlyLevels1.add(null);
+ expectedHourlyLevels1.add(BATTERY_LEVEL_UNKNOWN);
+ expectedHourlyLevels1.add(BATTERY_LEVEL_UNKNOWN);
final List<Integer> expectedHourlyLevels2 = new ArrayList<>();
- expectedHourlyLevels2.add(null);
+ expectedHourlyLevels2.add(BATTERY_LEVEL_UNKNOWN);
expectedHourlyLevels2.add(94);
expectedHourlyLevels2.add(90);
- expectedHourlyLevels2.add(null);
+ expectedHourlyLevels2.add(BATTERY_LEVEL_UNKNOWN);
expectedHourlyLevels2.add(82);
final List<List<Integer>> expectedHourlyLevels = List.of(
expectedHourlyLevels1,
@@ -502,8 +496,8 @@
);
final List<Integer> expectedDailyLevels = new ArrayList<>();
expectedDailyLevels.add(100);
- expectedDailyLevels.add(null);
- expectedDailyLevels.add(null);
+ expectedDailyLevels.add(BATTERY_LEVEL_UNKNOWN);
+ expectedDailyLevels.add(BATTERY_LEVEL_UNKNOWN);
expectedDailyLevels.add(88);
final List<List<Long>> expectedHourlyTimestamps = List.of(
List.of(
@@ -541,32 +535,32 @@
);
final List<Integer> expectedHourlyLevels1 = new ArrayList<>();
expectedHourlyLevels1.add(100);
- expectedHourlyLevels1.add(null);
- expectedHourlyLevels1.add(null);
- expectedHourlyLevels1.add(null);
- expectedHourlyLevels1.add(null);
- expectedHourlyLevels1.add(null);
- expectedHourlyLevels1.add(null);
- expectedHourlyLevels1.add(null);
+ expectedHourlyLevels1.add(BATTERY_LEVEL_UNKNOWN);
+ expectedHourlyLevels1.add(BATTERY_LEVEL_UNKNOWN);
+ expectedHourlyLevels1.add(BATTERY_LEVEL_UNKNOWN);
+ expectedHourlyLevels1.add(BATTERY_LEVEL_UNKNOWN);
+ expectedHourlyLevels1.add(BATTERY_LEVEL_UNKNOWN);
+ expectedHourlyLevels1.add(BATTERY_LEVEL_UNKNOWN);
+ expectedHourlyLevels1.add(BATTERY_LEVEL_UNKNOWN);
final List<Integer> expectedHourlyLevels2 = new ArrayList<>();
- expectedHourlyLevels2.add(null);
- expectedHourlyLevels2.add(null);
- expectedHourlyLevels2.add(null);
- expectedHourlyLevels2.add(null);
- expectedHourlyLevels2.add(null);
- expectedHourlyLevels2.add(null);
- expectedHourlyLevels2.add(null);
- expectedHourlyLevels2.add(null);
- expectedHourlyLevels2.add(null);
- expectedHourlyLevels2.add(null);
- expectedHourlyLevels2.add(null);
- expectedHourlyLevels2.add(null);
- expectedHourlyLevels2.add(null);
- expectedHourlyLevels2.add(null);
+ expectedHourlyLevels2.add(BATTERY_LEVEL_UNKNOWN);
+ expectedHourlyLevels2.add(BATTERY_LEVEL_UNKNOWN);
+ expectedHourlyLevels2.add(BATTERY_LEVEL_UNKNOWN);
+ expectedHourlyLevels2.add(BATTERY_LEVEL_UNKNOWN);
+ expectedHourlyLevels2.add(BATTERY_LEVEL_UNKNOWN);
+ expectedHourlyLevels2.add(BATTERY_LEVEL_UNKNOWN);
+ expectedHourlyLevels2.add(BATTERY_LEVEL_UNKNOWN);
+ expectedHourlyLevels2.add(BATTERY_LEVEL_UNKNOWN);
+ expectedHourlyLevels2.add(BATTERY_LEVEL_UNKNOWN);
+ expectedHourlyLevels2.add(BATTERY_LEVEL_UNKNOWN);
+ expectedHourlyLevels2.add(BATTERY_LEVEL_UNKNOWN);
+ expectedHourlyLevels2.add(BATTERY_LEVEL_UNKNOWN);
+ expectedHourlyLevels2.add(BATTERY_LEVEL_UNKNOWN);
+ expectedHourlyLevels2.add(BATTERY_LEVEL_UNKNOWN);
final List<Integer> expectedHourlyLevels3 = new ArrayList<>();
- expectedHourlyLevels3.add(null);
- expectedHourlyLevels3.add(null);
- expectedHourlyLevels3.add(null);
+ expectedHourlyLevels3.add(BATTERY_LEVEL_UNKNOWN);
+ expectedHourlyLevels3.add(BATTERY_LEVEL_UNKNOWN);
+ expectedHourlyLevels3.add(BATTERY_LEVEL_UNKNOWN);
expectedHourlyLevels3.add(88);
final List<List<Integer>> expectedHourlyLevels = List.of(
expectedHourlyLevels1,
@@ -605,8 +599,8 @@
);
final List<Integer> expectedDailyLevels = new ArrayList<>();
expectedDailyLevels.add(100);
- expectedDailyLevels.add(null);
- expectedDailyLevels.add(null);
+ expectedDailyLevels.add(BATTERY_LEVEL_UNKNOWN);
+ expectedDailyLevels.add(BATTERY_LEVEL_UNKNOWN);
expectedDailyLevels.add(88);
final List<List<Long>> expectedHourlyTimestamps = List.of(
List.of(
@@ -637,25 +631,25 @@
);
final List<Integer> expectedHourlyLevels1 = new ArrayList<>();
expectedHourlyLevels1.add(100);
- expectedHourlyLevels1.add(null);
+ expectedHourlyLevels1.add(BATTERY_LEVEL_UNKNOWN);
final List<Integer> expectedHourlyLevels2 = new ArrayList<>();
- expectedHourlyLevels2.add(null);
- expectedHourlyLevels2.add(null);
- expectedHourlyLevels2.add(null);
- expectedHourlyLevels2.add(null);
- expectedHourlyLevels2.add(null);
- expectedHourlyLevels2.add(null);
- expectedHourlyLevels2.add(null);
- expectedHourlyLevels2.add(null);
- expectedHourlyLevels2.add(null);
- expectedHourlyLevels2.add(null);
- expectedHourlyLevels2.add(null);
- expectedHourlyLevels2.add(null);
- expectedHourlyLevels2.add(null);
+ expectedHourlyLevels2.add(BATTERY_LEVEL_UNKNOWN);
+ expectedHourlyLevels2.add(BATTERY_LEVEL_UNKNOWN);
+ expectedHourlyLevels2.add(BATTERY_LEVEL_UNKNOWN);
+ expectedHourlyLevels2.add(BATTERY_LEVEL_UNKNOWN);
+ expectedHourlyLevels2.add(BATTERY_LEVEL_UNKNOWN);
+ expectedHourlyLevels2.add(BATTERY_LEVEL_UNKNOWN);
+ expectedHourlyLevels2.add(BATTERY_LEVEL_UNKNOWN);
+ expectedHourlyLevels2.add(BATTERY_LEVEL_UNKNOWN);
+ expectedHourlyLevels2.add(BATTERY_LEVEL_UNKNOWN);
+ expectedHourlyLevels2.add(BATTERY_LEVEL_UNKNOWN);
+ expectedHourlyLevels2.add(BATTERY_LEVEL_UNKNOWN);
+ expectedHourlyLevels2.add(BATTERY_LEVEL_UNKNOWN);
+ expectedHourlyLevels2.add(BATTERY_LEVEL_UNKNOWN);
final List<Integer> expectedHourlyLevels3 = new ArrayList<>();
- expectedHourlyLevels3.add(null);
- expectedHourlyLevels3.add(null);
- expectedHourlyLevels3.add(null);
+ expectedHourlyLevels3.add(BATTERY_LEVEL_UNKNOWN);
+ expectedHourlyLevels3.add(BATTERY_LEVEL_UNKNOWN);
+ expectedHourlyLevels3.add(BATTERY_LEVEL_UNKNOWN);
expectedHourlyLevels3.add(88);
final List<List<Integer>> expectedHourlyLevels = List.of(
expectedHourlyLevels1,
@@ -734,141 +728,6 @@
}
@Test
- public void getDailyTimestamps_notEnoughData_returnEmptyList() {
- assertThat(DataProcessor.getDailyTimestamps(new ArrayList<>())).isEmpty();
- assertThat(DataProcessor.getDailyTimestamps(List.of(100L))).isEmpty();
- }
-
- @Test
- public void getDailyTimestamps_allDataInOneHour_returnExpectedList() {
- // Timezone GMT+8
- final List<Long> timestamps = List.of(
- 1640970006000L, // 2022-01-01 01:00:06
- 1640973608000L // 2022-01-01 01:00:08
- );
-
- final List<Long> expectedTimestamps = List.of(
- 1640970006000L, // 2022-01-01 01:00:06
- 1640973608000L // 2022-01-01 01:00:08
- );
- assertThat(DataProcessor.getDailyTimestamps(timestamps)).isEqualTo(expectedTimestamps);
- }
-
- @Test
- public void getDailyTimestamps_OneHourDataPerDay_returnExpectedList() {
- // Timezone GMT+8
- final List<Long> timestamps = List.of(
- 1641049200000L, // 2022-01-01 23:00:00
- 1641052800000L, // 2022-01-02 00:00:00
- 1641056400000L // 2022-01-02 01:00:00
- );
-
- final List<Long> expectedTimestamps = List.of(
- 1641049200000L, // 2022-01-01 23:00:00
- 1641052800000L, // 2022-01-02 00:00:00
- 1641056400000L // 2022-01-02 01:00:00
- );
- assertThat(DataProcessor.getDailyTimestamps(timestamps)).isEqualTo(expectedTimestamps);
- }
-
- @Test
- public void getDailyTimestamps_OneDayData_returnExpectedList() {
- // Timezone GMT+8
- final List<Long> timestamps = List.of(
- 1640966400000L, // 2022-01-01 00:00:00
- 1640970000000L, // 2022-01-01 01:00:00
- 1640973600000L, // 2022-01-01 02:00:00
- 1640977200000L, // 2022-01-01 03:00:00
- 1640980800000L // 2022-01-01 04:00:00
- );
-
- final List<Long> expectedTimestamps = List.of(
- 1640966400000L, // 2022-01-01 00:00:00
- 1640980800000L // 2022-01-01 04:00:00
- );
- assertThat(DataProcessor.getDailyTimestamps(timestamps)).isEqualTo(expectedTimestamps);
- }
-
- @Test
- public void getDailyTimestamps_MultipleDaysData_returnExpectedList() {
- // Timezone GMT+8
- final List<Long> timestamps = List.of(
- 1641045600000L, // 2022-01-01 22:00:00
- 1641060000000L, // 2022-01-02 02:00:00
- 1641160800000L, // 2022-01-03 06:00:00
- 1641232800000L // 2022-01-04 02:00:00
- );
-
- final List<Long> expectedTimestamps = List.of(
- 1641045600000L, // 2022-01-01 22:00:00
- 1641052800000L, // 2022-01-02 00:00:00
- 1641139200000L, // 2022-01-03 00:00:00
- 1641225600000L, // 2022-01-04 00:00:00
- 1641232800000L // 2022-01-04 02:00:00
- );
- assertThat(DataProcessor.getDailyTimestamps(timestamps)).isEqualTo(expectedTimestamps);
- }
-
- @Test
- public void getDailyTimestamps_FirstDayOneHourData_returnExpectedList() {
- // Timezone GMT+8
- final List<Long> timestamps = List.of(
- 1641049200000L, // 2022-01-01 23:00:00
- 1641060000000L, // 2022-01-02 02:00:00
- 1641160800000L, // 2022-01-03 06:00:00
- 1641254400000L // 2022-01-04 08:00:00
- );
-
- final List<Long> expectedTimestamps = List.of(
- 1641049200000L, // 2022-01-01 23:00:00
- 1641052800000L, // 2022-01-02 00:00:00
- 1641139200000L, // 2022-01-03 00:00:00
- 1641225600000L, // 2022-01-04 00:00:00
- 1641254400000L // 2022-01-04 08:00:00
- );
- assertThat(DataProcessor.getDailyTimestamps(timestamps)).isEqualTo(expectedTimestamps);
- }
-
- @Test
- public void getDailyTimestamps_LastDayNoData_returnExpectedList() {
- // Timezone GMT+8
- final List<Long> timestamps = List.of(
- 1640988000000L, // 2022-01-01 06:00:00
- 1641060000000L, // 2022-01-02 02:00:00
- 1641160800000L, // 2022-01-03 06:00:00
- 1641225600000L // 2022-01-04 00:00:00
- );
-
- final List<Long> expectedTimestamps = List.of(
- 1640988000000L, // 2022-01-01 06:00:00
- 1641052800000L, // 2022-01-02 00:00:00
- 1641139200000L, // 2022-01-03 00:00:00
- 1641225600000L // 2022-01-04 00:00:00
- );
- assertThat(DataProcessor.getDailyTimestamps(timestamps)).isEqualTo(expectedTimestamps);
- }
-
- @Test
- public void getDailyTimestamps_LastDayOneHourData_returnExpectedList() {
- // Timezone GMT+8
- final List<Long> timestamps = List.of(
- 1640988000000L, // 2022-01-01 06:00:00
- 1641060000000L, // 2022-01-02 02:00:00
- 1641160800000L, // 2022-01-03 06:00:00
- 1641229200000L // 2022-01-04 01:00:00
- );
-
- final List<Long> expectedTimestamps = List.of(
- 1640988000000L, // 2022-01-01 06:00:00
- 1641052800000L, // 2022-01-02 00:00:00
- 1641139200000L, // 2022-01-03 00:00:00
- 1641225600000L, // 2022-01-04 00:00:00
- 1641229200000L // 2022-01-04 01:00:00
- );
- assertThat(DataProcessor.getDailyTimestamps(timestamps)).isEqualTo(expectedTimestamps);
- }
-
- @Test
public void isFromFullCharge_emptyData_returnFalse() {
assertThat(DataProcessor.isFromFullCharge(null)).isFalse();
assertThat(DataProcessor.isFromFullCharge(new HashMap<>())).isFalse();
@@ -915,20 +774,53 @@
}
@Test
- public void getBatteryUsageMap_emptyHistoryMap_returnNull() {
+ public void getBatteryDiffDataMap_emptyHistoryMap_returnEmpty() {
final List<BatteryLevelData.PeriodBatteryLevelData> hourlyBatteryLevelsPerDay =
new ArrayList<>();
hourlyBatteryLevelsPerDay.add(
- new BatteryLevelData.PeriodBatteryLevelData(new ArrayList<>(), new ArrayList<>()));
+ new BatteryLevelData.PeriodBatteryLevelData(new ArrayMap<>(), new ArrayList<>()));
- assertThat(DataProcessor.getBatteryUsageMap(
- mContext, hourlyBatteryLevelsPerDay, new HashMap<>(), /*appUsagePeriodMap=*/ null))
- .isNull();
+ assertThat(DataProcessor.getBatteryDiffDataMap(mContext, hourlyBatteryLevelsPerDay,
+ new HashMap<>(), /*appUsagePeriodMap=*/ null, Set.of(), Set.of())).isEmpty();
}
@Test
- public void getBatteryUsageMap_returnsExpectedResult() {
+ public void getBatteryDiffDataMap_normalFlow_returnExpectedResult() {
+ final int userId = mContext.getUserId();
+ final long[] batteryHistoryKeys = new long[]{
+ 1641045600000L, // 2022-01-01 22:00:00
+ 1641049200000L, // 2022-01-01 23:00:00
+ 1641052800000L, // 2022-01-02 00:00:00
+ };
+ final Map<Long, Map<String, BatteryHistEntry>> batteryHistoryMap = Map.of(
+ batteryHistoryKeys[0], Map.of(FAKE_PACKAGE_NAME, createBatteryHistEntry(
+ FAKE_PACKAGE_NAME, "fake_label", /*consumePower=*/ 0, 0, 0,
+ 0, 0, 0L, userId, ConvertUtils.CONSUMER_TYPE_UID_BATTERY, 0L, 0L, false)),
+ batteryHistoryKeys[1], Map.of(FAKE_PACKAGE_NAME, createBatteryHistEntry(
+ FAKE_PACKAGE_NAME, "fake_label", /*consumePower=*/ 5, 0, 0,
+ 0, 0, 0L, userId, ConvertUtils.CONSUMER_TYPE_UID_BATTERY, 0L, 0L, false)),
+ batteryHistoryKeys[2], Map.of(FAKE_PACKAGE_NAME, createBatteryHistEntry(
+ FAKE_PACKAGE_NAME, "fake_label", /*consumePower=*/ 16, 0, 0,
+ 0, 0, 0L, userId, ConvertUtils.CONSUMER_TYPE_UID_BATTERY, 0L, 0L, false)));
+ final BatteryLevelData batteryLevelData = generateBatteryLevelData(batteryHistoryKeys);
+ final Map<Integer, Map<Integer, Map<Long, Map<String, List<AppUsagePeriod>>>>>
+ appUsagePeriodMap = Map.of(0, Map.of(0, Map.of(Long.valueOf(userId), Map.of(
+ FAKE_PACKAGE_NAME, List.of(buildAppUsagePeriod(0, 6))))));
+
+ Map<Long, BatteryDiffData> batteryDiffDataMap = DataProcessor.getBatteryDiffDataMap(
+ mContext, batteryLevelData.getHourlyBatteryLevelsPerDay(), batteryHistoryMap,
+ appUsagePeriodMap, Set.of(), Set.of());
+
+ assertThat(batteryDiffDataMap).hasSize(1);
+ assertThat(batteryDiffDataMap).containsKey(batteryHistoryKeys[0]);
+ BatteryDiffData batteryDiffData = batteryDiffDataMap.get(batteryHistoryKeys[0]);
+ assertThat(batteryDiffData.getStartTimestamp()).isEqualTo(batteryHistoryKeys[0]);
+ assertThat(batteryDiffData.getEndTimestamp()).isEqualTo(batteryHistoryKeys[2]);
+ }
+
+ @Test
+ public void generateBatteryUsageMap_returnsExpectedResult() {
final long[] batteryHistoryKeys = new long[]{
1641045600000L, // 2022-01-01 22:00:00
1641049200000L, // 2022-01-01 23:00:00
@@ -939,7 +831,7 @@
final Map<Long, Map<String, BatteryHistEntry>> batteryHistoryMap = new HashMap<>();
final int currentUserId = mContext.getUserId();
final BatteryHistEntry fakeEntry = createBatteryHistEntry(
- ConvertUtils.FAKE_PACKAGE_NAME, "fake_label", /*consumePower=*/ 0,
+ FAKE_PACKAGE_NAME, "fake_label", /*consumePower=*/ 0,
/*foregroundUsageConsumePower=*/ 0, /*foregroundServiceUsageConsumePower=*/ 0,
/*backgroundUsageConsumePower=*/ 0, /*cachedUsageConsumePower=*/ 0,
/*uid=*/ 0L, currentUserId, ConvertUtils.CONSUMER_TYPE_UID_BATTERY,
@@ -1029,19 +921,7 @@
entryMap.put(entry.getKey(), entry);
entryMap.put(fakeEntry.getKey(), fakeEntry);
batteryHistoryMap.put(batteryHistoryKeys[4], entryMap);
- final List<BatteryLevelData.PeriodBatteryLevelData> hourlyBatteryLevelsPerDay =
- new ArrayList<>();
- // Adds the day 1 data.
- List<Long> timestamps =
- List.of(batteryHistoryKeys[0], batteryHistoryKeys[2]);
- final List<Integer> levels = List.of(100, 100);
- hourlyBatteryLevelsPerDay.add(
- new BatteryLevelData.PeriodBatteryLevelData(timestamps, levels));
- // Adds the day 2 data.
- timestamps = List.of(batteryHistoryKeys[2], batteryHistoryKeys[4]);
- hourlyBatteryLevelsPerDay.add(
- new BatteryLevelData.PeriodBatteryLevelData(timestamps, levels));
-
+ final BatteryLevelData batteryLevelData = generateBatteryLevelData(batteryHistoryKeys);
// Adds app usage data to test screen on time.
final Map<Integer, Map<Integer, Map<Long, Map<String, List<AppUsagePeriod>>>>>
appUsagePeriodMap = new HashMap<>();
@@ -1065,8 +945,12 @@
appUsagePeriodMap.get(1).put(0, appUsageMap);
final Map<Integer, Map<Integer, BatteryDiffData>> resultMap =
- DataProcessor.getBatteryUsageMap(
- mContext, hourlyBatteryLevelsPerDay, batteryHistoryMap, appUsagePeriodMap);
+ DataProcessor.generateBatteryUsageMap(
+ mContext,
+ DataProcessor.getBatteryDiffDataMap(mContext,
+ batteryLevelData.getHourlyBatteryLevelsPerDay(), batteryHistoryMap,
+ appUsagePeriodMap, Set.of(), Set.of()),
+ batteryLevelData);
BatteryDiffData resultDiffData =
resultMap
@@ -1127,7 +1011,7 @@
}
@Test
- public void getBatteryUsageMap_multipleUsers_returnsExpectedResult() {
+ public void generateBatteryUsageMap_multipleUsers_returnsExpectedResult() {
final long[] batteryHistoryKeys = new long[]{
1641052800000L, // 2022-01-02 00:00:00
1641056400000L, // 2022-01-02 01:00:00
@@ -1216,17 +1100,15 @@
/*backgroundUsageTimeInMs=*/ 30L, /*isHidden=*/ false);
entryMap.put(entry.getKey(), entry);
batteryHistoryMap.put(batteryHistoryKeys[2], entryMap);
- final List<BatteryLevelData.PeriodBatteryLevelData> hourlyBatteryLevelsPerDay =
- new ArrayList<>();
- List<Long> timestamps = List.of(batteryHistoryKeys[0], batteryHistoryKeys[2]);
- final List<Integer> levels = List.of(100, 100);
- hourlyBatteryLevelsPerDay.add(
- new BatteryLevelData.PeriodBatteryLevelData(timestamps, levels));
+ final BatteryLevelData batteryLevelData = generateBatteryLevelData(batteryHistoryKeys);
final Map<Integer, Map<Integer, BatteryDiffData>> resultMap =
- DataProcessor.getBatteryUsageMap(
- mContext, hourlyBatteryLevelsPerDay, batteryHistoryMap,
- /*appUsagePeriodMap=*/ null);
+ DataProcessor.generateBatteryUsageMap(
+ mContext,
+ DataProcessor.getBatteryDiffDataMap(mContext,
+ batteryLevelData.getHourlyBatteryLevelsPerDay(), batteryHistoryMap,
+ /*appUsagePeriodMap=*/ null, Set.of(), Set.of()),
+ batteryLevelData);
final BatteryDiffData resultDiffData =
resultMap
@@ -1246,7 +1128,7 @@
}
@Test
- public void getBatteryUsageMap_usageTimeExceed_returnsExpectedResult() {
+ public void generateBatteryUsageMap_usageTimeExceed_returnsExpectedResult() {
final long[] batteryHistoryKeys = new long[]{
1641052800000L, // 2022-01-02 00:00:00
1641056400000L, // 2022-01-02 01:00:00
@@ -1287,12 +1169,7 @@
/*backgroundUsageTimeInMs=*/ 7200000L, /*isHidden=*/ false);
entryMap.put(entry.getKey(), entry);
batteryHistoryMap.put(batteryHistoryKeys[2], entryMap);
- final List<BatteryLevelData.PeriodBatteryLevelData> hourlyBatteryLevelsPerDay =
- new ArrayList<>();
- List<Long> timestamps = List.of(batteryHistoryKeys[0], batteryHistoryKeys[2]);
- final List<Integer> levels = List.of(100, 100);
- hourlyBatteryLevelsPerDay.add(
- new BatteryLevelData.PeriodBatteryLevelData(timestamps, levels));
+ final BatteryLevelData batteryLevelData = generateBatteryLevelData(batteryHistoryKeys);
// Adds app usage data to test screen on time.
final Map<Integer, Map<Integer, Map<Long, Map<String, List<AppUsagePeriod>>>>>
@@ -1306,8 +1183,12 @@
appUsagePeriodMap.get(0).put(0, appUsageMap);
final Map<Integer, Map<Integer, BatteryDiffData>> resultMap =
- DataProcessor.getBatteryUsageMap(
- mContext, hourlyBatteryLevelsPerDay, batteryHistoryMap, appUsagePeriodMap);
+ DataProcessor.generateBatteryUsageMap(
+ mContext,
+ DataProcessor.getBatteryDiffDataMap(mContext,
+ batteryLevelData.getHourlyBatteryLevelsPerDay(), batteryHistoryMap,
+ appUsagePeriodMap, Set.of(), Set.of()),
+ batteryLevelData);
final BatteryDiffData resultDiffData =
resultMap
@@ -1337,7 +1218,7 @@
}
@Test
- public void getBatteryUsageMap_hideApplicationEntries_returnsExpectedResult() {
+ public void generateBatteryUsageMap_hideApplicationEntries_returnsExpectedResult() {
final long[] batteryHistoryKeys = new long[]{
1641052800000L, // 2022-01-02 00:00:00
1641056400000L, // 2022-01-02 01:00:00
@@ -1402,19 +1283,17 @@
/*backgroundUsageTimeInMs=*/ 20L, /*isHidden=*/ false);
entryMap.put(entry.getKey(), entry);
batteryHistoryMap.put(batteryHistoryKeys[2], entryMap);
- final List<BatteryLevelData.PeriodBatteryLevelData> hourlyBatteryLevelsPerDay =
- new ArrayList<>();
- List<Long> timestamps = List.of(batteryHistoryKeys[0], batteryHistoryKeys[2]);
- final List<Integer> levels = List.of(100, 100);
- hourlyBatteryLevelsPerDay.add(
- new BatteryLevelData.PeriodBatteryLevelData(timestamps, levels));
+ final BatteryLevelData batteryLevelData = generateBatteryLevelData(batteryHistoryKeys);
when(mPowerUsageFeatureProvider.getHideApplicationSet())
.thenReturn(Set.of("package1"));
final Map<Integer, Map<Integer, BatteryDiffData>> resultMap =
- DataProcessor.getBatteryUsageMap(
- mContext, hourlyBatteryLevelsPerDay, batteryHistoryMap,
- /*appUsagePeriodMap=*/ null);
+ DataProcessor.generateBatteryUsageMap(
+ mContext,
+ DataProcessor.getBatteryDiffDataMap(mContext,
+ batteryLevelData.getHourlyBatteryLevelsPerDay(), batteryHistoryMap,
+ /*appUsagePeriodMap=*/ null, Set.of(), Set.of()),
+ batteryLevelData);
final BatteryDiffData resultDiffData =
resultMap
@@ -1430,7 +1309,7 @@
}
@Test
- public void getBatteryUsageMap_hideBackgroundUsageTime_returnsExpectedResult() {
+ public void generateBatteryUsageMap_hideBackgroundUsageTime_returnsExpectedResult() {
final long[] batteryHistoryKeys = new long[]{
1641052800000L, // 2022-01-02 00:00:00
1641056400000L, // 2022-01-02 01:00:00
@@ -1495,19 +1374,17 @@
/*backgroundUsageTimeInMs=*/ 20L, /*isHidden=*/ false);
entryMap.put(entry.getKey(), entry);
batteryHistoryMap.put(batteryHistoryKeys[2], entryMap);
- final List<BatteryLevelData.PeriodBatteryLevelData> hourlyBatteryLevelsPerDay =
- new ArrayList<>();
- List<Long> timestamps = List.of(batteryHistoryKeys[0], batteryHistoryKeys[2]);
- final List<Integer> levels = List.of(100, 100);
- hourlyBatteryLevelsPerDay.add(
- new BatteryLevelData.PeriodBatteryLevelData(timestamps, levels));
+ final BatteryLevelData batteryLevelData = generateBatteryLevelData(batteryHistoryKeys);
when(mPowerUsageFeatureProvider.getHideBackgroundUsageTimeSet())
.thenReturn(new HashSet(Arrays.asList((CharSequence) "package2")));
final Map<Integer, Map<Integer, BatteryDiffData>> resultMap =
- DataProcessor.getBatteryUsageMap(
- mContext, hourlyBatteryLevelsPerDay, batteryHistoryMap,
- /*appUsagePeriodMap=*/ null);
+ DataProcessor.generateBatteryUsageMap(
+ mContext,
+ DataProcessor.getBatteryDiffDataMap(mContext,
+ batteryLevelData.getHourlyBatteryLevelsPerDay(), batteryHistoryMap,
+ /*appUsagePeriodMap=*/ null, Set.of(), Set.of()),
+ batteryLevelData);
final BatteryDiffData resultDiffData =
resultMap
@@ -1522,7 +1399,10 @@
@Test
public void generateBatteryDiffData_emptyBatteryEntryList_returnNull() {
assertThat(DataProcessor.generateBatteryDiffData(mContext,
- DataProcessor.convertToBatteryHistEntry(null, mBatteryUsageStats))).isNull();
+ System.currentTimeMillis(),
+ DataProcessor.convertToBatteryHistEntry(null, mBatteryUsageStats),
+ /* systemAppsPackageNames= */ Set.of(),
+ /* systemAppsUids= */ Set.of())).isNull();
}
@Test
@@ -1573,7 +1453,10 @@
.when(mMockBatteryEntry4).getPowerComponentId();
final BatteryDiffData batteryDiffData = DataProcessor.generateBatteryDiffData(mContext,
- DataProcessor.convertToBatteryHistEntry(batteryEntryList, mBatteryUsageStats));
+ System.currentTimeMillis(),
+ DataProcessor.convertToBatteryHistEntry(batteryEntryList, mBatteryUsageStats),
+ /* systemAppsPackageNames= */ Set.of(),
+ /* systemAppsUids= */ Set.of());
assertBatteryDiffEntry(
batteryDiffData.getAppDiffEntryList().get(0), 0, /*uid=*/ 2L,
@@ -1644,7 +1527,7 @@
final Map<Long, Map<String, List<AppUsagePeriod>>> appUsagePeriodMap =
DataProcessor.buildAppUsagePeriodList(
- appUsageEvents, new ArrayList<>(), 0, 5);
+ mContext, appUsageEvents, new ArrayList<>(), 0, 5);
assertThat(appUsagePeriodMap).hasSize(2);
final Map<String, List<AppUsagePeriod>> userMap1 = appUsagePeriodMap.get(1L);
@@ -1668,7 +1551,7 @@
@Test
public void buildAppUsagePeriodList_emptyEventList_returnNull() {
assertThat(DataProcessor.buildAppUsagePeriodList(
- new ArrayList<>(), new ArrayList<>(), 0, 1)).isNull();
+ mContext, new ArrayList<>(), new ArrayList<>(), 0, 1)).isNull();
}
@Test
@@ -1680,7 +1563,7 @@
AppUsageEventType.DEVICE_SHUTDOWN, /*timestamp=*/ 2));
assertThat(DataProcessor.buildAppUsagePeriodList(
- appUsageEvents, new ArrayList<>(), 0, 3)).isNull();
+ mContext, appUsageEvents, new ArrayList<>(), 0, 3)).isNull();
}
@Test
@@ -2040,9 +1923,9 @@
final double backgroundUsageConsumePower, final double cachedUsageConsumePower,
final long foregroundUsageTimeInMs, final long backgroundUsageTimeInMs,
final long screenOnTimeInMs) {
- assertThat(entry.mBatteryHistEntry.mUserId).isEqualTo(userId);
- assertThat(entry.mBatteryHistEntry.mUid).isEqualTo(uid);
- assertThat(entry.mBatteryHistEntry.mConsumerType).isEqualTo(consumerType);
+ assertThat(entry.mUserId).isEqualTo(userId);
+ assertThat(entry.mUid).isEqualTo(uid);
+ assertThat(entry.mConsumerType).isEqualTo(consumerType);
assertThat(entry.getPercentage()).isEqualTo(consumePercentage);
assertThat(entry.mForegroundUsageConsumePower).isEqualTo(foregroundUsageConsumePower);
assertThat(entry.mForegroundServiceUsageConsumePower)
@@ -2053,4 +1936,12 @@
assertThat(entry.mBackgroundUsageTimeInMs).isEqualTo(backgroundUsageTimeInMs);
assertThat(entry.mScreenOnTimeInMs).isEqualTo(screenOnTimeInMs);
}
+
+ private BatteryLevelData generateBatteryLevelData(long[] timestamps) {
+ Map<Long, Integer> batteryLevelMap = new ArrayMap<>();
+ for (long timestamp : timestamps) {
+ batteryLevelMap.put(timestamp, 100);
+ }
+ return new BatteryLevelData(batteryLevelMap);
+ }
}
diff --git a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/DatabaseUtilsTest.java b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/DatabaseUtilsTest.java
index efce44e..12bd457 100644
--- a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/DatabaseUtilsTest.java
+++ b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/DatabaseUtilsTest.java
@@ -16,22 +16,32 @@
package com.android.settings.fuelgauge.batteryusage;
+import static android.app.usage.UsageStatsManager.USAGE_SOURCE_CURRENT_ACTIVITY;
+import static android.app.usage.UsageStatsManager.USAGE_SOURCE_TASK_ROOT_ACTIVITY;
+
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+import android.app.usage.IUsageStatsManager;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
+import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.database.MatrixCursor;
+import android.net.Uri;
import android.os.BatteryManager;
import android.os.BatteryUsageStats;
+import android.os.RemoteException;
import android.os.UserHandle;
import android.os.UserManager;
@@ -67,6 +77,7 @@
@Mock private BatteryEntry mMockBatteryEntry2;
@Mock private BatteryEntry mMockBatteryEntry3;
@Mock private Context mMockContext;
+ @Mock private IUsageStatsManager mUsageStatsManager;
@Before
public void setUp() {
@@ -77,6 +88,7 @@
doReturn(mPackageManager).when(mMockContext).getPackageManager();
doReturn(mPackageManager).when(mContext).getPackageManager();
DatabaseUtils.getSharedPreferences(mContext).edit().clear().apply();
+ DataProcessor.sUsageStatsManager = mUsageStatsManager;
}
@Test
@@ -164,8 +176,8 @@
doReturn(null).when(mContext).registerReceiver(any(), any());
assertThat(
DatabaseUtils.sendBatteryEntryData(
- mContext, /*batteryEntryList=*/ null, mBatteryUsageStats,
- /*isFullChargeStart=*/ false))
+ mContext, System.currentTimeMillis(), /*batteryEntryList=*/ null,
+ mBatteryUsageStats, /*isFullChargeStart=*/ false))
.isNull();
}
@@ -184,7 +196,10 @@
final List<ContentValues> valuesList =
DatabaseUtils.sendBatteryEntryData(
- mContext, batteryEntryList, mBatteryUsageStats,
+ mContext,
+ System.currentTimeMillis(),
+ batteryEntryList,
+ mBatteryUsageStats,
/*isFullChargeStart=*/ false);
assertThat(valuesList).hasSize(2);
@@ -207,6 +222,7 @@
final List<ContentValues> valuesList =
DatabaseUtils.sendBatteryEntryData(
mContext,
+ System.currentTimeMillis(),
new ArrayList<>(),
mBatteryUsageStats,
/*isFullChargeStart=*/ false);
@@ -226,6 +242,7 @@
final List<ContentValues> valuesList =
DatabaseUtils.sendBatteryEntryData(
mContext,
+ System.currentTimeMillis(),
/*batteryEntryList=*/ null,
mBatteryUsageStats,
/*isFullChargeStart=*/ false);
@@ -245,6 +262,7 @@
final List<ContentValues> valuesList =
DatabaseUtils.sendBatteryEntryData(
mContext,
+ System.currentTimeMillis(),
/*batteryEntryList=*/ null,
/*batteryUsageStats=*/ null,
/*isFullChargeStart=*/ false);
@@ -350,7 +368,7 @@
}
@Test
- public void getHistoryMapSinceLastFullCharge_emptyCursorContent_returnEmptyMap() {
+ public void getHistoryMap_emptyCursorContent_returnEmptyMap() {
final MatrixCursor cursor = new MatrixCursor(
new String[] {
BatteryHistEntry.KEY_UID,
@@ -358,36 +376,33 @@
BatteryHistEntry.KEY_TIMESTAMP});
DatabaseUtils.sFakeSupplier = () -> cursor;
- assertThat(DatabaseUtils.getHistoryMapSinceLastFullCharge(
- mContext, /*calendar=*/ null)).isEmpty();
+ assertThat(DatabaseUtils.getHistoryMapSinceQueryTimestamp(mContext, 0)).isEmpty();
}
@Test
- public void getHistoryMapSinceLastFullCharge_nullCursor_returnEmptyMap() {
+ public void getHistoryMap_nullCursor_returnEmptyMap() {
DatabaseUtils.sFakeSupplier = () -> null;
- assertThat(DatabaseUtils.getHistoryMapSinceLastFullCharge(
- mContext, /*calendar=*/ null)).isEmpty();
+ assertThat(DatabaseUtils.getHistoryMapSinceQueryTimestamp(mContext, 0)).isEmpty();
}
@Test
- public void getHistoryMapSinceLastFullCharge_returnExpectedMap() {
+ public void getHistoryMap_returnExpectedMap() {
final Long timestamp1 = Long.valueOf(1001L);
final Long timestamp2 = Long.valueOf(1002L);
final MatrixCursor cursor = getMatrixCursor();
// Adds fake data into the cursor.
cursor.addRow(new Object[] {
- "app name1", timestamp1, 1, ConvertUtils.CONSUMER_TYPE_UID_BATTERY});
+ "app name1", timestamp1, 1, ConvertUtils.CONSUMER_TYPE_UID_BATTERY, true});
cursor.addRow(new Object[] {
- "app name2", timestamp2, 2, ConvertUtils.CONSUMER_TYPE_UID_BATTERY});
+ "app name2", timestamp2, 2, ConvertUtils.CONSUMER_TYPE_UID_BATTERY, false});
cursor.addRow(new Object[] {
- "app name3", timestamp2, 3, ConvertUtils.CONSUMER_TYPE_UID_BATTERY});
+ "app name3", timestamp2, 3, ConvertUtils.CONSUMER_TYPE_UID_BATTERY, false});
cursor.addRow(new Object[] {
- "app name4", timestamp2, 4, ConvertUtils.CONSUMER_TYPE_UID_BATTERY});
+ "app name4", timestamp2, 4, ConvertUtils.CONSUMER_TYPE_UID_BATTERY, false});
DatabaseUtils.sFakeSupplier = () -> cursor;
final Map<Long, Map<String, BatteryHistEntry>> batteryHistMap =
- DatabaseUtils.getHistoryMapSinceLastFullCharge(
- mContext, /*calendar=*/ null);
+ DatabaseUtils.getHistoryMapSinceQueryTimestamp(mContext, timestamp1);
assertThat(batteryHistMap).hasSize(2);
// Verifies the BatteryHistEntry data for timestamp1.
@@ -403,7 +418,7 @@
}
@Test
- public void getHistoryMapSinceLastFullCharge_withWorkProfile_returnExpectedMap()
+ public void getHistoryMap_withWorkProfile_returnExpectedMap()
throws PackageManager.NameNotFoundException {
doReturn("com.fake.package").when(mContext).getPackageName();
doReturn(mMockContext).when(mContext).createPackageContextAsUser(
@@ -416,13 +431,77 @@
DatabaseUtils.sFakeSupplier = () -> getMatrixCursor();
final Map<Long, Map<String, BatteryHistEntry>> batteryHistMap =
- DatabaseUtils.getHistoryMapSinceLastFullCharge(
- mContext, /*calendar=*/ null);
+ DatabaseUtils.getHistoryMapSinceQueryTimestamp(mContext, 0);
assertThat(batteryHistMap).isEmpty();
}
@Test
+ public void removeUsageSource_hasNoData() {
+ DatabaseUtils.removeUsageSource(mContext);
+ assertThat(
+ DatabaseUtils
+ .getSharedPreferences(mContext)
+ .contains(DatabaseUtils.KEY_LAST_USAGE_SOURCE))
+ .isFalse();
+ }
+
+ @Test
+ public void removeUsageSource_hasData_deleteUsageSource() {
+ final SharedPreferences sharedPreferences = DatabaseUtils.getSharedPreferences(mContext);
+ sharedPreferences
+ .edit()
+ .putInt(DatabaseUtils.KEY_LAST_USAGE_SOURCE, USAGE_SOURCE_TASK_ROOT_ACTIVITY)
+ .apply();
+
+ DatabaseUtils.removeUsageSource(mContext);
+
+ assertThat(
+ DatabaseUtils
+ .getSharedPreferences(mContext)
+ .contains(DatabaseUtils.KEY_LAST_USAGE_SOURCE))
+ .isFalse();
+ }
+
+ @Test
+ public void getUsageSource_hasData() {
+ final SharedPreferences sharedPreferences = DatabaseUtils.getSharedPreferences(mContext);
+ sharedPreferences
+ .edit()
+ .putInt(DatabaseUtils.KEY_LAST_USAGE_SOURCE, USAGE_SOURCE_TASK_ROOT_ACTIVITY)
+ .apply();
+
+ assertThat(DatabaseUtils.getUsageSource(mContext, mUsageStatsManager))
+ .isEqualTo(USAGE_SOURCE_TASK_ROOT_ACTIVITY);
+ }
+
+ @Test
+ public void getUsageSource_notHasData_writeLoadedData() throws RemoteException {
+ when(mUsageStatsManager.getUsageSource()).thenReturn(USAGE_SOURCE_TASK_ROOT_ACTIVITY);
+
+ assertThat(DatabaseUtils.getUsageSource(mContext, mUsageStatsManager))
+ .isEqualTo(USAGE_SOURCE_TASK_ROOT_ACTIVITY);
+ assertThat(
+ DatabaseUtils
+ .getSharedPreferences(mContext)
+ .getInt(DatabaseUtils.KEY_LAST_USAGE_SOURCE, USAGE_SOURCE_CURRENT_ACTIVITY))
+ .isEqualTo(USAGE_SOURCE_TASK_ROOT_ACTIVITY);
+ }
+
+ @Test
+ public void getUsageSource_throwException_writeDefaultData() throws RemoteException {
+ when(mUsageStatsManager.getUsageSource()).thenThrow(new RemoteException());
+
+ assertThat(DatabaseUtils.getUsageSource(mContext, mUsageStatsManager))
+ .isEqualTo(USAGE_SOURCE_CURRENT_ACTIVITY);
+ assertThat(
+ DatabaseUtils
+ .getSharedPreferences(mContext)
+ .getInt(DatabaseUtils.KEY_LAST_USAGE_SOURCE, USAGE_SOURCE_CURRENT_ACTIVITY))
+ .isEqualTo(USAGE_SOURCE_CURRENT_ACTIVITY);
+ }
+
+ @Test
public void recordDateTime_writeDataIntoSharedPreferences() {
final String preferenceKey = "test_preference_key";
DatabaseUtils.recordDateTime(mContext, preferenceKey);
@@ -454,6 +533,19 @@
assertThat(dumpContent.contains("LastUploadFullChargeTime")).isTrue();
}
+ @Test
+ public void loadFromContentProvider_workProfile_transferToUserProfile() throws Exception {
+ // Test to verify b/297036263
+ doReturn(mUserManager).when(mContext).getSystemService(UserManager.class);
+ doReturn(true).when(mUserManager).isManagedProfile();
+ doReturn(UserHandle.CURRENT).when(mContext).getUser();
+ doReturn(UserHandle.SYSTEM).when(mUserManager).getProfileParent(UserHandle.CURRENT);
+
+ DatabaseUtils.loadFromContentProvider(mContext, Uri.EMPTY, null, cursor -> 1);
+
+ verify(mContext).createPackageContextAsUser(anyString(), anyInt(), any());
+ }
+
private static void verifyBatteryEntryContentValues(
double consumedPower, ContentValues values) {
final BatteryInformation batteryInformation =
@@ -497,6 +589,7 @@
BatteryHistEntry.KEY_PACKAGE_NAME,
BatteryHistEntry.KEY_TIMESTAMP,
BatteryHistEntry.KEY_UID,
- BatteryHistEntry.KEY_CONSUMER_TYPE});
+ BatteryHistEntry.KEY_CONSUMER_TYPE,
+ BatteryHistEntry.KEY_IS_FULL_CHARGE_CYCLE_START});
}
}
diff --git a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/PowerUsageAdvancedTest.java b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/PowerUsageAdvancedTest.java
new file mode 100644
index 0000000..9753bd2
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/PowerUsageAdvancedTest.java
@@ -0,0 +1,214 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.settings.fuelgauge.batteryusage;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.isNull;
+import static org.mockito.ArgumentMatchers.notNull;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+
+import android.content.Context;
+import android.util.Pair;
+
+import com.android.settings.testutils.BatteryTestUtils;
+import com.android.settings.testutils.shadow.ShadowDashboardFragment;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Config;
+
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.TimeZone;
+import java.util.function.Predicate;
+
+@RunWith(RobolectricTestRunner.class)
+@Config(shadows = ShadowDashboardFragment.class)
+public final class PowerUsageAdvancedTest {
+
+ private Context mContext;
+ private PowerUsageAdvanced mPowerUsageAdvanced;
+
+ private Predicate<PowerAnomalyEvent> mCardFilterPredicate;
+ private Predicate<PowerAnomalyEvent> mSlotFilterPredicate;
+
+ @Mock
+ private BatteryTipsController mBatteryTipsController;
+ @Mock
+ private BatteryChartPreferenceController mBatteryChartPreferenceController;
+ @Mock
+ private ScreenOnTimeController mScreenOnTimeController;
+ @Mock
+ private BatteryUsageBreakdownController mBatteryUsageBreakdownController;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ TimeZone.setDefault(TimeZone.getTimeZone("GMT+8"));
+ mContext = spy(RuntimeEnvironment.application);
+
+ mPowerUsageAdvanced = spy(new PowerUsageAdvanced());
+ mPowerUsageAdvanced.mBatteryTipsController = mBatteryTipsController;
+ mPowerUsageAdvanced.mBatteryChartPreferenceController = mBatteryChartPreferenceController;
+ mPowerUsageAdvanced.mScreenOnTimeController = mScreenOnTimeController;
+ mPowerUsageAdvanced.mBatteryUsageBreakdownController = mBatteryUsageBreakdownController;
+ mPowerUsageAdvanced.mBatteryLevelData = Optional.of(new BatteryLevelData(Map.of(
+ 1694354400000L, 1, // 2023-09-10 22:00:00
+ 1694361600000L, 2, // 2023-09-11 00:00:00
+ 1694368800000L, 3))); // 2023-09-11 02:00:00
+ doReturn(mContext).when(mPowerUsageAdvanced).getContext();
+ mSlotFilterPredicate = PowerAnomalyEvent::hasWarningItemInfo;
+ }
+
+ @Test
+ public void getFilterAnomalyEvent_withEmptyOrNullList_getNull() {
+ prepareCardFilterPredicate(null);
+ assertThat(PowerUsageAdvanced
+ .getAnomalyEvent(null, mCardFilterPredicate)).isNull();
+ assertThat(PowerUsageAdvanced
+ .getAnomalyEvent(null, mSlotFilterPredicate)).isNull();
+ assertThat(PowerUsageAdvanced.getAnomalyEvent(
+ BatteryTestUtils.createEmptyPowerAnomalyEventList(), mCardFilterPredicate))
+ .isNull();
+ assertThat(PowerUsageAdvanced.getAnomalyEvent(
+ BatteryTestUtils.createEmptyPowerAnomalyEventList(), mSlotFilterPredicate))
+ .isNull();
+ }
+
+ @Test
+ public void getFilterAnomalyEvent_withoutDismissed_getHighestScoreEvent() {
+ final PowerAnomalyEventList powerAnomalyEventList =
+ BatteryTestUtils.createNonEmptyPowerAnomalyEventList();
+
+ final PowerAnomalyEvent slotEvent =
+ PowerUsageAdvanced.getAnomalyEvent(powerAnomalyEventList,
+ mSlotFilterPredicate);
+ prepareCardFilterPredicate(slotEvent);
+ final PowerAnomalyEvent cardEvent =
+ PowerUsageAdvanced.getAnomalyEvent(powerAnomalyEventList,
+ mCardFilterPredicate);
+
+ assertThat(cardEvent).isEqualTo(BatteryTestUtils.createAdaptiveBrightnessAnomalyEvent());
+ assertThat(slotEvent).isNull();
+ }
+
+ @Test
+ public void getFilterAnomalyEvent_withBrightnessDismissed_getScreenTimeout() {
+ final PowerAnomalyEventList powerAnomalyEventList =
+ BatteryTestUtils.createNonEmptyPowerAnomalyEventList();
+ DatabaseUtils.removeDismissedPowerAnomalyKeys(mContext);
+ DatabaseUtils.setDismissedPowerAnomalyKeys(mContext, PowerAnomalyKey.KEY_BRIGHTNESS.name());
+
+ final PowerAnomalyEvent slotEvent =
+ PowerUsageAdvanced.getAnomalyEvent(powerAnomalyEventList,
+ mSlotFilterPredicate);
+ prepareCardFilterPredicate(slotEvent);
+ final PowerAnomalyEvent cardEvent =
+ PowerUsageAdvanced.getAnomalyEvent(powerAnomalyEventList,
+ mCardFilterPredicate);
+
+ assertThat(cardEvent).isEqualTo(BatteryTestUtils.createScreenTimeoutAnomalyEvent());
+ assertThat(slotEvent).isNull();
+ }
+
+ @Test
+ public void getFilterAnomalyEvent_withAllDismissed_getNull() {
+ final PowerAnomalyEventList powerAnomalyEventList =
+ BatteryTestUtils.createNonEmptyPowerAnomalyEventList();
+ DatabaseUtils.removeDismissedPowerAnomalyKeys(mContext);
+ for (PowerAnomalyKey key : PowerAnomalyKey.values()) {
+ DatabaseUtils.setDismissedPowerAnomalyKeys(mContext, key.name());
+ }
+
+ final PowerAnomalyEvent slotEvent =
+ PowerUsageAdvanced.getAnomalyEvent(powerAnomalyEventList,
+ mSlotFilterPredicate);
+ prepareCardFilterPredicate(slotEvent);
+ final PowerAnomalyEvent cardEvent =
+ PowerUsageAdvanced.getAnomalyEvent(powerAnomalyEventList,
+ mCardFilterPredicate);
+
+ assertThat(cardEvent).isNull();
+ assertThat(slotEvent).isNull();
+ }
+
+ @Test
+ public void onDisplayAnomalyEventUpdated_withSettingsAnomalyEvent_skipHighlightSlotEffect() {
+ final PowerAnomalyEvent event = BatteryTestUtils.createAdaptiveBrightnessAnomalyEvent();
+
+ mPowerUsageAdvanced.onDisplayAnomalyEventUpdated(event, event);
+
+ assertThat(mPowerUsageAdvanced.mHighlightEventWrapper.get().getEventId())
+ .isEqualTo(event.getEventId());
+ verify(mPowerUsageAdvanced.mBatteryTipsController).setOnAnomalyConfirmListener(isNull());
+ verify(mPowerUsageAdvanced.mBatteryTipsController).setOnAnomalyRejectListener(isNull());
+ verify(mPowerUsageAdvanced.mBatteryChartPreferenceController).onHighlightSlotIndexUpdate(
+ eq(BatteryChartViewModel.SELECTED_INDEX_INVALID),
+ eq(BatteryChartViewModel.SELECTED_INDEX_INVALID));
+ }
+
+ @Test
+ public void onDisplayAnomalyEventUpdated_onlyAppAnomalyEvent_setHighlightSlotEffect() {
+ final PowerAnomalyEvent event = BatteryTestUtils.createAppAnomalyEvent();
+
+ mPowerUsageAdvanced.onDisplayAnomalyEventUpdated(event, event);
+
+ assertThat(mPowerUsageAdvanced.mHighlightEventWrapper.get().getEventId())
+ .isEqualTo(event.getEventId());
+ verify(mBatteryTipsController).setOnAnomalyConfirmListener(isNull());
+ verify(mBatteryTipsController).setOnAnomalyRejectListener(isNull());
+ assertThat(mPowerUsageAdvanced.mBatteryLevelData.get().getIndexByTimestamps(
+ event.getWarningItemInfo().getStartTimestamp(),
+ event.getWarningItemInfo().getEndTimestamp()
+ )).isEqualTo(Pair.create(1, 0));
+ verify(mBatteryChartPreferenceController).onHighlightSlotIndexUpdate(eq(1), eq(0));
+ verify(mBatteryTipsController).setOnAnomalyConfirmListener(notNull());
+ }
+
+ @Test
+ public void onDisplayAnomalyEventUpdated_withSettingsCardAndAppsSlotEvent_showExpected() {
+ final PowerAnomalyEvent settingsEvent =
+ BatteryTestUtils.createAdaptiveBrightnessAnomalyEvent();
+ final PowerAnomalyEvent appsEvent =
+ BatteryTestUtils.createAppAnomalyEvent();
+
+ mPowerUsageAdvanced.onDisplayAnomalyEventUpdated(settingsEvent, appsEvent);
+
+ assertThat(mPowerUsageAdvanced.mHighlightEventWrapper.get().getEventId())
+ .isEqualTo(appsEvent.getEventId());
+ verify(mBatteryChartPreferenceController).onHighlightSlotIndexUpdate(eq(1), eq(0));
+ verify(mBatteryTipsController).setOnAnomalyConfirmListener(isNull());
+ verify(mBatteryTipsController).setOnAnomalyRejectListener(isNull());
+ }
+
+ private void prepareCardFilterPredicate(PowerAnomalyEvent slotEvent) {
+ final Set<String> dismissedPowerAnomalyKeys =
+ DatabaseUtils.getDismissedPowerAnomalyKeys(mContext);
+ mCardFilterPredicate = event -> !dismissedPowerAnomalyKeys.contains(
+ event.getDismissRecordKey())
+ && (event.equals(slotEvent) || !event.hasWarningItemInfo());
+ }
+}
\ No newline at end of file
diff --git a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/PowerUsageBaseTest.java b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/PowerUsageBaseTest.java
index 6ed10cd..68766e6 100644
--- a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/PowerUsageBaseTest.java
+++ b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/PowerUsageBaseTest.java
@@ -135,11 +135,6 @@
}
@Override
- protected boolean isBatteryHistoryNeeded() {
- return false;
- }
-
- @Override
protected void refreshUi(int refreshType) {
// Do nothing
}
diff --git a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/bugreport/BatteryUsageLogUtilsTest.java b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/bugreport/BatteryUsageLogUtilsTest.java
new file mode 100644
index 0000000..12c040e
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/bugreport/BatteryUsageLogUtilsTest.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.fuelgauge.batteryusage.bugreport;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.Context;
+
+import androidx.test.core.app.ApplicationProvider;
+
+import com.android.settings.fuelgauge.BatteryUsageHistoricalLogEntry.Action;
+
+import org.junit.Before;
+import org.junit.FixMethodOrder;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.MethodSorters;
+import org.robolectric.RobolectricTestRunner;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+@RunWith(RobolectricTestRunner.class)
+public final class BatteryUsageLogUtilsTest {
+
+ private StringWriter mTestStringWriter;
+ private PrintWriter mTestPrintWriter;
+ private Context mContext;
+
+ @Before
+ public void setUp() {
+ mContext = ApplicationProvider.getApplicationContext();
+ mTestStringWriter = new StringWriter();
+ mTestPrintWriter = new PrintWriter(mTestStringWriter);
+ BatteryUsageLogUtils.getSharedPreferences(mContext).edit().clear().commit();
+ }
+
+ @Test
+ public void printHistoricalLog_withDefaultLogs() {
+ final String expectedInformation = "nothing to dump";
+ // Environment checking.
+ assertThat(mTestStringWriter.toString().contains(expectedInformation)).isFalse();
+
+ BatteryUsageLogUtils.printHistoricalLog(mContext, mTestPrintWriter);
+ assertThat(mTestStringWriter.toString()).contains(expectedInformation);
+ }
+
+ @Test
+ public void writeLog_multipleLogs_withCorrectCounts() {
+ final int expectedCount = 10;
+ for (int i = 0; i < expectedCount; i++) {
+ BatteryUsageLogUtils.writeLog(mContext, Action.SCHEDULE_JOB, "");
+ }
+ BatteryUsageLogUtils.writeLog(mContext, Action.EXECUTE_JOB, "");
+
+ BatteryUsageLogUtils.printHistoricalLog(mContext, mTestPrintWriter);
+
+ assertActionCount("SCHEDULE_JOB", expectedCount);
+ assertActionCount("EXECUTE_JOB", 1);
+ }
+
+ @Test
+ public void writeLog_overMaxEntriesLogs_withCorrectCounts() {
+ BatteryUsageLogUtils.writeLog(mContext, Action.SCHEDULE_JOB, "");
+ BatteryUsageLogUtils.writeLog(mContext, Action.SCHEDULE_JOB, "");
+ for (int i = 0; i < BatteryUsageLogUtils.MAX_ENTRIES * 2; i++) {
+ BatteryUsageLogUtils.writeLog(mContext, Action.EXECUTE_JOB, "");
+ }
+
+ BatteryUsageLogUtils.printHistoricalLog(mContext, mTestPrintWriter);
+
+ final String dumpResults = mTestStringWriter.toString();
+ assertThat(dumpResults.contains("SCHEDULE_JOB")).isFalse();
+ assertActionCount("EXECUTE_JOB", BatteryUsageLogUtils.MAX_ENTRIES);
+ }
+
+ private void assertActionCount(String token, int count) {
+ final String dumpResults = mTestStringWriter.toString();
+ assertThat(dumpResults.split(token).length).isEqualTo(count + 1);
+ }
+}
diff --git a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/bugreport/BugReportContentProviderTest.java b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/bugreport/BugReportContentProviderTest.java
index 8365ae4..45d4065 100644
--- a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/bugreport/BugReportContentProviderTest.java
+++ b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/bugreport/BugReportContentProviderTest.java
@@ -87,6 +87,7 @@
mBugReportContentProvider.dump(FileDescriptor.out, mPrintWriter, new String[] {});
String dumpContent = mStringWriter.toString();
+ assertThat(dumpContent).contains("Battery PeriodicJob History");
assertThat(dumpContent).contains("Battery DatabaseHistory");
assertThat(dumpContent).contains(PACKAGE_NAME1);
assertThat(dumpContent).contains(PACKAGE_NAME2);
diff --git a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/db/BatteryEventDaoTest.java b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/db/BatteryEventDaoTest.java
index 941f444..8462867 100644
--- a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/db/BatteryEventDaoTest.java
+++ b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/db/BatteryEventDaoTest.java
@@ -16,6 +16,10 @@
package com.android.settings.fuelgauge.batteryusage.db;
+import static com.android.settings.fuelgauge.batteryusage.db.BatteryEventEntity.KEY_BATTERY_EVENT_TYPE;
+import static com.android.settings.fuelgauge.batteryusage.db.BatteryEventEntity.KEY_BATTERY_LEVEL;
+import static com.android.settings.fuelgauge.batteryusage.db.BatteryEventEntity.KEY_TIMESTAMP;
+
import static com.google.common.truth.Truth.assertThat;
import android.content.Context;
@@ -31,9 +35,14 @@
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
+import java.util.List;
+
/** Tests for {@link BatteryEventDao}. */
@RunWith(RobolectricTestRunner.class)
public final class BatteryEventDaoTest {
+ private static final long TIMESTAMP1 = System.currentTimeMillis();
+ private static final long TIMESTAMP2 = TIMESTAMP1 + 2;
+
private Context mContext;
private BatteryStateDatabase mDatabase;
private BatteryEventDao mBatteryEventDao;
@@ -51,8 +60,44 @@
BatteryStateDatabase.setBatteryStateDatabase(/*database=*/ null);
}
+
@Test
- public void getAllAfter_returnExpectedResult() {
+ public void getLastFullChargeTimestamp_normalFlow_expectedBehavior() throws Exception {
+ mBatteryEventDao.insert(BatteryEventEntity.newBuilder()
+ .setTimestamp(TIMESTAMP1)
+ .setBatteryEventType(3)
+ .setBatteryLevel(100)
+ .build());
+ mBatteryEventDao.insert(BatteryEventEntity.newBuilder()
+ .setTimestamp(TIMESTAMP2)
+ .setBatteryEventType(4)
+ .setBatteryLevel(96)
+ .build());
+
+ final Cursor cursor = mBatteryEventDao.getLastFullChargeTimestamp();
+ assertThat(cursor.getCount()).isEqualTo(1);
+ cursor.moveToFirst();
+ assertThat(cursor.getLong(0)).isEqualTo(TIMESTAMP1);
+ }
+
+ @Test
+ public void getLastFullChargeTimestamp_noLastFullChargeTime_returns0() throws Exception {
+ mBatteryEventDao.clearAll();
+ mBatteryEventDao.insert(BatteryEventEntity.newBuilder()
+ .setTimestamp(TIMESTAMP2)
+ .setBatteryEventType(4)
+ .setBatteryLevel(96)
+ .build());
+
+ final Cursor cursor = mBatteryEventDao.getLastFullChargeTimestamp();
+
+ assertThat(cursor.getCount()).isEqualTo(1);
+ cursor.moveToFirst();
+ assertThat(cursor.getLong(0)).isEqualTo(0L);
+ }
+
+ @Test
+ public void getAllAfter_normalFlow_returnExpectedResult() {
mBatteryEventDao.insert(BatteryEventEntity.newBuilder()
.setTimestamp(100L)
.setBatteryEventType(1)
@@ -64,17 +109,44 @@
.setBatteryLevel(88)
.build());
- final Cursor cursor = mBatteryEventDao.getAllAfter(160L);
+ final Cursor cursor = mBatteryEventDao.getAllAfter(160L, List.of(1, 2));
assertThat(cursor.getCount()).isEqualTo(1);
cursor.moveToFirst();
- assertThat(cursor.getLong(cursor.getColumnIndex(BatteryEventEntity.KEY_TIMESTAMP)))
+ assertThat(cursor.getLong(cursor.getColumnIndex(KEY_TIMESTAMP)))
.isEqualTo(200L);
- assertThat(cursor.getInt(cursor.getColumnIndex(BatteryEventEntity.KEY_BATTERY_EVENT_TYPE)))
+ assertThat(cursor.getInt(cursor.getColumnIndex(KEY_BATTERY_EVENT_TYPE)))
.isEqualTo(2);
- assertThat(cursor.getInt(cursor.getColumnIndex(BatteryEventEntity.KEY_BATTERY_LEVEL)))
+ assertThat(cursor.getInt(cursor.getColumnIndex(KEY_BATTERY_LEVEL)))
.isEqualTo(88);
mBatteryEventDao.clearAll();
assertThat(mBatteryEventDao.getAll()).isEmpty();
}
+
+ @Test
+ public void getAllAfter_filterBatteryTypes_returnExpectedResult() {
+ mBatteryEventDao.insert(BatteryEventEntity.newBuilder()
+ .setTimestamp(100L)
+ .setBatteryEventType(1)
+ .setBatteryLevel(66)
+ .build());
+ mBatteryEventDao.insert(BatteryEventEntity.newBuilder()
+ .setTimestamp(200L)
+ .setBatteryEventType(2)
+ .setBatteryLevel(88)
+ .build());
+
+ final Cursor cursor = mBatteryEventDao.getAllAfter(0L, List.of(1));
+ assertThat(cursor.getCount()).isEqualTo(1);
+ cursor.moveToFirst();
+ assertThat(cursor.getLong(cursor.getColumnIndex(KEY_TIMESTAMP)))
+ .isEqualTo(100L);
+ assertThat(cursor.getInt(cursor.getColumnIndex(KEY_BATTERY_EVENT_TYPE)))
+ .isEqualTo(1);
+ assertThat(cursor.getInt(cursor.getColumnIndex(KEY_BATTERY_LEVEL)))
+ .isEqualTo(66);
+
+ mBatteryEventDao.clearAll();
+ assertThat(mBatteryEventDao.getAll()).isEmpty();
+ }
}
diff --git a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/db/BatteryStateDaoTest.java b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/db/BatteryStateDaoTest.java
index 57cf648..b3dba4e 100644
--- a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/db/BatteryStateDaoTest.java
+++ b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/db/BatteryStateDaoTest.java
@@ -37,9 +37,10 @@
@RunWith(RobolectricTestRunner.class)
public final class BatteryStateDaoTest {
private static final int CURSOR_COLUMN_SIZE = 9;
- private static final long TIMESTAMP1 = System.currentTimeMillis();
- private static final long TIMESTAMP2 = System.currentTimeMillis() + 2;
- private static final long TIMESTAMP3 = System.currentTimeMillis() + 4;
+ private static final long CURRENT = System.currentTimeMillis();
+ private static final long TIMESTAMP1 = CURRENT;
+ private static final long TIMESTAMP2 = CURRENT + 2;
+ private static final long TIMESTAMP3 = CURRENT + 4;
private static final String PACKAGE_NAME1 = "com.android.apps.settings";
private static final String PACKAGE_NAME2 = "com.android.apps.calendar";
private static final String PACKAGE_NAME3 = "com.android.apps.gmail";
@@ -67,7 +68,7 @@
}
@Test
- public void batteryStateDao_insertAll() throws Exception {
+ public void insertAll_normalFlow_expectedBehavior() throws Exception {
final List<BatteryState> states = mBatteryStateDao.getAllAfter(TIMESTAMP1);
assertThat(states).hasSize(2);
// Verifies the queried battery states.
@@ -76,8 +77,26 @@
}
@Test
- public void batteryStateDao_getCursorSinceLastFullCharge() throws Exception {
- final Cursor cursor1 = mBatteryStateDao.getCursorSinceLastFullCharge(TIMESTAMP1);
+ public void getLatestTimestamp_normalFlow_expectedBehavior() throws Exception {
+ final Cursor cursor1 = mBatteryStateDao.getLatestTimestampBefore(TIMESTAMP1 - 1);
+ assertThat(cursor1.getCount()).isEqualTo(1);
+ cursor1.moveToFirst();
+ assertThat(cursor1.getLong(0)).isEqualTo(0L);
+
+ final Cursor cursor2 = mBatteryStateDao.getLatestTimestampBefore(TIMESTAMP2);
+ assertThat(cursor2.getCount()).isEqualTo(1);
+ cursor2.moveToFirst();
+ assertThat(cursor2.getLong(0)).isEqualTo(TIMESTAMP2);
+
+ final Cursor cursor3 = mBatteryStateDao.getLatestTimestampBefore(TIMESTAMP3 + 1);
+ assertThat(cursor3.getCount()).isEqualTo(1);
+ cursor3.moveToFirst();
+ assertThat(cursor3.getLong(0)).isEqualTo(TIMESTAMP3);
+ }
+
+ @Test
+ public void getBatteryStatesAfter_normalFlow_expectedBehavior() throws Exception {
+ final Cursor cursor1 = mBatteryStateDao.getBatteryStatesAfter(TIMESTAMP1);
assertThat(cursor1.getCount()).isEqualTo(3);
assertThat(cursor1.getColumnCount()).isEqualTo(CURSOR_COLUMN_SIZE);
// Verifies the queried first battery state.
@@ -90,7 +109,7 @@
cursor1.moveToNext();
assertThat(cursor1.getString(3 /*packageName*/)).isEqualTo(PACKAGE_NAME3);
- final Cursor cursor2 = mBatteryStateDao.getCursorSinceLastFullCharge(TIMESTAMP3);
+ final Cursor cursor2 = mBatteryStateDao.getBatteryStatesAfter(TIMESTAMP3);
assertThat(cursor2.getCount()).isEqualTo(1);
assertThat(cursor2.getColumnCount()).isEqualTo(CURSOR_COLUMN_SIZE);
// Verifies the queried first battery state.
@@ -99,25 +118,7 @@
}
@Test
- public void batteryStateDao_getCursorSinceLastFullCharge_noFullChargeData_returnSevenDaysData()
- throws Exception {
- mBatteryStateDao.clearAll();
- BatteryTestUtils.insertDataToBatteryStateTable(mContext, TIMESTAMP3, PACKAGE_NAME3);
- BatteryTestUtils.insertDataToBatteryStateTable(mContext, TIMESTAMP2, PACKAGE_NAME2);
- BatteryTestUtils.insertDataToBatteryStateTable(mContext, TIMESTAMP1, PACKAGE_NAME1);
- final Cursor cursor = mBatteryStateDao.getCursorSinceLastFullCharge(TIMESTAMP2);
- assertThat(cursor.getCount()).isEqualTo(2);
- assertThat(cursor.getColumnCount()).isEqualTo(CURSOR_COLUMN_SIZE);
- // Verifies the queried first battery state.
- cursor.moveToFirst();
- assertThat(cursor.getString(3 /*packageName*/)).isEqualTo(PACKAGE_NAME2);
- // Verifies the queried third battery state.
- cursor.moveToNext();
- assertThat(cursor.getString(3 /*packageName*/)).isEqualTo(PACKAGE_NAME3);
- }
-
- @Test
- public void batteryStateDao_clearAllBefore() throws Exception {
+ public void clearAllBefore_normalFlow_expectedBehavior() throws Exception {
mBatteryStateDao.clearAllBefore(TIMESTAMP2);
final List<BatteryState> states = mBatteryStateDao.getAllAfter(0);
@@ -127,20 +128,20 @@
}
@Test
- public void batteryStateDao_clearAll() throws Exception {
+ public void clearAll_normalFlow_expectedBehavior() throws Exception {
assertThat(mBatteryStateDao.getAllAfter(0)).hasSize(3);
mBatteryStateDao.clearAll();
assertThat(mBatteryStateDao.getAllAfter(0)).isEmpty();
}
@Test
- public void getInstance_createNewInstance() throws Exception {
+ public void getInstance_createNewInstance_returnsExpectedResult() throws Exception {
BatteryStateDatabase.setBatteryStateDatabase(/*database=*/ null);
assertThat(BatteryStateDatabase.getInstance(mContext)).isNotNull();
}
@Test
- public void getDistinctTimestampCount_returnsExpectedResult() {
+ public void getDistinctTimestampCount_normalFlow_returnsExpectedResult() {
assertThat(mBatteryStateDao.getDistinctTimestampCount(/*timestamp=*/ 0))
.isEqualTo(3);
assertThat(mBatteryStateDao.getDistinctTimestampCount(TIMESTAMP1))
@@ -148,7 +149,7 @@
}
@Test
- public void getDistinctTimestamps_returnsExpectedResult() {
+ public void getDistinctTimestamps_normalFlow_returnsExpectedResult() {
final List<Long> timestamps =
mBatteryStateDao.getDistinctTimestamps(/*timestamp=*/ 0);
diff --git a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/db/BatteryUsageSlotDaoTest.java b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/db/BatteryUsageSlotDaoTest.java
new file mode 100644
index 0000000..6f73954
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/db/BatteryUsageSlotDaoTest.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.fuelgauge.batteryusage.db;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.Context;
+import android.database.Cursor;
+
+import androidx.test.core.app.ApplicationProvider;
+
+import com.android.settings.testutils.BatteryTestUtils;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+import java.util.List;
+
+/** Tests for {@link BatteryUsageSlotDao}. */
+@RunWith(RobolectricTestRunner.class)
+public final class BatteryUsageSlotDaoTest {
+ private static final int CURSOR_COLUMN_SIZE = 3;
+ private static final long CURRENT = System.currentTimeMillis();
+ private static final long TIMESTAMP1 = CURRENT;
+ private static final long TIMESTAMP2 = CURRENT + 2;
+ private static final String BATTERY_USAGE_SLOT_STRING1 = "BATTERY_USAGE_SLOT_STRING1";
+ private static final String BATTERY_USAGE_SLOT_STRING2 = "BATTERY_USAGE_SLOT_STRING2";
+
+ private Context mContext;
+ private BatteryStateDatabase mDatabase;
+ private BatteryUsageSlotDao mBatteryUsageSlotDao;
+
+ @Before
+ public void setUp() {
+ mContext = ApplicationProvider.getApplicationContext();
+ mDatabase = BatteryTestUtils.setUpBatteryStateDatabase(mContext);
+ mBatteryUsageSlotDao = mDatabase.batteryUsageSlotDao();
+ mBatteryUsageSlotDao.insert(
+ new BatteryUsageSlotEntity(TIMESTAMP1, BATTERY_USAGE_SLOT_STRING1));
+ mBatteryUsageSlotDao.insert(
+ new BatteryUsageSlotEntity(TIMESTAMP2, BATTERY_USAGE_SLOT_STRING2));
+ }
+
+ @After
+ public void closeDb() {
+ mDatabase.close();
+ BatteryStateDatabase.setBatteryStateDatabase(/*database=*/ null);
+ }
+
+ @Test
+ public void getAll_normalFlow_expectedBehavior() throws Exception {
+ final List<BatteryUsageSlotEntity> entities = mBatteryUsageSlotDao.getAll();
+ assertThat(entities).hasSize(2);
+ assertThat(entities.get(0).timestamp).isEqualTo(TIMESTAMP1);
+ assertThat(entities.get(0).batteryUsageSlot).isEqualTo(BATTERY_USAGE_SLOT_STRING1);
+ assertThat(entities.get(1).timestamp).isEqualTo(TIMESTAMP2);
+ assertThat(entities.get(1).batteryUsageSlot).isEqualTo(BATTERY_USAGE_SLOT_STRING2);
+ }
+
+ @Test
+ public void getAllAfter_normalFlow_expectedBehavior() throws Exception {
+ final Cursor cursor1 = mBatteryUsageSlotDao.getAllAfter(TIMESTAMP1);
+ assertThat(cursor1.getCount()).isEqualTo(2);
+ assertThat(cursor1.getColumnCount()).isEqualTo(CURSOR_COLUMN_SIZE);
+ cursor1.moveToFirst();
+ assertThat(cursor1.getLong(1 /*timestamp*/)).isEqualTo(TIMESTAMP1);
+ cursor1.moveToNext();
+ assertThat(cursor1.getLong(1 /*timestamp*/)).isEqualTo(TIMESTAMP2);
+
+ final Cursor cursor2 = mBatteryUsageSlotDao.getAllAfter(TIMESTAMP1 + 1);
+ assertThat(cursor2.getCount()).isEqualTo(1);
+ assertThat(cursor2.getColumnCount()).isEqualTo(CURSOR_COLUMN_SIZE);
+ cursor2.moveToFirst();
+ assertThat(cursor2.getLong(1 /*timestamp*/)).isEqualTo(TIMESTAMP2);
+ }
+
+ @Test
+ public void clearAllBefore_normalFlow_expectedBehavior() throws Exception {
+ mBatteryUsageSlotDao.clearAllBefore(TIMESTAMP1);
+
+ final List<BatteryUsageSlotEntity> entities = mBatteryUsageSlotDao.getAll();
+ assertThat(entities).hasSize(1);
+ assertThat(entities.get(0).timestamp).isEqualTo(TIMESTAMP2);
+ assertThat(entities.get(0).batteryUsageSlot).isEqualTo(BATTERY_USAGE_SLOT_STRING2);
+ }
+
+ @Test
+ public void clearAll_normalFlow_expectedBehavior() throws Exception {
+ mBatteryUsageSlotDao.clearAll();
+
+ assertThat(mBatteryUsageSlotDao.getAll()).isEmpty();
+ }
+}
diff --git a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/db/BatteryUsageSlotEntityTest.java b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/db/BatteryUsageSlotEntityTest.java
new file mode 100644
index 0000000..ef276eb
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/db/BatteryUsageSlotEntityTest.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.fuelgauge.batteryusage.db;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+/** Tests for {@link BatteryUsageSlotEntity}. */
+@RunWith(RobolectricTestRunner.class)
+public final class BatteryUsageSlotEntityTest {
+
+ @Test
+ public void testBuilder_returnsExpectedResult() {
+ final long timestamp = 10001L;
+ final String batteryUsageSlotString = "batteryUsageSlotString";
+
+ BatteryUsageSlotEntity entity = BatteryUsageSlotEntity
+ .newBuilder()
+ .setTimestamp(timestamp)
+ .setBatteryUsageSlot(batteryUsageSlotString)
+ .build();
+
+ // Verifies the app relative information.
+ assertThat(entity.timestamp).isEqualTo(timestamp);
+ assertThat(entity.batteryUsageSlot).isEqualTo(batteryUsageSlotString);
+ }
+}
diff --git a/tests/robotests/src/com/android/settings/inputmethod/TrackpadReverseScrollingPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/inputmethod/TrackpadReverseScrollingPreferenceControllerTest.java
index a99abb8..93c4844 100644
--- a/tests/robotests/src/com/android/settings/inputmethod/TrackpadReverseScrollingPreferenceControllerTest.java
+++ b/tests/robotests/src/com/android/settings/inputmethod/TrackpadReverseScrollingPreferenceControllerTest.java
@@ -16,7 +16,6 @@
package com.android.settings.inputmethod;
-
import static com.google.common.truth.Truth.assertThat;
import android.content.Context;
diff --git a/tests/robotests/src/com/android/settings/localepicker/LocaleDialogFragmentTest.java b/tests/robotests/src/com/android/settings/localepicker/LocaleDialogFragmentTest.java
new file mode 100644
index 0000000..57f2b01
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/localepicker/LocaleDialogFragmentTest.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.localepicker;
+
+import static com.android.settings.localepicker.LocaleDialogFragment.ARG_DIALOG_TYPE;
+import static com.android.settings.localepicker.LocaleDialogFragment.ARG_TARGET_LOCALE;
+import static com.android.settings.localepicker.LocaleDialogFragment.DIALOG_CONFIRM_SYSTEM_DEFAULT;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.verify;
+
+import android.os.Bundle;
+import android.window.OnBackInvokedDispatcher;
+
+import androidx.appcompat.app.AlertDialog;
+import androidx.fragment.app.FragmentActivity;
+import androidx.fragment.app.FragmentManager;
+import androidx.fragment.app.FragmentTransaction;
+
+import com.android.internal.app.LocaleStore;
+import com.android.settings.testutils.shadow.ShadowAlertDialogCompat;
+import com.android.settings.utils.ActivityControllerWrapper;
+import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.Robolectric;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Config;
+
+import java.util.Locale;
+
+@RunWith(RobolectricTestRunner.class)
+@Config(shadows = {ShadowAlertDialogCompat.class})
+public class LocaleDialogFragmentTest {
+
+ @Mock
+ private OnBackInvokedDispatcher mOnBackInvokedDispatcher;
+
+ private FragmentActivity mActivity;
+ private LocaleDialogFragment mDialogFragment;
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+
+ mActivity = (FragmentActivity) ActivityControllerWrapper.setup(
+ Robolectric.buildActivity(FragmentActivity.class)).get();
+ mDialogFragment = LocaleDialogFragment.newInstance();
+ LocaleStore.LocaleInfo localeInfo = LocaleStore.getLocaleInfo(Locale.ENGLISH);
+ Bundle args = new Bundle();
+ args.putInt(ARG_DIALOG_TYPE, DIALOG_CONFIRM_SYSTEM_DEFAULT);
+ args.putSerializable(ARG_TARGET_LOCALE, localeInfo);
+ mDialogFragment.setArguments(args);
+ FragmentManager fragmentManager = mActivity.getSupportFragmentManager();
+ FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
+ fragmentTransaction.add(mDialogFragment, null);
+ fragmentTransaction.commit();
+ }
+
+ @Test
+ public void onCreateDialog_onBackInvokedCallbackIsRegistered() {
+ mDialogFragment.setBackDispatcher(mOnBackInvokedDispatcher);
+ mDialogFragment.onCreateDialog(null);
+
+ verify(mOnBackInvokedDispatcher).registerOnBackInvokedCallback(
+ eq(OnBackInvokedDispatcher.PRIORITY_DEFAULT), any());
+ }
+
+ @Test
+ public void onBackInvoked_dialogIsStillDisplaying() {
+ mDialogFragment.setBackDispatcher(mOnBackInvokedDispatcher);
+ AlertDialog alertDialog = (AlertDialog) mDialogFragment.onCreateDialog(null);
+ alertDialog.show();
+ assertThat(alertDialog).isNotNull();
+ assertThat(alertDialog.isShowing()).isTrue();
+
+ mOnBackInvokedDispatcher.registerOnBackInvokedCallback(
+ eq(OnBackInvokedDispatcher.PRIORITY_DEFAULT), any());
+
+ mDialogFragment.getBackInvokedCallback().onBackInvoked();
+
+ assertThat(alertDialog.isShowing()).isTrue();
+
+ }
+}
diff --git a/tests/robotests/src/com/android/settings/localepicker/LocaleListEditorTest.java b/tests/robotests/src/com/android/settings/localepicker/LocaleListEditorTest.java
index 16d51be..985f681 100644
--- a/tests/robotests/src/com/android/settings/localepicker/LocaleListEditorTest.java
+++ b/tests/robotests/src/com/android/settings/localepicker/LocaleListEditorTest.java
@@ -20,6 +20,7 @@
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.anyBoolean;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -27,12 +28,17 @@
import android.app.Activity;
import android.app.IActivityManager;
import android.content.Context;
+import android.content.DialogInterface;
import android.content.Intent;
import android.content.res.Configuration;
import android.os.Bundle;
import android.os.LocaleList;
import android.view.MotionEvent;
import android.view.View;
+import android.view.ViewGroup;
+import android.widget.CheckBox;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
import android.widget.TextView;
import androidx.appcompat.app.AlertDialog;
@@ -45,6 +51,7 @@
import com.android.settings.testutils.FakeFeatureFactory;
import com.android.settings.testutils.shadow.ShadowActivityManager;
import com.android.settings.testutils.shadow.ShadowAlertDialogCompat;
+import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
import org.junit.After;
import org.junit.Before;
@@ -73,11 +80,12 @@
private static final int REQUEST_CONFIRM_SYSTEM_DEFAULT = 1;
private LocaleListEditor mLocaleListEditor;
-
private Context mContext;
private FragmentActivity mActivity;
- private List mLocaleList;
+ private List<LocaleStore.LocaleInfo> mLocaleList;
private Intent mIntent = new Intent();
+ private LocaleDragCell mLocaleDragCell;
+ private LocaleDragAndDropAdapter.CustomViewHolder mCustomViewHolder;
@Mock
private LocaleDragAndDropAdapter mAdapter;
@@ -91,11 +99,25 @@
private View mView;
@Mock
private IActivityManager mActivityService;
+ @Mock
+ private MetricsFeatureProvider mMetricsFeatureProvider;
+ @Mock
+ private TextView mLabel;
+ @Mock
+ private CheckBox mCheckbox;
+ @Mock
+ private TextView mMiniLabel;
+ @Mock
+ private TextView mLocalized;
+ @Mock
+ private TextView mCurrentDefault;
+ @Mock
+ private ImageView mDragHandle;
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
- mContext = RuntimeEnvironment.application;
+ mContext = spy(RuntimeEnvironment.application);
mLocaleListEditor = spy(new LocaleListEditor());
when(mLocaleListEditor.getContext()).thenReturn(mContext);
mActivity = Robolectric.buildActivity(FragmentActivity.class).get();
@@ -108,6 +130,8 @@
RuntimeEnvironment.application.getSystemService(Context.USER_SERVICE));
ReflectionHelpers.setField(mLocaleListEditor, "mAdapter", mAdapter);
ReflectionHelpers.setField(mLocaleListEditor, "mFragmentManager", mFragmentManager);
+ ReflectionHelpers.setField(mLocaleListEditor, "mMetricsFeatureProvider",
+ mMetricsFeatureProvider);
when(mFragmentManager.beginTransaction()).thenReturn(mFragmentTransaction);
FakeFeatureFactory.setupForTest();
}
@@ -200,6 +224,38 @@
}
@Test
+ public void showConfirmDialog_systemLocaleSelected_shouldShowLocaleChangeDialog()
+ throws Exception {
+ //pre-condition
+ setUpLocaleConditions();
+ final Configuration config = new Configuration();
+ config.setLocales((LocaleList.forLanguageTags("zh-TW,en-US")));
+ when(mActivityService.getConfiguration()).thenReturn(config);
+ when(mAdapter.getFeedItemList()).thenReturn(mLocaleList);
+ when(mAdapter.getCheckedCount()).thenReturn(1);
+ when(mAdapter.getItemCount()).thenReturn(2);
+ when(mAdapter.isFirstLocaleChecked()).thenReturn(true);
+ ReflectionHelpers.setField(mLocaleListEditor, "mRemoveMode", true);
+ ReflectionHelpers.setField(mLocaleListEditor, "mShowingRemoveDialog", true);
+
+ //launch the first dialog
+ mLocaleListEditor.showRemoveLocaleWarningDialog();
+
+ final AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
+
+ assertThat(dialog).isNotNull();
+
+ // click the remove button
+ dialog.getButton(DialogInterface.BUTTON_POSITIVE).performClick();
+
+ assertThat(dialog.isShowing()).isFalse();
+
+ // check the second dialog is showing
+ verify(mFragmentTransaction).add(any(LocaleDialogFragment.class),
+ eq(TAG_DIALOG_CONFIRM_SYSTEM_DEFAULT));
+ }
+
+ @Test
public void mayAppendUnicodeTags_appendUnicodeTags_success() {
LocaleStore.LocaleInfo localeInfo = LocaleStore.fromLocale(Locale.forLanguageTag("en-US"));
@@ -262,6 +318,34 @@
verify(mAdapter).doTheUpdate();
}
+ @Test
+ public void onBindViewHolder_shouldSetCheckedBoxText() {
+ ReflectionHelpers.setField(mLocaleListEditor, "mRemoveMode", true);
+ mLocaleList = new ArrayList<>();
+ mLocaleList.add(mLocaleInfo);
+ when(mLocaleInfo.getFullNameNative()).thenReturn("English");
+ when(mLocaleInfo.getLocale()).thenReturn(LocaleList.forLanguageTags("en-US").get(0));
+
+ mAdapter = spy(new LocaleDragAndDropAdapter(mLocaleListEditor, mLocaleList));
+ ReflectionHelpers.setField(mAdapter, "mFeedItemList", mLocaleList);
+ ReflectionHelpers.setField(mAdapter, "mCacheItemList", new ArrayList<>(mLocaleList));
+ ReflectionHelpers.setField(mAdapter, "mContext", mContext);
+ ViewGroup view = new FrameLayout(mContext);
+ mCustomViewHolder = mAdapter.onCreateViewHolder(view, 0);
+ mLocaleDragCell = new LocaleDragCell(mContext, null);
+ ReflectionHelpers.setField(mCustomViewHolder, "mLocaleDragCell", mLocaleDragCell);
+ ReflectionHelpers.setField(mLocaleDragCell, "mLabel", mLabel);
+ ReflectionHelpers.setField(mLocaleDragCell, "mLocalized", mLocalized);
+ ReflectionHelpers.setField(mLocaleDragCell, "mCurrentDefault", mCurrentDefault);
+ ReflectionHelpers.setField(mLocaleDragCell, "mMiniLabel", mMiniLabel);
+ ReflectionHelpers.setField(mLocaleDragCell, "mDragHandle", mDragHandle);
+ ReflectionHelpers.setField(mLocaleDragCell, "mCheckbox", mCheckbox);
+
+ mAdapter.onBindViewHolder(mCustomViewHolder, 0);
+
+ verify(mAdapter).setCheckBoxDescription(any(LocaleDragCell.class), any(), anyBoolean());
+ }
+
private void setUpLocaleConditions() {
ShadowActivityManager.setService(mActivityService);
mLocaleList = new ArrayList<>();
diff --git a/tests/robotests/src/com/android/settings/media/MediaOutputIndicatorWorkerTest.java b/tests/robotests/src/com/android/settings/media/MediaOutputIndicatorWorkerTest.java
index cd06bae..444fa1e 100644
--- a/tests/robotests/src/com/android/settings/media/MediaOutputIndicatorWorkerTest.java
+++ b/tests/robotests/src/com/android/settings/media/MediaOutputIndicatorWorkerTest.java
@@ -313,4 +313,18 @@
assertThat(mMediaOutputIndicatorWorker.isBroadcastSupported()).isTrue();
}
+
+ @Test
+ public void isBroadcastSupported_noLocalMediaManager_returnFalse() {
+ mMediaOutputIndicatorWorker.mLocalMediaManager = null;
+
+ assertThat(mMediaOutputIndicatorWorker.isBroadcastSupported()).isFalse();
+ }
+
+ @Test
+ public void isDeviceBroadcasting_noLocalMediaManager_returnFalse() {
+ mMediaOutputIndicatorWorker.mLocalMediaManager = null;
+
+ assertThat(mMediaOutputIndicatorWorker.isDeviceBroadcasting()).isFalse();
+ }
}
diff --git a/tests/robotests/src/com/android/settings/network/telephony/DataUsagePreferenceControllerTest.java b/tests/robotests/src/com/android/settings/network/telephony/DataUsagePreferenceControllerTest.java
deleted file mode 100644
index a3be60d..0000000
--- a/tests/robotests/src/com/android/settings/network/telephony/DataUsagePreferenceControllerTest.java
+++ /dev/null
@@ -1,139 +0,0 @@
-/*
- * Copyright (C) 2018 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.settings.network.telephony;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Mockito.doNothing;
-import static org.mockito.Mockito.doReturn;
-import static org.mockito.Mockito.spy;
-
-import android.app.usage.NetworkStatsManager;
-import android.content.Context;
-import android.content.Intent;
-import android.provider.Settings;
-import android.telephony.SubscriptionManager;
-import android.telephony.TelephonyManager;
-import android.util.DataUnit;
-
-import androidx.preference.SwitchPreference;
-
-import com.android.settings.core.BasePreferenceController;
-import com.android.settingslib.net.DataUsageController;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.ArgumentCaptor;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-import org.robolectric.RobolectricTestRunner;
-import org.robolectric.RuntimeEnvironment;
-import org.robolectric.Shadows;
-import org.robolectric.shadows.ShadowTelephonyManager;
-
-@RunWith(RobolectricTestRunner.class)
-public class DataUsagePreferenceControllerTest {
- private static final int SUB_ID = 2;
-
- @Mock
- private NetworkStatsManager mNetworkStatsManager;
- private DataUsagePreferenceController mController;
- private SwitchPreference mPreference;
- private Context mContext;
-
- @Before
- public void setUp() {
- MockitoAnnotations.initMocks(this);
-
- mContext = spy(RuntimeEnvironment.application);
-
- final TelephonyManager telephonyManager = mContext.getSystemService(TelephonyManager.class);
- final ShadowTelephonyManager shadowTelephonyManager = Shadows.shadowOf(telephonyManager);
- shadowTelephonyManager.setTelephonyManagerForSubscriptionId(SUB_ID, telephonyManager);
- shadowTelephonyManager.setTelephonyManagerForSubscriptionId(
- SubscriptionManager.INVALID_SUBSCRIPTION_ID, telephonyManager);
-
- doReturn(mNetworkStatsManager).when(mContext).getSystemService(NetworkStatsManager.class);
-
- mPreference = new SwitchPreference(mContext);
- mController = spy(new DataUsagePreferenceController(mContext, "data_usage"));
- mController.init(SUB_ID);
- mPreference.setKey(mController.getPreferenceKey());
- }
-
- @Test
- public void getAvailabilityStatus_validSubId_returnAvailable() {
- assertThat(mController.getAvailabilityStatus()).isEqualTo(
- BasePreferenceController.AVAILABLE);
- }
-
- @Test
- public void getAvailabilityStatus_invalidSubId_returnUnsearchable() {
- mController.init(SubscriptionManager.INVALID_SUBSCRIPTION_ID);
-
- assertThat(mController.getAvailabilityStatus()).isEqualTo(
- BasePreferenceController.AVAILABLE_UNSEARCHABLE);
- }
-
- @Test
- public void handlePreferenceTreeClick_needDialog_showDialog() {
- final ArgumentCaptor<Intent> captor = ArgumentCaptor.forClass(Intent.class);
- doNothing().when(mContext).startActivity(captor.capture());
-
- mController.handlePreferenceTreeClick(mPreference);
-
- final Intent intent = captor.getValue();
-
- assertThat(intent.getAction()).isEqualTo(Settings.ACTION_MOBILE_DATA_USAGE);
- assertThat(intent.getIntExtra(Settings.EXTRA_SUB_ID, 0)).isEqualTo(SUB_ID);
- }
-
- @Test
- public void updateState_invalidSubId_disabled() {
- mController.init(SubscriptionManager.INVALID_SUBSCRIPTION_ID);
-
- mController.updateState(mPreference);
-
- assertThat(mPreference.isEnabled()).isFalse();
- }
-
- @Test
- public void updateState_noUsageData_shouldDisablePreference() {
- final DataUsageController.DataUsageInfo usageInfo =
- new DataUsageController.DataUsageInfo();
- doReturn(usageInfo).when(mController).getDataUsageInfo(any());
-
- mController.updateState(mPreference);
-
- assertThat(mPreference.isEnabled()).isFalse();
- }
-
- @Test
- public void updateState_shouldUseIECUnit() {
- final DataUsageController.DataUsageInfo usageInfo =
- new DataUsageController.DataUsageInfo();
- usageInfo.usageLevel = DataUnit.MEBIBYTES.toBytes(1);
- doReturn(usageInfo).when(mController).getDataUsageInfo(any());
-
- mController.updateState(mPreference);
-
- assertThat(mPreference.getSummary().toString())
- .contains("1.00 MB");
- }
-}
diff --git a/tests/robotests/src/com/android/settings/network/tether/TetherSettingsTest.java b/tests/robotests/src/com/android/settings/network/tether/TetherSettingsTest.java
index bf03e82..7fe1187 100644
--- a/tests/robotests/src/com/android/settings/network/tether/TetherSettingsTest.java
+++ b/tests/robotests/src/com/android/settings/network/tether/TetherSettingsTest.java
@@ -131,10 +131,12 @@
@Test
@Config(shadows = ShadowRestrictedSettingsFragment.class)
public void onCreate_isUiRestricted_doNotSetupViewModel() {
+ doNothing().when(mTetherSettings).addPreferencesFromResource(anyInt());
when(mTetherSettings.isUiRestricted()).thenReturn(true);
mTetherSettings.onCreate(null);
+ verify(mTetherSettings).addPreferencesFromResource(anyInt());
verify(mTetherSettings, never()).setupViewModel();
}
diff --git a/tests/robotests/src/com/android/settings/notification/VolumeSeekBarPreferenceTest.java b/tests/robotests/src/com/android/settings/notification/VolumeSeekBarPreferenceTest.java
index 59f0bcb..47bf99d 100644
--- a/tests/robotests/src/com/android/settings/notification/VolumeSeekBarPreferenceTest.java
+++ b/tests/robotests/src/com/android/settings/notification/VolumeSeekBarPreferenceTest.java
@@ -17,62 +17,81 @@
package com.android.settings.notification;
import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doCallRealMethod;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.content.Context;
+import android.content.res.Configuration;
+import android.content.res.Resources;
import android.media.AudioManager;
+import android.os.LocaleList;
import android.preference.SeekBarVolumizer;
import android.widget.SeekBar;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.robolectric.RobolectricTestRunner;
+import java.util.Locale;
+
@RunWith(RobolectricTestRunner.class)
public class VolumeSeekBarPreferenceTest {
private static final CharSequence CONTENT_DESCRIPTION = "TEST";
+ private static final int STREAM = 5;
@Mock
private AudioManager mAudioManager;
@Mock
private VolumeSeekBarPreference mPreference;
@Mock
private Context mContext;
+
+ @Mock
+ private Resources mRes;
+ @Mock
+ private Configuration mConfig;
@Mock
private SeekBar mSeekBar;
+ @Captor
+ private ArgumentCaptor<SeekBarVolumizer.Callback> mSbvc;
@Mock
private SeekBarVolumizer mVolumizer;
+ @Mock
+ private SeekBarVolumizerFactory mSeekBarVolumizerFactory;
private VolumeSeekBarPreference.Listener mListener;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
when(mContext.getSystemService(Context.AUDIO_SERVICE)).thenReturn(mAudioManager);
+ when(mSeekBarVolumizerFactory.create(eq(STREAM), eq(null), mSbvc.capture()))
+ .thenReturn(mVolumizer);
+ doCallRealMethod().when(mPreference).setStream(anyInt());
doCallRealMethod().when(mPreference).updateContentDescription(CONTENT_DESCRIPTION);
mPreference.mSeekBar = mSeekBar;
mPreference.mAudioManager = mAudioManager;
- mPreference.mVolumizer = mVolumizer;
+ mPreference.mSeekBarVolumizerFactory = mSeekBarVolumizerFactory;
mListener = () -> mPreference.updateContentDescription(CONTENT_DESCRIPTION);
}
@Test
public void setStream_shouldSetMinMaxAndProgress() {
- final int stream = 5;
final int max = 17;
final int min = 1;
final int progress = 4;
- when(mAudioManager.getStreamMaxVolume(stream)).thenReturn(max);
- when(mAudioManager.getStreamMinVolumeInt(stream)).thenReturn(min);
- when(mAudioManager.getStreamVolume(stream)).thenReturn(progress);
- doCallRealMethod().when(mPreference).setStream(anyInt());
+ when(mAudioManager.getStreamMaxVolume(STREAM)).thenReturn(max);
+ when(mAudioManager.getStreamMinVolumeInt(STREAM)).thenReturn(min);
+ when(mAudioManager.getStreamVolume(STREAM)).thenReturn(progress);
- mPreference.setStream(stream);
+ mPreference.setStream(STREAM);
verify(mPreference).setMax(max);
verify(mPreference).setMin(min);
@@ -84,6 +103,7 @@
doCallRealMethod().when(mPreference).setListener(mListener);
doCallRealMethod().when(mPreference).init();
+ mPreference.setStream(STREAM);
mPreference.setListener(mListener);
mPreference.init();
@@ -94,8 +114,69 @@
public void init_listenerNotSet_noException() {
doCallRealMethod().when(mPreference).init();
+ mPreference.setStream(STREAM);
mPreference.init();
verify(mPreference, never()).updateContentDescription(CONTENT_DESCRIPTION);
}
+
+ @Test
+ public void init_changeProgress_overrideStateDescriptionCalled() {
+ final int progress = 4;
+ when(mPreference.formatStateDescription(progress)).thenReturn(CONTENT_DESCRIPTION);
+ doCallRealMethod().when(mPreference).init();
+
+ mPreference.setStream(STREAM);
+ mPreference.init();
+
+ verify(mSeekBarVolumizerFactory).create(eq(STREAM), eq(null), mSbvc.capture());
+
+ mSbvc.getValue().onProgressChanged(mSeekBar, 4, true);
+
+ verify(mPreference).overrideSeekBarStateDescription(CONTENT_DESCRIPTION);
+ }
+
+ @Test
+ public void init_changeProgress_stateDescriptionValueUpdated() {
+ final int max = 17;
+ final int min = 1;
+ int progress = 4;
+ when(mAudioManager.getStreamMaxVolume(STREAM)).thenReturn(max);
+ when(mAudioManager.getStreamMinVolumeInt(STREAM)).thenReturn(min);
+ when(mAudioManager.getStreamVolume(STREAM)).thenReturn(progress);
+ when(mPreference.getMin()).thenReturn(min);
+ when(mPreference.getMax()).thenReturn(max);
+ when(mPreference.getContext()).thenReturn(mContext);
+ when(mContext.getResources()).thenReturn(mRes);
+ when(mRes.getConfiguration()).thenReturn(mConfig);
+ when(mConfig.getLocales()).thenReturn(new LocaleList(Locale.US));
+ doCallRealMethod().when(mPreference).init();
+
+ mPreference.setStream(STREAM);
+ mPreference.init();
+
+ // On progress change, Round down the percent to match it with what the talkback says.
+ // (b/285458191)
+ // when progress is 4, the percent is 0.187. The state description should be set to 18%.
+ testFormatStateDescription(progress, "18%");
+
+ progress = 6;
+
+ // when progress is 6, the percent is 0.3125. The state description should be set to 31%.
+ testFormatStateDescription(progress, "31%");
+
+ progress = 7;
+
+ // when progress is 7, the percent is 0.375. The state description should be set to 37%.
+ testFormatStateDescription(progress, "37%");
+ }
+
+ private void testFormatStateDescription(int progress, String expected) {
+ doCallRealMethod().when(mPreference).formatStateDescription(progress);
+ doCallRealMethod().when(mPreference).getPercent(progress);
+
+ mSbvc.getValue().onProgressChanged(mSeekBar, progress, true);
+
+ verify(mPreference).overrideSeekBarStateDescription(expected);
+ }
}
diff --git a/tests/robotests/src/com/android/settings/panel/PanelSlicesAdapterTest.java b/tests/robotests/src/com/android/settings/panel/PanelSlicesAdapterTest.java
index cc5b2f8..9322317 100644
--- a/tests/robotests/src/com/android/settings/panel/PanelSlicesAdapterTest.java
+++ b/tests/robotests/src/com/android/settings/panel/PanelSlicesAdapterTest.java
@@ -19,6 +19,7 @@
import static com.android.settings.panel.PanelContent.VIEW_TYPE_SLIDER;
import static com.android.settings.panel.PanelSlicesAdapter.MAX_NUM_OF_SLICES;
import static com.android.settings.slices.CustomSliceRegistry.MEDIA_OUTPUT_INDICATOR_SLICE_URI;
+import static com.android.settings.slices.CustomSliceRegistry.VOLUME_NOTIFICATION_URI;
import static com.google.common.truth.Truth.assertThat;
@@ -32,9 +33,13 @@
import android.content.Context;
import android.net.Uri;
+import android.text.TextUtils;
import android.view.LayoutInflater;
+import android.view.View;
import android.view.ViewGroup;
+import android.view.accessibility.AccessibilityNodeInfo;
import android.widget.FrameLayout;
+import android.widget.LinearLayout;
import androidx.lifecycle.LiveData;
import androidx.slice.Slice;
@@ -43,6 +48,7 @@
import com.android.settings.panel.PanelSlicesAdapter.SliceRowViewHolder;
import com.android.settings.testutils.FakeFeatureFactory;
+import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -93,7 +99,6 @@
.get()
.getSupportFragmentManager()
.findFragmentById(R.id.main_content));
-
}
private void addTestLiveData(Uri uri) {
@@ -105,6 +110,61 @@
mData.put(uri, liveData);
}
+ /**
+ * Edge case where fragment context is not available.
+ */
+ @Test
+ public void withPanelFragmentContextNull_createAdapter_noExceptionThrown() {
+ when(mPanelFragment.getContext()).thenReturn(null);
+
+ final PanelSlicesAdapter adapter = spy(new PanelSlicesAdapter(mPanelFragment, mData, 0));
+
+ Assert.assertNotNull(adapter);
+ }
+
+ /**
+ * ViewHolder should load and set the action label correctly.
+ */
+ @Test
+ public void setActionLabel_loadsActionLabel() {
+ addTestLiveData(VOLUME_NOTIFICATION_URI);
+ final PanelSlicesAdapter adapter = new PanelSlicesAdapter(mPanelFragment, mData, 0);
+ final ViewGroup view = new FrameLayout(mContext);
+ final SliceRowViewHolder viewHolder = adapter.onCreateViewHolder(view, VIEW_TYPE_SLIDER);
+
+ // now let's see if setActionLabel can load and set the label correctly.
+ LinearLayout llRow = new LinearLayout(mContext);
+ viewHolder.setActionLabel(llRow);
+
+ boolean isLabelSet = isActionLabelSet(llRow);
+ Assert.assertTrue("Action label was not set correctly.", isLabelSet);
+ }
+
+ /**
+ * @param rowView the view with id row_view
+ * @return whether the accessibility action label is set
+ */
+ private boolean isActionLabelSet(View rowView) {
+ View.AccessibilityDelegate delegate = rowView.getAccessibilityDelegate();
+ if (delegate == null) {
+ return false;
+ }
+ AccessibilityNodeInfo node = new AccessibilityNodeInfo(rowView);
+ delegate.onInitializeAccessibilityNodeInfo(rowView, node);
+
+ boolean foundLabel = false;
+ final String expectedLabel =
+ mContext.getString(R.string.accessibility_action_label_panel_slice);
+ for (AccessibilityNodeInfo.AccessibilityAction action : node.getActionList()) {
+ if (action.equals(AccessibilityNodeInfo.AccessibilityAction.ACTION_CLICK)
+ && TextUtils.equals(action.getLabel(), expectedLabel)) {
+ foundLabel = true;
+ break;
+ }
+ }
+ return foundLabel;
+ }
+
@Test
public void sizeOfAdapter_shouldNotExceedMaxNum() {
for (int i = 0; i < MAX_NUM_OF_SLICES + 2; i++) {
@@ -140,6 +200,19 @@
}
@Test
+ public void onBindViewHolder_viewTypeSlider_verifyActionLabelSet() {
+ addTestLiveData(VOLUME_NOTIFICATION_URI);
+
+ final PanelSlicesAdapter adapter =
+ new PanelSlicesAdapter(mPanelFragment, mData, 0);
+ final ViewGroup view = new FrameLayout(mContext);
+ SliceRowViewHolder viewHolder = spy(adapter.onCreateViewHolder(view, 0 /* view type*/));
+ adapter.onBindViewHolder(viewHolder, 0);
+
+ verify(viewHolder).updateActionLabel();
+ }
+
+ @Test
public void onCreateViewHolder_viewTypeSlider_verifyLayout() {
final PanelSlicesAdapter adapter =
new PanelSlicesAdapter(mPanelFragment, mData, 0);
diff --git a/tests/robotests/src/com/android/settings/password/ChooseLockGenericTest.java b/tests/robotests/src/com/android/settings/password/ChooseLockGenericTest.java
index 12a540d..5db998a 100644
--- a/tests/robotests/src/com/android/settings/password/ChooseLockGenericTest.java
+++ b/tests/robotests/src/com/android/settings/password/ChooseLockGenericTest.java
@@ -60,6 +60,7 @@
import com.android.internal.widget.LockscreenCredential;
import com.android.settings.R;
import com.android.settings.biometrics.BiometricEnrollBase;
+import com.android.settings.biometrics.BiometricUtils;
import com.android.settings.password.ChooseLockGeneric.ChooseLockGenericFragment;
import com.android.settings.search.SearchFeatureProvider;
import com.android.settings.testutils.FakeFeatureFactory;
@@ -543,29 +544,38 @@
}
@Test
- public void updatePreferenceText_supportBiometrics_showFaceAndFingerprint() {
+ public void updatePreferenceText_supportBiometrics_setScreenLockFingerprintFace_inOrder() {
ShadowStorageManager.setIsFileEncrypted(false);
final Intent intent = new Intent().putExtra(EXTRA_KEY_FOR_BIOMETRICS, true);
initActivity(intent);
-
final String supportFingerprint = capitalize(mActivity.getResources().getString(
R.string.security_settings_fingerprint));
final String supportFace = capitalize(mActivity.getResources().getString(
R.string.keywords_face_settings));
- String pinTitle =
+
+ // The strings of golden copy
+ final String pinFingerprintFace = mActivity.getText(R.string.unlock_set_unlock_pin_title)
+ + BiometricUtils.SEPARATOR + supportFingerprint + BiometricUtils.SEPARATOR
+ + supportFace;
+ final String patternFingerprintFace = mActivity.getText(
+ R.string.unlock_set_unlock_pattern_title) + BiometricUtils.SEPARATOR
+ + supportFingerprint + BiometricUtils.SEPARATOR + supportFace;
+ final String passwordFingerprintFace = mActivity.getText(
+ R.string.unlock_set_unlock_password_title) + BiometricUtils.SEPARATOR
+ + supportFingerprint + BiometricUtils.SEPARATOR + supportFace;
+
+ // The strings obtain from preferences
+ final String pinTitle =
(String) mFragment.findPreference(ScreenLockType.PIN.preferenceKey).getTitle();
- String patternTitle =
+ final String patternTitle =
(String) mFragment.findPreference(ScreenLockType.PATTERN.preferenceKey).getTitle();
- String passwordTitle =
+ final String passwordTitle =
(String) mFragment.findPreference(ScreenLockType.PASSWORD.preferenceKey).getTitle();
- assertThat(pinTitle).contains(supportFingerprint);
- assertThat(pinTitle).contains(supportFace);
- assertThat(patternTitle).contains(supportFingerprint);
- assertThat(patternTitle).contains(supportFace);
- assertThat(passwordTitle).contains(supportFingerprint);
- assertThat(passwordTitle).contains(supportFace);
+ assertThat(pinTitle).isEqualTo(pinFingerprintFace);
+ assertThat(patternTitle).isEqualTo(patternFingerprintFace);
+ assertThat(passwordTitle).isEqualTo(passwordFingerprintFace);
}
@Test
diff --git a/tests/robotests/src/com/android/settings/password/ChooseLockPasswordTest.java b/tests/robotests/src/com/android/settings/password/ChooseLockPasswordTest.java
index d3c09d0..dea936d 100644
--- a/tests/robotests/src/com/android/settings/password/ChooseLockPasswordTest.java
+++ b/tests/robotests/src/com/android/settings/password/ChooseLockPasswordTest.java
@@ -27,10 +27,8 @@
import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_NUMERIC_COMPLEX;
import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_SOMETHING;
import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED;
-import static android.provider.DeviceConfig.NAMESPACE_AUTO_PIN_CONFIRMATION;
import static android.view.WindowManager.LayoutParams.FLAG_SECURE;
-import static com.android.internal.widget.LockPatternUtils.FLAG_ENABLE_AUTO_PIN_CONFIRMATION;
import static com.android.internal.widget.LockPatternUtils.PASSWORD_TYPE_KEY;
import static com.android.settings.password.ChooseLockGeneric.CONFIRM_CREDENTIALS;
@@ -45,7 +43,6 @@
import android.app.admin.PasswordPolicy;
import android.content.Intent;
import android.os.UserHandle;
-import android.provider.DeviceConfig;
import android.view.View;
import android.widget.CheckBox;
import android.widget.TextView;
@@ -55,7 +52,6 @@
import com.android.settings.password.ChooseLockPassword.ChooseLockPasswordFragment;
import com.android.settings.password.ChooseLockPassword.IntentBuilder;
import com.android.settings.testutils.shadow.SettingsShadowResources;
-import com.android.settings.testutils.shadow.ShadowDeviceConfig;
import com.android.settings.testutils.shadow.ShadowDevicePolicyManager;
import com.android.settings.testutils.shadow.ShadowLockPatternUtils;
import com.android.settings.testutils.shadow.ShadowUtils;
@@ -65,7 +61,6 @@
import org.junit.After;
import org.junit.Before;
-import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.Robolectric;
@@ -74,14 +69,12 @@
import org.robolectric.annotation.Config;
import org.robolectric.shadows.ShadowDrawable;
-@Ignore
@RunWith(RobolectricTestRunner.class)
@Config(shadows = {
SettingsShadowResources.class,
ShadowLockPatternUtils.class,
ShadowUtils.class,
ShadowDevicePolicyManager.class,
- ShadowDeviceConfig.class,
})
public class ChooseLockPasswordTest {
@Before
@@ -415,24 +408,7 @@
}
@Test
- public void processAndValidatePasswordRequirements_autoPinDisabled_defaultPinMinimumLength() {
- DeviceConfig.setProperty(NAMESPACE_AUTO_PIN_CONFIRMATION, FLAG_ENABLE_AUTO_PIN_CONFIRMATION,
- /* value= */ "false", /* makeDefault= */ false);
- PasswordPolicy policy = new PasswordPolicy();
- policy.quality = PASSWORD_QUALITY_UNSPECIFIED;
-
- assertPasswordValidationResult(
- /* minMetrics */ policy.getMinMetrics(),
- /* minComplexity= */ PASSWORD_COMPLEXITY_NONE,
- /* passwordType= */ PASSWORD_QUALITY_NUMERIC,
- /* userEnteredPassword= */ LockscreenCredential.createPassword("11"),
- "PIN must be at least 4 digits");
- }
-
- @Test
- public void processAndValidatePasswordRequirements_autoPinEnabled_defaultPinMinimumLength() {
- DeviceConfig.setProperty(NAMESPACE_AUTO_PIN_CONFIRMATION, FLAG_ENABLE_AUTO_PIN_CONFIRMATION,
- /* value= */ "true", /* makeDefault= */ false);
+ public void processAndValidatePasswordRequirements_defaultPinMinimumLength() {
PasswordPolicy policy = new PasswordPolicy();
policy.quality = PASSWORD_QUALITY_UNSPECIFIED;
@@ -472,8 +448,6 @@
@Test
public void autoPinConfirmOption_featureEnabledAndUntouchedByUser_changeStateAsPerRules() {
- DeviceConfig.setProperty(NAMESPACE_AUTO_PIN_CONFIRMATION, FLAG_ENABLE_AUTO_PIN_CONFIRMATION,
- /* value= */ "true", /* makeDefault= */ false);
ChooseLockPassword passwordActivity = setupActivityWithPinTypeAndDefaultPolicy();
ChooseLockPasswordFragment fragment = getChooseLockPasswordFragment(passwordActivity);
@@ -510,8 +484,6 @@
@Test
public void autoPinConfirmOption_featureEnabledAndModifiedByUser_shouldChangeStateAsPerRules() {
- DeviceConfig.setProperty(NAMESPACE_AUTO_PIN_CONFIRMATION, FLAG_ENABLE_AUTO_PIN_CONFIRMATION,
- /* value= */ "true", /* makeDefault= */ false);
ChooseLockPassword passwordActivity = setupActivityWithPinTypeAndDefaultPolicy();
ChooseLockPasswordFragment fragment = getChooseLockPasswordFragment(passwordActivity);
@@ -543,38 +515,6 @@
assertThat(pinAutoConfirmOption.isChecked()).isFalse();
}
- @Test
- public void autoPinConfirmOption_featureDisabled_shouldRemainInvisibleAndUnchecked() {
- DeviceConfig.setProperty(NAMESPACE_AUTO_PIN_CONFIRMATION, FLAG_ENABLE_AUTO_PIN_CONFIRMATION,
- /* value= */ "false", /* makeDefault= */ false);
- ChooseLockPassword passwordActivity = setupActivityWithPinTypeAndDefaultPolicy();
-
- ChooseLockPasswordFragment fragment = getChooseLockPasswordFragment(passwordActivity);
- ScrollToParentEditText passwordEntry = passwordActivity.findViewById(R.id.password_entry);
- CheckBox pinAutoConfirmOption = passwordActivity
- .findViewById(R.id.auto_pin_confirm_enabler);
- TextView securityMessage =
- passwordActivity.findViewById(R.id.auto_pin_confirm_security_message);
-
- passwordEntry.setText("1234");
- fragment.updateUi();
- assertThat(pinAutoConfirmOption.getVisibility()).isEqualTo(View.GONE);
- assertThat(securityMessage.getVisibility()).isEqualTo(View.GONE);
- assertThat(pinAutoConfirmOption.isChecked()).isFalse();
-
- passwordEntry.setText("123456");
- fragment.updateUi();
- assertThat(pinAutoConfirmOption.getVisibility()).isEqualTo(View.GONE);
- assertThat(securityMessage.getVisibility()).isEqualTo(View.GONE);
- assertThat(pinAutoConfirmOption.isChecked()).isFalse();
-
- passwordEntry.setText("12345678");
- fragment.updateUi();
- assertThat(pinAutoConfirmOption.getVisibility()).isEqualTo(View.GONE);
- assertThat(securityMessage.getVisibility()).isEqualTo(View.GONE);
- assertThat(pinAutoConfirmOption.isChecked()).isFalse();
- }
-
private ChooseLockPassword setupActivityWithPinTypeAndDefaultPolicy() {
PasswordPolicy policy = new PasswordPolicy();
policy.quality = PASSWORD_QUALITY_UNSPECIFIED;
diff --git a/tests/robotests/src/com/android/settings/password/ChooseLockPatternTest.java b/tests/robotests/src/com/android/settings/password/ChooseLockPatternTest.java
index 442d021..301a6db 100644
--- a/tests/robotests/src/com/android/settings/password/ChooseLockPatternTest.java
+++ b/tests/robotests/src/com/android/settings/password/ChooseLockPatternTest.java
@@ -18,12 +18,15 @@
import static android.view.WindowManager.LayoutParams.FLAG_SECURE;
+import static com.android.settings.password.ChooseLockPattern.ChooseLockPatternFragment.KEY_UI_STAGE;
+
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
import static org.robolectric.RuntimeEnvironment.application;
import android.content.Intent;
+import android.os.Bundle;
import android.os.UserHandle;
import android.view.View;
@@ -34,6 +37,8 @@
import com.android.settings.password.ChooseLockPattern.IntentBuilder;
import com.android.settings.testutils.shadow.ShadowUtils;
+import com.google.android.setupdesign.GlifLayout;
+
import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -119,6 +124,21 @@
assertThat(flags & FLAG_SECURE).isEqualTo(FLAG_SECURE);
}
+ @Test
+ public void headerText_stageConfirmWrong() {
+ ChooseLockPattern activity = createActivity(true);
+ ChooseLockPatternFragment fragment = (ChooseLockPatternFragment)
+ activity.getSupportFragmentManager().findFragmentById(R.id.main_content);
+ final GlifLayout layout = fragment.getView().findViewById(R.id.setup_wizard_layout);
+ Bundle savedInstanceState = new Bundle();
+ savedInstanceState.putInt(KEY_UI_STAGE,
+ ChooseLockPatternFragment.Stage.ConfirmWrong.ordinal());
+
+ fragment.onViewCreated(layout, savedInstanceState);
+ assertThat(layout.getHeaderText().toString()).isEqualTo(activity.getResources().getString(
+ R.string.lockpassword_draw_your_pattern_again_header));
+ }
+
private ChooseLockPattern createActivity(boolean addFingerprintExtra) {
return Robolectric.buildActivity(
ChooseLockPattern.class,
diff --git a/tests/robotests/src/com/android/settings/password/SetupChooseLockPasswordTest.java b/tests/robotests/src/com/android/settings/password/SetupChooseLockPasswordTest.java
index a3e2ed4..8bccf1a 100644
--- a/tests/robotests/src/com/android/settings/password/SetupChooseLockPasswordTest.java
+++ b/tests/robotests/src/com/android/settings/password/SetupChooseLockPasswordTest.java
@@ -26,6 +26,7 @@
import android.view.View;
import android.view.inputmethod.InputMethodManager;
import android.widget.Button;
+import android.widget.LinearLayout;
import androidx.appcompat.app.AlertDialog;
@@ -107,6 +108,20 @@
}
@Test
+ public void createActivity_withShowOptionsButtonExtra_shouldShowButtonUnderSudHeader() {
+ SetupChooseLockPassword activity = createSetupChooseLockPassword();
+ final LinearLayout headerLayout = activity.findViewById(
+ R.id.sud_layout_header);
+ assertThat(headerLayout).isNotNull();
+
+ final Button optionsButton = headerLayout.findViewById(R.id.screen_lock_options);
+ assertThat(optionsButton).isNotNull();
+
+ optionsButton.performClick();
+ assertThat(ShadowDialog.getLatestDialog()).isNotNull();
+ }
+
+ @Test
@Config(shadows = ShadowChooseLockGenericController.class)
public void createActivity_withShowOptionsButtonExtra_buttonNotVisibleIfNoVisibleLockTypes() {
SetupChooseLockPassword activity = createSetupChooseLockPassword();
diff --git a/tests/robotests/src/com/android/settings/search/SettingsSearchIndexablesProviderTest.java b/tests/robotests/src/com/android/settings/search/SettingsSearchIndexablesProviderTest.java
index c70411c..bee2d99 100644
--- a/tests/robotests/src/com/android/settings/search/SettingsSearchIndexablesProviderTest.java
+++ b/tests/robotests/src/com/android/settings/search/SettingsSearchIndexablesProviderTest.java
@@ -1,5 +1,6 @@
package com.android.settings.search;
+import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_SEARCHABLE;
import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_TITLE;
import static com.google.common.truth.Truth.assertThat;
@@ -207,6 +208,19 @@
assertThat(mProvider.isEligibleForIndexing(PACKAGE_NAME, activityTile)).isTrue();
}
+ @Test
+ public void isEligibleForIndexing_disabledByMetadata_shouldReturnFalse() {
+ final ActivityInfo activityInfo = new ActivityInfo();
+ activityInfo.packageName = PACKAGE_NAME;
+ activityInfo.name = "class";
+ activityInfo.metaData = new Bundle();
+ activityInfo.metaData.putBoolean(META_DATA_PREFERENCE_SEARCHABLE, false);
+ final ActivityTile activityTile = new ActivityTile(activityInfo,
+ CategoryKey.CATEGORY_CONNECT);
+
+ assertThat(mProvider.isEligibleForIndexing(PACKAGE_NAME, activityTile)).isFalse();
+ }
+
@Implements(CategoryManager.class)
public static class ShadowCategoryManager {
diff --git a/tests/robotests/src/com/android/settings/security/screenlock/AutoPinConfirmPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/security/screenlock/AutoPinConfirmPreferenceControllerTest.java
index 715913c..86c1244 100644
--- a/tests/robotests/src/com/android/settings/security/screenlock/AutoPinConfirmPreferenceControllerTest.java
+++ b/tests/robotests/src/com/android/settings/security/screenlock/AutoPinConfirmPreferenceControllerTest.java
@@ -16,22 +16,16 @@
package com.android.settings.security.screenlock;
-import static android.provider.DeviceConfig.NAMESPACE_AUTO_PIN_CONFIRMATION;
-
-import static com.android.internal.widget.LockPatternUtils.FLAG_ENABLE_AUTO_PIN_CONFIRMATION;
-
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Mockito.when;
import android.content.Context;
-import android.provider.DeviceConfig;
import androidx.preference.SwitchPreference;
import androidx.test.core.app.ApplicationProvider;
import com.android.internal.widget.LockPatternUtils;
-import com.android.settings.testutils.shadow.ShadowDeviceConfig;
import com.android.settingslib.core.lifecycle.ObservablePreferenceFragment;
import org.junit.Before;
@@ -40,10 +34,8 @@
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.robolectric.RobolectricTestRunner;
-import org.robolectric.annotation.Config;
@RunWith(RobolectricTestRunner.class)
-@Config(shadows = {ShadowDeviceConfig.class})
public class AutoPinConfirmPreferenceControllerTest {
private static final Integer TEST_USER_ID = 1;
@Mock
@@ -65,8 +57,6 @@
@Test
public void isAvailable_featureEnabledAndLockSetToNone_shouldReturnFalse() {
- DeviceConfig.setProperty(NAMESPACE_AUTO_PIN_CONFIRMATION, FLAG_ENABLE_AUTO_PIN_CONFIRMATION,
- "true", /* makeDefault */ false);
when(mLockPatternUtils.isSecure(TEST_USER_ID)).thenReturn(true);
assertThat(mController.isAvailable()).isFalse();
@@ -74,8 +64,6 @@
@Test
public void isAvailable_featureEnabledAndLockSetToPassword_shouldReturnFalse() {
- DeviceConfig.setProperty(NAMESPACE_AUTO_PIN_CONFIRMATION, FLAG_ENABLE_AUTO_PIN_CONFIRMATION,
- "true", /* makeDefault */ false);
when(mLockPatternUtils.isSecure(TEST_USER_ID)).thenReturn(true);
when(mLockPatternUtils.getCredentialTypeForUser(TEST_USER_ID))
.thenReturn(LockPatternUtils.CREDENTIAL_TYPE_PASSWORD);
@@ -85,8 +73,6 @@
@Test
public void isAvailable_featureEnabledAndLockSetToPIN_lengthLessThanSix_shouldReturnFalse() {
- DeviceConfig.setProperty(NAMESPACE_AUTO_PIN_CONFIRMATION, FLAG_ENABLE_AUTO_PIN_CONFIRMATION,
- "true", /* makeDefault */ false);
when(mLockPatternUtils.getCredentialTypeForUser(TEST_USER_ID))
.thenReturn(LockPatternUtils.CREDENTIAL_TYPE_PIN);
when(mLockPatternUtils.getPinLength(TEST_USER_ID)).thenReturn(5);
@@ -96,8 +82,6 @@
@Test
public void isAvailable_featureEnabledAndLockSetToPIN_lengthMoreThanEqSix_shouldReturnTrue() {
- DeviceConfig.setProperty(NAMESPACE_AUTO_PIN_CONFIRMATION, FLAG_ENABLE_AUTO_PIN_CONFIRMATION,
- "true", /* makeDefault */ false);
when(mLockPatternUtils.isSecure(TEST_USER_ID)).thenReturn(true);
when(mLockPatternUtils.getCredentialTypeForUser(TEST_USER_ID))
.thenReturn(LockPatternUtils.CREDENTIAL_TYPE_PIN);
@@ -107,20 +91,7 @@
}
@Test
- public void isAvailable_featureDisabledAndLockSetToPIN_shouldReturnFalse() {
- DeviceConfig.setProperty(NAMESPACE_AUTO_PIN_CONFIRMATION, FLAG_ENABLE_AUTO_PIN_CONFIRMATION,
- "false", /* makeDefault */ false);
- when(mLockPatternUtils.isSecure(TEST_USER_ID)).thenReturn(true);
- when(mLockPatternUtils.getCredentialTypeForUser(TEST_USER_ID))
- .thenReturn(LockPatternUtils.CREDENTIAL_TYPE_PIN);
-
- assertThat(mController.isAvailable()).isFalse();
- }
-
- @Test
public void updateState_ChangingSettingState_shouldSetPreferenceToAppropriateCheckedState() {
- DeviceConfig.setProperty(NAMESPACE_AUTO_PIN_CONFIRMATION, FLAG_ENABLE_AUTO_PIN_CONFIRMATION,
- "true", /* makeDefault */ false);
// When auto_pin_confirm setting is disabled, switchPreference is unchecked
when(mLockPatternUtils.isAutoPinConfirmEnabled(TEST_USER_ID)).thenReturn(false);
mController.updateState(mPreference);
diff --git a/tests/robotests/src/com/android/settings/shortcut/CreateShortcutPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/shortcut/CreateShortcutPreferenceControllerTest.java
index df1fec3..fdb7feb 100644
--- a/tests/robotests/src/com/android/settings/shortcut/CreateShortcutPreferenceControllerTest.java
+++ b/tests/robotests/src/com/android/settings/shortcut/CreateShortcutPreferenceControllerTest.java
@@ -36,8 +36,11 @@
import android.content.pm.ResolveInfo;
import android.content.pm.ShortcutInfo;
import android.content.pm.ShortcutManager;
+import android.content.res.Resources;
import android.os.SystemProperties;
+import android.os.UserManager;
+import com.android.settings.R;
import com.android.settings.Settings;
import com.android.settings.testutils.shadow.ShadowConnectivityManager;
@@ -69,6 +72,10 @@
private ShortcutManager mShortcutManager;
@Mock
private Activity mHost;
+ @Mock
+ private Resources mResources;
+ @Mock
+ private UserManager mUserManager;
private Context mContext;
private ShadowConnectivityManager mShadowConnectivityManager;
@@ -188,6 +195,70 @@
assertThat(mController.queryShortcuts()).hasSize(0);
}
+ @Test
+ public void queryShortcuts_configShowDataUsage_ShouldEnableShortcuts() {
+ doReturn(true).when(mController).canShowDataUsage();
+ setupActivityInfo(Settings.DataUsageSummaryActivity.class.getSimpleName());
+
+ assertThat(mController.queryShortcuts()).hasSize(1);
+ }
+
+ @Test
+ public void queryShortcuts_configNotShowDataUsage_ShouldDisableShortcuts() {
+ doReturn(false).when(mController).canShowDataUsage();
+ setupActivityInfo(Settings.DataUsageSummaryActivity.class.getSimpleName());
+
+ assertThat(mController.queryShortcuts()).hasSize(0);
+ }
+
+ @Test
+ public void canShowDataUsage_configShowDataUsage_returnTrue() {
+ when(mContext.getResources()).thenReturn(mResources);
+ when(mResources.getBoolean(R.bool.config_show_sim_info)).thenReturn(true);
+ when(mContext.getSystemService(UserManager.class)).thenReturn(mUserManager);
+ when(mUserManager.isGuestUser()).thenReturn(false);
+ when(mUserManager.hasUserRestriction(
+ UserManager.DISALLOW_CONFIG_MOBILE_NETWORKS)).thenReturn(false);
+
+ assertThat(mController.canShowDataUsage()).isTrue();
+ }
+
+ @Test
+ public void canShowDataUsage_noSimCapability_returnFalse() {
+ when(mContext.getResources()).thenReturn(mResources);
+ when(mResources.getBoolean(R.bool.config_show_sim_info)).thenReturn(false);
+ when(mContext.getSystemService(UserManager.class)).thenReturn(mUserManager);
+ when(mUserManager.isGuestUser()).thenReturn(false);
+ when(mUserManager.hasUserRestriction(
+ UserManager.DISALLOW_CONFIG_MOBILE_NETWORKS)).thenReturn(false);
+
+ assertThat(mController.canShowDataUsage()).isFalse();
+ }
+
+ @Test
+ public void canShowDataUsage_isGuestUser_returnFalse() {
+ when(mContext.getResources()).thenReturn(mResources);
+ when(mResources.getBoolean(R.bool.config_show_sim_info)).thenReturn(true);
+ when(mContext.getSystemService(UserManager.class)).thenReturn(mUserManager);
+ when(mUserManager.isGuestUser()).thenReturn(true);
+ when(mUserManager.hasUserRestriction(
+ UserManager.DISALLOW_CONFIG_MOBILE_NETWORKS)).thenReturn(false);
+
+ assertThat(mController.canShowDataUsage()).isFalse();
+ }
+
+ @Test
+ public void canShowDataUsage_isMobileNetworkUserRestricted_returnFalse() {
+ when(mContext.getResources()).thenReturn(mResources);
+ when(mResources.getBoolean(R.bool.config_show_sim_info)).thenReturn(true);
+ when(mContext.getSystemService(UserManager.class)).thenReturn(mUserManager);
+ when(mUserManager.isGuestUser()).thenReturn(false);
+ when(mUserManager.hasUserRestriction(
+ UserManager.DISALLOW_CONFIG_MOBILE_NETWORKS)).thenReturn(true);
+
+ assertThat(mController.canShowDataUsage()).isFalse();
+ }
+
private void setupActivityInfo(String name) {
ResolveInfo ri = new ResolveInfo();
ri.activityInfo = new ActivityInfo();
diff --git a/tests/robotests/src/com/android/settings/slices/SettingsSliceProviderTest.java b/tests/robotests/src/com/android/settings/slices/SettingsSliceProviderTest.java
index b7d249d..4903a28 100644
--- a/tests/robotests/src/com/android/settings/slices/SettingsSliceProviderTest.java
+++ b/tests/robotests/src/com/android/settings/slices/SettingsSliceProviderTest.java
@@ -119,6 +119,7 @@
private Context mContext;
private SettingsSliceProvider mProvider;
private ShadowPackageManager mPackageManager;
+ private ShadowUserManager mShadowUserManager;
@Mock
private SliceManager mManager;
@@ -157,6 +158,7 @@
when(mManager.getPinnedSlices()).thenReturn(Collections.emptyList());
mPackageManager = Shadows.shadowOf(mContext.getPackageManager());
+ mShadowUserManager = ShadowUserManager.getShadow();
SliceProvider.setSpecs(SliceLiveData.SUPPORTED_SPECS);
}
@@ -293,6 +295,37 @@
}
@Test
+ public void onBindSlice_guestRestricted_returnsNull() {
+ final String key = "enable_usb_tethering";
+ mShadowUserManager.setGuestUser(true);
+ final Uri testUri = new Uri.Builder()
+ .scheme(ContentResolver.SCHEME_CONTENT)
+ .authority(SettingsSliceProvider.SLICE_AUTHORITY)
+ .appendPath(SettingsSlicesContract.PATH_SETTING_ACTION)
+ .appendPath(key)
+ .build();
+
+ final Slice slice = mProvider.onBindSlice(testUri);
+
+ assertThat(slice).isNull();
+ }
+
+ @Test
+ public void onBindSlice_notGuestRestricted_returnsNotNull() {
+ final String key = "enable_usb_tethering";
+ final Uri testUri = new Uri.Builder()
+ .scheme(ContentResolver.SCHEME_CONTENT)
+ .authority(SettingsSliceProvider.SLICE_AUTHORITY)
+ .appendPath(SettingsSlicesContract.PATH_SETTING_ACTION)
+ .appendPath(key)
+ .build();
+
+ final Slice slice = mProvider.onBindSlice(testUri);
+
+ assertThat(slice).isNotNull();
+ }
+
+ @Test
public void getDescendantUris_fullActionUri_returnsSelf() {
final Collection<Uri> descendants = mProvider.onGetSliceDescendants(ACTION_SLICE_URI);
diff --git a/tests/robotests/src/com/android/settings/system/FactoryResetDemoUserPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/system/FactoryResetDemoUserPreferenceControllerTest.java
new file mode 100644
index 0000000..0c92b05
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/system/FactoryResetDemoUserPreferenceControllerTest.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2017 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.settings.system;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.Context;
+import android.content.pm.UserInfo;
+import android.os.UserHandle;
+import android.provider.Settings;
+
+import com.android.settings.testutils.shadow.ShadowUserManager;
+import com.android.settings.testutils.shadow.ShadowUtils;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Config;
+
+@RunWith(RobolectricTestRunner.class)
+@Config(shadows = ShadowUserManager.class)
+public class FactoryResetDemoUserPreferenceControllerTest {
+
+ private static final String FACTORY_RESET_DEMO_USER_KEY = "factory_reset_demo_user";
+
+ private ShadowUserManager mShadowUserManager;
+
+ private Context mContext;
+ private FactoryResetDemoUserPreferenceController mController;
+
+ @Before
+ public void setUp() {
+ mContext = RuntimeEnvironment.application;
+ mShadowUserManager = ShadowUserManager.getShadow();
+
+ mController = new FactoryResetDemoUserPreferenceController(
+ mContext, FACTORY_RESET_DEMO_USER_KEY);
+ }
+
+ @After
+ public void tearDown() {
+ ShadowUtils.reset();
+ mShadowUserManager.setIsAdminUser(false);
+ mShadowUserManager.setIsDemoUser(false);
+ Settings.Global.putInt(mContext.getContentResolver(), Settings.Global.DEVICE_DEMO_MODE, 0);
+ }
+
+ @Test
+ public void isAvailable_systemUser() {
+ mShadowUserManager.setIsAdminUser(true);
+
+ assertThat(mController.isAvailable()).isFalse();
+ }
+
+ @Test
+ public void isAvailable_nonSystemUser() {
+ mShadowUserManager.setIsAdminUser(false);
+ mShadowUserManager.setIsDemoUser(false);
+
+ assertThat(mController.isAvailable()).isFalse();
+ }
+
+ @Test
+ public void isAvailable_demoUser() {
+ mShadowUserManager.setIsAdminUser(false);
+
+ // Place the device in demo mode.
+ Settings.Global.putInt(mContext.getContentResolver(), Settings.Global.DEVICE_DEMO_MODE, 1);
+
+ // Indicate the user is a demo user.
+ mShadowUserManager.addUser(UserHandle.myUserId(), "test", UserInfo.FLAG_DEMO);
+
+ assertThat(mController.isAvailable()).isTrue();
+ }
+
+ @Test
+ public void getPreferenceKey() {
+ assertThat(mController.getPreferenceKey()).isEqualTo(FACTORY_RESET_DEMO_USER_KEY);
+ }
+}
diff --git a/tests/robotests/src/com/android/settings/system/FactoryResetPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/system/FactoryResetPreferenceControllerTest.java
index f2a932e..6e6fad8 100644
--- a/tests/robotests/src/com/android/settings/system/FactoryResetPreferenceControllerTest.java
+++ b/tests/robotests/src/com/android/settings/system/FactoryResetPreferenceControllerTest.java
@@ -49,7 +49,7 @@
mContext = RuntimeEnvironment.application;
mShadowUserManager = ShadowUserManager.getShadow();
- mController = new FactoryResetPreferenceController(mContext);
+ mController = new FactoryResetPreferenceController(mContext, FACTORY_RESET_KEY);
}
@After
@@ -85,7 +85,7 @@
// Indicate the user is a demo user.
mShadowUserManager.addUser(UserHandle.myUserId(), "test", UserInfo.FLAG_DEMO);
- assertThat(mController.isAvailable()).isTrue();
+ assertThat(mController.isAvailable()).isFalse();
}
@Test
diff --git a/tests/robotests/src/com/android/settings/testutils/BatteryTestUtils.java b/tests/robotests/src/com/android/settings/testutils/BatteryTestUtils.java
index c54b750..e98ea1b 100644
--- a/tests/robotests/src/com/android/settings/testutils/BatteryTestUtils.java
+++ b/tests/robotests/src/com/android/settings/testutils/BatteryTestUtils.java
@@ -18,6 +18,7 @@
import static org.mockito.Mockito.when;
+import android.app.settings.SettingsEnums;
import android.content.Context;
import android.content.Intent;
import android.hardware.usb.UsbManager;
@@ -25,11 +26,20 @@
import android.hardware.usb.UsbPortStatus;
import android.os.BatteryManager;
import android.os.UserManager;
+
import androidx.room.Room;
+import com.android.settings.DisplaySettings;
+import com.android.settings.display.ScreenTimeoutSettings;
import com.android.settings.fuelgauge.batteryusage.BatteryInformation;
import com.android.settings.fuelgauge.batteryusage.ConvertUtils;
import com.android.settings.fuelgauge.batteryusage.DeviceBatteryState;
+import com.android.settings.fuelgauge.batteryusage.PowerAnomalyEvent;
+import com.android.settings.fuelgauge.batteryusage.PowerAnomalyEventList;
+import com.android.settings.fuelgauge.batteryusage.PowerAnomalyKey;
+import com.android.settings.fuelgauge.batteryusage.PowerAnomalyType;
+import com.android.settings.fuelgauge.batteryusage.WarningBannerInfo;
+import com.android.settings.fuelgauge.batteryusage.WarningItemInfo;
import com.android.settings.fuelgauge.batteryusage.db.AppUsageEventDao;
import com.android.settings.fuelgauge.batteryusage.db.AppUsageEventEntity;
import com.android.settings.fuelgauge.batteryusage.db.BatteryState;
@@ -61,14 +71,18 @@
BatteryManager.BATTERY_STATUS_DISCHARGING);
}
- /** Sets the work profile mode. */
+ /**
+ * Sets the work profile mode.
+ */
public static void setWorkProfile(Context context) {
final UserManager userManager = context.getSystemService(UserManager.class);
Shadows.shadowOf(userManager).setManagedProfile(true);
Shadows.shadowOf(userManager).setIsSystemUser(false);
}
- /** Creates and sets up the in-memory {@link BatteryStateDatabase}. */
+ /**
+ * Creates and sets up the in-memory {@link BatteryStateDatabase}.
+ */
public static BatteryStateDatabase setUpBatteryStateDatabase(Context context) {
final BatteryStateDatabase inMemoryDatabase =
Room.inMemoryDatabaseBuilder(context, BatteryStateDatabase.class)
@@ -78,21 +92,27 @@
return inMemoryDatabase;
}
- /** Inserts a fake data into the database for testing. */
+ /**
+ * Inserts a fake data into the database for testing.
+ */
public static void insertDataToBatteryStateTable(
Context context, long timestamp, String packageName) {
insertDataToBatteryStateTable(
context, timestamp, packageName, /*multiple=*/ false, /*isFullChargeStart=*/ false);
}
- /** Inserts a fake data into the database for testing. */
+ /**
+ * Inserts a fake data into the database for testing.
+ */
public static void insertDataToBatteryStateTable(
Context context, long timestamp, String packageName, boolean isFullChargeStart) {
insertDataToBatteryStateTable(
context, timestamp, packageName, /*multiple=*/ false, isFullChargeStart);
}
- /** Inserts a fake data into the database for testing. */
+ /**
+ * Inserts a fake data into the database for testing.
+ */
public static void insertDataToBatteryStateTable(
Context context, long timestamp, String packageName, boolean multiple,
boolean isFullChargeStart) {
@@ -142,14 +162,18 @@
}
}
- /** Inserts a fake data into the database for testing. */
+ /**
+ * Inserts a fake data into the database for testing.
+ */
public static void insertDataToAppUsageEventTable(
Context context, long userId, long timestamp, String packageName) {
insertDataToAppUsageEventTable(
context, userId, timestamp, packageName, /*multiple=*/ false);
}
- /** Inserts a fake data into the database for testing. */
+ /**
+ * Inserts a fake data into the database for testing.
+ */
public static void insertDataToAppUsageEventTable(
Context context, long userId, long timestamp, String packageName, boolean multiple) {
final AppUsageEventEntity entity =
@@ -170,7 +194,9 @@
}
}
- /** Gets customized battery changed intent. */
+ /**
+ * Gets customized battery changed intent.
+ */
public static Intent getCustomBatteryIntent(int plugged, int level, int scale, int status) {
Intent intent = new Intent();
intent.putExtra(BatteryManager.EXTRA_PLUGGED, plugged);
@@ -181,7 +207,9 @@
return intent;
}
- /** Configures the incompatible charger environment. */
+ /**
+ * Configures the incompatible charger environment.
+ */
public static void setupIncompatibleEvent(
UsbPort mockUsbPort, UsbManager mockUsbManager, UsbPortStatus mockUsbPortStatus) {
final List<UsbPort> usbPorts = new ArrayList<>();
@@ -193,4 +221,74 @@
when(mockUsbPortStatus.getComplianceWarnings())
.thenReturn(new int[]{UsbPortStatus.COMPLIANCE_WARNING_OTHER});
}
+
+ /**
+ * Create an empty power anomaly event list proto.
+ */
+ public static PowerAnomalyEventList createEmptyPowerAnomalyEventList() {
+ return PowerAnomalyEventList.getDefaultInstance();
+ }
+
+ /**
+ * Create an non-empty power anomaly event list proto.
+ */
+ public static PowerAnomalyEventList createNonEmptyPowerAnomalyEventList() {
+ return PowerAnomalyEventList.newBuilder()
+ .addPowerAnomalyEvents(0, createAdaptiveBrightnessAnomalyEvent())
+ .addPowerAnomalyEvents(1, createScreenTimeoutAnomalyEvent())
+ .build();
+ }
+
+ /**
+ * Create a power anomaly event proto of adaptive brightness.
+ */
+ public static PowerAnomalyEvent createAdaptiveBrightnessAnomalyEvent() {
+ return PowerAnomalyEvent.newBuilder()
+ .setEventId("BrightnessAnomaly")
+ .setType(PowerAnomalyType.TYPE_SETTINGS_BANNER)
+ .setKey(PowerAnomalyKey.KEY_BRIGHTNESS)
+ .setDismissRecordKey(PowerAnomalyKey.KEY_BRIGHTNESS.name())
+ .setScore(1.2f)
+ .setWarningBannerInfo(WarningBannerInfo.newBuilder()
+ .setMainButtonDestination(DisplaySettings.class.getName())
+ .setMainButtonSourceMetricsCategory(SettingsEnums.DISPLAY)
+ .setMainButtonSourceHighlightKey("auto_brightness_entry")
+ .build())
+ .build();
+ }
+
+ /**
+ * Create a power anomaly event proto of screen timeout.
+ */
+ public static PowerAnomalyEvent createScreenTimeoutAnomalyEvent() {
+ return PowerAnomalyEvent.newBuilder()
+ .setEventId("ScreenTimeoutAnomaly")
+ .setType(PowerAnomalyType.TYPE_SETTINGS_BANNER)
+ .setKey(PowerAnomalyKey.KEY_SCREEN_TIMEOUT)
+ .setDismissRecordKey(PowerAnomalyKey.KEY_SCREEN_TIMEOUT.name())
+ .setScore(1.1f)
+ .setWarningBannerInfo(WarningBannerInfo.newBuilder()
+ .setMainButtonDestination(ScreenTimeoutSettings.class.getName())
+ .setMainButtonSourceMetricsCategory(SettingsEnums.SCREEN_TIMEOUT)
+ .setMainButtonSourceHighlightKey("60000")
+ .build())
+ .build();
+ }
+
+ /**
+ * Create a power anomaly event proto of app anomaly.
+ */
+ public static PowerAnomalyEvent createAppAnomalyEvent() {
+ return PowerAnomalyEvent.newBuilder()
+ .setEventId("AppAnomaly")
+ .setType(PowerAnomalyType.TYPE_APPS_ITEM)
+ .setKey(PowerAnomalyKey.KEY_APP_TOTAL_HIGHER_THAN_USUAL)
+ .setDismissRecordKey("KEY_APP_1")
+ .setScore(2.0f)
+ .setWarningItemInfo(WarningItemInfo.newBuilder()
+ .setStartTimestamp(1694361600000L) // 2023-09-11 00:00:00
+ .setEndTimestamp(1694368800000L) // 2023-09-11 02:00:00
+ .build())
+ .build();
+ }
}
diff --git a/tests/robotests/src/com/android/settings/testutils/FakeFeatureFactory.java b/tests/robotests/src/com/android/settings/testutils/FakeFeatureFactory.java
index 29a6da3..5891aa1 100644
--- a/tests/robotests/src/com/android/settings/testutils/FakeFeatureFactory.java
+++ b/tests/robotests/src/com/android/settings/testutils/FakeFeatureFactory.java
@@ -29,6 +29,7 @@
import com.android.settings.biometrics.face.FaceFeatureProvider;
import com.android.settings.biometrics2.factory.BiometricsRepositoryProvider;
import com.android.settings.bluetooth.BluetoothFeatureProvider;
+import com.android.settings.connecteddevice.stylus.StylusFeatureProvider;
import com.android.settings.dashboard.DashboardFeatureProvider;
import com.android.settings.dashboard.suggestions.SuggestionFeatureProvider;
import com.android.settings.deviceinfo.hardwareinfo.HardwareInfoFeatureProvider;
@@ -39,6 +40,7 @@
import com.android.settings.fuelgauge.PowerUsageFeatureProvider;
import com.android.settings.gestures.AssistGestureFeatureProvider;
import com.android.settings.homepage.contextualcards.ContextualCardFeatureProvider;
+import com.android.settings.inputmethod.KeyboardSettingsFeatureProvider;
import com.android.settings.localepicker.LocaleFeatureProvider;
import com.android.settings.overlay.DockUpdaterFeatureProvider;
import com.android.settings.overlay.FeatureFactory;
@@ -95,6 +97,8 @@
public AccessibilityMetricsFeatureProvider mAccessibilityMetricsFeatureProvider;
public AdvancedVpnFeatureProvider mAdvancedVpnFeatureProvider;
public WifiFeatureProvider mWifiFeatureProvider;
+ public KeyboardSettingsFeatureProvider mKeyboardSettingsFeatureProvider;
+ public StylusFeatureProvider mStylusFeatureProvider;
/**
* Call this in {@code @Before} method of the test class to use fake factory.
@@ -147,6 +151,8 @@
mAccessibilityMetricsFeatureProvider = mock(AccessibilityMetricsFeatureProvider.class);
mAdvancedVpnFeatureProvider = mock(AdvancedVpnFeatureProvider.class);
mWifiFeatureProvider = mock(WifiFeatureProvider.class);
+ mKeyboardSettingsFeatureProvider = mock(KeyboardSettingsFeatureProvider.class);
+ mStylusFeatureProvider = mock(StylusFeatureProvider.class);
}
@Override
@@ -170,7 +176,7 @@
}
@Override
- public BatterySettingsFeatureProvider getBatterySettingsFeatureProvider(Context context) {
+ public BatterySettingsFeatureProvider getBatterySettingsFeatureProvider() {
return batterySettingsFeatureProvider;
}
@@ -303,4 +309,14 @@
public WifiFeatureProvider getWifiFeatureProvider() {
return mWifiFeatureProvider;
}
+
+ @Override
+ public KeyboardSettingsFeatureProvider getKeyboardSettingsFeatureProvider() {
+ return mKeyboardSettingsFeatureProvider;
+ }
+
+ @Override
+ public StylusFeatureProvider getStylusFeatureProvider() {
+ return mStylusFeatureProvider;
+ }
}
diff --git a/tests/robotests/src/com/android/settings/testutils/shadow/ShadowUserManager.java b/tests/robotests/src/com/android/settings/testutils/shadow/ShadowUserManager.java
index df38420..324a829 100644
--- a/tests/robotests/src/com/android/settings/testutils/shadow/ShadowUserManager.java
+++ b/tests/robotests/src/com/android/settings/testutils/shadow/ShadowUserManager.java
@@ -55,6 +55,7 @@
private int[] profileIdsForUser = new int[0];
private boolean mUserSwitchEnabled;
private Bundle mDefaultGuestUserRestriction = new Bundle();
+ private boolean mIsGuestUser = false;
private @UserManager.UserSwitchabilityResult int mSwitchabilityStatus =
UserManager.SWITCHABILITY_STATUS_OK;
@@ -270,4 +271,13 @@
mUserProfileInfos.get(i).flags |= UserInfo.FLAG_ADMIN;
}
}
+
+ @Implementation
+ protected boolean isGuestUser() {
+ return mIsGuestUser;
+ }
+
+ public void setGuestUser(boolean isGuestUser) {
+ mIsGuestUser = isGuestUser;
+ }
}
diff --git a/tests/robotests/src/com/android/settings/users/GuestTelephonyPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/users/GuestTelephonyPreferenceControllerTest.java
index aa84cb6..c4b514c 100644
--- a/tests/robotests/src/com/android/settings/users/GuestTelephonyPreferenceControllerTest.java
+++ b/tests/robotests/src/com/android/settings/users/GuestTelephonyPreferenceControllerTest.java
@@ -18,12 +18,14 @@
import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assume.assumeTrue;
import static org.mockito.Answers.RETURNS_DEEP_STUBS;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.content.Context;
+import android.content.pm.PackageManager;
import android.os.SystemProperties;
import android.os.UserManager;
@@ -103,6 +105,8 @@
@Test
public void updateState_Admin_shouldDisplayPreference() {
+ assumeTrue("Device does not have telephony feature ",
+ mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_TELEPHONY));
SystemProperties.set("fw.max_users", Long.toBinaryString(4));
mDpm.setDeviceOwner(null);
mUserManager.setIsAdminUser(true);
diff --git a/tests/robotests/src/com/android/settings/wifi/LongPressWifiEntryPreferenceTest.java b/tests/robotests/src/com/android/settings/wifi/LongPressWifiEntryPreferenceTest.java
index efc2018..457d9ab 100644
--- a/tests/robotests/src/com/android/settings/wifi/LongPressWifiEntryPreferenceTest.java
+++ b/tests/robotests/src/com/android/settings/wifi/LongPressWifiEntryPreferenceTest.java
@@ -18,6 +18,10 @@
import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.content.Context;
@@ -60,7 +64,7 @@
when(mWifiEntry.canDisconnect()).thenReturn(false);
when(mWifiEntry.isSaved()).thenReturn(false);
- mPreference = new LongPressWifiEntryPreference(mContext, mWifiEntry, mFragment);
+ mPreference = spy(new LongPressWifiEntryPreference(mContext, mWifiEntry, mFragment));
}
@Test
@@ -106,4 +110,23 @@
assertThat(mPreference.shouldEnabled()).isTrue();
}
+
+ @Test
+ public void checkRestrictionAndSetDisabled_hasAdminRestrictions_doSetDisabledByAdmin() {
+ when(mContext.getUser()).thenReturn(null);
+ when(mWifiEntry.hasAdminRestrictions()).thenReturn(true);
+
+ mPreference.checkRestrictionAndSetDisabled();
+
+ verify(mPreference).setDisabledByAdmin(any());
+ }
+
+ @Test
+ public void checkRestrictionAndSetDisabled_noAdminRestrictions_doNotSetDisabledByAdmin() {
+ when(mWifiEntry.hasAdminRestrictions()).thenReturn(false);
+
+ mPreference.checkRestrictionAndSetDisabled();
+
+ verify(mPreference, never()).setDisabledByAdmin(any());
+ }
}
diff --git a/tests/robotests/src/com/android/settings/wifi/WifiEntryPreferenceTest.java b/tests/robotests/src/com/android/settings/wifi/WifiEntryPreferenceTest.java
index a60b531..316beb3 100644
--- a/tests/robotests/src/com/android/settings/wifi/WifiEntryPreferenceTest.java
+++ b/tests/robotests/src/com/android/settings/wifi/WifiEntryPreferenceTest.java
@@ -18,11 +18,15 @@
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.content.Context;
import android.graphics.drawable.Drawable;
+import android.net.wifi.sharedconnectivity.app.NetworkProviderInfo;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.LinearLayout;
@@ -31,6 +35,7 @@
import com.android.settingslib.R;
import com.android.settingslib.wifi.WifiUtils;
+import com.android.wifitrackerlib.HotspotNetworkEntry;
import com.android.wifitrackerlib.WifiEntry;
import org.junit.Before;
@@ -52,6 +57,8 @@
@Mock
private WifiEntry mMockWifiEntry;
@Mock
+ private HotspotNetworkEntry mHotspotNetworkEntry;
+ @Mock
private WifiUtils.InternetIconInjector mMockIconInjector;
@Mock
@@ -256,4 +263,26 @@
public void getSecondTargetResId_shouldNotReturnZero() {
assertThat(mPref.getSecondTargetResId()).isNotEqualTo(0);
}
+
+ @Test
+ public void refresh_itsHotspotNetworkEntry_shouldUpdateHotspotIcon() {
+ int deviceType = NetworkProviderInfo.DEVICE_TYPE_PHONE;
+ when(mHotspotNetworkEntry.getDeviceType()).thenReturn(deviceType);
+ WifiEntryPreference pref = spy(
+ new WifiEntryPreference(mContext, mHotspotNetworkEntry, mMockIconInjector));
+
+ pref.refresh();
+
+ verify(pref).updateHotspotIcon(deviceType);
+ }
+
+ @Test
+ public void refresh_notHotspotNetworkEntry_shouldNotUpdateHotspotIcon() {
+ WifiEntryPreference pref = spy(
+ new WifiEntryPreference(mContext, mMockWifiEntry, mMockIconInjector));
+
+ pref.refresh();
+
+ verify(pref, never()).updateHotspotIcon(anyInt());
+ }
}
diff --git a/tests/robotests/src/com/android/settings/wifi/details/WifiNetworkDetailsFragmentTest.java b/tests/robotests/src/com/android/settings/wifi/details/WifiNetworkDetailsFragmentTest.java
index 4f74254..52a1a6c 100644
--- a/tests/robotests/src/com/android/settings/wifi/details/WifiNetworkDetailsFragmentTest.java
+++ b/tests/robotests/src/com/android/settings/wifi/details/WifiNetworkDetailsFragmentTest.java
@@ -16,7 +16,19 @@
package com.android.settings.wifi.details;
+import static android.net.wifi.sharedconnectivity.app.HotspotNetwork.NETWORK_TYPE_CELLULAR;
+import static android.net.wifi.sharedconnectivity.app.HotspotNetwork.NETWORK_TYPE_ETHERNET;
+import static android.net.wifi.sharedconnectivity.app.HotspotNetwork.NETWORK_TYPE_UNKNOWN;
+import static android.net.wifi.sharedconnectivity.app.HotspotNetwork.NETWORK_TYPE_WIFI;
+import static android.telephony.SignalStrength.SIGNAL_STRENGTH_GREAT;
+
import static com.android.settings.wifi.WifiSettings.WIFI_DIALOG_ID;
+import static com.android.settings.wifi.details.WifiNetworkDetailsFragment.KEY_HOTSPOT_CONNECTION_CATEGORY;
+import static com.android.settings.wifi.details.WifiNetworkDetailsFragment.KEY_HOTSPOT_DEVICE_BATTERY;
+import static com.android.settings.wifi.details.WifiNetworkDetailsFragment.KEY_HOTSPOT_DEVICE_CATEGORY;
+import static com.android.settings.wifi.details.WifiNetworkDetailsFragment.KEY_HOTSPOT_DEVICE_INTERNET_SOURCE;
+import static com.android.settingslib.Utils.formatPercentage;
+import static com.android.wifitrackerlib.WifiEntry.WIFI_LEVEL_MAX;
import static com.google.common.truth.Truth.assertThat;
@@ -29,25 +41,35 @@
import static org.mockito.Mockito.verify;
import android.app.settings.SettingsEnums;
+import android.content.Context;
+import android.graphics.drawable.Drawable;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.widget.TextView;
import androidx.preference.Preference;
+import androidx.preference.PreferenceManager;
import androidx.preference.PreferenceScreen;
+import androidx.test.core.app.ApplicationProvider;
import com.android.settings.R;
import com.android.settings.core.BasePreferenceController;
+import com.android.settings.wifi.WifiUtils;
+import com.android.settings.wifi.details2.WifiDetailPreferenceController2;
import com.android.settingslib.core.AbstractPreferenceController;
import com.android.wifitrackerlib.NetworkDetailsTracker;
import com.android.wifitrackerlib.WifiEntry;
import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.mockito.Answers;
import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
+import org.mockito.Spy;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment;
@@ -56,23 +78,61 @@
@RunWith(RobolectricTestRunner.class)
public class WifiNetworkDetailsFragmentTest {
- private static final String TEST_PREFERENCE_KEY = "TEST_PREFERENCE_KEY";
+ static final String TEST_PREFERENCE_KEY = "TEST_PREFERENCE_KEY";
+ static final int BATTERY_PERCENTAGE_MAX = 100;
+ @Rule
+ public final MockitoRule mMockitoRule = MockitoJUnit.rule();
+ @Spy
+ Context mContext = ApplicationProvider.getApplicationContext();
+ @Mock(answer = Answers.RETURNS_DEEP_STUBS)
+ PreferenceManager mPreferenceManager;
+ @Mock
+ Preference mHotspotDeviceCategory;
+ @Mock
+ Preference mInternetSource;
+ @Mock
+ Preference mBattery;
+ @Mock
+ Preference mHotspotConnectionCategory;
+ @Mock
+ Menu mMenu;
+ @Mock
+ Drawable mDrawable;
+ @Mock
+ WifiDetailPreferenceController2 mWifiDetailPreferenceController2;
@Mock
WifiEntry mWifiEntry;
@Mock
NetworkDetailsTracker mNetworkDetailsTracker;
@Mock
- Menu mMenu;
- private WifiNetworkDetailsFragment mFragment;
+ WifiNetworkDetailsViewModel.HotspotNetworkData mHotspotNetworkData;
+
+ FakeFragment mFragment;
+ PreferenceScreen mScreen;
@Before
public void setUp() {
- MockitoAnnotations.initMocks(this);
doReturn(mWifiEntry).when(mNetworkDetailsTracker).getWifiEntry();
doReturn(true).when(mWifiEntry).isSaved();
+ doReturn(NETWORK_TYPE_WIFI).when(mHotspotNetworkData).getNetworkType();
+ doReturn(WIFI_LEVEL_MAX).when(mHotspotNetworkData).getUpstreamConnectionStrength();
+ doReturn(BATTERY_PERCENTAGE_MAX).when(mHotspotNetworkData).getBatteryPercentage();
+ doReturn(true).when(mHotspotNetworkData).isBatteryCharging();
- mFragment = new WifiNetworkDetailsFragment();
+ mFragment = spy(new FakeFragment());
+ doReturn(mPreferenceManager).when(mFragment).getPreferenceManager();
+ doReturn(mContext).when(mPreferenceManager).getContext();
+ doReturn(mContext).when(mFragment).getContext();
+ mScreen = spy(new PreferenceScreen(mContext, /* attrs= */ null));
+ doReturn(mPreferenceManager).when(mScreen).getPreferenceManager();
+ doReturn(mScreen).when(mFragment).getPreferenceScreen();
+
+ doReturn(mHotspotDeviceCategory).when(mScreen).findPreference(KEY_HOTSPOT_DEVICE_CATEGORY);
+ doReturn(mInternetSource).when(mScreen).findPreference(KEY_HOTSPOT_DEVICE_INTERNET_SOURCE);
+ doReturn(mBattery).when(mScreen).findPreference(KEY_HOTSPOT_DEVICE_BATTERY);
+ doReturn(mHotspotConnectionCategory).when(mScreen)
+ .findPreference(KEY_HOTSPOT_CONNECTION_CATEGORY);
mFragment.mNetworkDetailsTracker = mNetworkDetailsTracker;
}
@@ -94,7 +154,7 @@
@Test
public void onCreateOptionsMenu_shouldSetCorrectIcon() {
- final MenuItem menuItem = mock(MenuItem.class);
+ MenuItem menuItem = mock(MenuItem.class);
doReturn(menuItem).when(mMenu).add(anyInt(), eq(Menu.FIRST), anyInt(), anyInt());
mFragment.onCreateOptionsMenu(mMenu, mock(MenuInflater.class));
@@ -122,47 +182,124 @@
@Test
public void restrictUi_shouldShowRestrictedText() {
- final FakeFragment fragment = spy(new FakeFragment());
- final PreferenceScreen screen = mock(PreferenceScreen.class);
- final TextView restrictedText = mock(TextView.class);
- doReturn(screen).when(fragment).getPreferenceScreen();
- doReturn(false).when(fragment).isUiRestrictedByOnlyAdmin();
- doReturn(restrictedText).when(fragment).getEmptyTextView();
+ TextView restrictedText = mock(TextView.class);
+ doReturn(mScreen).when(mFragment).getPreferenceScreen();
+ doReturn(false).when(mFragment).isUiRestrictedByOnlyAdmin();
+ doReturn(restrictedText).when(mFragment).getEmptyTextView();
- fragment.restrictUi();
+ mFragment.restrictUi();
verify(restrictedText).setText(anyInt());
}
@Test
public void restrictUi_shouldRemoveAllPreferences() {
- final FakeFragment fragment = spy(new FakeFragment());
- final PreferenceScreen screen = mock(PreferenceScreen.class);
- doReturn(screen).when(fragment).getPreferenceScreen();
- doReturn(true).when(fragment).isUiRestrictedByOnlyAdmin();
+ doReturn(mScreen).when(mFragment).getPreferenceScreen();
+ doReturn(true).when(mFragment).isUiRestrictedByOnlyAdmin();
- fragment.restrictUi();
+ mFragment.restrictUi();
- verify(screen).removeAll();
+ verify(mScreen).removeAll();
}
@Test
public void refreshPreferences_controllerShouldUpdateStateAndDisplayPreference() {
- final FakeFragment fragment = spy(new FakeFragment());
- final PreferenceScreen screen = mock(PreferenceScreen.class);
- final Preference preference = mock(Preference.class);
- final TestController controller = mock(TestController.class);
- doReturn(screen).when(fragment).getPreferenceScreen();
- doReturn(preference).when(screen).findPreference(TEST_PREFERENCE_KEY);
+ Preference preference = mock(Preference.class);
+ TestController controller = mock(TestController.class);
+ doReturn(mScreen).when(mFragment).getPreferenceScreen();
+ doReturn(preference).when(mScreen).findPreference(TEST_PREFERENCE_KEY);
doReturn(TEST_PREFERENCE_KEY).when(controller).getPreferenceKey();
- fragment.mControllers = new ArrayList<>();
- fragment.mControllers.add(controller);
- fragment.addPreferenceController(controller);
+ mFragment.mControllers = new ArrayList<>();
+ mFragment.mControllers.add(controller);
+ mFragment.addPreferenceController(controller);
- fragment.refreshPreferences();
+ mFragment.refreshPreferences();
verify(controller).updateState(preference);
- verify(controller).displayPreference(screen);
+ verify(controller).displayPreference(mScreen);
+ }
+
+ @Test
+ public void onHotspotNetworkChanged_dataNull_hotspotSetVisibleFalse() {
+ mFragment.mWifiDetailPreferenceController2 = mWifiDetailPreferenceController2;
+
+ mFragment.onHotspotNetworkChanged(null);
+
+ verify(mHotspotDeviceCategory).setVisible(false);
+ verify(mHotspotConnectionCategory).setVisible(false);
+ verify(mWifiDetailPreferenceController2).setSignalStrengthTitle(R.string.wifi_signal);
+ }
+
+ @Test
+ public void onHotspotNetworkChanged_dataNotNull_hotspotSetVisibleTrue() {
+ mFragment.mWifiDetailPreferenceController2 = mWifiDetailPreferenceController2;
+
+ mFragment.onHotspotNetworkChanged(mHotspotNetworkData);
+
+ verify(mHotspotDeviceCategory).setVisible(true);
+ verify(mFragment).updateInternetSource(mHotspotNetworkData.getNetworkType(),
+ mHotspotNetworkData.getUpstreamConnectionStrength());
+ verify(mFragment).updateBattery(mHotspotNetworkData.isBatteryCharging(),
+ mHotspotNetworkData.getBatteryPercentage());
+ verify(mHotspotConnectionCategory).setVisible(true);
+ verify(mWifiDetailPreferenceController2)
+ .setSignalStrengthTitle(R.string.hotspot_connection_strength);
+ }
+
+ @Test
+ public void updateInternetSource_networkTypeWifi_setWifiResource() {
+ doReturn(mDrawable).when(mContext)
+ .getDrawable(WifiUtils.getInternetIconResource(WIFI_LEVEL_MAX, false));
+
+ mFragment.updateInternetSource(NETWORK_TYPE_WIFI, WIFI_LEVEL_MAX);
+
+ verify(mInternetSource).setSummary(R.string.internet_source_wifi);
+ verify(mInternetSource).setIcon(mDrawable);
+ }
+
+ @Test
+ public void updateInternetSource_networkTypeMobileData_setMobileDataResource() {
+ doReturn(mDrawable).when(mFragment).getMobileDataIcon(SIGNAL_STRENGTH_GREAT);
+
+ mFragment.updateInternetSource(NETWORK_TYPE_CELLULAR, SIGNAL_STRENGTH_GREAT);
+
+ verify(mInternetSource).setSummary(R.string.internet_source_mobile_data);
+ verify(mInternetSource).setIcon(mDrawable);
+ }
+
+ @Test
+ public void updateInternetSource_networkTypeEthernet_setEthernetResource() {
+ doReturn(mDrawable).when(mContext).getDrawable(R.drawable.ic_settings_ethernet);
+
+ mFragment.updateInternetSource(NETWORK_TYPE_ETHERNET, 0 /* don't care */);
+
+ verify(mInternetSource).setSummary(R.string.internet_source_ethernet);
+ verify(mInternetSource).setIcon(mDrawable);
+ }
+
+ @Test
+ public void updateInternetSource_networkTypeUnknown_setPlaceholderResource() {
+ mFragment.updateInternetSource(NETWORK_TYPE_UNKNOWN, 0 /* don't care */);
+
+ verify(mInternetSource).setSummary(R.string.summary_placeholder);
+ verify(mInternetSource).setIcon(null);
+ }
+
+ @Test
+ public void updateBattery_hiPercentageNoCharging_setSummaryCorrect() {
+ mFragment.updateBattery(false /* isChanging */, BATTERY_PERCENTAGE_MAX);
+
+ verify(mBattery).setSummary(formatPercentage(BATTERY_PERCENTAGE_MAX));
+ }
+
+ @Test
+ public void updateBattery_lowPercentageWithCharging_setSummaryCorrect() {
+ String summary = mContext.getString(R.string.hotspot_battery_charging_summary,
+ formatPercentage(0));
+
+ mFragment.updateBattery(true /* isChanging */, 0 /* percentage */);
+
+ verify(mBattery).setSummary(summary);
}
// Fake WifiNetworkDetailsFragment to override the protected method as public.
diff --git a/tests/robotests/src/com/android/settings/wifi/details2/WifiDetailPreferenceController2Test.java b/tests/robotests/src/com/android/settings/wifi/details2/WifiDetailPreferenceController2Test.java
index c86a023..406e0c3 100644
--- a/tests/robotests/src/com/android/settings/wifi/details2/WifiDetailPreferenceController2Test.java
+++ b/tests/robotests/src/com/android/settings/wifi/details2/WifiDetailPreferenceController2Test.java
@@ -15,8 +15,13 @@
*/
package com.android.settings.wifi.details2;
+import static android.net.wifi.sharedconnectivity.app.NetworkProviderInfo.DEVICE_TYPE_PHONE;
+
+import static com.android.settingslib.wifi.WifiUtils.getHotspotIconResource;
+
import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.anyBoolean;
import static org.mockito.Mockito.anyInt;
@@ -83,6 +88,7 @@
import com.android.settingslib.utils.StringUtil;
import com.android.settingslib.widget.ActionButtonsPreference;
import com.android.settingslib.widget.LayoutPreference;
+import com.android.wifitrackerlib.HotspotNetworkEntry;
import com.android.wifitrackerlib.NetworkDetailsTracker;
import com.android.wifitrackerlib.WifiEntry;
import com.android.wifitrackerlib.WifiEntry.ConnectCallback;
@@ -299,6 +305,8 @@
ShadowEntityHeaderController.setUseMock(mMockHeaderController);
// builder pattern
+ when(mMockHeaderController.setLabel(any(CharSequence.class)))
+ .thenReturn(mMockHeaderController);
when(mMockHeaderController.setRecyclerView(mMockFragment.getListView(), mLifecycle))
.thenReturn(mMockHeaderController);
when(mMockHeaderController.setSummary(nullable(String.class)))
@@ -526,12 +534,12 @@
}
@Test
- public void entityHeader_shouldNotHaveIconSetForNotInRangeNetwork() {
+ public void entityHeader_shouldHaveIconSetForNotInRangeNetwork() {
setUpForNotInRangeNetwork();
displayAndResume();
- verify(mMockHeaderController, never()).setIcon(any(Drawable.class));
+ verify(mMockHeaderController).setIcon(any(Drawable.class));
}
@Test
@@ -701,10 +709,10 @@
}
@Test
- public void linkSpeedPref_shouldNotShowIfNotSet() {
+ public void linkSpeedPref_shouldNotShowIfSpeedStringIsEmpty() {
setUpForConnectedNetwork();
setUpSpyController();
- when(mMockWifiInfo.getTxLinkSpeedMbps()).thenReturn(WifiInfo.LINK_SPEED_UNKNOWN);
+ when(mMockWifiEntry.getTxSpeedString()).thenReturn("");
displayAndResume();
@@ -712,42 +720,22 @@
}
@Test
- public void linkSpeedPref_shouldVisibleForConnectedNetwork() {
+ public void linkSpeedPref_shouldBeVisibleIfSpeedStringIsNotEmpty() {
setUpForConnectedNetwork();
setUpSpyController();
- String expectedLinkSpeed = mContext.getString(R.string.tx_link_speed, TX_LINK_SPEED);
+ when(mMockWifiEntry.getTxSpeedString()).thenReturn("100 Mbps");
displayAndResume();
verify(mMockTxLinkSpeedPref).setVisible(true);
- verify(mMockTxLinkSpeedPref).setSummary(expectedLinkSpeed);
+ verify(mMockTxLinkSpeedPref).setSummary("100 Mbps");
}
@Test
- public void linkSpeedPref_shouldInvisibleForDisconnectedNetwork() {
- setUpForDisconnectedNetwork();
-
- displayAndResume();
-
- verify(mMockTxLinkSpeedPref).setVisible(false);
- verify(mMockTxLinkSpeedPref, never()).setSummary(any(String.class));
- }
-
- @Test
- public void linkSpeedPref_shouldInvisibleForNotInRangeNetwork() {
- setUpForNotInRangeNetwork();
-
- displayAndResume();
-
- verify(mMockTxLinkSpeedPref).setVisible(false);
- verify(mMockTxLinkSpeedPref, never()).setSummary(any(String.class));
- }
-
- @Test
- public void rxLinkSpeedPref_shouldNotShowIfNotSet() {
+ public void rxLinkSpeedPref_shouldNotShowIfSpeedStringIsEmpty() {
setUpForConnectedNetwork();
setUpSpyController();
- when(mMockWifiInfo.getRxLinkSpeedMbps()).thenReturn(WifiInfo.LINK_SPEED_UNKNOWN);
+ when(mMockWifiEntry.getRxSpeedString()).thenReturn("");
displayAndResume();
@@ -755,35 +743,15 @@
}
@Test
- public void rxLinkSpeedPref_shouldVisibleForConnectedNetwork() {
+ public void rxLinkSpeedPref_shouldBeVisibleIfSpeedStringIsNotEmpty() {
setUpForConnectedNetwork();
setUpSpyController();
- String expectedLinkSpeed = mContext.getString(R.string.rx_link_speed, RX_LINK_SPEED);
+ when(mMockWifiEntry.getRxSpeedString()).thenReturn("100 Mbps");
displayAndResume();
verify(mMockRxLinkSpeedPref).setVisible(true);
- verify(mMockRxLinkSpeedPref).setSummary(expectedLinkSpeed);
- }
-
- @Test
- public void rxLinkSpeedPref_shouldInvisibleForDisconnectedNetwork() {
- setUpForDisconnectedNetwork();
-
- displayAndResume();
-
- verify(mMockRxLinkSpeedPref).setVisible(false);
- verify(mMockRxLinkSpeedPref, never()).setSummary(any(String.class));
- }
-
- @Test
- public void rxLinkSpeedPref_shouldInvisibleForNotInRangeNetwork() {
- setUpForNotInRangeNetwork();
-
- displayAndResume();
-
- verify(mMockRxLinkSpeedPref).setVisible(false);
- verify(mMockRxLinkSpeedPref, never()).setSummary(any(String.class));
+ verify(mMockRxLinkSpeedPref).setSummary("100 Mbps");
}
@Test
@@ -1856,6 +1824,71 @@
assertThat(info.getDisplayName().toString()).isEqualTo("sim2");
}
+ @Test
+ public void refreshEntryHeaderIcon_entityHeaderControllerNull_doNothing() {
+ setUpSpyController();
+ mController.mEntityHeaderController = null;
+
+ mController.refreshEntryHeaderIcon();
+
+ verify(mController, never()).getWifiDrawable(any());
+ }
+
+ @Test
+ public void refreshEntryHeaderIcon_entityHeaderControllerNotNull_setIcon() {
+ setUpSpyController();
+ mController.mEntityHeaderController = mMockHeaderController;
+
+ mController.refreshEntryHeaderIcon();
+
+ verify(mController).getWifiDrawable(any());
+ verify(mMockHeaderController).setIcon(any(Drawable.class));
+ }
+
+ @Test
+ public void getWifiDrawable_withHotspotNetworkEntry_returnHotspotDrawable() {
+ setUpSpyController();
+ HotspotNetworkEntry entry = mock(HotspotNetworkEntry.class);
+ when(entry.getDeviceType()).thenReturn(DEVICE_TYPE_PHONE);
+
+ mController.getWifiDrawable(entry);
+
+ verify(mContext).getDrawable(getHotspotIconResource(DEVICE_TYPE_PHONE));
+ }
+
+ @Test
+ public void getWifiDrawable_withWifiEntryNotShowXLevelIcon_getIconWithInternet() {
+ setUpSpyController();
+ when(mMockWifiEntry.getLevel()).thenReturn(WifiEntry.WIFI_LEVEL_MAX);
+ when(mMockWifiEntry.shouldShowXLevelIcon()).thenReturn(false);
+
+ mController.getWifiDrawable(mMockWifiEntry);
+
+ verify(mMockIconInjector).getIcon(eq(false) /* noInternet */, anyInt());
+ }
+
+ @Test
+ public void getWifiDrawable_withWifiEntryShowXLevelIcon_getIconWithNoInternet() {
+ setUpSpyController();
+ when(mMockWifiEntry.getLevel()).thenReturn(WifiEntry.WIFI_LEVEL_MAX);
+ when(mMockWifiEntry.shouldShowXLevelIcon()).thenReturn(true);
+
+ mController.getWifiDrawable(mMockWifiEntry);
+
+ verify(mMockIconInjector).getIcon(eq(true) /* noInternet */, anyInt());
+ verify(mMockIconInjector).getIcon(eq(true) /* noInternet */, anyInt());
+ }
+
+ @Test
+ public void setSignalStrengthTitle_prefNotNull_setPrefTitle() {
+ setUpSpyController();
+ mController.displayPreference(mMockScreen);
+
+ mController.setSignalStrengthTitle(R.string.hotspot_connection_strength);
+
+ verify(mMockSignalStrengthPref).setTitle(R.string.hotspot_connection_strength);
+ }
+
private SubscriptionInfo mockSubscriptionInfo(int subId, String displayName, int carrierId) {
SubscriptionInfo info = mock(SubscriptionInfo.class);
when(info.getSubscriptionId()).thenReturn(subId);
diff --git a/tests/robotests/src/com/android/settings/wifi/p2p/WifiP2pSettingsTest.java b/tests/robotests/src/com/android/settings/wifi/p2p/WifiP2pSettingsTest.java
index fbe184d..25a59a9 100644
--- a/tests/robotests/src/com/android/settings/wifi/p2p/WifiP2pSettingsTest.java
+++ b/tests/robotests/src/com/android/settings/wifi/p2p/WifiP2pSettingsTest.java
@@ -21,6 +21,7 @@
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
@@ -151,6 +152,13 @@
}
@Test
+ public void onDeviceInfoAvailable_nullChannel_shouldBeIgnored() {
+ mFragment.sChannel = null;
+ mFragment.onDeviceInfoAvailable(mock(WifiP2pDevice.class));
+ verify(mWifiP2pManager, never()).requestNetworkInfo(any(), any());
+ }
+
+ @Test
public void beSearching_getP2pStateDisabledIntent_shouldBeFalse() {
final Bundle bundle = new Bundle();
final Intent intent = new Intent(WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION);
diff --git a/tests/robotests/src/com/android/settings/wifi/slice/WifiSliceTest.java b/tests/robotests/src/com/android/settings/wifi/slice/WifiSliceTest.java
index 5ed2e8b..5c3f920 100644
--- a/tests/robotests/src/com/android/settings/wifi/slice/WifiSliceTest.java
+++ b/tests/robotests/src/com/android/settings/wifi/slice/WifiSliceTest.java
@@ -18,8 +18,10 @@
import static android.app.slice.Slice.HINT_LIST_ITEM;
import static android.app.slice.SliceItem.FORMAT_SLICE;
+import static android.net.wifi.sharedconnectivity.app.NetworkProviderInfo.DEVICE_TYPE_PHONE;
import static com.android.settings.wifi.slice.WifiSlice.DEFAULT_EXPANDED_ROW_COUNT;
+import static com.android.settingslib.wifi.WifiUtils.getHotspotIconResource;
import static com.google.common.truth.Truth.assertThat;
@@ -49,6 +51,7 @@
import com.android.settings.slices.SliceBackgroundWorker;
import com.android.settings.testutils.SliceTester;
import com.android.settings.testutils.shadow.ShadowWifiSlice;
+import com.android.settings.wifi.WifiUtils;
import com.android.wifitrackerlib.WifiEntry;
import com.android.wifitrackerlib.WifiEntry.ConnectedState;
@@ -335,6 +338,27 @@
assertThat(wifiManager.getWifiState()).isEqualTo(WifiManager.WIFI_STATE_ENABLED);
}
+ @Test
+ public void getWifiIconResId_isInstantHotspotNetwork_returnHotspotIcon() {
+ WifiSliceItem wifiSliceItem = mock(WifiSliceItem.class);
+ when(wifiSliceItem.isInstantHotspotNetwork()).thenReturn(true);
+ when(wifiSliceItem.getInstantHotspotDeviceType()).thenReturn(DEVICE_TYPE_PHONE);
+
+ assertThat(mWifiSlice.getWifiIconResId(wifiSliceItem))
+ .isEqualTo(getHotspotIconResource(DEVICE_TYPE_PHONE));
+ }
+
+ @Test
+ public void getWifiIconResId_notInstantHotspotNetwork_returnInternetIcon() {
+ WifiSliceItem wifiSliceItem = mock(WifiSliceItem.class);
+ when(wifiSliceItem.isInstantHotspotNetwork()).thenReturn(false);
+ when(wifiSliceItem.getLevel()).thenReturn(WifiEntry.WIFI_LEVEL_MAX);
+ when(wifiSliceItem.shouldShowXLevelIcon()).thenReturn(false);
+
+ assertThat(mWifiSlice.getWifiIconResId(wifiSliceItem))
+ .isEqualTo(WifiUtils.getInternetIconResource(WifiEntry.WIFI_LEVEL_MAX, false));
+ }
+
@Implements(SliceBackgroundWorker.class)
public static class ShadowSliceBackgroundWorker {
private static WifiScanWorker mWifiScanWorker = mock(WifiScanWorker.class);
diff --git a/tests/robotests/src/com/android/settings/wifi/tether/WifiHotspotSpeedSettingsTest.java b/tests/robotests/src/com/android/settings/wifi/tether/WifiHotspotSpeedSettingsTest.java
index 969f992..31f4c09 100644
--- a/tests/robotests/src/com/android/settings/wifi/tether/WifiHotspotSpeedSettingsTest.java
+++ b/tests/robotests/src/com/android/settings/wifi/tether/WifiHotspotSpeedSettingsTest.java
@@ -25,8 +25,10 @@
import static com.android.settings.wifi.tether.WifiHotspotSpeedSettings.KEY_SPEED_5GHZ;
import static com.android.settings.wifi.tether.WifiHotspotSpeedSettings.KEY_SPEED_6GHZ;
+import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.anyBoolean;
import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -89,98 +91,156 @@
@Test
public void onSpeedInfoMapDataChanged_checkedSpeed2g_checkedToRadioButton2g() {
- mSpeedInfo2g = new WifiHotspotSpeedViewModel.SpeedInfo(false, true, false);
+ mSpeedInfo2g = new WifiHotspotSpeedViewModel.SpeedInfo(true, true, true);
updateSpeedInfoMap();
- mockRadioButton(true, false, true);
+ mockRadioButton(false, false, false);
mSettings.mSpeedPreferenceMap.put(SPEED_2GHZ, mRadioButton);
mSettings.onSpeedInfoMapDataChanged(mSpeedInfoMap);
- verifyRadioButton(false, true, false);
+ verifyRadioButton(true, true, true);
}
@Test
public void onSpeedInfoMapDataChanged_uncheckedSpeed2g_uncheckedToRadioButton2g() {
- mSpeedInfo2g = new WifiHotspotSpeedViewModel.SpeedInfo(true, false, true);
+ mSpeedInfo2g = new WifiHotspotSpeedViewModel.SpeedInfo(false, false, true);
updateSpeedInfoMap();
- mockRadioButton(false, true, false);
+ mockRadioButton(true, true, true);
mSettings.mSpeedPreferenceMap.put(SPEED_2GHZ, mRadioButton);
mSettings.onSpeedInfoMapDataChanged(mSpeedInfoMap);
- verifyRadioButton(true, false, true);
+ verifyRadioButton(false, false, true);
}
@Test
public void onSpeedInfoMapDataChanged_checkedSpeed5g_checkedToRadioButton5g() {
- mSpeedInfo5g = new WifiHotspotSpeedViewModel.SpeedInfo(false, true, false);
+ mSpeedInfo5g = new WifiHotspotSpeedViewModel.SpeedInfo(true, true, true);
updateSpeedInfoMap();
- mockRadioButton(true, false, true);
+ mockRadioButton(false, false, false);
mSettings.mSpeedPreferenceMap.put(SPEED_5GHZ, mRadioButton);
mSettings.onSpeedInfoMapDataChanged(mSpeedInfoMap);
- verifyRadioButton(false, true, false);
+ verifyRadioButton(true, true, true);
}
@Test
public void onSpeedInfoMapDataChanged_uncheckedSpeed5g_uncheckedToRadioButton5g() {
- mSpeedInfo5g = new WifiHotspotSpeedViewModel.SpeedInfo(true, false, true);
+ mSpeedInfo5g = new WifiHotspotSpeedViewModel.SpeedInfo(false, false, true);
updateSpeedInfoMap();
- mockRadioButton(false, true, false);
+ mockRadioButton(true, true, true);
mSettings.mSpeedPreferenceMap.put(SPEED_5GHZ, mRadioButton);
mSettings.onSpeedInfoMapDataChanged(mSpeedInfoMap);
- verifyRadioButton(true, false, true);
+ verifyRadioButton(false, false, true);
}
@Test
public void onSpeedInfoMapDataChanged_checkedSpeed2g5g_checkedToRadioButton2g5g() {
- mSpeedInfo2g5g = new WifiHotspotSpeedViewModel.SpeedInfo(false, true, false);
+ mSpeedInfo2g5g = new WifiHotspotSpeedViewModel.SpeedInfo(true, true, true);
updateSpeedInfoMap();
- mockRadioButton(true, false, true);
+ mockRadioButton(false, false, false);
mSettings.mSpeedPreferenceMap.put(SPEED_2GHZ_5GHZ, mRadioButton);
mSettings.onSpeedInfoMapDataChanged(mSpeedInfoMap);
- verifyRadioButton(false, true, false);
+ verifyRadioButton(true, true, true);
}
@Test
- public void onSpeedInfoMapDataChanged_uncheckedSpeed25g_uncheckedToRadioButton25g() {
- mSpeedInfo2g5g = new WifiHotspotSpeedViewModel.SpeedInfo(true, false, true);
+ public void onSpeedInfoMapDataChanged_uncheckedSpeed2g5g_uncheckedToRadioButton2g5g() {
+ mSpeedInfo2g5g = new WifiHotspotSpeedViewModel.SpeedInfo(false, false, true);
updateSpeedInfoMap();
- mockRadioButton(false, true, false);
+ mockRadioButton(true, true, true);
mSettings.mSpeedPreferenceMap.put(SPEED_2GHZ_5GHZ, mRadioButton);
mSettings.onSpeedInfoMapDataChanged(mSpeedInfoMap);
- verifyRadioButton(true, false, true);
+ verifyRadioButton(false, false, true);
}
@Test
public void onSpeedInfoMapDataChanged_checkedSpeed6g_checkedToRadioButton6g() {
- mSpeedInfo6g = new WifiHotspotSpeedViewModel.SpeedInfo(false, true, false);
+ mSpeedInfo6g = new WifiHotspotSpeedViewModel.SpeedInfo(true, true, true);
updateSpeedInfoMap();
+ mockRadioButton(false, false, false);
+ mSettings.mSpeedPreferenceMap.put(SPEED_6GHZ, mRadioButton);
+
+ mSettings.onSpeedInfoMapDataChanged(mSpeedInfoMap);
+
+ verifyRadioButton(true, true, true);
+ }
+
+ @Test
+ public void onSpeedInfoMapDataChanged_uncheckedSpeed6g_uncheckedToRadioButton6g() {
+ mSpeedInfo6g = new WifiHotspotSpeedViewModel.SpeedInfo(false, false, true);
+ updateSpeedInfoMap();
+ mockRadioButton(true, true, true);
+ mSettings.mSpeedPreferenceMap.put(SPEED_6GHZ, mRadioButton);
+
+ mSettings.onSpeedInfoMapDataChanged(mSpeedInfoMap);
+
+ verifyRadioButton(false, false, true);
+ }
+
+ @Test
+ public void onSpeedInfoMapDataChanged_setVisibleFalse_setVisibleOnly() {
+ mSpeedInfo6g = new WifiHotspotSpeedViewModel.SpeedInfo(true, true, false);
+ mSpeedInfo6g.mSummary = "summary";
+ mSpeedInfoMap.put(SPEED_6GHZ, mSpeedInfo6g);
+ mockRadioButton(true, true, true);
+ mSettings.mSpeedPreferenceMap.put(SPEED_6GHZ, mRadioButton);
+
+ mSettings.onSpeedInfoMapDataChanged(mSpeedInfoMap);
+
+ verify(mRadioButton).setVisible(false);
+ verify(mRadioButton, never()).setChecked(anyBoolean());
+ verify(mRadioButton, never()).setEnabled(anyBoolean());
+ verify(mRadioButton, never()).setSummary(anyString());
+ }
+
+ @Test
+ public void onSpeedInfoMapDataChanged_setVisibleTrue_setAllProperties() {
+ mSpeedInfo6g = new WifiHotspotSpeedViewModel.SpeedInfo(true, true, true);
+ mSpeedInfo6g.mSummary = "summary";
+ mSpeedInfoMap.put(SPEED_6GHZ, mSpeedInfo6g);
+ mockRadioButton(true, true, true);
+ mSettings.mSpeedPreferenceMap.put(SPEED_6GHZ, mRadioButton);
+
+ mSettings.onSpeedInfoMapDataChanged(mSpeedInfoMap);
+
+ verify(mRadioButton).setVisible(true);
+ verify(mRadioButton).setChecked(anyBoolean());
+ verify(mRadioButton).setEnabled(anyBoolean());
+ verify(mRadioButton).setSummary(anyString());
+ }
+
+ @Test
+ public void onSpeedInfoMapDataChanged_summaryIsNull_doNotSetSummary() {
+ mSpeedInfo6g = new WifiHotspotSpeedViewModel.SpeedInfo(true, true, true);
+ mSpeedInfo6g.mSummary = null;
+ mSpeedInfoMap.put(SPEED_6GHZ, mSpeedInfo6g);
+ mockRadioButton(true, true, true);
+ mSettings.mSpeedPreferenceMap.put(SPEED_6GHZ, mRadioButton);
+
+ mSettings.onSpeedInfoMapDataChanged(mSpeedInfoMap);
+
+ verify(mRadioButton, never()).setSummary(anyString());
+ }
+
+ @Test
+ public void onSpeedInfoMapDataChanged_summaryNotNull_setSummary() {
+ mSpeedInfo6g = new WifiHotspotSpeedViewModel.SpeedInfo(true, false, true);
+ mSpeedInfo6g.mSummary = "summary";
+ mSpeedInfoMap.put(SPEED_6GHZ, mSpeedInfo6g);
mockRadioButton(true, false, true);
mSettings.mSpeedPreferenceMap.put(SPEED_6GHZ, mRadioButton);
mSettings.onSpeedInfoMapDataChanged(mSpeedInfoMap);
- verifyRadioButton(false, true, false);
- }
-
- @Test
- public void onSpeedInfoMapDataChanged_uncheckedSpeed6g_uncheckedToRadioButton6g() {
- mSpeedInfo6g = new WifiHotspotSpeedViewModel.SpeedInfo(true, false, true);
- updateSpeedInfoMap();
- mockRadioButton(false, true, false);
- mSettings.mSpeedPreferenceMap.put(SPEED_6GHZ, mRadioButton);
-
- mSettings.onSpeedInfoMapDataChanged(mSpeedInfoMap);
-
- verifyRadioButton(true, false, true);
+ verify(mRadioButton).setSummary(mSpeedInfo6g.mSummary);
}
@Test
diff --git a/tests/robotests/src/com/android/settings/wifi/tether/WifiTetherAutoOffPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/wifi/tether/WifiTetherAutoOffPreferenceControllerTest.java
index fbc4aaa..535e4ab 100644
--- a/tests/robotests/src/com/android/settings/wifi/tether/WifiTetherAutoOffPreferenceControllerTest.java
+++ b/tests/robotests/src/com/android/settings/wifi/tether/WifiTetherAutoOffPreferenceControllerTest.java
@@ -18,6 +18,7 @@
import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -28,6 +29,10 @@
import androidx.preference.SwitchPreference;
+import com.android.settings.testutils.FakeFeatureFactory;
+import com.android.settings.wifi.factory.WifiFeatureProvider;
+import com.android.settings.wifi.repository.WifiHotspotRepository;
+
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -54,6 +59,8 @@
mContext = spy(RuntimeEnvironment.application);
+ WifiFeatureProvider provider = FakeFeatureFactory.setupForTest().getWifiFeatureProvider();
+ when(provider.getWifiHotspotRepository()).thenReturn(mock(WifiHotspotRepository.class));
when(mContext.getSystemService(WifiManager.class)).thenReturn(mWifiManager);
mSoftApConfiguration = new SoftApConfiguration.Builder().build();
when(mWifiManager.getSoftApConfiguration()).thenReturn(mSoftApConfiguration);
@@ -101,6 +108,32 @@
assertThat(mSwitchPreference.isChecked()).isTrue();
}
+ @Test
+ public void onPreferenceChange_needShutdownSecondarySap_setSecondarySap() {
+ mController.mNeedShutdownSecondarySap = true;
+ setConfigShutdownSecondarySap(false);
+
+ mController.onPreferenceChange(mSwitchPreference, true);
+
+ ArgumentCaptor<SoftApConfiguration> config =
+ ArgumentCaptor.forClass(SoftApConfiguration.class);
+ verify(mWifiManager).setSoftApConfiguration(config.capture());
+ assertThat(config.getValue().isBridgedModeOpportunisticShutdownEnabled()).isTrue();
+ }
+
+ @Test
+ public void onPreferenceChange_noNeedShutdownSecondarySap_doNotSetSecondarySap() {
+ mController.mNeedShutdownSecondarySap = false;
+ setConfigShutdownSecondarySap(false);
+
+ mController.onPreferenceChange(mSwitchPreference, true);
+
+ ArgumentCaptor<SoftApConfiguration> config =
+ ArgumentCaptor.forClass(SoftApConfiguration.class);
+ verify(mWifiManager).setSoftApConfiguration(config.capture());
+ assertThat(config.getValue().isBridgedModeOpportunisticShutdownEnabled()).isFalse();
+ }
+
private boolean getAutoOffSetting() {
ArgumentCaptor<SoftApConfiguration> softApConfigCaptor =
ArgumentCaptor.forClass(SoftApConfiguration.class);
@@ -115,4 +148,12 @@
.build();
when(mWifiManager.getSoftApConfiguration()).thenReturn(mSoftApConfiguration);
}
+
+ private void setConfigShutdownSecondarySap(boolean enabled) {
+ mSoftApConfiguration =
+ new SoftApConfiguration.Builder(mSoftApConfiguration)
+ .setBridgedModeOpportunisticShutdownEnabled(enabled)
+ .build();
+ when(mWifiManager.getSoftApConfiguration()).thenReturn(mSoftApConfiguration);
+ }
}
diff --git a/tests/robotests/src/com/android/settings/wifi/tether/WifiTetherSettingsTest.java b/tests/robotests/src/com/android/settings/wifi/tether/WifiTetherSettingsTest.java
index 5005f4c..e67717d 100644
--- a/tests/robotests/src/com/android/settings/wifi/tether/WifiTetherSettingsTest.java
+++ b/tests/robotests/src/com/android/settings/wifi/tether/WifiTetherSettingsTest.java
@@ -23,6 +23,7 @@
import static com.android.settings.wifi.WifiUtils.setCanShowWifiHotspotCached;
import static com.android.settings.wifi.repository.WifiHotspotRepository.BAND_2GHZ_5GHZ_6GHZ;
+import static com.android.settings.wifi.tether.WifiTetherSettings.KEY_INSTANT_HOTSPOT;
import static com.android.settings.wifi.tether.WifiTetherSettings.KEY_WIFI_HOTSPOT_SECURITY;
import static com.android.settings.wifi.tether.WifiTetherSettings.KEY_WIFI_HOTSPOT_SPEED;
@@ -90,6 +91,7 @@
private static final String[] WIFI_REGEXS = {"wifi_regexs"};
private static final String SSID = "ssid";
private static final String PASSWORD = "password";
+ private static final String SUMMARY = "summary";
@Rule
public final MockitoRule mMockitoRule = MockitoJUnit.rule();
@@ -133,6 +135,10 @@
private WifiTetherAutoOffPreferenceController mWifiTetherAutoOffPreferenceController;
@Mock
private WifiTetherMaximizeCompatibilityPreferenceController mMaxCompatibilityPrefController;
+ @Mock
+ private Preference mInstantHotspot;
+ @Mock
+ private LiveData<String> mInstantHotspotSummary;
private WifiTetherSettings mSettings;
@@ -146,6 +152,7 @@
doReturn(mTetheringManager).when(mContext).getSystemService(Context.TETHERING_SERVICE);
doReturn(WIFI_REGEXS).when(mTetheringManager).getTetherableWifiRegexs();
doReturn(mUserManager).when(mContext).getSystemService(Context.USER_SERVICE);
+ doReturn(true).when(mUserManager).isAdminUser();
when(mWifiRestriction.isTetherAvailable(mContext)).thenReturn(true);
when(mWifiRestriction.isHotspotAvailable(mContext)).thenReturn(true);
@@ -154,8 +161,10 @@
when(provider.getWifiTetherViewModel(mock(ViewModelStoreOwner.class)))
.thenReturn(mWifiTetherViewModel);
when(mWifiTetherViewModel.isSpeedFeatureAvailable()).thenReturn(false);
+ when(mWifiTetherViewModel.isInstantHotspotFeatureAvailable()).thenReturn(true);
when(mWifiTetherViewModel.getSecuritySummary()).thenReturn(mSecuritySummary);
when(mWifiTetherViewModel.getSpeedSummary()).thenReturn(mSpeedSummary);
+ when(mWifiTetherViewModel.getInstantHotspotSummary()).thenReturn(mInstantHotspotSummary);
mSettings = spy(new WifiTetherSettings(mWifiRestriction));
mSettings.mMainSwitchBar = mMainSwitchBar;
@@ -171,6 +180,8 @@
mSettings.mWifiTetherViewModel = mWifiTetherViewModel;
when(mSettings.findPreference(KEY_WIFI_HOTSPOT_SECURITY)).thenReturn(mWifiHotspotSecurity);
when(mSettings.findPreference(KEY_WIFI_HOTSPOT_SPEED)).thenReturn(mWifiHotspotSpeed);
+ when(mSettings.findPreference(KEY_INSTANT_HOTSPOT)).thenReturn(mInstantHotspot);
+ mSettings.mInstantHotspot = mInstantHotspot;
}
@Test
@@ -185,9 +196,21 @@
}
@Test
+ @Config(shadows = ShadowRestrictedDashboardFragment.class)
+ public void onCreate_uiIsRestricted_shouldNotGetViewModel() {
+ mSettings.mWifiTetherViewModel = null;
+ when(mWifiRestriction.isHotspotAvailable(mContext)).thenReturn(false);
+
+ mSettings.onCreate(null);
+
+ assertThat(mSettings.mWifiTetherViewModel).isNull();
+ }
+
+ @Test
@Config(shadows = ShadowFragment.class)
public void onStart_uiIsRestricted_removeAllPreferences() {
spyWifiTetherSettings();
+ mSettings.mUnavailable = true;
mSettings.onStart();
@@ -306,7 +329,7 @@
}
@Test
- public void isPageSearchEnabled_canShowWifiHotspot_returnTrue() {
+ public void isPageSearchEnabled_allReady_returnTrue() {
setCanShowWifiHotspotCached(true);
assertThat(WifiTetherSettings.SEARCH_INDEX_DATA_PROVIDER.isPageSearchEnabled(mContext))
@@ -314,6 +337,14 @@
}
@Test
+ public void isPageSearchEnabled_isNotAdminUser_returnFalse() {
+ doReturn(false).when(mUserManager).isAdminUser();
+
+ assertThat(WifiTetherSettings.SEARCH_INDEX_DATA_PROVIDER.isPageSearchEnabled(mContext))
+ .isFalse();
+ }
+
+ @Test
public void isPageSearchEnabled_canNotShowWifiHotspot_returnFalse() {
setCanShowWifiHotspotCached(false);
@@ -352,6 +383,47 @@
}
@Test
+ public void setupInstantHotspot_featureNotAvailable_doNothing() {
+ mSettings.setupInstantHotspot(false /* isFeatureAvailable */);
+
+ verify(mSettings, never()).findPreference(KEY_INSTANT_HOTSPOT);
+ verify(mWifiTetherViewModel, never()).getInstantHotspotSummary();
+ }
+
+ @Test
+ public void setupInstantHotspot_featureAvailable_doSetup() {
+ when(mWifiTetherViewModel.isInstantHotspotFeatureAvailable()).thenReturn(true);
+
+ mSettings.setupInstantHotspot(true /* isFeatureAvailable */);
+
+ verify(mSettings).findPreference(KEY_INSTANT_HOTSPOT);
+ verify(mInstantHotspotSummary).observe(any(), any());
+ verify(mInstantHotspot).setOnPreferenceClickListener(any());
+ }
+
+ @Test
+ public void onInstantHotspotChanged_nullRecord_setVisibleFalse() {
+ mSettings.onInstantHotspotChanged(null);
+
+ verify(mInstantHotspot).setVisible(false);
+ }
+
+ @Test
+ public void onInstantHotspotChanged_summaryNull_setVisibleFalse() {
+ mSettings.onInstantHotspotChanged(null);
+
+ verify(mInstantHotspot).setVisible(false);
+ }
+
+ @Test
+ public void onInstantHotspotChanged_summaryNotNull_setVisibleAndSummary() {
+ mSettings.onInstantHotspotChanged(SUMMARY);
+
+ verify(mInstantHotspot).setVisible(true);
+ verify(mInstantHotspot).setSummary(SUMMARY);
+ }
+
+ @Test
public void buildNewConfig_speedFeatureIsAvailableAndPasswordChanged_bandShouldNotBeLost() {
String newPassword = "new" + PASSWORD;
SoftApConfiguration currentConfig = new SoftApConfiguration.Builder()
@@ -419,5 +491,10 @@
public void onCreate(Bundle icicle) {
// do nothing
}
+
+ @Implementation
+ public boolean isUiRestricted() {
+ return false;
+ }
}
}
diff --git a/tests/spa_unit/res/drawable/regulatory_info.xml b/tests/spa_unit/res/drawable/regulatory_info.xml
new file mode 100644
index 0000000..7cc3803
--- /dev/null
+++ b/tests/spa_unit/res/drawable/regulatory_info.xml
@@ -0,0 +1,20 @@
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+ <solid android:color="#ff0000"/>
+ <size android:width="24dp" android:height="24dp" />
+</shape>
diff --git a/tests/spa_unit/res/drawable/regulatory_info_sku.xml b/tests/spa_unit/res/drawable/regulatory_info_sku.xml
new file mode 100644
index 0000000..634e55e
--- /dev/null
+++ b/tests/spa_unit/res/drawable/regulatory_info_sku.xml
@@ -0,0 +1,21 @@
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+ <solid android:color="#00ff00"/>
+ <size android:width="24dp" android:height="24dp" />
+</shape>
+
diff --git a/tests/spa_unit/res/drawable/regulatory_info_sku1_coo.xml b/tests/spa_unit/res/drawable/regulatory_info_sku1_coo.xml
new file mode 100644
index 0000000..7e6b9ef
--- /dev/null
+++ b/tests/spa_unit/res/drawable/regulatory_info_sku1_coo.xml
@@ -0,0 +1,20 @@
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+ <solid android:color="#0000ff"/>
+ <size android:width="24dp" android:height="24dp" />
+</shape>
diff --git a/tests/spa_unit/src/com/android/settings/applications/specialaccess/DataSaverControllerTest.kt b/tests/spa_unit/src/com/android/settings/applications/specialaccess/DataSaverControllerTest.kt
new file mode 100644
index 0000000..c2413af
--- /dev/null
+++ b/tests/spa_unit/src/com/android/settings/applications/specialaccess/DataSaverControllerTest.kt
@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.applications.specialaccess
+
+import android.content.Context
+import android.content.pm.ApplicationInfo
+import android.content.res.Resources
+import android.net.NetworkPolicyManager
+import android.net.NetworkPolicyManager.POLICY_ALLOW_METERED_BACKGROUND
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.settings.R
+import com.android.settings.applications.specialaccess.DataSaverController.Companion.getUnrestrictedSummary
+import com.android.settings.core.BasePreferenceController.AVAILABLE
+import com.android.settings.core.BasePreferenceController.UNSUPPORTED_ON_DEVICE
+import com.android.settingslib.spaprivileged.model.app.AppListRepository
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Spy
+import org.mockito.junit.MockitoJUnit
+import org.mockito.junit.MockitoRule
+import org.mockito.Mockito.`when` as whenever
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@RunWith(AndroidJUnit4::class)
+class DataSaverControllerTest {
+ @get:Rule
+ val mockito: MockitoRule = MockitoJUnit.rule()
+
+ @Spy
+ private val context: Context = ApplicationProvider.getApplicationContext()
+
+ @Spy
+ private val resources: Resources = context.resources
+
+ @Mock
+ private lateinit var networkPolicyManager: NetworkPolicyManager
+
+ @Mock
+ private lateinit var dataSaverController: DataSaverController
+
+ @Before
+ fun setUp() {
+ whenever(context.applicationContext).thenReturn(context)
+ whenever(context.resources).thenReturn(resources)
+ whenever(NetworkPolicyManager.from(context)).thenReturn(networkPolicyManager)
+
+ dataSaverController = DataSaverController(context, "key")
+ }
+
+ @Test
+ fun getAvailabilityStatus_whenConfigOn_available() {
+ whenever(resources.getBoolean(R.bool.config_show_data_saver)).thenReturn(true)
+ assertThat(dataSaverController.availabilityStatus).isEqualTo(AVAILABLE)
+ }
+
+ @Test
+ fun getAvailabilityStatus_whenConfigOff_unsupportedOnDevice() {
+ whenever(resources.getBoolean(R.bool.config_show_data_saver)).thenReturn(false)
+ assertThat(dataSaverController.availabilityStatus).isEqualTo(UNSUPPORTED_ON_DEVICE)
+ }
+
+ @Test
+ fun getUnrestrictedSummary_whenTwoAppsAllowed() = runTest {
+ whenever(
+ networkPolicyManager.getUidsWithPolicy(POLICY_ALLOW_METERED_BACKGROUND)
+ ).thenReturn(intArrayOf(APP1.uid, APP2.uid))
+
+ val summary =
+ getUnrestrictedSummary(context = context, appListRepository = FakeAppListRepository)
+
+ assertThat(summary)
+ .isEqualTo("2 apps allowed to use unrestricted data when Data Saver is on")
+ }
+
+ @Test
+ fun getUnrestrictedSummary_whenNoAppsAllowed() = runTest {
+ whenever(
+ networkPolicyManager.getUidsWithPolicy(POLICY_ALLOW_METERED_BACKGROUND)
+ ).thenReturn(intArrayOf())
+
+ val summary =
+ getUnrestrictedSummary(context = context, appListRepository = FakeAppListRepository)
+
+ assertThat(summary)
+ .isEqualTo("0 apps allowed to use unrestricted data when Data Saver is on")
+ }
+
+ private companion object {
+ val APP1 = ApplicationInfo().apply { uid = 10001 }
+ val APP2 = ApplicationInfo().apply { uid = 10002 }
+ val APP3 = ApplicationInfo().apply { uid = 10003 }
+
+ object FakeAppListRepository : AppListRepository {
+ override suspend fun loadApps(
+ userId: Int,
+ loadInstantApps: Boolean,
+ matchAnyUserForAdmin: Boolean,
+ ) = emptyList<ApplicationInfo>()
+
+ override fun showSystemPredicate(
+ userIdFlow: Flow<Int>,
+ showSystemFlow: Flow<Boolean>,
+ ): Flow<(app: ApplicationInfo) -> Boolean> = flowOf { false }
+
+ override fun getSystemPackageNamesBlocking(userId: Int): Set<String> = emptySet()
+
+ override suspend fun loadAndFilterApps(userId: Int, isSystemApp: Boolean) =
+ listOf(APP1, APP2, APP3)
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/spa_unit/src/com/android/settings/datausage/DataUsageFormatterTest.kt b/tests/spa_unit/src/com/android/settings/datausage/DataUsageFormatterTest.kt
new file mode 100644
index 0000000..dc6a421
--- /dev/null
+++ b/tests/spa_unit/src/com/android/settings/datausage/DataUsageFormatterTest.kt
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.datausage
+
+import android.content.Context
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.settings.datausage.DataUsageFormatter.getBytesDisplayUnit
+import com.google.common.truth.Truth.assertThat
+
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class DataUsageFormatterTest {
+ private val context: Context = ApplicationProvider.getApplicationContext()
+
+ @Test
+ fun getUnitDisplayName_megaByte() {
+ val displayName = context.resources.getBytesDisplayUnit(ONE_MEGA_BYTE_IN_BYTES)
+
+ assertThat(displayName).isEqualTo("MB")
+ }
+
+ @Test
+ fun getUnitDisplayName_gigaByte() {
+ val displayName = context.resources.getBytesDisplayUnit(ONE_GIGA_BYTE_IN_BYTES)
+
+ assertThat(displayName).isEqualTo("GB")
+ }
+
+ private companion object {
+ const val ONE_MEGA_BYTE_IN_BYTES = 1024L * 1024
+ const val ONE_GIGA_BYTE_IN_BYTES = 1024L * 1024 * 1024
+ }
+}
\ No newline at end of file
diff --git a/tests/spa_unit/src/com/android/settings/deviceinfo/regulatory/RegulatoryInfoTest.kt b/tests/spa_unit/src/com/android/settings/deviceinfo/regulatory/RegulatoryInfoTest.kt
new file mode 100644
index 0000000..f1e18fc
--- /dev/null
+++ b/tests/spa_unit/src/com/android/settings/deviceinfo/regulatory/RegulatoryInfoTest.kt
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.deviceinfo.regulatory
+
+import android.content.Context
+import android.graphics.drawable.Drawable
+import android.os.SystemProperties
+import androidx.annotation.DrawableRes
+import androidx.core.graphics.drawable.toBitmap
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.dx.mockito.inline.extended.ExtendedMockito
+import com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn
+import com.android.settings.deviceinfo.regulatory.RegulatoryInfo.KEY_COO
+import com.android.settings.deviceinfo.regulatory.RegulatoryInfo.KEY_SKU
+import com.android.settings.deviceinfo.regulatory.RegulatoryInfo.getRegulatoryInfo
+import com.android.settings.tests.spa_unit.R
+import com.google.common.truth.Truth.assertThat
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.MockitoSession
+import org.mockito.Spy
+import org.mockito.quality.Strictness
+
+@RunWith(AndroidJUnit4::class)
+class RegulatoryInfoTest {
+ private lateinit var mockSession: MockitoSession
+
+ @Spy
+ private val context: Context = ApplicationProvider.getApplicationContext()
+
+ @Before
+ fun setUp() {
+ mockSession = ExtendedMockito.mockitoSession()
+ .initMocks(this)
+ .mockStatic(SystemProperties::class.java)
+ .strictness(Strictness.LENIENT)
+ .startMocking()
+ }
+
+ @After
+ fun tearDown() {
+ mockSession.finishMocking()
+ }
+
+ @Test
+ fun getRegulatoryInfo_noSkuProperty_shouldReturnDefaultLabel() {
+ doReturn("").`when` { SystemProperties.get(KEY_SKU) }
+
+ val regulatoryInfo = context.getRegulatoryInfo()
+
+ assertDrawableSameAs(regulatoryInfo, R.drawable.regulatory_info)
+ }
+
+ @Test
+ fun getResourceId_noCooProperty_shouldReturnSkuLabel() {
+ doReturn("sku").`when` { SystemProperties.get(KEY_SKU) }
+ doReturn("").`when` { SystemProperties.get(KEY_COO) }
+
+ val regulatoryInfo = context.getRegulatoryInfo()
+
+ assertDrawableSameAs(regulatoryInfo, R.drawable.regulatory_info_sku)
+ }
+
+ @Test
+ fun getResourceId_hasSkuAndCooProperties_shouldReturnCooLabel() {
+ doReturn("sku1").`when` { SystemProperties.get(KEY_SKU) }
+ doReturn("coo").`when` { SystemProperties.get(KEY_COO) }
+
+ val regulatoryInfo = context.getRegulatoryInfo()
+
+ assertDrawableSameAs(regulatoryInfo, R.drawable.regulatory_info_sku1_coo)
+ }
+
+ @Test
+ fun getResourceId_noCorrespondingCooLabel_shouldReturnSkuLabel() {
+ doReturn("sku").`when` { SystemProperties.get(KEY_SKU) }
+ doReturn("unknown").`when` { SystemProperties.get(KEY_COO) }
+
+ val regulatoryInfo = context.getRegulatoryInfo()
+
+ assertDrawableSameAs(regulatoryInfo, R.drawable.regulatory_info_sku)
+ }
+
+ private fun assertDrawableSameAs(drawable: Drawable?, @DrawableRes resId: Int) {
+ val expected = context.getDrawable(resId)!!.toBitmap()
+ assertThat(drawable!!.toBitmap().sameAs(expected)).isTrue()
+ }
+}
diff --git a/tests/spa_unit/src/com/android/settings/network/telephony/DataUsagePreferenceControllerTest.kt b/tests/spa_unit/src/com/android/settings/network/telephony/DataUsagePreferenceControllerTest.kt
new file mode 100644
index 0000000..a42e41e
--- /dev/null
+++ b/tests/spa_unit/src/com/android/settings/network/telephony/DataUsagePreferenceControllerTest.kt
@@ -0,0 +1,174 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.network.telephony
+
+import android.content.Context
+import android.content.Intent
+import android.net.NetworkTemplate
+import android.provider.Settings
+import android.telephony.SubscriptionManager
+import android.util.DataUnit
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.testing.TestLifecycleOwner
+import androidx.preference.Preference
+import androidx.preference.PreferenceScreen
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.dx.mockito.inline.extended.ExtendedMockito
+import com.android.settings.core.BasePreferenceController.AVAILABLE
+import com.android.settings.core.BasePreferenceController.AVAILABLE_UNSEARCHABLE
+import com.android.settings.datausage.DataUsageUtils
+import com.android.settings.datausage.lib.DataUsageLib
+import com.android.settingslib.net.DataUsageController
+import com.android.settingslib.net.DataUsageController.DataUsageInfo
+import com.android.settingslib.spa.testutils.waitUntil
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.Mock
+import org.mockito.Mockito.any
+import org.mockito.Mockito.doNothing
+import org.mockito.Mockito.verify
+import org.mockito.MockitoSession
+import org.mockito.Spy
+import org.mockito.quality.Strictness
+import org.mockito.Mockito.`when` as whenever
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@RunWith(AndroidJUnit4::class)
+class DataUsagePreferenceControllerTest {
+
+ private lateinit var mockSession: MockitoSession
+
+ @Spy
+ private val context: Context = ApplicationProvider.getApplicationContext()
+
+ private lateinit var controller: DataUsagePreferenceController
+
+ private val preference = Preference(context)
+
+ @Mock
+ private lateinit var networkTemplate: NetworkTemplate
+
+ @Mock
+ private lateinit var dataUsageController: DataUsageController
+
+ @Mock
+ private lateinit var preferenceScreen: PreferenceScreen
+
+ @Before
+ fun setUp() {
+ mockSession = ExtendedMockito.mockitoSession()
+ .initMocks(this)
+ .mockStatic(SubscriptionManager::class.java)
+ .spyStatic(DataUsageUtils::class.java)
+ .spyStatic(DataUsageLib::class.java)
+ .strictness(Strictness.LENIENT)
+ .startMocking()
+
+ whenever(SubscriptionManager.isValidSubscriptionId(SUB_ID)).thenReturn(true)
+ ExtendedMockito.doReturn(true).`when` { DataUsageUtils.hasMobileData(context) }
+ ExtendedMockito.doReturn(networkTemplate)
+ .`when` { DataUsageLib.getMobileTemplate(context, SUB_ID) }
+ preference.key = TEST_KEY
+ whenever(preferenceScreen.findPreference<Preference>(TEST_KEY)).thenReturn(preference)
+
+ controller =
+ DataUsagePreferenceController(context, TEST_KEY).apply {
+ init(SUB_ID)
+ displayPreference(preferenceScreen)
+ dataUsageControllerFactory = { dataUsageController }
+ }
+ }
+
+ @After
+ fun tearDown() {
+ mockSession.finishMocking()
+ }
+
+ @Test
+ fun getAvailabilityStatus_validSubId_returnAvailable() {
+ assertThat(controller.availabilityStatus).isEqualTo(AVAILABLE)
+ }
+
+ @Test
+ fun getAvailabilityStatus_invalidSubId_returnUnsearchable() {
+ controller.init(SubscriptionManager.INVALID_SUBSCRIPTION_ID)
+
+ assertThat(controller.availabilityStatus).isEqualTo(AVAILABLE_UNSEARCHABLE)
+ }
+
+ @Test
+ fun handlePreferenceTreeClick_startActivity() = runTest {
+ val usageInfo = DataUsageInfo().apply {
+ usageLevel = DataUnit.MEBIBYTES.toBytes(1)
+ }
+ whenever(dataUsageController.getDataUsageInfo(networkTemplate)).thenReturn(usageInfo)
+ doNothing().`when`(context).startActivity(any())
+ controller.whenViewCreated(TestLifecycleOwner(initialState = Lifecycle.State.STARTED))
+ waitUntil { preference.summary != null }
+
+ controller.handlePreferenceTreeClick(preference)
+
+ val captor = ArgumentCaptor.forClass(Intent::class.java)
+ verify(context).startActivity(captor.capture())
+ val intent = captor.value
+ assertThat(intent.action).isEqualTo(Settings.ACTION_MOBILE_DATA_USAGE)
+ assertThat(intent.getIntExtra(Settings.EXTRA_SUB_ID, 0)).isEqualTo(SUB_ID)
+ }
+
+ @Test
+ fun updateState_invalidSubId_disabled() = runTest {
+ controller.init(SubscriptionManager.INVALID_SUBSCRIPTION_ID)
+
+ controller.whenViewCreated(TestLifecycleOwner(initialState = Lifecycle.State.STARTED))
+
+ waitUntil { !preference.isEnabled }
+ }
+
+ @Test
+ fun updateState_noUsageData_shouldDisablePreference() = runTest {
+ val usageInfo = DataUsageInfo()
+ whenever(dataUsageController.getDataUsageInfo(networkTemplate)).thenReturn(usageInfo)
+
+ controller.whenViewCreated(TestLifecycleOwner(initialState = Lifecycle.State.STARTED))
+
+ waitUntil { !preference.isEnabled }
+ }
+
+ @Test
+ fun updateState_shouldUseIecUnit() = runTest {
+ val usageInfo = DataUsageInfo().apply {
+ usageLevel = DataUnit.MEBIBYTES.toBytes(1)
+ }
+ whenever(dataUsageController.getDataUsageInfo(networkTemplate)).thenReturn(usageInfo)
+
+ controller.whenViewCreated(TestLifecycleOwner(initialState = Lifecycle.State.STARTED))
+
+ waitUntil { preference.summary?.contains("1.00 MB") == true }
+ }
+
+ private companion object {
+ const val TEST_KEY = "test_key"
+ const val SUB_ID = 2
+ }
+}
diff --git a/tests/spa_unit/src/com/android/settings/network/telephony/TelephonyStatusControlSessionTest.kt b/tests/spa_unit/src/com/android/settings/network/telephony/TelephonyStatusControlSessionTest.kt
new file mode 100644
index 0000000..7e6a91b
--- /dev/null
+++ b/tests/spa_unit/src/com/android/settings/network/telephony/TelephonyStatusControlSessionTest.kt
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.network.telephony
+
+import android.content.Context
+import androidx.lifecycle.testing.TestLifecycleOwner
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.settings.core.BasePreferenceController
+import com.android.settingslib.spa.testutils.waitUntil
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@RunWith(AndroidJUnit4::class)
+class TelephonyStatusControlSessionTest {
+ private val context: Context = ApplicationProvider.getApplicationContext()
+
+ @Test
+ fun init() = runTest {
+ val controller = TestController(context)
+
+ val session = TelephonyStatusControlSession(
+ controllers = listOf(controller),
+ lifecycle = TestLifecycleOwner().lifecycle,
+ )
+
+ waitUntil { controller.availabilityStatus == STATUS }
+ session.close()
+ }
+
+ @Test
+ fun close() = runTest {
+ val controller = TestController(context)
+
+ val session = TelephonyStatusControlSession(
+ controllers = listOf(controller),
+ lifecycle = TestLifecycleOwner().lifecycle,
+ )
+ session.close()
+
+ assertThat(controller.availabilityStatus).isNull()
+ }
+
+ private companion object {
+ const val KEY = "key"
+ const val STATUS = BasePreferenceController.AVAILABLE
+ }
+
+ private class TestController(context: Context) : BasePreferenceController(context, KEY),
+ TelephonyAvailabilityHandler {
+
+ var availabilityStatus: Int? = null
+ override fun getAvailabilityStatus(): Int = STATUS
+
+ override fun setAvailabilityStatus(status: Int) {
+ availabilityStatus = status
+ }
+
+ override fun unsetAvailabilityStatus() {
+ availabilityStatus = null
+ }
+ }
+}
diff --git a/tests/spa_unit/src/com/android/settings/spa/app/appcompat/UserAspectRatioAppPreferenceTest.kt b/tests/spa_unit/src/com/android/settings/spa/app/appcompat/UserAspectRatioAppPreferenceTest.kt
new file mode 100644
index 0000000..dd5b929
--- /dev/null
+++ b/tests/spa_unit/src/com/android/settings/spa/app/appcompat/UserAspectRatioAppPreferenceTest.kt
@@ -0,0 +1,208 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.spa.app.appcompat
+
+import android.content.Context
+import android.content.pm.ApplicationInfo
+import android.content.pm.LauncherActivityInfo
+import android.content.pm.LauncherApps
+import android.content.pm.PackageManager
+import android.provider.DeviceConfig.NAMESPACE_WINDOW_MANAGER
+import android.view.WindowManager.PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_OVERRIDE
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.assertIsEnabled
+import androidx.compose.ui.test.assertIsNotDisplayed
+import androidx.compose.ui.test.hasTextExactly
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onRoot
+import androidx.compose.ui.test.performClick
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.dx.mockito.inline.extended.ExtendedMockito
+import com.android.settings.R
+import com.android.settings.applications.appcompat.UserAspectRatioDetails
+import com.android.settings.applications.appinfo.AppInfoDashboardFragment
+import com.android.settings.spa.app.appinfo.AppInfoSettingsProvider
+import com.android.settings.testutils.TestDeviceConfig
+import com.android.settingslib.spa.testutils.delay
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyString
+import org.mockito.Mock
+import org.mockito.Mockito.any
+import org.mockito.MockitoSession
+import org.mockito.Spy
+import org.mockito.quality.Strictness
+import org.mockito.Mockito.`when` as whenever
+
+/**
+ * To run this test: atest SettingsSpaUnitTests:UserAspectRatioAppPreferenceTest
+ */
+@RunWith(AndroidJUnit4::class)
+class UserAspectRatioAppPreferenceTest {
+ @get:Rule
+ val composeTestRule = createComposeRule()
+
+ private lateinit var mockSession: MockitoSession
+
+ @Spy
+ private val context: Context = ApplicationProvider.getApplicationContext()
+
+ @Spy
+ private val resources = context.resources
+
+ private val aspectRatioEnabledConfig =
+ TestDeviceConfig(NAMESPACE_WINDOW_MANAGER, "enable_app_compat_aspect_ratio_user_settings")
+
+ @Mock
+ private lateinit var packageManager: PackageManager
+
+ @Mock
+ private lateinit var launcherApps: LauncherApps
+
+ @Mock
+ private lateinit var launcherActivities: List<LauncherActivityInfo>
+
+ @Before
+ fun setUp() {
+ mockSession = ExtendedMockito.mockitoSession()
+ .initMocks(this)
+ .mockStatic(UserAspectRatioDetails::class.java)
+ .mockStatic(AppInfoDashboardFragment::class.java)
+ .strictness(Strictness.LENIENT)
+ .startMocking()
+ whenever(context.resources).thenReturn(resources)
+ whenever(context.packageManager).thenReturn(packageManager)
+ whenever(context.getSystemService(Context.LAUNCHER_APPS_SERVICE)).thenReturn(launcherApps)
+ whenever(launcherApps.getActivityList(anyString(), any())).thenReturn(launcherActivities)
+ // True is ignored but need this here or getBoolean will complain null object
+ mockProperty(PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_OVERRIDE, true)
+ }
+
+ @After
+ fun tearDown() {
+ aspectRatioEnabledConfig.reset()
+ mockSession.finishMocking()
+ }
+
+ @Test
+ fun whenConfigIsFalse_notDisplayed() {
+ setConfig(false)
+
+ setContent()
+
+ composeTestRule.onRoot().assertIsNotDisplayed()
+ }
+
+ @Test
+ fun whenCannotDisplayAspectRatioUi_notDisplayed() {
+ whenever(launcherActivities.isEmpty()).thenReturn(true)
+
+ setContent()
+
+ composeTestRule.onRoot().assertIsNotDisplayed()
+ }
+
+ @Test
+ fun whenCanDisplayAspectRatioUiAndConfigFalse_notDisplayed() {
+ setConfig(false)
+ whenever(launcherActivities.isEmpty()).thenReturn(false)
+
+ setContent()
+
+ composeTestRule.onRoot().assertIsNotDisplayed()
+ }
+
+ @Test
+ fun whenCannotDisplayAspectRatioUiAndConfigTrue_notDisplayed() {
+ setConfig(true)
+
+ whenever(launcherActivities.isEmpty()).thenReturn(true)
+
+ setContent()
+
+ composeTestRule.onRoot().assertIsNotDisplayed()
+ }
+
+ @Test
+ fun whenCanDisplayAspectRatioUiAndConfigTrue_Displayed() {
+ setConfig(true)
+ whenever(launcherActivities.isEmpty()).thenReturn(false)
+ setContent()
+
+ composeTestRule.onNode(
+ hasTextExactly(
+ context.getString(R.string.aspect_ratio_experimental_title),
+ context.getString(R.string.user_aspect_ratio_app_default)
+ ),
+ ).assertIsDisplayed().assertIsEnabled()
+ }
+
+ @Test
+ fun onClick_startActivity() {
+ setConfig(true)
+ whenever(launcherActivities.isEmpty()).thenReturn(false)
+
+ setContent()
+ composeTestRule.onRoot().performClick()
+
+ ExtendedMockito.verify {
+ AppInfoDashboardFragment.startAppInfoFragment(
+ UserAspectRatioDetails::class.java,
+ APP,
+ context,
+ AppInfoSettingsProvider.METRICS_CATEGORY,
+ )
+ }
+ }
+
+ private fun setConfig(enabled: Boolean) {
+ whenever(resources.getBoolean(
+ com.android.internal.R.bool.config_appCompatUserAppAspectRatioSettingsIsEnabled
+ )).thenReturn(enabled)
+ aspectRatioEnabledConfig.override(enabled)
+ }
+
+ private fun setContent() {
+ composeTestRule.setContent {
+ CompositionLocalProvider(LocalContext provides context) {
+ UserAspectRatioAppPreference(APP)
+ }
+ }
+ composeTestRule.delay()
+ }
+
+ private fun mockProperty(propertyName: String, value: Boolean) {
+ val prop = PackageManager.Property(
+ propertyName, value, PACKAGE_NAME, "" /* className */)
+ whenever(packageManager.getProperty(propertyName, PACKAGE_NAME)).thenReturn(prop)
+ }
+
+ private companion object {
+ const val PACKAGE_NAME = "com.test.mypackage"
+ const val UID = 123
+ val APP = ApplicationInfo().apply {
+ packageName = PACKAGE_NAME
+ uid = UID
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/spa_unit/src/com/android/settings/spa/app/appcompat/UserAspectRatioAppsPageProviderTest.kt b/tests/spa_unit/src/com/android/settings/spa/app/appcompat/UserAspectRatioAppsPageProviderTest.kt
new file mode 100644
index 0000000..1126cc5
--- /dev/null
+++ b/tests/spa_unit/src/com/android/settings/spa/app/appcompat/UserAspectRatioAppsPageProviderTest.kt
@@ -0,0 +1,198 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.spa.app.appcompat
+
+import android.content.Context
+import android.content.pm.ApplicationInfo
+import android.content.pm.PackageManager.USER_MIN_ASPECT_RATIO_SPLIT_SCREEN
+import android.content.pm.PackageManager.USER_MIN_ASPECT_RATIO_UNSET
+import android.os.Build
+import androidx.compose.runtime.State
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithText
+import androidx.compose.ui.test.performClick
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.settings.R
+import com.android.settingslib.spa.framework.compose.stateOf
+import com.android.settingslib.spa.testutils.FakeNavControllerWrapper
+import com.android.settingslib.spa.testutils.firstWithTimeoutOrNull
+import com.android.settingslib.spaprivileged.template.app.AppListItemModel
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.test.runTest
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/**
+ * To run this test: atest SettingsSpaUnitTests:UserAspectRatioAppsPageProviderTest
+ */
+@RunWith(AndroidJUnit4::class)
+class UserAspectRatioAppsPageProviderTest {
+ @get:Rule
+ val composeTestRule = createComposeRule()
+
+ private val context: Context = ApplicationProvider.getApplicationContext()
+ private val fakeNavControllerWrapper = FakeNavControllerWrapper()
+
+ @Test
+ fun aspectRatioAppsPageProvider_name() {
+ assertThat(UserAspectRatioAppsPageProvider.name).isEqualTo(EXPECTED_PROVIDER_NAME)
+ }
+
+ @Test
+ fun injectEntry_title() {
+ setInjectEntry()
+ composeTestRule.onNodeWithText(context.getString(R.string.aspect_ratio_experimental_title))
+ .assertIsDisplayed()
+ }
+
+ @Test
+ fun injectEntry_summary() {
+ setInjectEntry()
+ composeTestRule
+ .onNodeWithText(context.getString(R.string.aspect_ratio_summary_text, Build.MODEL))
+ .assertIsDisplayed()
+ }
+
+ @Test
+ fun injectEntry_onClick_navigate() {
+ setInjectEntry()
+ composeTestRule.onNodeWithText(context.getString(R.string.aspect_ratio_experimental_title))
+ .performClick()
+ assertThat(fakeNavControllerWrapper.navigateCalledWith).isEqualTo("UserAspectRatioAppsPage")
+ }
+
+ private fun setInjectEntry() {
+ composeTestRule.setContent {
+ fakeNavControllerWrapper.Wrapper {
+ UserAspectRatioAppsPageProvider.buildInjectEntry().build().UiLayout()
+ }
+ }
+ }
+
+ @Test
+ fun title_displayed() {
+ composeTestRule.setContent {
+ UserAspectRatioAppList {}
+ }
+
+ composeTestRule.onNodeWithText(context.getString(R.string.aspect_ratio_experimental_title))
+ .assertIsDisplayed()
+ }
+
+ @Test
+ fun item_labelDisplayed() {
+ setItemContent()
+
+ composeTestRule.onNodeWithText(LABEL).assertIsDisplayed()
+ }
+
+ @OptIn(ExperimentalCoroutinesApi::class)
+ @Test
+ fun aspectRatioAppListModel_transform() = runTest {
+ val listModel = UserAspectRatioAppListModel(context)
+ val recordListFlow = listModel.transform(flowOf(USER_ID), flowOf(listOf(APP)))
+ val recordList = recordListFlow.firstWithTimeoutOrNull()!!
+
+ assertThat(recordList).hasSize(1)
+ assertThat(recordList[0].app).isSameInstanceAs(APP)
+ }
+
+ @OptIn(ExperimentalCoroutinesApi::class)
+ @Test
+ fun aspectRatioAppListModel_filter() = runTest {
+ val listModel = UserAspectRatioAppListModel(context)
+
+ val recordListFlow = listModel.filter(flowOf(USER_ID), 0,
+ flowOf(listOf(APP_RECORD_NOT_DISPLAYED, APP_RECORD_SUGGESTED)))
+
+ val recordList = checkNotNull(recordListFlow.firstWithTimeoutOrNull())
+ assertThat(recordList).containsExactly(APP_RECORD_SUGGESTED)
+ }
+
+ private fun setItemContent() {
+ composeTestRule.setContent {
+ fakeNavControllerWrapper.Wrapper {
+ with(UserAspectRatioAppListModel(context)) {
+ AppListItemModel(
+ record = APP_RECORD_SUGGESTED,
+ label = LABEL,
+ summary = stateOf(SUMMARY)
+ ).AppItem()
+ }
+ }
+ }
+ }
+
+ @Test
+ fun aspectRatioAppListModel_getSummaryDefault() {
+ val summaryState = setSummaryState(USER_MIN_ASPECT_RATIO_UNSET)
+ assertThat(summaryState.value)
+ .isEqualTo(context.getString(R.string.user_aspect_ratio_app_default))
+ }
+
+ @Test
+ fun aspectRatioAppListModel_getSummaryWhenSplitScreen() {
+ val summaryState = setSummaryState(USER_MIN_ASPECT_RATIO_SPLIT_SCREEN)
+ assertThat(summaryState.value)
+ .isEqualTo(context.getString(R.string.user_aspect_ratio_half_screen))
+ }
+
+ private fun setSummaryState(userOverride: Int): State<String> {
+ val listModel = UserAspectRatioAppListModel(context)
+ lateinit var summaryState: State<String>
+ composeTestRule.setContent {
+ summaryState = listModel.getSummary(option = 0,
+ record = UserAspectRatioAppListItemModel(
+ app = APP,
+ userOverride = userOverride,
+ suggested = false,
+ canDisplay = true,
+ ))
+ }
+ return summaryState
+ }
+
+
+ private companion object {
+ private const val EXPECTED_PROVIDER_NAME = "UserAspectRatioAppsPage"
+ private const val PACKAGE_NAME = "package.name"
+ private const val USER_ID = 0
+ private const val LABEL = "Label"
+ private const val SUMMARY = "Summary"
+
+ private val APP = ApplicationInfo().apply {
+ packageName = PACKAGE_NAME
+ }
+ private val APP_RECORD_SUGGESTED = UserAspectRatioAppListItemModel(
+ APP,
+ userOverride = USER_MIN_ASPECT_RATIO_UNSET,
+ suggested = true,
+ canDisplay = true
+ )
+ private val APP_RECORD_NOT_DISPLAYED = UserAspectRatioAppListItemModel(
+ APP,
+ userOverride = USER_MIN_ASPECT_RATIO_UNSET,
+ suggested = true,
+ canDisplay = false
+ )
+ }
+}
\ No newline at end of file
diff --git a/tests/spa_unit/src/com/android/settings/spa/app/appinfo/InteractAcrossProfilesDetailsPreferenceTest.kt b/tests/spa_unit/src/com/android/settings/spa/app/appinfo/InteractAcrossProfilesDetailsPreferenceTest.kt
index f4489c6..e123389 100644
--- a/tests/spa_unit/src/com/android/settings/spa/app/appinfo/InteractAcrossProfilesDetailsPreferenceTest.kt
+++ b/tests/spa_unit/src/com/android/settings/spa/app/appinfo/InteractAcrossProfilesDetailsPreferenceTest.kt
@@ -40,6 +40,7 @@
import com.android.settingslib.spaprivileged.framework.common.crossProfileApps
import org.junit.After
import org.junit.Before
+import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -116,6 +117,7 @@
composeTestRule.waitUntilExists(hasText(SUMMARY))
}
+ @Ignore
@Test
fun whenClick_startActivity() {
mockCanConfig(true)
diff --git a/tests/spa_unit/src/com/android/settings/spa/development/compat/PlatformCompatAppListModelTest.kt b/tests/spa_unit/src/com/android/settings/spa/development/compat/PlatformCompatAppListModelTest.kt
new file mode 100644
index 0000000..78aca85
--- /dev/null
+++ b/tests/spa_unit/src/com/android/settings/spa/development/compat/PlatformCompatAppListModelTest.kt
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.spa.development.compat
+
+import android.content.Context
+import android.content.pm.ApplicationInfo
+import android.content.pm.PackageManager
+import android.content.pm.PackageManager.PackageInfoFlags
+import androidx.compose.runtime.State
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.any
+import org.mockito.Mockito.anyInt
+import org.mockito.Spy
+import org.mockito.junit.MockitoJUnit
+import org.mockito.junit.MockitoRule
+import org.mockito.Mockito.`when` as whenever
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@RunWith(AndroidJUnit4::class)
+class PlatformCompatAppListModelTest {
+ @get:Rule
+ val composeTestRule = createComposeRule()
+
+ @get:Rule
+ val mockito: MockitoRule = MockitoJUnit.rule()
+
+ @Spy
+ private val context: Context = ApplicationProvider.getApplicationContext()
+
+ @Mock
+ private lateinit var packageManager: PackageManager
+
+ private lateinit var listModel: PlatformCompatAppListModel
+
+ @Before
+ fun setUp() {
+ whenever(context.packageManager).thenReturn(packageManager)
+ whenever(packageManager.getInstalledPackagesAsUser(any<PackageInfoFlags>(), anyInt()))
+ .thenReturn(emptyList())
+ listModel = PlatformCompatAppListModel(context)
+ }
+
+ @Test
+ fun transform() = runTest {
+ val recordListFlow = listModel.transform(
+ userIdFlow = flowOf(USER_ID),
+ appListFlow = flowOf(listOf(APP)),
+ )
+
+ val recordList = recordListFlow.first()
+ assertThat(recordList).hasSize(1)
+ val record = recordList[0]
+ assertThat(record.app).isSameInstanceAs(APP)
+ }
+
+ @Test
+ fun getSummary() = runTest {
+ val summaryState = getSummaryState(APP)
+
+ assertThat(summaryState.value).isEqualTo(PACKAGE_NAME)
+ }
+
+ private fun getSummaryState(app: ApplicationInfo): State<String> {
+ lateinit var summary: State<String>
+ composeTestRule.setContent {
+ summary = listModel.getSummary(
+ option = 0,
+ record = PlatformCompatAppRecord(app),
+ )
+ }
+ return summary
+ }
+
+ private companion object {
+ const val USER_ID = 0
+ const val PACKAGE_NAME = "package.name"
+ val APP = ApplicationInfo().apply {
+ packageName = PACKAGE_NAME
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/spa_unit/src/com/android/settings/testutils/FakeFeatureFactory.kt b/tests/spa_unit/src/com/android/settings/testutils/FakeFeatureFactory.kt
index 99d4f32..6320fc7 100644
--- a/tests/spa_unit/src/com/android/settings/testutils/FakeFeatureFactory.kt
+++ b/tests/spa_unit/src/com/android/settings/testutils/FakeFeatureFactory.kt
@@ -25,6 +25,7 @@
import com.android.settings.biometrics.face.FaceFeatureProvider
import com.android.settings.biometrics2.factory.BiometricsRepositoryProvider
import com.android.settings.bluetooth.BluetoothFeatureProvider
+import com.android.settings.connecteddevice.stylus.StylusFeatureProvider
import com.android.settings.dashboard.DashboardFeatureProvider
import com.android.settings.dashboard.suggestions.SuggestionFeatureProvider
import com.android.settings.deviceinfo.hardwareinfo.HardwareInfoFeatureProvider
@@ -34,6 +35,7 @@
import com.android.settings.fuelgauge.PowerUsageFeatureProvider
import com.android.settings.gestures.AssistGestureFeatureProvider
import com.android.settings.homepage.contextualcards.ContextualCardFeatureProvider
+import com.android.settings.inputmethod.KeyboardSettingsFeatureProvider
import com.android.settings.localepicker.LocaleFeatureProvider
import com.android.settings.overlay.DockUpdaterFeatureProvider
import com.android.settings.overlay.FeatureFactory
@@ -84,9 +86,7 @@
TODO("Not yet implemented")
}
- override fun getBatterySettingsFeatureProvider(
- context: Context?,
- ): BatterySettingsFeatureProvider {
+ override fun getBatterySettingsFeatureProvider(): BatterySettingsFeatureProvider {
TODO("Not yet implemented")
}
@@ -187,4 +187,12 @@
override fun getWifiFeatureProvider(): WifiFeatureProvider {
TODO("Not yet implemented")
}
+
+ override fun getKeyboardSettingsFeatureProvider(): KeyboardSettingsFeatureProvider {
+ TODO("Not yet implemented")
+ }
+
+ override fun getStylusFeatureProvider(): StylusFeatureProvider {
+ TODO("Not yet implemented")
+ }
}
diff --git a/tests/unit/Android.bp b/tests/unit/Android.bp
index e0dd078..3ac0842 100644
--- a/tests/unit/Android.bp
+++ b/tests/unit/Android.bp
@@ -21,6 +21,7 @@
],
static_libs: [
+ "androidx.arch.core_core-testing",
"androidx.test.core",
"androidx.test.rules",
"androidx.test.espresso.core",
@@ -30,8 +31,10 @@
"androidx.preference_preference",
"mockito-target-minus-junit4",
"platform-test-annotations",
+ "platform-test-rules",
"truth",
"androidx.test.uiautomator_uiautomator",
+ "kotlinx_coroutines_test",
// Don't add SettingsLib libraries here - you can use them directly as they are in the
// instrumented Settings app.
],
@@ -40,8 +43,11 @@
javacflags: ["-Xep:CheckReturnValue:WARN"],
},
- // Include all test java files.
- srcs: ["src/**/*.java"],
+ // Include all test java/kotlin files.
+ srcs: [
+ "src/**/*.java",
+ "src/**/*.kt",
+ ],
platform_apis: true,
test_suites: ["device-tests"],
diff --git a/tests/unit/src/com/android/settings/applications/appcompat/UserAspectRatioManagerTest.java b/tests/unit/src/com/android/settings/applications/appcompat/UserAspectRatioManagerTest.java
new file mode 100644
index 0000000..cf2a1f0
--- /dev/null
+++ b/tests/unit/src/com/android/settings/applications/appcompat/UserAspectRatioManagerTest.java
@@ -0,0 +1,309 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.applications.appcompat;
+
+import static android.content.pm.PackageManager.USER_MIN_ASPECT_RATIO_16_9;
+import static android.content.pm.PackageManager.USER_MIN_ASPECT_RATIO_FULLSCREEN;
+import static android.content.pm.PackageManager.USER_MIN_ASPECT_RATIO_SPLIT_SCREEN;
+import static android.content.pm.PackageManager.USER_MIN_ASPECT_RATIO_UNSET;
+import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_FULLSCREEN_OVERRIDE;
+import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_OVERRIDE;
+
+import static com.android.settings.applications.appcompat.UserAspectRatioManager.KEY_ENABLE_USER_ASPECT_RATIO_FULLSCREEN;
+import static com.android.settings.applications.appcompat.UserAspectRatioManager.KEY_ENABLE_USER_ASPECT_RATIO_SETTINGS;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.LauncherActivityInfo;
+import android.content.pm.LauncherApps;
+import android.content.pm.PackageManager;
+import android.content.res.Resources;
+import android.platform.test.rule.DeviceTypeRule;
+import android.platform.test.rule.FoldableOnly;
+import android.platform.test.rule.LargeScreenOnly;
+import android.platform.test.rule.TabletOnly;
+import android.provider.DeviceConfig;
+
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import com.android.internal.R;
+import com.android.settings.testutils.ResourcesUtils;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TestRule;
+import org.junit.runner.RunWith;
+
+import java.util.List;
+
+/**
+ * To run this test: atest SettingsUnitTests:UserAspectRatioManagerTest
+ */
+@RunWith(AndroidJUnit4.class)
+@LargeScreenOnly
+public class UserAspectRatioManagerTest {
+
+ private Context mContext;
+ private Resources mResources;
+ private UserAspectRatioManager mUtils;
+ private String mOriginalSettingsFlag;
+ private String mOriginalFullscreenFlag;
+ private String mPackageName = "com.test.mypackage";
+ private LauncherApps mLauncherApps;
+ private List<LauncherActivityInfo> mLauncherActivities;
+
+ @Rule
+ public TestRule mDeviceTypeRule = new DeviceTypeRule();
+
+ @Before
+ public void setUp() {
+ mContext = spy(ApplicationProvider.getApplicationContext());
+ mResources = spy(mContext.getResources());
+ mLauncherApps = mock(LauncherApps.class);
+ mLauncherActivities = mock(List.class);
+ mUtils = new UserAspectRatioManager(mContext) {
+ @Override
+ LauncherApps getLauncherApps() {
+ return mLauncherApps;
+ }
+ };
+
+ when(mContext.getResources()).thenReturn(mResources);
+ doReturn(mLauncherActivities).when(mLauncherApps).getActivityList(anyString(), any());
+
+ mOriginalSettingsFlag = DeviceConfig.getProperty(
+ DeviceConfig.NAMESPACE_WINDOW_MANAGER, KEY_ENABLE_USER_ASPECT_RATIO_SETTINGS);
+ setAspectRatioSettingsBuildTimeFlagEnabled(true);
+ setAspectRatioSettingsDeviceConfigEnabled("true" /* enabled */, false /* makeDefault */);
+
+ mOriginalFullscreenFlag = DeviceConfig.getProperty(
+ DeviceConfig.NAMESPACE_WINDOW_MANAGER, KEY_ENABLE_USER_ASPECT_RATIO_FULLSCREEN);
+ setAspectRatioFullscreenBuildTimeFlagEnabled(true);
+ setAspectRatioFullscreenDeviceConfigEnabled("true" /* enabled */, false /* makeDefault */);
+ }
+
+ @After
+ public void tearDown() {
+ setAspectRatioSettingsDeviceConfigEnabled(mOriginalSettingsFlag, true /* makeDefault */);
+ setAspectRatioFullscreenDeviceConfigEnabled(mOriginalFullscreenFlag,
+ true /* makeDefault */);
+ }
+
+ @Test
+ public void testCanDisplayAspectRatioUi() {
+ final ApplicationInfo canDisplay = new ApplicationInfo();
+ canDisplay.packageName = "com.app.candisplay";
+
+ doReturn(false).when(mLauncherActivities).isEmpty();
+ assertTrue(mUtils.canDisplayAspectRatioUi(canDisplay));
+
+ final ApplicationInfo noLauncherEntry = new ApplicationInfo();
+ noLauncherEntry.packageName = "com.app.nolauncherentry";
+
+ doReturn(true).when(mLauncherActivities).isEmpty();
+ assertFalse(mUtils.canDisplayAspectRatioUi(noLauncherEntry));
+ }
+
+ @Test
+ public void testCanDisplayAspectRatioUi_hasLauncher_propertyFalse_returnFalse()
+ throws PackageManager.NameNotFoundException {
+ mockProperty(PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_OVERRIDE, false);
+ doReturn(true).when(mLauncherActivities).isEmpty();
+
+ final ApplicationInfo canDisplay = new ApplicationInfo();
+ canDisplay.packageName = mPackageName;
+
+ assertFalse(mUtils.canDisplayAspectRatioUi(canDisplay));
+ }
+
+ @Test
+ public void testCanDisplayAspectRatioUi_noLauncher_propertyTrue_returnFalse()
+ throws PackageManager.NameNotFoundException {
+ mockProperty(PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_OVERRIDE, true);
+ doReturn(true).when(mLauncherActivities).isEmpty();
+
+ final ApplicationInfo noLauncherEntry = new ApplicationInfo();
+ noLauncherEntry.packageName = mPackageName;
+
+ assertFalse(mUtils.canDisplayAspectRatioUi(noLauncherEntry));
+ }
+
+ @Test
+ public void testIsFeatureEnabled() {
+ assertTrue(UserAspectRatioManager.isFeatureEnabled(mContext));
+ }
+
+ @Test
+ public void testIsFeatureEnabled_disabledBuildTimeFlag_returnFalse() {
+ setAspectRatioSettingsBuildTimeFlagEnabled(false);
+ assertFalse(UserAspectRatioManager.isFeatureEnabled(mContext));
+ }
+
+ @Test
+ public void testIsFeatureEnabled_disabledRuntimeFlag_returnFalse() {
+ setAspectRatioSettingsDeviceConfigEnabled("false" /* enabled */, false /* makeDefault */);
+ assertFalse(UserAspectRatioManager.isFeatureEnabled(mContext));
+ }
+
+ @Test
+ public void testIsFullscreenOptionEnabled() {
+ assertTrue(mUtils.isFullscreenOptionEnabled(mPackageName));
+ }
+
+ @Test
+ public void testIsFullscreenOptionEnabled_settingsDisabled_returnFalse() {
+ setAspectRatioFullscreenBuildTimeFlagEnabled(false);
+ assertFalse(mUtils.isFullscreenOptionEnabled(mPackageName));
+ }
+
+ @Test
+ public void testIsFullscreenOptionEnabled_disabledBuildTimeFlag_returnFalse() {
+ setAspectRatioFullscreenBuildTimeFlagEnabled(false);
+ assertFalse(mUtils.isFullscreenOptionEnabled(mPackageName));
+ }
+
+ @Test
+ public void testIsFullscreenOptionEnabled_disabledRuntimeFlag_returnFalse() {
+ setAspectRatioFullscreenDeviceConfigEnabled("false" /* enabled */, false /*makeDefault */);
+ assertFalse(mUtils.isFullscreenOptionEnabled(mPackageName));
+ }
+
+ @Test
+ public void testIsFullscreenOptionEnabled_propertyFalse_returnsFalse()
+ throws PackageManager.NameNotFoundException {
+ mockProperty(PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_FULLSCREEN_OVERRIDE, false);
+ assertFalse(mUtils.isFullscreenOptionEnabled(mPackageName));
+ }
+
+ @Test
+ public void testIsFullscreenOptionEnabled_propertyTrue_configDisabled_returnsFalse()
+ throws PackageManager.NameNotFoundException {
+ mockProperty(PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_FULLSCREEN_OVERRIDE, true);
+ setAspectRatioFullscreenDeviceConfigEnabled("false" /* enabled */, false /*makeDefault */);
+
+ assertFalse(mUtils.isFullscreenOptionEnabled(mPackageName));
+ }
+
+ @Test
+ public void testHasAspectRatioOption_fullscreen() {
+ assertTrue(mUtils.hasAspectRatioOption(USER_MIN_ASPECT_RATIO_FULLSCREEN,
+ mPackageName));
+ assertTrue(mUtils.hasAspectRatioOption(USER_MIN_ASPECT_RATIO_SPLIT_SCREEN,
+ mPackageName));
+
+ // Only fullscreen option should be disabled
+ when(mUtils.isFullscreenOptionEnabled(mPackageName)).thenReturn(false);
+ assertFalse(mUtils.hasAspectRatioOption(USER_MIN_ASPECT_RATIO_FULLSCREEN,
+ mPackageName));
+ assertTrue(mUtils.hasAspectRatioOption(USER_MIN_ASPECT_RATIO_SPLIT_SCREEN,
+ mPackageName));
+ }
+
+ @Test
+ @FoldableOnly
+ public void testGetUserMinAspectRatioEntry_Foldable() {
+ // R.string.user_aspect_ratio_app_default
+ final String appDefault = ResourcesUtils.getResourcesString(mContext,
+ "user_aspect_ratio_app_default");
+ assertThat(mUtils.getUserMinAspectRatioEntry(USER_MIN_ASPECT_RATIO_UNSET, mPackageName))
+ .isEqualTo(appDefault);
+ // should always return default if value does not correspond to anything
+ assertThat(mUtils.getUserMinAspectRatioEntry(-1, mPackageName))
+ .isEqualTo(appDefault);
+ // R.string.user_aspect_ratio_half_screen
+ assertThat(mUtils.getUserMinAspectRatioEntry(USER_MIN_ASPECT_RATIO_SPLIT_SCREEN,
+ mPackageName)).isEqualTo(ResourcesUtils.getResourcesString(mContext,
+ "user_aspect_ratio_half_screen"));
+ assertThat(mUtils.getUserMinAspectRatioEntry(USER_MIN_ASPECT_RATIO_FULLSCREEN,
+ mPackageName)).isEqualTo(ResourcesUtils.getResourcesString(mContext,
+ "user_aspect_ratio_fullscreen"));
+ }
+
+ @Test
+ @TabletOnly
+ public void testGetUserMinAspectRatioEntry_Tablet() {
+ // R.string.user_aspect_ratio_app_default
+ final String appDefault = ResourcesUtils.getResourcesString(mContext,
+ "user_aspect_ratio_app_default");
+ assertThat(mUtils.getUserMinAspectRatioEntry(USER_MIN_ASPECT_RATIO_UNSET, mPackageName))
+ .isEqualTo(appDefault);
+ // should always return default if value does not correspond to anything
+ assertThat(mUtils.getUserMinAspectRatioEntry(-1, mPackageName))
+ .isEqualTo(appDefault);
+ // R.string.user_aspect_ratio_half_screen
+ assertThat(mUtils.getUserMinAspectRatioEntry(USER_MIN_ASPECT_RATIO_SPLIT_SCREEN,
+ mPackageName)).isEqualTo(ResourcesUtils.getResourcesString(mContext,
+ "user_aspect_ratio_half_screen"));
+ assertThat(mUtils.getUserMinAspectRatioEntry(USER_MIN_ASPECT_RATIO_16_9, mPackageName))
+ .isEqualTo(ResourcesUtils.getResourcesString(mContext, "user_aspect_ratio_16_9"));
+ // R.string.user_aspect_ratio_fullscreen
+ assertThat(mUtils.getUserMinAspectRatioEntry(USER_MIN_ASPECT_RATIO_FULLSCREEN,
+ mPackageName)).isEqualTo(ResourcesUtils.getResourcesString(mContext,
+ "user_aspect_ratio_fullscreen"));
+ }
+
+ @Test
+ public void testGetUserMinAspectRatioEntry_fullscreenDisabled_shouldReturnDefault() {
+ setAspectRatioFullscreenBuildTimeFlagEnabled(false);
+ assertThat(mUtils.getUserMinAspectRatioEntry(USER_MIN_ASPECT_RATIO_FULLSCREEN,
+ mPackageName)).isEqualTo(ResourcesUtils.getResourcesString(mContext,
+ "user_aspect_ratio_app_default"));
+ }
+
+ private void mockProperty(String propertyName, boolean value)
+ throws PackageManager.NameNotFoundException {
+ PackageManager.Property prop = new PackageManager.Property(
+ propertyName, value, mPackageName, "" /* className */);
+ PackageManager pm = mock(PackageManager.class);
+ when(mContext.getPackageManager()).thenReturn(pm);
+ when(pm.getProperty(propertyName, mPackageName)).thenReturn(prop);
+ }
+
+ private void setAspectRatioSettingsBuildTimeFlagEnabled(boolean enabled) {
+ when(mResources.getBoolean(R.bool.config_appCompatUserAppAspectRatioSettingsIsEnabled))
+ .thenReturn(enabled);
+ }
+
+ private void setAspectRatioSettingsDeviceConfigEnabled(String enabled, boolean makeDefault) {
+ DeviceConfig.setProperty(DeviceConfig.NAMESPACE_WINDOW_MANAGER,
+ KEY_ENABLE_USER_ASPECT_RATIO_SETTINGS, enabled, makeDefault);
+ }
+
+ private void setAspectRatioFullscreenBuildTimeFlagEnabled(boolean enabled) {
+ when(mResources.getBoolean(R.bool.config_appCompatUserAppAspectRatioFullscreenIsEnabled))
+ .thenReturn(enabled);
+ }
+
+ private void setAspectRatioFullscreenDeviceConfigEnabled(String enabled, boolean makeDefault) {
+ DeviceConfig.setProperty(DeviceConfig.NAMESPACE_WINDOW_MANAGER,
+ KEY_ENABLE_USER_ASPECT_RATIO_FULLSCREEN, enabled, makeDefault);
+ }
+}
diff --git a/tests/unit/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollProgressViewModelTest.java b/tests/unit/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollProgressViewModelTest.java
index bdb45b0..2c830ad 100644
--- a/tests/unit/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollProgressViewModelTest.java
+++ b/tests/unit/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollProgressViewModelTest.java
@@ -379,7 +379,7 @@
// Notify acquire message
final int value = 33;
- mCallbackWrapper.mValue.onPointerDown(value);
+ mCallbackWrapper.mValue.onUdfpsPointerDown(value);
assertThat(liveData.getValue()).isEqualTo(value);
}
@@ -397,7 +397,7 @@
// Notify acquire message
final int value = 44;
- mCallbackWrapper.mValue.onPointerUp(value);
+ mCallbackWrapper.mValue.onUdfpsPointerUp(value);
assertThat(liveData.getValue()).isEqualTo(value);
}
diff --git a/tests/unit/src/com/android/settings/bluetooth/BlockingPrefWithSliceControllerTest.java b/tests/unit/src/com/android/settings/bluetooth/BlockingPrefWithSliceControllerTest.java
index 65b6977..d5a2585 100644
--- a/tests/unit/src/com/android/settings/bluetooth/BlockingPrefWithSliceControllerTest.java
+++ b/tests/unit/src/com/android/settings/bluetooth/BlockingPrefWithSliceControllerTest.java
@@ -16,6 +16,8 @@
package com.android.settings.bluetooth;
+import static androidx.slice.builders.ListBuilder.ICON_IMAGE;
+
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
@@ -24,8 +26,8 @@
import static org.mockito.Mockito.verify;
import android.app.PendingIntent;
-import android.content.Context;
import android.content.ContentResolver;
+import android.content.Context;
import android.content.Intent;
import android.net.Uri;
@@ -42,20 +44,20 @@
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
-import com.android.settings.bluetooth.BlockingPrefWithSliceController;
-
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
+@RunWith(AndroidJUnit4.class)
public class BlockingPrefWithSliceControllerTest {
private static final String KEY = "bt_device_slice_category";
- private static final String TEST_URI_AUTHORITY = "com.android.authority.test";
+ private static final String TEST_URI_AUTHORITY = "com.android.settings";
private static final String TEST_EXTRA_INTENT = "EXTRA_INTENT";
private static final String TEST_EXTRA_PENDING_INTENT = "EXTRA_PENDING_INTENT";
private static final String TEST_INTENT_ACTION = "test";
@@ -71,6 +73,8 @@
private LiveData<Slice> mLiveData;
@Mock
private PreferenceCategory mPreferenceCategory;
+ @Captor
+ ArgumentCaptor<Preference> mPreferenceArgumentCaptor;
private Context mContext;
private BlockingPrefWithSliceController mController;
@@ -130,6 +134,14 @@
verify(mController.mPreferenceCategory).addPreference(any());
}
+ @Test
+ public void onChanged_sliceWithoutValidIntent_makePreferenceUnselectable() {
+ mController.onChanged(buildTestSlice());
+
+ verify(mController.mPreferenceCategory).addPreference(mPreferenceArgumentCaptor.capture());
+ assertThat(mPreferenceArgumentCaptor.getValue().isSelectable()).isFalse();
+ }
+
private Slice buildTestSlice() {
Uri uri =
new Uri.Builder()
@@ -141,7 +153,7 @@
IconCompat icon = mock(IconCompat.class);
listBuilder.addRow(
new RowBuilder()
- .setTitleItem(icon, ListBuilder.ICON_IMAGE)
+ .setTitleItem(icon, ICON_IMAGE)
.setTitle(TEST_SLICE_TITLE)
.setSubtitle(TEST_SLICE_SUBTITLE)
.setPrimaryAction(
@@ -153,7 +165,7 @@
PendingIntent.FLAG_UPDATE_CURRENT
| PendingIntent.FLAG_IMMUTABLE),
icon,
- ListBuilder.ICON_IMAGE,
+ ICON_IMAGE,
"")));
return listBuilder.build();
}
diff --git a/tests/unit/src/com/android/settings/fingerprint2/domain/interactor/FakeFingerprintManagerInteractor.kt b/tests/unit/src/com/android/settings/fingerprint2/domain/interactor/FakeFingerprintManagerInteractor.kt
new file mode 100644
index 0000000..0509d8a
--- /dev/null
+++ b/tests/unit/src/com/android/settings/fingerprint2/domain/interactor/FakeFingerprintManagerInteractor.kt
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.fingerprint2.domain.interactor
+
+import android.hardware.biometrics.SensorProperties
+import android.hardware.fingerprint.FingerprintSensorProperties.TYPE_POWER_BUTTON
+import android.hardware.fingerprint.FingerprintSensorPropertiesInternal
+import com.android.settings.biometrics.fingerprint2.domain.interactor.FingerprintManagerInteractor
+import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FingerprintAuthAttemptViewModel
+import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FingerprintViewModel
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flow
+
+/** Fake to be used by other classes to easily fake the FingerprintManager implementation. */
+class FakeFingerprintManagerInteractor : FingerprintManagerInteractor {
+
+ var enrollableFingerprints: Int = 5
+ var enrolledFingerprintsInternal: MutableList<FingerprintViewModel> = mutableListOf()
+ var challengeToGenerate: Pair<Long, ByteArray> = Pair(-1L, byteArrayOf())
+ var authenticateAttempt = FingerprintAuthAttemptViewModel.Success(1)
+ var pressToAuthEnabled = true
+
+ var sensorProps =
+ listOf(
+ FingerprintSensorPropertiesInternal(
+ 0 /* sensorId */,
+ SensorProperties.STRENGTH_STRONG,
+ 5 /* maxEnrollmentsPerUser */,
+ emptyList() /* ComponentInfoInternal */,
+ TYPE_POWER_BUTTON,
+ true /* resetLockoutRequiresHardwareAuthToken */
+ )
+ )
+
+ override suspend fun authenticate(): FingerprintAuthAttemptViewModel {
+ return authenticateAttempt
+ }
+
+ override suspend fun generateChallenge(gateKeeperPasswordHandle: Long): Pair<Long, ByteArray> {
+ return challengeToGenerate
+ }
+ override val enrolledFingerprints: Flow<List<FingerprintViewModel>> = flow {
+ emit(enrolledFingerprintsInternal)
+ }
+
+ override fun canEnrollFingerprints(numFingerprints: Int): Flow<Boolean> = flow {
+ emit(numFingerprints < enrollableFingerprints)
+ }
+
+ override val maxEnrollableFingerprints: Flow<Int> = flow { emit(enrollableFingerprints) }
+
+ override suspend fun removeFingerprint(fp: FingerprintViewModel): Boolean {
+ return enrolledFingerprintsInternal.remove(fp)
+ }
+
+ override suspend fun renameFingerprint(fp: FingerprintViewModel, newName: String) {}
+
+ override suspend fun hasSideFps(): Boolean {
+ return sensorProps.any { it.isAnySidefpsType }
+ }
+
+ override suspend fun pressToAuthEnabled(): Boolean {
+ return pressToAuthEnabled
+ }
+
+ override suspend fun sensorPropertiesInternal(): List<FingerprintSensorPropertiesInternal> =
+ sensorProps
+}
diff --git a/tests/unit/src/com/android/settings/fingerprint2/domain/interactor/FingerprintManagerInteractorTest.kt b/tests/unit/src/com/android/settings/fingerprint2/domain/interactor/FingerprintManagerInteractorTest.kt
new file mode 100644
index 0000000..7af740a
--- /dev/null
+++ b/tests/unit/src/com/android/settings/fingerprint2/domain/interactor/FingerprintManagerInteractorTest.kt
@@ -0,0 +1,287 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.fingerprint2.domain.interactor
+
+import android.content.Context
+import android.content.Intent
+import android.content.res.Resources
+import android.hardware.fingerprint.Fingerprint
+import android.hardware.fingerprint.FingerprintManager
+import android.hardware.fingerprint.FingerprintManager.CryptoObject
+import android.hardware.fingerprint.FingerprintManager.FINGERPRINT_ERROR_LOCKOUT_PERMANENT
+import android.os.CancellationSignal
+import android.os.Handler
+import androidx.test.core.app.ApplicationProvider
+import com.android.settings.biometrics.GatekeeperPasswordProvider
+import com.android.settings.biometrics.fingerprint2.domain.interactor.FingerprintManagerInteractor
+import com.android.settings.biometrics.fingerprint2.domain.interactor.FingerprintManagerInteractorImpl
+import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FingerprintAuthAttemptViewModel
+import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FingerprintViewModel
+import com.android.settings.password.ChooseLockSettingsHelper
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.cancelAndJoin
+import kotlinx.coroutines.flow.last
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.ArgumentMatchers.anyLong
+import org.mockito.ArgumentMatchers.eq
+import org.mockito.ArgumentMatchers.nullable
+import org.mockito.Mock
+import org.mockito.Mockito
+import org.mockito.junit.MockitoJUnit
+import org.mockito.junit.MockitoJUnitRunner
+
+@RunWith(MockitoJUnitRunner::class)
+class FingerprintManagerInteractorTest {
+
+ @JvmField @Rule var rule = MockitoJUnit.rule()
+ private lateinit var underTest: FingerprintManagerInteractor
+ private var context: Context = ApplicationProvider.getApplicationContext()
+ private var backgroundDispatcher = StandardTestDispatcher()
+ @Mock private lateinit var fingerprintManager: FingerprintManager
+ @Mock private lateinit var gateKeeperPasswordProvider: GatekeeperPasswordProvider
+
+ private var testScope = TestScope(backgroundDispatcher)
+ private var pressToAuthProvider = { true }
+
+ @Before
+ fun setup() {
+ underTest =
+ FingerprintManagerInteractorImpl(
+ context,
+ backgroundDispatcher,
+ fingerprintManager,
+ gateKeeperPasswordProvider,
+ pressToAuthProvider,
+ )
+ }
+
+ @Test
+ fun testEmptyFingerprints() =
+ testScope.runTest {
+ Mockito.`when`(fingerprintManager.getEnrolledFingerprints(Mockito.anyInt()))
+ .thenReturn(emptyList())
+
+ val emptyFingerprintList: List<Fingerprint> = emptyList()
+ assertThat(underTest.enrolledFingerprints.last()).isEqualTo(emptyFingerprintList)
+ }
+
+ @Test
+ fun testOneFingerprint() =
+ testScope.runTest {
+ val expected = Fingerprint("Finger 1,", 2, 3L)
+ val fingerprintList: List<Fingerprint> = listOf(expected)
+ Mockito.`when`(fingerprintManager.getEnrolledFingerprints(Mockito.anyInt()))
+ .thenReturn(fingerprintList)
+
+ val list = underTest.enrolledFingerprints.last()
+ assertThat(list.size).isEqualTo(fingerprintList.size)
+ val actual = list[0]
+ assertThat(actual.name).isEqualTo(expected.name)
+ assertThat(actual.fingerId).isEqualTo(expected.biometricId)
+ assertThat(actual.deviceId).isEqualTo(expected.deviceId)
+ }
+
+ @Test
+ fun testCanEnrollFingerprint() =
+ testScope.runTest {
+ val mockContext = Mockito.mock(Context::class.java)
+ val resources = Mockito.mock(Resources::class.java)
+ Mockito.`when`(mockContext.resources).thenReturn(resources)
+ Mockito.`when`(resources.getInteger(anyInt())).thenReturn(3)
+ underTest =
+ FingerprintManagerInteractorImpl(
+ mockContext,
+ backgroundDispatcher,
+ fingerprintManager,
+ gateKeeperPasswordProvider,
+ pressToAuthProvider,
+ )
+
+ assertThat(underTest.canEnrollFingerprints(2).last()).isTrue()
+ assertThat(underTest.canEnrollFingerprints(3).last()).isFalse()
+ }
+
+ @Test
+ fun testGenerateChallenge() =
+ testScope.runTest {
+ val byteArray = byteArrayOf(5, 3, 2)
+ val challenge = 100L
+ val intent = Intent()
+ intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_GK_PW_HANDLE, challenge)
+ Mockito.`when`(
+ gateKeeperPasswordProvider.requestGatekeeperHat(
+ any(Intent::class.java),
+ anyLong(),
+ anyInt()
+ )
+ )
+ .thenReturn(byteArray)
+
+ val generateChallengeCallback: ArgumentCaptor<FingerprintManager.GenerateChallengeCallback> =
+ ArgumentCaptor.forClass(FingerprintManager.GenerateChallengeCallback::class.java)
+
+ var result: Pair<Long, ByteArray?>? = null
+ val job = testScope.launch { result = underTest.generateChallenge(1L) }
+ runCurrent()
+
+ Mockito.verify(fingerprintManager)
+ .generateChallenge(anyInt(), capture(generateChallengeCallback))
+ generateChallengeCallback.value.onChallengeGenerated(1, 2, challenge)
+
+ runCurrent()
+ job.cancelAndJoin()
+
+ assertThat(result?.first).isEqualTo(challenge)
+ assertThat(result?.second).isEqualTo(byteArray)
+ }
+
+ @Test
+ fun testRemoveFingerprint_succeeds() =
+ testScope.runTest {
+ val fingerprintViewModelToRemove = FingerprintViewModel("Finger 2", 1, 2L)
+ val fingerprintToRemove = Fingerprint("Finger 2", 1, 2L)
+
+ val removalCallback: ArgumentCaptor<FingerprintManager.RemovalCallback> =
+ ArgumentCaptor.forClass(FingerprintManager.RemovalCallback::class.java)
+
+ var result: Boolean? = null
+ val job =
+ testScope.launch { result = underTest.removeFingerprint(fingerprintViewModelToRemove) }
+ runCurrent()
+
+ Mockito.verify(fingerprintManager)
+ .remove(any(Fingerprint::class.java), anyInt(), capture(removalCallback))
+ removalCallback.value.onRemovalSucceeded(fingerprintToRemove, 1)
+
+ runCurrent()
+ job.cancelAndJoin()
+
+ assertThat(result).isTrue()
+ }
+
+ @Test
+ fun testRemoveFingerprint_fails() =
+ testScope.runTest {
+ val fingerprintViewModelToRemove = FingerprintViewModel("Finger 2", 1, 2L)
+ val fingerprintToRemove = Fingerprint("Finger 2", 1, 2L)
+
+ val removalCallback: ArgumentCaptor<FingerprintManager.RemovalCallback> =
+ ArgumentCaptor.forClass(FingerprintManager.RemovalCallback::class.java)
+
+ var result: Boolean? = null
+ val job =
+ testScope.launch { result = underTest.removeFingerprint(fingerprintViewModelToRemove) }
+ runCurrent()
+
+ Mockito.verify(fingerprintManager)
+ .remove(any(Fingerprint::class.java), anyInt(), capture(removalCallback))
+ removalCallback.value.onRemovalError(
+ fingerprintToRemove,
+ 100,
+ "Oh no, we couldn't find that one"
+ )
+
+ runCurrent()
+ job.cancelAndJoin()
+
+ assertThat(result).isFalse()
+ }
+
+ @Test
+ fun testRenameFingerprint_succeeds() =
+ testScope.runTest {
+ val fingerprintToRename = FingerprintViewModel("Finger 2", 1, 2L)
+
+ underTest.renameFingerprint(fingerprintToRename, "Woo")
+
+ Mockito.verify(fingerprintManager)
+ .rename(eq(fingerprintToRename.fingerId), anyInt(), safeEq("Woo"))
+ }
+
+ @Test
+ fun testAuth_succeeds() =
+ testScope.runTest {
+ val fingerprint = Fingerprint("Woooo", 100, 101L)
+
+ var result: FingerprintAuthAttemptViewModel? = null
+ val job = launch { result = underTest.authenticate() }
+
+ val authCallback: ArgumentCaptor<FingerprintManager.AuthenticationCallback> =
+ ArgumentCaptor.forClass(FingerprintManager.AuthenticationCallback::class.java)
+
+ runCurrent()
+
+ Mockito.verify(fingerprintManager)
+ .authenticate(
+ nullable(CryptoObject::class.java),
+ any(CancellationSignal::class.java),
+ capture(authCallback),
+ nullable(Handler::class.java),
+ anyInt()
+ )
+ authCallback.value.onAuthenticationSucceeded(
+ FingerprintManager.AuthenticationResult(null, fingerprint, 1, false)
+ )
+
+ runCurrent()
+ job.cancelAndJoin()
+ assertThat(result).isEqualTo(FingerprintAuthAttemptViewModel.Success(fingerprint.biometricId))
+ }
+
+ @Test
+ fun testAuth_lockout() =
+ testScope.runTest {
+ var result: FingerprintAuthAttemptViewModel? = null
+ val job = launch { result = underTest.authenticate() }
+
+ val authCallback: ArgumentCaptor<FingerprintManager.AuthenticationCallback> =
+ ArgumentCaptor.forClass(FingerprintManager.AuthenticationCallback::class.java)
+
+ runCurrent()
+
+ Mockito.verify(fingerprintManager)
+ .authenticate(
+ nullable(CryptoObject::class.java),
+ any(CancellationSignal::class.java),
+ capture(authCallback),
+ nullable(Handler::class.java),
+ anyInt()
+ )
+ authCallback.value.onAuthenticationError(FINGERPRINT_ERROR_LOCKOUT_PERMANENT, "Lockout!!")
+
+ runCurrent()
+ job.cancelAndJoin()
+ assertThat(result)
+ .isEqualTo(
+ FingerprintAuthAttemptViewModel.Error(FINGERPRINT_ERROR_LOCKOUT_PERMANENT, "Lockout!!")
+ )
+ }
+
+ private fun <T : Any> safeEq(value: T): T = eq(value) ?: value
+ private fun <T> capture(argumentCaptor: ArgumentCaptor<T>): T = argumentCaptor.capture()
+ private fun <T> any(type: Class<T>): T = Mockito.any<T>(type)
+}
diff --git a/tests/unit/src/com/android/settings/fingerprint2/viewmodel/FingerprintSettingsNavigationViewModelTest.kt b/tests/unit/src/com/android/settings/fingerprint2/viewmodel/FingerprintSettingsNavigationViewModelTest.kt
new file mode 100644
index 0000000..4e1f6b1
--- /dev/null
+++ b/tests/unit/src/com/android/settings/fingerprint2/viewmodel/FingerprintSettingsNavigationViewModelTest.kt
@@ -0,0 +1,275 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.fingerprint2.viewmodel
+
+import androidx.arch.core.executor.testing.InstantTaskExecutorRule
+import com.android.settings.biometrics.fingerprint2.ui.viewmodel.EnrollFirstFingerprint
+import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FingerprintSettingsNavigationViewModel
+import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FingerprintViewModel
+import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FinishSettings
+import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FinishSettingsWithResult
+import com.android.settings.biometrics.fingerprint2.ui.viewmodel.LaunchConfirmDeviceCredential
+import com.android.settings.biometrics.fingerprint2.ui.viewmodel.NextStepViewModel
+import com.android.settings.biometrics.fingerprint2.ui.viewmodel.ShowSettings
+import com.android.settings.fingerprint2.domain.interactor.FakeFingerprintManagerInteractor
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.resetMain
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import kotlinx.coroutines.test.setMain
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.junit.MockitoJUnit
+import org.mockito.junit.MockitoJUnitRunner
+
+@RunWith(MockitoJUnitRunner::class)
+class FingerprintSettingsNavigationViewModelTest {
+
+ @JvmField @Rule var rule = MockitoJUnit.rule()
+
+ @get:Rule val instantTaskRule = InstantTaskExecutorRule()
+
+ private lateinit var underTest: FingerprintSettingsNavigationViewModel
+ private val defaultUserId = 0
+ private var backgroundDispatcher = StandardTestDispatcher()
+ private var testScope = TestScope(backgroundDispatcher)
+ private lateinit var fakeFingerprintManagerInteractor: FakeFingerprintManagerInteractor
+
+ @Before
+ fun setup() {
+ fakeFingerprintManagerInteractor = FakeFingerprintManagerInteractor()
+ backgroundDispatcher = StandardTestDispatcher()
+ testScope = TestScope(backgroundDispatcher)
+ Dispatchers.setMain(backgroundDispatcher)
+
+ underTest =
+ FingerprintSettingsNavigationViewModel.FingerprintSettingsNavigationModelFactory(
+ defaultUserId,
+ fakeFingerprintManagerInteractor,
+ backgroundDispatcher,
+ null,
+ null,
+ )
+ .create(FingerprintSettingsNavigationViewModel::class.java)
+ }
+
+ @After
+ fun tearDown() {
+ Dispatchers.resetMain()
+ }
+
+ @Test
+ fun testNoGateKeeper_launchesConfirmDeviceCredential() =
+ testScope.runTest {
+ var nextStep: NextStepViewModel? = null
+ val job = launch { underTest.nextStep.collect { nextStep = it } }
+
+ runCurrent()
+ assertThat(nextStep).isEqualTo(LaunchConfirmDeviceCredential(defaultUserId))
+ job.cancel()
+ }
+
+ @Test
+ fun testConfirmDevice_fails() =
+ testScope.runTest {
+ var nextStep: NextStepViewModel? = null
+ val job = launch { underTest.nextStep.collect { nextStep = it } }
+
+ underTest.onConfirmDevice(false, null)
+ runCurrent()
+
+ assertThat(nextStep).isInstanceOf(FinishSettings::class.java)
+ job.cancel()
+ }
+
+ @Test
+ fun confirmDeviceSuccess_noGateKeeper() =
+ testScope.runTest {
+ var nextStep: NextStepViewModel? = null
+ val job = launch { underTest.nextStep.collect { nextStep = it } }
+
+ underTest.onConfirmDevice(true, null)
+ runCurrent()
+
+ assertThat(nextStep).isInstanceOf(FinishSettings::class.java)
+ job.cancel()
+ }
+
+ @Test
+ fun confirmDeviceSuccess_launchesEnrollment_ifNoPreviousEnrollments() =
+ testScope.runTest {
+ fakeFingerprintManagerInteractor.enrolledFingerprintsInternal = mutableListOf()
+
+ var nextStep: NextStepViewModel? = null
+ val job = launch { underTest.nextStep.collect { nextStep = it } }
+
+ underTest.onConfirmDevice(true, 10L)
+ runCurrent()
+
+ assertThat(nextStep).isEqualTo(EnrollFirstFingerprint(defaultUserId, 10L, null, null))
+ job.cancel()
+ }
+
+ @Test
+ fun firstEnrollment_fails() =
+ testScope.runTest {
+ fakeFingerprintManagerInteractor.enrolledFingerprintsInternal = mutableListOf()
+
+ var nextStep: NextStepViewModel? = null
+ val job = launch { underTest.nextStep.collect { nextStep = it } }
+
+ underTest.onConfirmDevice(true, 10L)
+ underTest.onEnrollFirstFailure("We failed!!")
+ runCurrent()
+
+ assertThat(nextStep).isInstanceOf(FinishSettings::class.java)
+ job.cancel()
+ }
+
+ @Test
+ fun firstEnrollment_failsWithReason() =
+ testScope.runTest {
+ fakeFingerprintManagerInteractor.enrolledFingerprintsInternal = mutableListOf()
+
+ var nextStep: NextStepViewModel? = null
+ val job = launch { underTest.nextStep.collect { nextStep = it } }
+
+ val failStr = "We failed!!"
+ val failReason = 101
+
+ underTest.onConfirmDevice(true, 10L)
+ underTest.onEnrollFirstFailure(failStr, failReason)
+ runCurrent()
+
+ assertThat(nextStep).isEqualTo(FinishSettingsWithResult(failReason, failStr))
+ job.cancel()
+ }
+
+ @Test
+ fun firstEnrollmentSucceeds_noToken() =
+ testScope.runTest {
+ fakeFingerprintManagerInteractor.enrolledFingerprintsInternal = mutableListOf()
+
+ var nextStep: NextStepViewModel? = null
+ val job = launch { underTest.nextStep.collect { nextStep = it } }
+
+ underTest.onConfirmDevice(true, 10L)
+ underTest.onEnrollFirst(null, null)
+ runCurrent()
+
+ assertThat(nextStep).isEqualTo(FinishSettings("Error, empty token"))
+ job.cancel()
+ }
+
+ @Test
+ fun firstEnrollmentSucceeds_noKeyChallenge() =
+ testScope.runTest {
+ fakeFingerprintManagerInteractor.enrolledFingerprintsInternal = mutableListOf()
+
+ var nextStep: NextStepViewModel? = null
+ val job = launch { underTest.nextStep.collect { nextStep = it } }
+
+ val byteArray = ByteArray(1) { 3 }
+
+ underTest.onConfirmDevice(true, 10L)
+ underTest.onEnrollFirst(byteArray, null)
+ runCurrent()
+
+ assertThat(nextStep).isEqualTo(FinishSettings("Error, empty keyChallenge"))
+ job.cancel()
+ }
+
+ @Test
+ fun firstEnrollment_succeeds() =
+ testScope.runTest {
+ fakeFingerprintManagerInteractor.enrolledFingerprintsInternal = mutableListOf()
+
+ var nextStep: NextStepViewModel? = null
+ val job = testScope.launch { underTest.nextStep.collect { nextStep = it } }
+
+ val byteArray = ByteArray(1) { 3 }
+ val keyChallenge = 89L
+
+ underTest.onConfirmDevice(true, 10L)
+ underTest.onEnrollFirst(byteArray, keyChallenge)
+ runCurrent()
+
+ assertThat(nextStep).isEqualTo(ShowSettings)
+ job.cancel()
+ }
+
+ @Test
+ fun enrollAdditionalFingerprints_fails() =
+ testScope.runTest {
+ fakeFingerprintManagerInteractor.enrolledFingerprintsInternal =
+ mutableListOf(FingerprintViewModel("a", 1, 3L))
+ fakeFingerprintManagerInteractor.challengeToGenerate = Pair(4L, byteArrayOf(3, 3, 1))
+
+ var nextStep: NextStepViewModel? = null
+ val job = launch { underTest.nextStep.collect { nextStep = it } }
+
+ underTest.onConfirmDevice(true, 10L)
+ runCurrent()
+ underTest.onEnrollAdditionalFailure()
+ runCurrent()
+
+ assertThat(nextStep).isInstanceOf(FinishSettings::class.java)
+ job.cancel()
+ }
+
+ @Test
+ fun enrollAdditional_success() =
+ testScope.runTest {
+ fakeFingerprintManagerInteractor.enrolledFingerprintsInternal =
+ mutableListOf(FingerprintViewModel("a", 1, 3L))
+
+ var nextStep: NextStepViewModel? = null
+ val job = launch { underTest.nextStep.collect { nextStep = it } }
+
+ underTest.onConfirmDevice(true, 10L)
+ underTest.onEnrollSuccess()
+
+ runCurrent()
+
+ assertThat(nextStep).isEqualTo(ShowSettings)
+ job.cancel()
+ }
+
+ @Test
+ fun confirmDeviceCredential_withEnrolledFingerprint_showsSettings() =
+ testScope.runTest {
+ fakeFingerprintManagerInteractor.enrolledFingerprintsInternal =
+ mutableListOf(FingerprintViewModel("a", 1, 3L))
+ fakeFingerprintManagerInteractor.challengeToGenerate = Pair(10L, byteArrayOf(1, 2, 3))
+
+ var nextStep: NextStepViewModel? = null
+ val job = launch { underTest.nextStep.collect { nextStep = it } }
+
+ underTest.onConfirmDevice(true, 10L)
+ runCurrent()
+
+ assertThat(nextStep).isEqualTo(ShowSettings)
+ job.cancel()
+ }
+}
diff --git a/tests/unit/src/com/android/settings/fingerprint2/viewmodel/FingerprintSettingsViewModelTest.kt b/tests/unit/src/com/android/settings/fingerprint2/viewmodel/FingerprintSettingsViewModelTest.kt
new file mode 100644
index 0000000..d430827
--- /dev/null
+++ b/tests/unit/src/com/android/settings/fingerprint2/viewmodel/FingerprintSettingsViewModelTest.kt
@@ -0,0 +1,247 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.fingerprint2.viewmodel
+
+import android.hardware.biometrics.SensorProperties
+import android.hardware.fingerprint.FingerprintSensorProperties
+import android.hardware.fingerprint.FingerprintSensorPropertiesInternal
+import androidx.arch.core.executor.testing.InstantTaskExecutorRule
+import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FingerprintAuthAttemptViewModel
+import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FingerprintSettingsNavigationViewModel
+import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FingerprintSettingsViewModel
+import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FingerprintViewModel
+import com.android.settings.biometrics.fingerprint2.ui.viewmodel.PreferenceViewModel
+import com.android.settings.fingerprint2.domain.interactor.FakeFingerprintManagerInteractor
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.flow.take
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.advanceTimeBy
+import kotlinx.coroutines.test.resetMain
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import kotlinx.coroutines.test.setMain
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.junit.MockitoJUnit
+import org.mockito.junit.MockitoJUnitRunner
+
+@RunWith(MockitoJUnitRunner::class)
+class FingerprintSettingsViewModelTest {
+
+ @JvmField @Rule var rule = MockitoJUnit.rule()
+
+ @get:Rule val instantTaskRule = InstantTaskExecutorRule()
+
+ private lateinit var underTest: FingerprintSettingsViewModel
+ private lateinit var navigationViewModel: FingerprintSettingsNavigationViewModel
+ private val defaultUserId = 0
+ private var backgroundDispatcher = StandardTestDispatcher()
+ private var testScope = TestScope(backgroundDispatcher)
+ private lateinit var fakeFingerprintManagerInteractor: FakeFingerprintManagerInteractor
+
+ @Before
+ fun setup() {
+ fakeFingerprintManagerInteractor = FakeFingerprintManagerInteractor()
+ backgroundDispatcher = StandardTestDispatcher()
+ testScope = TestScope(backgroundDispatcher)
+ Dispatchers.setMain(backgroundDispatcher)
+
+ navigationViewModel =
+ FingerprintSettingsNavigationViewModel.FingerprintSettingsNavigationModelFactory(
+ defaultUserId,
+ fakeFingerprintManagerInteractor,
+ backgroundDispatcher,
+ null,
+ null,
+ )
+ .create(FingerprintSettingsNavigationViewModel::class.java)
+
+ underTest =
+ FingerprintSettingsViewModel.FingerprintSettingsViewModelFactory(
+ defaultUserId,
+ fakeFingerprintManagerInteractor,
+ backgroundDispatcher,
+ navigationViewModel,
+ )
+ .create(FingerprintSettingsViewModel::class.java)
+ }
+
+ @After
+ fun tearDown() {
+ Dispatchers.resetMain()
+ }
+
+ @Test
+ fun authenticate_DoesNotRun_ifOptical() =
+ testScope.runTest {
+ fakeFingerprintManagerInteractor.sensorProps =
+ listOf(
+ FingerprintSensorPropertiesInternal(
+ 0 /* sensorId */,
+ SensorProperties.STRENGTH_STRONG,
+ 5 /* maxEnrollmentsPerUser */,
+ emptyList() /* ComponentInfoInternal */,
+ FingerprintSensorProperties.TYPE_UDFPS_OPTICAL,
+ true /* resetLockoutRequiresHardwareAuthToken */
+ )
+ )
+ fakeFingerprintManagerInteractor.enrolledFingerprintsInternal =
+ mutableListOf(FingerprintViewModel("a", 1, 3L))
+
+ underTest =
+ FingerprintSettingsViewModel.FingerprintSettingsViewModelFactory(
+ defaultUserId,
+ fakeFingerprintManagerInteractor,
+ backgroundDispatcher,
+ navigationViewModel,
+ )
+ .create(FingerprintSettingsViewModel::class.java)
+
+ var authAttempt: FingerprintAuthAttemptViewModel? = null
+ val job = launch { underTest.authFlow.take(5).collectLatest { authAttempt = it } }
+
+ underTest.shouldAuthenticate(true)
+ // Ensure we are showing settings
+ navigationViewModel.onConfirmDevice(true, 10L)
+
+ runCurrent()
+ advanceTimeBy(400)
+
+ assertThat(authAttempt).isNull()
+ job.cancel()
+ }
+
+ @Test
+ fun authenticate_DoesNotRun_ifUltrasonic() =
+ testScope.runTest {
+ fakeFingerprintManagerInteractor.sensorProps =
+ listOf(
+ FingerprintSensorPropertiesInternal(
+ 0 /* sensorId */,
+ SensorProperties.STRENGTH_STRONG,
+ 5 /* maxEnrollmentsPerUser */,
+ emptyList() /* ComponentInfoInternal */,
+ FingerprintSensorProperties.TYPE_UDFPS_ULTRASONIC,
+ true /* resetLockoutRequiresHardwareAuthToken */
+ )
+ )
+ fakeFingerprintManagerInteractor.enrolledFingerprintsInternal =
+ mutableListOf(FingerprintViewModel("a", 1, 3L))
+
+ underTest =
+ FingerprintSettingsViewModel.FingerprintSettingsViewModelFactory(
+ defaultUserId,
+ fakeFingerprintManagerInteractor,
+ backgroundDispatcher,
+ navigationViewModel,
+ )
+ .create(FingerprintSettingsViewModel::class.java)
+
+ var authAttempt: FingerprintAuthAttemptViewModel? = null
+ val job = launch { underTest.authFlow.take(5).collectLatest { authAttempt = it } }
+
+ underTest.shouldAuthenticate(true)
+ navigationViewModel.onConfirmDevice(true, 10L)
+ advanceTimeBy(400)
+ runCurrent()
+
+ assertThat(authAttempt).isNull()
+ job.cancel()
+ }
+
+ @Test
+ fun authenticate_DoesRun_ifNotUdfps() =
+ testScope.runTest {
+ fakeFingerprintManagerInteractor.sensorProps =
+ listOf(
+ FingerprintSensorPropertiesInternal(
+ 0 /* sensorId */,
+ SensorProperties.STRENGTH_STRONG,
+ 5 /* maxEnrollmentsPerUser */,
+ emptyList() /* ComponentInfoInternal */,
+ FingerprintSensorProperties.TYPE_POWER_BUTTON,
+ true /* resetLockoutRequiresHardwareAuthToken */
+ )
+ )
+ fakeFingerprintManagerInteractor.enrolledFingerprintsInternal =
+ mutableListOf(FingerprintViewModel("a", 1, 3L))
+ val success = FingerprintAuthAttemptViewModel.Success(1)
+ fakeFingerprintManagerInteractor.authenticateAttempt = success
+
+ underTest =
+ FingerprintSettingsViewModel.FingerprintSettingsViewModelFactory(
+ defaultUserId,
+ fakeFingerprintManagerInteractor,
+ backgroundDispatcher,
+ navigationViewModel,
+ )
+ .create(FingerprintSettingsViewModel::class.java)
+
+ var authAttempt: FingerprintAuthAttemptViewModel? = null
+
+ val job = launch { underTest.authFlow.take(5).collectLatest { authAttempt = it } }
+ underTest.shouldAuthenticate(true)
+ navigationViewModel.onConfirmDevice(true, 10L)
+ advanceTimeBy(400)
+ runCurrent()
+
+ assertThat(authAttempt).isEqualTo(success)
+ job.cancel()
+ }
+
+ @Test
+ fun deleteDialog_showAndDismiss() = runTest {
+ val fingerprintToDelete = FingerprintViewModel("A", 1, 10L)
+ fakeFingerprintManagerInteractor.enrolledFingerprintsInternal = mutableListOf(fingerprintToDelete)
+
+ underTest =
+ FingerprintSettingsViewModel.FingerprintSettingsViewModelFactory(
+ defaultUserId,
+ fakeFingerprintManagerInteractor,
+ backgroundDispatcher,
+ navigationViewModel,
+ )
+ .create(FingerprintSettingsViewModel::class.java)
+
+ var dialog: PreferenceViewModel? = null
+ val dialogJob = launch { underTest.isShowingDialog.collect { dialog = it } }
+
+ // Move to the ShowSettings state
+ navigationViewModel.onConfirmDevice(true, 10L)
+ runCurrent()
+ underTest.onDeleteClicked(fingerprintToDelete)
+ runCurrent()
+
+ assertThat(dialog is PreferenceViewModel.DeleteDialog)
+ assertThat(dialog).isEqualTo(PreferenceViewModel.DeleteDialog(fingerprintToDelete))
+
+ underTest.deleteFingerprint(fingerprintToDelete)
+ underTest.onDeleteDialogFinished()
+ runCurrent()
+
+ assertThat(dialog).isNull()
+
+ dialogJob.cancel()
+ }
+}
diff --git a/tests/unit/src/com/android/settings/inputmethod/KeyboardSettingsFeatureProviderImplTest.java b/tests/unit/src/com/android/settings/inputmethod/KeyboardSettingsFeatureProviderImplTest.java
new file mode 100644
index 0000000..6675d5a
--- /dev/null
+++ b/tests/unit/src/com/android/settings/inputmethod/KeyboardSettingsFeatureProviderImplTest.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2021 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.settings.inputmethod;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.Context;
+import android.os.Looper;
+
+import androidx.preference.PreferenceManager;
+import androidx.preference.PreferenceScreen;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class KeyboardSettingsFeatureProviderImplTest {
+
+ private Context mContext;
+ private KeyboardSettingsFeatureProviderImpl mFeatureProvider;
+
+ @Before
+ public void setUp() {
+ mContext = ApplicationProvider.getApplicationContext();
+ mFeatureProvider = new KeyboardSettingsFeatureProviderImpl();
+ }
+
+ @Test
+ public void supportsFirmwareUpdate_defaultValue_returnsFalse() {
+ assertThat(mFeatureProvider.supportsFirmwareUpdate()).isFalse();
+ }
+
+ @Test
+ public void addFirmwareUpdateCategory_defaultValue_returnsFalse() {
+ if (Looper.myLooper() == null) {
+ Looper.prepare();
+ }
+ PreferenceManager preferenceManager = new PreferenceManager(mContext);
+ PreferenceScreen screen = preferenceManager.createPreferenceScreen(mContext);
+
+ assertThat(mFeatureProvider.addFirmwareUpdateCategory(mContext, screen)).isFalse();
+ }
+
+ @Test
+ public void getActionKeyIcon_defaultValue_returnsNull() {
+ assertThat(mFeatureProvider.getActionKeyIcon(mContext)).isNull();
+ }
+}
diff --git a/tests/unit/src/com/android/settings/localepicker/LocaleHelperPreferenceControllerTest.java b/tests/unit/src/com/android/settings/localepicker/LocaleHelperPreferenceControllerTest.java
index 5ac367e..31b8e79 100644
--- a/tests/unit/src/com/android/settings/localepicker/LocaleHelperPreferenceControllerTest.java
+++ b/tests/unit/src/com/android/settings/localepicker/LocaleHelperPreferenceControllerTest.java
@@ -19,14 +19,12 @@
import static org.mockito.Mockito.anyString;
import static org.mockito.Mockito.verify;
-import android.app.settings.SettingsEnums;
import android.content.Context;
import android.os.Looper;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
-import com.android.settings.testutils.FakeFeatureFactory;
import com.android.settingslib.widget.FooterPreference;
import org.junit.Before;
@@ -39,7 +37,6 @@
public class LocaleHelperPreferenceControllerTest {
private Context mContext;
private LocaleHelperPreferenceController mLocaleHelperPreferenceController;
- private FakeFeatureFactory mFeatureFactory;
@Mock
private FooterPreference mMockFooterPreference;
@@ -52,16 +49,11 @@
}
mContext = ApplicationProvider.getApplicationContext();
mLocaleHelperPreferenceController = new LocaleHelperPreferenceController(mContext);
- mFeatureFactory = FakeFeatureFactory.setupForTest();
}
@Test
public void updateFooterPreference_setFooterPreference_hasClickAction() {
mLocaleHelperPreferenceController.updateFooterPreference(mMockFooterPreference);
verify(mMockFooterPreference).setLearnMoreText(anyString());
- mMockFooterPreference.setLearnMoreAction(v -> {
- verify(mFeatureFactory.metricsFeatureProvider).action(
- mContext, SettingsEnums.ACTION_LANGUAGES_LEARN_MORE);
- });
}
}
diff --git a/tests/unit/src/com/android/settings/network/SubscriptionUtilTest.java b/tests/unit/src/com/android/settings/network/SubscriptionUtilTest.java
index 63dca7e..587e734 100644
--- a/tests/unit/src/com/android/settings/network/SubscriptionUtilTest.java
+++ b/tests/unit/src/com/android/settings/network/SubscriptionUtilTest.java
@@ -16,26 +16,30 @@
package com.android.settings.network;
+import static com.android.settings.network.SubscriptionUtil.KEY_UNIQUE_SUBSCRIPTION_DISPLAYNAME;
+import static com.android.settings.network.SubscriptionUtil.SUB_ID;
import static com.google.common.truth.Truth.assertThat;
-
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.when;
import android.content.Context;
+import android.content.SharedPreferences;
import android.content.res.Resources;
import android.telephony.SubscriptionInfo;
import android.telephony.SubscriptionManager;
import android.telephony.TelephonyManager;
import android.text.TextUtils;
-import com.android.settings.R;
-
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
+import com.android.settings.R;
+
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
@@ -179,7 +183,7 @@
@Ignore
@Test
public void getUniqueDisplayNames_identicalCarriers_fourDigitsUsed() {
- // Both subscriptoins have the same display name.
+ // Both subscriptions have the same display name.
final SubscriptionInfo info1 = mock(SubscriptionInfo.class);
final SubscriptionInfo info2 = mock(SubscriptionInfo.class);
when(info1.getSubscriptionId()).thenReturn(SUBID_1);
@@ -209,7 +213,7 @@
@Ignore
@Test
public void getUniqueDisplayNames_identicalCarriersAfterTrim_fourDigitsUsed() {
- // Both subscriptoins have the same display name.
+ // Both subscriptions have the same display name.
final SubscriptionInfo info1 = mock(SubscriptionInfo.class);
final SubscriptionInfo info2 = mock(SubscriptionInfo.class);
when(info1.getSubscriptionId()).thenReturn(SUBID_1);
@@ -238,8 +242,8 @@
@Ignore
@Test
- public void getUniqueDisplayNames_phoneNumberBlocked_subscriptoinIdFallback() {
- // Both subscriptoins have the same display name.
+ public void getUniqueDisplayNames_phoneNumberBlocked_subscriptionIdFallback() {
+ // Both subscriptions have the same display name.
final SubscriptionInfo info1 = mock(SubscriptionInfo.class);
final SubscriptionInfo info2 = mock(SubscriptionInfo.class);
when(info1.getSubscriptionId()).thenReturn(SUBID_1);
@@ -267,9 +271,9 @@
@Ignore
@Test
- public void getUniqueDisplayNames_phoneNumberIdentical_subscriptoinIdFallback() {
+ public void getUniqueDisplayNames_phoneNumberIdentical_subscriptionIdFallback() {
// TODO have three here from the same carrier
- // Both subscriptoins have the same display name.
+ // Both subscriptions have the same display name.
final SubscriptionInfo info1 = mock(SubscriptionInfo.class);
final SubscriptionInfo info2 = mock(SubscriptionInfo.class);
final SubscriptionInfo info3 = mock(SubscriptionInfo.class);
@@ -445,6 +449,68 @@
}
@Test
+ public void getUniqueDisplayName_hasRecord_useRecordBeTheResult() {
+ final SubscriptionInfo info1 = mock(SubscriptionInfo.class);
+ final SubscriptionInfo info2 = mock(SubscriptionInfo.class);
+ when(info1.getSubscriptionId()).thenReturn(SUBID_1);
+ when(info2.getSubscriptionId()).thenReturn(SUBID_2);
+ when(info1.getDisplayName()).thenReturn(CARRIER_1);
+ when(info2.getDisplayName()).thenReturn(CARRIER_1);
+ when(mSubMgr.getAvailableSubscriptionInfoList()).thenReturn(
+ Arrays.asList(info1, info2));
+
+ SharedPreferences sp = mock(SharedPreferences.class);
+ when(mContext.getSharedPreferences(
+ KEY_UNIQUE_SUBSCRIPTION_DISPLAYNAME, Context.MODE_PRIVATE)).thenReturn(sp);
+ when(sp.getString(eq(SUB_ID + SUBID_1), anyString())).thenReturn(CARRIER_1 + " 6789");
+ when(sp.getString(eq(SUB_ID + SUBID_2), anyString())).thenReturn(CARRIER_1 + " 4321");
+
+
+ final CharSequence nameOfSub1 =
+ SubscriptionUtil.getUniqueSubscriptionDisplayName(info1, mContext);
+ final CharSequence nameOfSub2 =
+ SubscriptionUtil.getUniqueSubscriptionDisplayName(info2, mContext);
+
+ assertThat(nameOfSub1).isNotNull();
+ assertThat(nameOfSub2).isNotNull();
+ assertEquals(CARRIER_1 + " 6789", nameOfSub1.toString());
+ assertEquals(CARRIER_1 + " 4321", nameOfSub2.toString());
+ }
+
+ @Test
+ public void getUniqueDisplayName_hasRecordAndNameIsChanged_doesNotUseRecordBeTheResult() {
+ final SubscriptionInfo info1 = mock(SubscriptionInfo.class);
+ final SubscriptionInfo info2 = mock(SubscriptionInfo.class);
+ when(info1.getSubscriptionId()).thenReturn(SUBID_1);
+ when(info2.getSubscriptionId()).thenReturn(SUBID_2);
+ when(info1.getDisplayName()).thenReturn(CARRIER_1);
+ when(info2.getDisplayName()).thenReturn(CARRIER_2);
+ when(mSubMgr.getAvailableSubscriptionInfoList()).thenReturn(
+ Arrays.asList(info1, info2));
+
+ SharedPreferences sp = mock(SharedPreferences.class);
+ SharedPreferences.Editor editor = mock(SharedPreferences.Editor.class);
+ when(mContext.getSharedPreferences(
+ KEY_UNIQUE_SUBSCRIPTION_DISPLAYNAME, Context.MODE_PRIVATE)).thenReturn(sp);
+ when(sp.edit()).thenReturn(editor);
+ when(editor.remove(anyString())).thenReturn(editor);
+
+ when(sp.getString(eq(SUB_ID + SUBID_1), anyString())).thenReturn(CARRIER_1 + " 6789");
+ when(sp.getString(eq(SUB_ID + SUBID_2), anyString())).thenReturn(CARRIER_1 + " 4321");
+
+
+ final CharSequence nameOfSub1 =
+ SubscriptionUtil.getUniqueSubscriptionDisplayName(info1, mContext);
+ final CharSequence nameOfSub2 =
+ SubscriptionUtil.getUniqueSubscriptionDisplayName(info2, mContext);
+
+ assertThat(nameOfSub1).isNotNull();
+ assertThat(nameOfSub2).isNotNull();
+ assertEquals(CARRIER_1 + " 6789", nameOfSub1.toString());
+ assertEquals(CARRIER_2.toString(), nameOfSub2.toString());
+ }
+
+ @Test
public void isInactiveInsertedPSim_nullSubInfo_doesNotCrash() {
assertThat(SubscriptionUtil.isInactiveInsertedPSim(null)).isFalse();
}
@@ -466,4 +532,60 @@
assertTrue(SubscriptionUtil.isSimHardwareVisible(mContext));
}
+
+ @Test
+ public void isValidCachedDisplayName_matchesRule1_returnTrue() {
+ String originalName = "originalName";
+ String cacheString = "originalName 1234";
+
+ assertThat(SubscriptionUtil.isValidCachedDisplayName(cacheString, originalName)).isTrue();
+ }
+
+ @Test
+ public void isValidCachedDisplayName_matchesRule2_returnTrue() {
+ String originalName = "original Name";
+ String cacheString = originalName + " " + 1234;
+
+ assertThat(SubscriptionUtil.isValidCachedDisplayName(cacheString, originalName)).isTrue();
+ }
+
+ @Test
+ public void isValidCachedDisplayName_nameIsEmpty1_returnFalse() {
+ String originalName = "original Name";
+ String cacheString = "";
+
+ assertThat(SubscriptionUtil.isValidCachedDisplayName(cacheString, originalName)).isFalse();
+ }
+
+ @Test
+ public void isValidCachedDisplayName_nameIsEmpty2_returnFalse() {
+ String originalName = "";
+ String cacheString = "originalName 1234";
+
+ assertThat(SubscriptionUtil.isValidCachedDisplayName(cacheString, originalName)).isFalse();
+ }
+
+ @Test
+ public void isValidCachedDisplayName_nameIsDifferent_returnFalse() {
+ String originalName = "original Name";
+ String cacheString = "originalName 1234";
+
+ assertThat(SubscriptionUtil.isValidCachedDisplayName(cacheString, originalName)).isFalse();
+ }
+
+ @Test
+ public void isValidCachedDisplayName_noNumber_returnFalse() {
+ String originalName = "original Name";
+ String cacheString = originalName;
+
+ assertThat(SubscriptionUtil.isValidCachedDisplayName(cacheString, originalName)).isFalse();
+ }
+
+ @Test
+ public void isValidCachedDisplayName_noSpace_returnFalse() {
+ String originalName = "original Name";
+ String cacheString = originalName;
+
+ assertThat(SubscriptionUtil.isValidCachedDisplayName(cacheString, originalName)).isFalse();
+ }
}
diff --git a/tests/unit/src/com/android/settings/network/UiccSlotUtilTest.java b/tests/unit/src/com/android/settings/network/UiccSlotUtilTest.java
index 9a2c611..2e17fb2 100644
--- a/tests/unit/src/com/android/settings/network/UiccSlotUtilTest.java
+++ b/tests/unit/src/com/android/settings/network/UiccSlotUtilTest.java
@@ -20,10 +20,13 @@
import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.content.Context;
+import android.content.Intent;
import android.telephony.SubscriptionInfo;
import android.telephony.SubscriptionManager;
import android.telephony.TelephonyManager;
@@ -49,6 +52,7 @@
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
+import java.util.concurrent.CountDownLatch;
@RunWith(AndroidJUnit4.class)
public class UiccSlotUtilTest {
@@ -738,6 +742,25 @@
assertThat(testSlot).isFalse();
}
+ @Test
+ public void performSwitchToSlot_setSimSlotMapping() throws UiccSlotsException {
+ Collection<UiccSlotMapping> uiccSlotMappings = createUiccSlotMappingDualPortsBNoOrding();
+
+ UiccSlotUtil.performSwitchToSlot(mTelephonyManager, uiccSlotMappings, mContext);
+
+ verify(mTelephonyManager).setSimSlotMapping(any());
+ }
+
+ @Test
+ public void onReceiveSimSlotChangeReceiver_receiveAction_timerCountDown() {
+ CountDownLatch latch = spy(new CountDownLatch(1));
+ UiccSlotUtil.SimSlotChangeReceiver receive = new UiccSlotUtil.SimSlotChangeReceiver(latch);
+
+ receive.onReceive(mContext, new Intent(TelephonyManager.ACTION_SIM_SLOT_STATUS_CHANGED));
+
+ verify(latch).countDown();
+ }
+
private void compareTwoUiccSlotMappings(Collection<UiccSlotMapping> testUiccSlotMappings,
Collection<UiccSlotMapping> verifyUiccSlotMappings) {
assertThat(testUiccSlotMappings.size()).isEqualTo(verifyUiccSlotMappings.size());
diff --git a/tests/unit/src/com/android/settings/network/telephony/MmsMessagePreferenceControllerTest.java b/tests/unit/src/com/android/settings/network/telephony/MmsMessagePreferenceControllerTest.java
index a0219a0..127cdfd 100644
--- a/tests/unit/src/com/android/settings/network/telephony/MmsMessagePreferenceControllerTest.java
+++ b/tests/unit/src/com/android/settings/network/telephony/MmsMessagePreferenceControllerTest.java
@@ -21,15 +21,20 @@
import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.content.Context;
+import android.os.Looper;
import android.telephony.SubscriptionManager;
import android.telephony.TelephonyManager;
import android.telephony.data.ApnSetting;
+import androidx.preference.PreferenceManager;
+import androidx.preference.PreferenceScreen;
import androidx.preference.SwitchPreference;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -63,7 +68,7 @@
when(mContext.getSystemService(SubscriptionManager.class)).thenReturn(mSubscriptionManager);
when(mTelephonyManager.createForSubscriptionId(SUB_ID)).thenReturn(mTelephonyManager);
- mPreference = new SwitchPreference(mContext);
+ mPreference = spy(new SwitchPreference(mContext));
mController = new MmsMessagePreferenceController(mContext, "mms_message");
mController.init(SUB_ID);
mPreference.setKey(mController.getPreferenceKey());
@@ -118,4 +123,20 @@
verify(mTelephonyManager).setMobileDataPolicyEnabled(
TelephonyManager.MOBILE_DATA_POLICY_MMS_ALWAYS_ALLOWED, false);
}
+
+ @Test
+ public void onStart_updatePreferenceUiState() {
+ if (Looper.myLooper() == null) {
+ Looper.prepare();
+ }
+ PreferenceManager preferenceManager = new PreferenceManager(mContext);
+ PreferenceScreen preferenceScreen = preferenceManager.createPreferenceScreen(mContext);
+ preferenceScreen.addPreference(mPreference);
+ mController.displayPreference(preferenceScreen);
+
+ mController.onStart();
+
+ // First is preference initialization, and second is in onStart();
+ verify(mPreference, times(2)).setChecked(anyBoolean());
+ }
}
diff --git a/tests/unit/src/com/android/settings/network/telephony/MobileNetworkSwitchControllerTest.java b/tests/unit/src/com/android/settings/network/telephony/MobileNetworkSwitchControllerTest.java
index e460d67..3cdd23a 100644
--- a/tests/unit/src/com/android/settings/network/telephony/MobileNetworkSwitchControllerTest.java
+++ b/tests/unit/src/com/android/settings/network/telephony/MobileNetworkSwitchControllerTest.java
@@ -35,6 +35,8 @@
import android.os.Looper;
import android.telephony.SubscriptionInfo;
import android.telephony.SubscriptionManager;
+import android.telephony.TelephonyCallback;
+import android.telephony.TelephonyManager;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.LinearLayout;
@@ -44,28 +46,32 @@
import androidx.preference.PreferenceViewHolder;
import androidx.test.annotation.UiThreadTest;
import androidx.test.core.app.ApplicationProvider;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.android.settings.network.SubscriptionUtil;
import com.android.settings.widget.SettingsMainSwitchPreference;
import org.junit.After;
import org.junit.Before;
-import org.junit.Ignore;
+import org.junit.Rule;
import org.junit.Test;
-import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
import java.util.Arrays;
+import java.util.concurrent.Executor;
-@RunWith(AndroidJUnit4.class)
public class MobileNetworkSwitchControllerTest {
+ @Rule
+ public final MockitoRule mMockitoRule = MockitoJUnit.rule();
+
@Mock
private SubscriptionManager mSubscriptionManager;
@Mock
private SubscriptionInfo mSubscription;
+ @Mock
+ private TelephonyManager mTelephonyManager;
private PreferenceScreen mScreen;
private PreferenceManager mPreferenceManager;
@@ -76,7 +82,9 @@
@Before
public void setUp() {
- MockitoAnnotations.initMocks(this);
+ if (Looper.myLooper() == null) {
+ Looper.prepare();
+ }
mContext = spy(ApplicationProvider.getApplicationContext());
when(mContext.getSystemService(SubscriptionManager.class)).thenReturn(mSubscriptionManager);
when(mSubscriptionManager.setSubscriptionEnabled(eq(mSubId), anyBoolean()))
@@ -89,18 +97,19 @@
when(sub2.getSubscriptionId()).thenReturn(456);
SubscriptionUtil.setAvailableSubscriptionsForTesting(Arrays.asList(mSubscription, sub2));
+ when(mContext.getSystemService(TelephonyManager.class)).thenReturn(mTelephonyManager);
+ when(mTelephonyManager.createForSubscriptionId(mSubId))
+ .thenReturn(mTelephonyManager);
+
final String key = "prefKey";
mController = new MobileNetworkSwitchController(mContext, key);
mController.init(mSubscription.getSubscriptionId());
- if (Looper.myLooper() == null) {
- Looper.prepare();
- }
-
mPreferenceManager = new PreferenceManager(mContext);
mScreen = mPreferenceManager.createPreferenceScreen(mContext);
mSwitchBar = new SettingsMainSwitchPreference(mContext);
mSwitchBar.setKey(key);
+ mSwitchBar.setTitle("123");
mScreen.addPreference(mSwitchBar);
final LayoutInflater inflater = LayoutInflater.from(mContext);
@@ -117,7 +126,6 @@
@Test
@UiThreadTest
- @Ignore
public void isAvailable_pSIM_isNotAvailable() {
when(mSubscription.isEmbedded()).thenReturn(false);
mController.displayPreference(mScreen);
@@ -130,7 +138,6 @@
@Test
@UiThreadTest
- @Ignore
public void displayPreference_oneEnabledSubscription_switchBarNotHidden() {
doReturn(true).when(mSubscriptionManager).isActiveSubscriptionId(mSubId);
SubscriptionUtil.setAvailableSubscriptionsForTesting(Arrays.asList(mSubscription));
@@ -140,7 +147,6 @@
@Test
@UiThreadTest
- @Ignore
public void displayPreference_oneDisabledSubscription_switchBarNotHidden() {
doReturn(false).when(mSubscriptionManager).isActiveSubscriptionId(mSubId);
SubscriptionUtil.setAvailableSubscriptionsForTesting(Arrays.asList(mSubscription));
@@ -152,7 +158,6 @@
@Test
@UiThreadTest
- @Ignore
public void displayPreference_subscriptionEnabled_switchIsOn() {
when(mSubscriptionManager.isActiveSubscriptionId(mSubId)).thenReturn(true);
mController.displayPreference(mScreen);
@@ -162,7 +167,6 @@
@Test
@UiThreadTest
- @Ignore
public void displayPreference_subscriptionDisabled_switchIsOff() {
when(mSubscriptionManager.isActiveSubscriptionId(mSubId)).thenReturn(false);
@@ -174,7 +178,6 @@
@Test
@UiThreadTest
- @Ignore
public void switchChangeListener_fromEnabledToDisabled_setSubscriptionEnabledCalledCorrectly() {
when(mSubscriptionManager.isActiveSubscriptionId(mSubId)).thenReturn(true);
mController.displayPreference(mScreen);
@@ -183,18 +186,24 @@
final ArgumentCaptor<Intent> intentCaptor = ArgumentCaptor.forClass(Intent.class);
doNothing().when(mContext).startActivity(intentCaptor.capture());
+
+ // set switch off then should start a Activity.
mSwitchBar.setChecked(false);
+
+ when(mSubscriptionManager.isActiveSubscriptionId(mSubId)).thenReturn(false);
+ // Simulate action of back from previous activity.
+ mController.displayPreference(mScreen);
Bundle extra = intentCaptor.getValue().getExtras();
verify(mContext, times(1)).startActivity(any());
assertThat(extra.getInt(ToggleSubscriptionDialogActivity.ARG_SUB_ID)).isEqualTo(mSubId);
assertThat(extra.getBoolean(ToggleSubscriptionDialogActivity.ARG_enable))
.isEqualTo(false);
+ assertThat(mSwitchBar.isChecked()).isFalse();
}
@Test
@UiThreadTest
- @Ignore
public void switchChangeListener_fromEnabledToDisabled_setSubscriptionEnabledFailed() {
when(mSubscriptionManager.setSubscriptionEnabled(eq(mSubId), anyBoolean()))
.thenReturn(false);
@@ -205,7 +214,12 @@
final ArgumentCaptor<Intent> intentCaptor = ArgumentCaptor.forClass(Intent.class);
doNothing().when(mContext).startActivity(intentCaptor.capture());
+
+ // set switch off then should start a Activity.
mSwitchBar.setChecked(false);
+
+ // Simulate action of back from previous activity.
+ mController.displayPreference(mScreen);
Bundle extra = intentCaptor.getValue().getExtras();
verify(mContext, times(1)).startActivity(any());
@@ -217,7 +231,6 @@
@Test
@UiThreadTest
- @Ignore
public void switchChangeListener_fromDisabledToEnabled_setSubscriptionEnabledCalledCorrectly() {
when(mSubscriptionManager.isActiveSubscriptionId(mSubId)).thenReturn(false);
mController.displayPreference(mScreen);
@@ -233,4 +246,24 @@
assertThat(extra.getInt(ToggleSubscriptionDialogActivity.ARG_SUB_ID)).isEqualTo(mSubId);
assertThat(extra.getBoolean(ToggleSubscriptionDialogActivity.ARG_enable)).isEqualTo(true);
}
+ @Test
+ @UiThreadTest
+ public void onResumeAndonPause_registerAndUnregisterTelephonyCallback() {
+ mController.onResume();
+
+ verify(mTelephonyManager)
+ .registerTelephonyCallback(any(Executor.class), any(TelephonyCallback.class));
+
+ mController.onPause();
+ verify(mTelephonyManager)
+ .unregisterTelephonyCallback(any(TelephonyCallback.class));
+ }
+
+ @Test
+ @UiThreadTest
+ public void onPause_doNotRegisterAndUnregisterTelephonyCallback() {
+ mController.onPause();
+ verify(mTelephonyManager, times(0))
+ .unregisterTelephonyCallback(any(TelephonyCallback.class));
+ }
}
diff --git a/tests/unit/src/com/android/settings/network/telephony/NetworkSelectSettingsTest.java b/tests/unit/src/com/android/settings/network/telephony/NetworkSelectSettingsTest.java
index 7d86018..404f56c 100644
--- a/tests/unit/src/com/android/settings/network/telephony/NetworkSelectSettingsTest.java
+++ b/tests/unit/src/com/android/settings/network/telephony/NetworkSelectSettingsTest.java
@@ -25,7 +25,6 @@
import android.content.res.Resources;
import android.os.Bundle;
import android.os.PersistableBundle;
-import android.provider.Settings;
import android.telephony.CarrierConfigManager;
import android.telephony.CellIdentity;
import android.telephony.CellIdentityGsm;
@@ -37,6 +36,7 @@
import android.telephony.CellSignalStrengthLte;
import android.telephony.TelephonyManager;
+import androidx.preference.Preference;
import androidx.preference.PreferenceCategory;
import androidx.preference.PreferenceManager;
import androidx.preference.PreferenceScreen;
@@ -310,4 +310,13 @@
cellInfoGsm.setCellSignalStrength(cssg);
return cellInfoGsm;
}
+
+ @Test
+ @UiThreadTest
+ public void onPreferenceTreeClick_notNetworkOperatorPreference_noCrash() {
+ mNetworkSelectSettings.onCreateInitialization();
+ mNetworkSelectSettings.enablePreferenceScreen(true);
+
+ mNetworkSelectSettings.onPreferenceTreeClick(new Preference(mContext));
+ }
}
diff --git a/tests/unit/src/com/android/settings/password/SaveAndFinishWorkerTest.java b/tests/unit/src/com/android/settings/password/SaveAndFinishWorkerTest.java
new file mode 100644
index 0000000..88e3150
--- /dev/null
+++ b/tests/unit/src/com/android/settings/password/SaveAndFinishWorkerTest.java
@@ -0,0 +1,136 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.password;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import com.android.internal.widget.LockPatternUtils;
+import com.android.internal.widget.LockscreenCredential;
+import com.android.internal.widget.VerifyCredentialResponse;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class SaveAndFinishWorkerTest {
+ @Test
+ public void testSetRequestWriteRepairModePassword_setLockCredentialFail() {
+ int userId = 0;
+ int flags = LockPatternUtils.VERIFY_FLAG_WRITE_REPAIR_MODE_PW;
+ var chosenCredential = LockscreenCredential.createPassword("1234");
+ var currentCredential = LockscreenCredential.createNone();
+ var worker = new SaveAndFinishWorker();
+ var lpu = mock(LockPatternUtils.class);
+
+ when(lpu.setLockCredential(chosenCredential, currentCredential, userId)).thenReturn(false);
+
+ worker.setRequestWriteRepairModePassword(true);
+ worker.prepare(lpu, chosenCredential, currentCredential, userId);
+ var result = worker.saveAndVerifyInBackground();
+
+ verify(lpu).setLockCredential(chosenCredential, currentCredential, userId);
+ verify(lpu, never()).verifyCredential(chosenCredential, userId, flags);
+ assertThat(result.first).isFalse();
+ }
+
+ @Test
+ public void testSetRequestWriteRepairModePassword_verifyCredentialFail() {
+ int userId = 0;
+ int flags = LockPatternUtils.VERIFY_FLAG_WRITE_REPAIR_MODE_PW;
+ var chosenCredential = LockscreenCredential.createPassword("1234");
+ var currentCredential = LockscreenCredential.createNone();
+ var worker = new SaveAndFinishWorker();
+ var lpu = mock(LockPatternUtils.class);
+ var response = VerifyCredentialResponse.fromError();
+
+ when(lpu.setLockCredential(chosenCredential, currentCredential, userId)).thenReturn(true);
+ when(lpu.verifyCredential(chosenCredential, userId, flags)).thenReturn(response);
+
+ worker.setRequestWriteRepairModePassword(true);
+ worker.prepare(lpu, chosenCredential, currentCredential, userId);
+ var result = worker.saveAndVerifyInBackground();
+
+ verify(lpu).setLockCredential(chosenCredential, currentCredential, userId);
+ verify(lpu).verifyCredential(chosenCredential, userId, flags);
+ assertThat(result.first).isTrue();
+ assertThat(result.second.getBooleanExtra(
+ ChooseLockSettingsHelper.EXTRA_KEY_WROTE_REPAIR_MODE_CREDENTIAL, true))
+ .isFalse();
+ }
+
+ @Test
+ public void testSetRequestWriteRepairModePassword_verifyCredentialSucceed() {
+ int userId = 0;
+ int flags = LockPatternUtils.VERIFY_FLAG_WRITE_REPAIR_MODE_PW;
+ var chosenCredential = LockscreenCredential.createPassword("1234");
+ var currentCredential = LockscreenCredential.createNone();
+ var worker = new SaveAndFinishWorker();
+ var lpu = mock(LockPatternUtils.class);
+ var response = new VerifyCredentialResponse.Builder().build();
+
+ when(lpu.setLockCredential(chosenCredential, currentCredential, userId)).thenReturn(true);
+ when(lpu.verifyCredential(chosenCredential, userId, flags)).thenReturn(response);
+
+ worker.setRequestWriteRepairModePassword(true);
+ worker.prepare(lpu, chosenCredential, currentCredential, userId);
+ var result = worker.saveAndVerifyInBackground();
+
+ verify(lpu).setLockCredential(chosenCredential, currentCredential, userId);
+ verify(lpu).verifyCredential(chosenCredential, userId, flags);
+ assertThat(result.first).isTrue();
+ assertThat(result.second.getBooleanExtra(
+ ChooseLockSettingsHelper.EXTRA_KEY_WROTE_REPAIR_MODE_CREDENTIAL, false))
+ .isTrue();
+ }
+
+ @Test
+ public void testSetRequestWriteRepairModePassword_verifyCredentialSucceed_noGkPwHandle() {
+ int userId = 0;
+ int flags = LockPatternUtils.VERIFY_FLAG_WRITE_REPAIR_MODE_PW
+ | LockPatternUtils.VERIFY_FLAG_REQUEST_GK_PW_HANDLE;
+ var chosenCredential = LockscreenCredential.createPassword("1234");
+ var currentCredential = LockscreenCredential.createNone();
+ var worker = new SaveAndFinishWorker();
+ var lpu = mock(LockPatternUtils.class);
+ var response = new VerifyCredentialResponse.Builder().build();
+
+ when(lpu.setLockCredential(chosenCredential, currentCredential, userId)).thenReturn(true);
+ when(lpu.verifyCredential(chosenCredential, userId, flags)).thenReturn(response);
+
+ worker.setRequestWriteRepairModePassword(true);
+ worker.setRequestGatekeeperPasswordHandle(true);
+ worker.prepare(lpu, chosenCredential, currentCredential, userId);
+ var result = worker.saveAndVerifyInBackground();
+
+ verify(lpu).setLockCredential(chosenCredential, currentCredential, userId);
+ verify(lpu).verifyCredential(chosenCredential, userId, flags);
+ assertThat(result.first).isTrue();
+ assertThat(result.second.getBooleanExtra(
+ ChooseLockSettingsHelper.EXTRA_KEY_WROTE_REPAIR_MODE_CREDENTIAL, false))
+ .isTrue();
+ assertThat(result.second.getLongExtra(
+ ChooseLockSettingsHelper.EXTRA_KEY_GK_PW_HANDLE, -1))
+ .isEqualTo(-1);
+ }
+}
diff --git a/tests/unit/src/com/android/settings/regionalpreferences/NumberingSystemItemControllerTest.java b/tests/unit/src/com/android/settings/regionalpreferences/NumberingSystemItemControllerTest.java
index 5c42ad9..0a67824 100644
--- a/tests/unit/src/com/android/settings/regionalpreferences/NumberingSystemItemControllerTest.java
+++ b/tests/unit/src/com/android/settings/regionalpreferences/NumberingSystemItemControllerTest.java
@@ -24,7 +24,6 @@
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
-import android.app.settings.SettingsEnums;
import android.content.Context;
import android.os.Bundle;
import android.os.LocaleList;
@@ -52,7 +51,6 @@
private NumberingPreferencesFragment mFragment;
private PreferenceScreen mPreferenceScreen;
private LocaleList mCacheLocale;
- private FakeFeatureFactory mFeatureFactory;
@Before
@UiThreadTest
@@ -61,7 +59,6 @@
Looper.prepare();
}
mApplicationContext = ApplicationProvider.getApplicationContext();
- mFeatureFactory = FakeFeatureFactory.setupForTest();
mFragment = spy(new NumberingPreferencesFragment());
PreferenceManager preferenceManager = new PreferenceManager(mApplicationContext);
mPreferenceScreen = preferenceManager.createPreferenceScreen(mApplicationContext);
@@ -97,10 +94,6 @@
}
assertTrue(isCallingStartActivity);
- verify(mFeatureFactory.metricsFeatureProvider).action(
- mApplicationContext,
- SettingsEnums.ACTION_CHOOSE_LANGUAGE_FOR_NUMBERS_PREFERENCES,
- "I_am_the_key");
}
@Test
@@ -121,9 +114,6 @@
mController.handlePreferenceTreeClick(preference);
verify(mFragment).setArguments(any());
- verify(mFeatureFactory.metricsFeatureProvider).action(
- mApplicationContext, SettingsEnums.ACTION_SET_NUMBERS_PREFERENCES,
- "test_key");
}
@Test
diff --git a/tests/unit/src/com/android/settings/testutils/FakeFeatureFactory.java b/tests/unit/src/com/android/settings/testutils/FakeFeatureFactory.java
index 697217b..49ce2cc 100644
--- a/tests/unit/src/com/android/settings/testutils/FakeFeatureFactory.java
+++ b/tests/unit/src/com/android/settings/testutils/FakeFeatureFactory.java
@@ -27,6 +27,7 @@
import com.android.settings.biometrics.face.FaceFeatureProvider;
import com.android.settings.biometrics2.factory.BiometricsRepositoryProvider;
import com.android.settings.bluetooth.BluetoothFeatureProvider;
+import com.android.settings.connecteddevice.stylus.StylusFeatureProvider;
import com.android.settings.dashboard.DashboardFeatureProvider;
import com.android.settings.dashboard.suggestions.SuggestionFeatureProvider;
import com.android.settings.deviceinfo.hardwareinfo.HardwareInfoFeatureProvider;
@@ -37,6 +38,7 @@
import com.android.settings.fuelgauge.PowerUsageFeatureProvider;
import com.android.settings.gestures.AssistGestureFeatureProvider;
import com.android.settings.homepage.contextualcards.ContextualCardFeatureProvider;
+import com.android.settings.inputmethod.KeyboardSettingsFeatureProvider;
import com.android.settings.localepicker.LocaleFeatureProvider;
import com.android.settings.overlay.DockUpdaterFeatureProvider;
import com.android.settings.overlay.FeatureFactory;
@@ -90,6 +92,8 @@
public AccessibilityMetricsFeatureProvider mAccessibilityMetricsFeatureProvider;
public AdvancedVpnFeatureProvider mAdvancedVpnFeatureProvider;
public WifiFeatureProvider mWifiFeatureProvider;
+ public KeyboardSettingsFeatureProvider mKeyboardSettingsFeatureProvider;
+ public StylusFeatureProvider mStylusFeatureProvider;
/**
* Call this in {@code @Before} method of the test class to use fake factory.
@@ -133,6 +137,8 @@
mAccessibilityMetricsFeatureProvider = mock(AccessibilityMetricsFeatureProvider.class);
mAdvancedVpnFeatureProvider = mock(AdvancedVpnFeatureProvider.class);
mWifiFeatureProvider = mock(WifiFeatureProvider.class);
+ mKeyboardSettingsFeatureProvider = mock(KeyboardSettingsFeatureProvider.class);
+ mStylusFeatureProvider = mock(StylusFeatureProvider.class);
}
@Override
@@ -156,7 +162,7 @@
}
@Override
- public BatterySettingsFeatureProvider getBatterySettingsFeatureProvider(Context context) {
+ public BatterySettingsFeatureProvider getBatterySettingsFeatureProvider() {
return batterySettingsFeatureProvider;
}
@@ -289,4 +295,14 @@
public WifiFeatureProvider getWifiFeatureProvider() {
return mWifiFeatureProvider;
}
+
+ @Override
+ public KeyboardSettingsFeatureProvider getKeyboardSettingsFeatureProvider() {
+ return mKeyboardSettingsFeatureProvider;
+ }
+
+ @Override
+ public StylusFeatureProvider getStylusFeatureProvider() {
+ return mStylusFeatureProvider;
+ }
}
diff --git a/tests/unit/src/com/android/settings/wifi/details/WifiNetworkDetailsViewModelTest.java b/tests/unit/src/com/android/settings/wifi/details/WifiNetworkDetailsViewModelTest.java
new file mode 100644
index 0000000..5f2c561
--- /dev/null
+++ b/tests/unit/src/com/android/settings/wifi/details/WifiNetworkDetailsViewModelTest.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.wifi.details;
+
+import static android.net.wifi.sharedconnectivity.app.HotspotNetwork.NETWORK_TYPE_CELLULAR;
+import static android.net.wifi.sharedconnectivity.app.HotspotNetwork.NETWORK_TYPE_ETHERNET;
+import static android.net.wifi.sharedconnectivity.app.HotspotNetwork.NETWORK_TYPE_WIFI;
+import static android.telephony.CellSignalStrength.SIGNAL_STRENGTH_NONE_OR_UNKNOWN;
+
+import static com.android.wifitrackerlib.WifiEntry.WIFI_LEVEL_MAX;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.Application;
+
+import androidx.lifecycle.MutableLiveData;
+import androidx.test.core.app.ApplicationProvider;
+
+import com.android.wifitrackerlib.HotspotNetworkEntry;
+import com.android.wifitrackerlib.WifiEntry;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.Spy;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+public class WifiNetworkDetailsViewModelTest {
+
+ @Rule
+ public final MockitoRule mMockitoRule = MockitoJUnit.rule();
+ @Spy
+ Application mApplication = ApplicationProvider.getApplicationContext();
+ @Mock
+ MutableLiveData<WifiNetworkDetailsViewModel.HotspotNetworkData> mHotspotNetworkData;
+ @Mock
+ HotspotNetworkEntry mHotspotNetworkEntry;
+
+ WifiNetworkDetailsViewModel mViewModel;
+ ArgumentCaptor<WifiNetworkDetailsViewModel.HotspotNetworkData> mHotspotNetworkDataCaptor =
+ ArgumentCaptor.forClass(WifiNetworkDetailsViewModel.HotspotNetworkData.class);
+
+ @Before
+ public void setUp() {
+ mViewModel = new WifiNetworkDetailsViewModel(mApplication);
+ mViewModel.mHotspotNetworkData = mHotspotNetworkData;
+ }
+
+ @Test
+ public void setWifiEntry_notHotspotNetworkEntry_postValueNull() {
+ mViewModel.setWifiEntry(mock(WifiEntry.class));
+
+ verify(mHotspotNetworkData).postValue(null);
+ }
+
+ @Test
+ public void setWifiEntry_hotspotNetworkEntryWifi_postValueCorrect() {
+ when(mHotspotNetworkEntry.getNetworkType()).thenReturn(NETWORK_TYPE_WIFI);
+ when(mHotspotNetworkEntry.getUpstreamConnectionStrength()).thenReturn(WIFI_LEVEL_MAX);
+ when(mHotspotNetworkEntry.getBatteryPercentage()).thenReturn(100);
+ when(mHotspotNetworkEntry.isBatteryCharging()).thenReturn(false);
+
+
+ mViewModel.setWifiEntry(mHotspotNetworkEntry);
+
+ verify(mHotspotNetworkData).postValue(mHotspotNetworkDataCaptor.capture());
+ WifiNetworkDetailsViewModel.HotspotNetworkData data = mHotspotNetworkDataCaptor.getValue();
+ assertThat(data.getNetworkType()).isEqualTo(NETWORK_TYPE_WIFI);
+ assertThat(data.getUpstreamConnectionStrength()).isEqualTo(WIFI_LEVEL_MAX);
+ assertThat(data.getBatteryPercentage()).isEqualTo(100);
+ assertThat(data.isBatteryCharging()).isEqualTo(false);
+ }
+
+ @Test
+ public void setWifiEntry_hotspotNetworkEntryMobileData_postValueCorrect() {
+ when(mHotspotNetworkEntry.getNetworkType()).thenReturn(NETWORK_TYPE_CELLULAR);
+ when(mHotspotNetworkEntry.getUpstreamConnectionStrength())
+ .thenReturn(SIGNAL_STRENGTH_NONE_OR_UNKNOWN);
+ when(mHotspotNetworkEntry.getBatteryPercentage()).thenReturn(0);
+ when(mHotspotNetworkEntry.isBatteryCharging()).thenReturn(true);
+
+
+ mViewModel.setWifiEntry(mHotspotNetworkEntry);
+
+ verify(mHotspotNetworkData).postValue(mHotspotNetworkDataCaptor.capture());
+ WifiNetworkDetailsViewModel.HotspotNetworkData data = mHotspotNetworkDataCaptor.getValue();
+ assertThat(data.getNetworkType()).isEqualTo(NETWORK_TYPE_CELLULAR);
+ assertThat(data.getUpstreamConnectionStrength()).isEqualTo(SIGNAL_STRENGTH_NONE_OR_UNKNOWN);
+ assertThat(data.getBatteryPercentage()).isEqualTo(0);
+ assertThat(data.isBatteryCharging()).isEqualTo(true);
+ }
+
+ @Test
+ public void setWifiEntry_hotspotNetworkEntryEthernet_postValueCorrect() {
+ when(mHotspotNetworkEntry.getNetworkType()).thenReturn(NETWORK_TYPE_ETHERNET);
+ when(mHotspotNetworkEntry.getBatteryPercentage()).thenReturn(50);
+ when(mHotspotNetworkEntry.isBatteryCharging()).thenReturn(true);
+
+
+ mViewModel.setWifiEntry(mHotspotNetworkEntry);
+
+ verify(mHotspotNetworkData).postValue(mHotspotNetworkDataCaptor.capture());
+ WifiNetworkDetailsViewModel.HotspotNetworkData data = mHotspotNetworkDataCaptor.getValue();
+ assertThat(data.getNetworkType()).isEqualTo(NETWORK_TYPE_ETHERNET);
+ assertThat(data.getBatteryPercentage()).isEqualTo(50);
+ assertThat(data.isBatteryCharging()).isEqualTo(true);
+ }
+
+ @Test
+ public void getSecuritySummary_returnNotNull() {
+ assertThat(mViewModel.getHotspotNetworkData()).isNotNull();
+ }
+}
diff --git a/tests/unit/src/com/android/settings/wifi/dpp/WifiQrCodeTest.java b/tests/unit/src/com/android/settings/wifi/dpp/WifiQrCodeTest.java
new file mode 100644
index 0000000..e3a8ca5
--- /dev/null
+++ b/tests/unit/src/com/android/settings/wifi/dpp/WifiQrCodeTest.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.wifi.dpp;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class WifiQrCodeTest {
+ @Test
+ public void testZxParsing_validCode() {
+ WifiNetworkConfig config = new WifiQrCode("WIFI:S:testAbC;T:nopass").getWifiNetworkConfig();
+ assertThat(config.getSsid()).isEqualTo("testAbC");
+ assertThat(config.getSecurity()).isEqualTo("nopass");
+
+ config = new WifiQrCode(
+ "WIFI:S:reallyLONGone;T:WEP;P:somepasswo#%^**123rd").getWifiNetworkConfig();
+ assertThat(config.getSsid()).isEqualTo("reallyLONGone");
+ assertThat(config.getSecurity()).isEqualTo("WEP");
+ assertThat(config.getPreSharedKey()).isEqualTo("somepasswo#%^**123rd");
+
+ config = new WifiQrCode("WIFI:S:anotherone;T:WPA;P:3#=3j9asicla").getWifiNetworkConfig();
+ assertThat(config.getSsid()).isEqualTo("anotherone");
+ assertThat(config.getSecurity()).isEqualTo("WPA");
+ assertThat(config.getPreSharedKey()).isEqualTo("3#=3j9asicla");
+
+ config = new WifiQrCode("WIFI:S:xx;T:SAE;P:a").getWifiNetworkConfig();
+ assertThat(config.getSsid()).isEqualTo("xx");
+ assertThat(config.getSecurity()).isEqualTo("SAE");
+ assertThat(config.getPreSharedKey()).isEqualTo("a");
+ }
+
+ @Test
+ public void testZxParsing_invalidCodeButShouldWork() {
+ WifiNetworkConfig config = new WifiQrCode(
+ "WIFI:S:testAbC; T:nopass").getWifiNetworkConfig();
+ assertThat(config.getSsid()).isEqualTo("testAbC");
+ assertThat(config.getSecurity()).isEqualTo("nopass");
+
+ config = new WifiQrCode(
+ "WIFI:S:reallyLONGone;T:WEP; P:somepassword").getWifiNetworkConfig();
+ assertThat(config.getSsid()).isEqualTo("reallyLONGone");
+ assertThat(config.getSecurity()).isEqualTo("WEP");
+ assertThat(config.getPreSharedKey()).isEqualTo("somepassword");
+
+ config = new WifiQrCode("WIFI: S:anotherone;T:WPA;P:abcdefghihklmn").getWifiNetworkConfig();
+ assertThat(config.getSsid()).isEqualTo("anotherone");
+ assertThat(config.getSecurity()).isEqualTo("WPA");
+ assertThat(config.getPreSharedKey()).isEqualTo("abcdefghihklmn");
+
+ config = new WifiQrCode("WIFI: S:xx; T:SAE; P:a").getWifiNetworkConfig();
+ assertThat(config.getSsid()).isEqualTo("xx");
+ assertThat(config.getSecurity()).isEqualTo("SAE");
+ assertThat(config.getPreSharedKey()).isEqualTo("a");
+ }
+}
diff --git a/tests/unit/src/com/android/settings/wifi/repository/SharedConnectivityRepositoryTest.java b/tests/unit/src/com/android/settings/wifi/repository/SharedConnectivityRepositoryTest.java
new file mode 100644
index 0000000..4aef552
--- /dev/null
+++ b/tests/unit/src/com/android/settings/wifi/repository/SharedConnectivityRepositoryTest.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.wifi.repository;
+
+import static android.app.PendingIntent.FLAG_IMMUTABLE;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.net.wifi.sharedconnectivity.app.SharedConnectivityManager;
+import android.net.wifi.sharedconnectivity.app.SharedConnectivitySettingsState;
+
+import androidx.test.core.app.ApplicationProvider;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.Spy;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+public class SharedConnectivityRepositoryTest {
+
+ @Rule
+ public final MockitoRule mMockitoRule = MockitoJUnit.rule();
+ @Spy
+ private Context mContext = ApplicationProvider.getApplicationContext();
+ @Mock
+ private SharedConnectivityManager mManager;
+
+ private SharedConnectivityRepository mRepository;
+ private PendingIntent mIntent = PendingIntent
+ .getActivity(mContext, 0, new Intent("test"), FLAG_IMMUTABLE);
+ private SharedConnectivitySettingsState mState = new SharedConnectivitySettingsState.Builder()
+ .setInstantTetherSettingsPendingIntent(mIntent).build();
+
+ @Before
+ public void setUp() {
+ when(mContext.getSystemService(SharedConnectivityManager.class)).thenReturn(mManager);
+ when(mManager.getSettingsState()).thenReturn(mState);
+
+ mRepository = spy(new SharedConnectivityRepository(mContext, true /* isConfigEnabled */));
+ }
+
+ @Test
+ public void constructor_configEnabled_registerCallback() {
+ verify(mManager).registerCallback(any(), any());
+ }
+
+ @Test
+ public void constructor_configNotEnabled_doNotRegisterCallback() {
+ SharedConnectivityManager manager = mock(SharedConnectivityManager.class);
+ when(mContext.getSystemService(SharedConnectivityManager.class)).thenReturn(manager);
+
+ mRepository = new SharedConnectivityRepository(mContext, false /* isConfigEnabled */);
+
+ verify(manager, never()).registerCallback(any(), any());
+ }
+
+ @Test
+ public void isServiceAvailable_configEnabled_returnTrue() {
+ mRepository = new SharedConnectivityRepository(mContext, true /* isConfigEnabled */);
+
+ assertThat(mRepository.isServiceAvailable()).isTrue();
+ }
+
+ @Test
+ public void isServiceAvailable_configNotEnabled_returnFalse() {
+ mRepository = new SharedConnectivityRepository(mContext, false /* isConfigEnabled */);
+
+ assertThat(mRepository.isServiceAvailable()).isFalse();
+ }
+
+ @Test
+ public void getSettingsState_isNotNull() {
+ assertThat(mRepository.getSettingsState()).isNotNull();
+ }
+
+ @Test
+ public void handleLaunchSettings_managerNull_doNothing() {
+ when(mContext.getSystemService(SharedConnectivityManager.class)).thenReturn(null);
+ mRepository = spy(new SharedConnectivityRepository(mContext, true /* isConfigEnabled */));
+
+ mRepository.handleLaunchSettings();
+
+ verify(mRepository, never()).sendSettingsIntent(mIntent);
+ }
+
+ @Test
+ public void handleLaunchSettings_stageNull_doNothing() {
+ when(mManager.getSettingsState()).thenReturn(null);
+
+ mRepository.handleLaunchSettings();
+
+ verify(mRepository, never()).sendSettingsIntent(mIntent);
+ }
+
+ @Test
+ public void handleLaunchSettings_intentNull_doNothing() {
+ mState = new SharedConnectivitySettingsState.Builder()
+ .setInstantTetherSettingsPendingIntent(null).build();
+ when(mManager.getSettingsState()).thenReturn(mState);
+
+ mRepository.handleLaunchSettings();
+
+ verify(mRepository, never()).sendSettingsIntent(mIntent);
+ }
+
+ @Test
+ public void handleLaunchSettings_allReady_sendSettingsIntent() {
+ mRepository.handleLaunchSettings();
+
+ verify(mRepository).sendSettingsIntent(mIntent);
+ }
+}
diff --git a/tests/unit/src/com/android/settings/wifi/tether/WifiHotspotSpeedViewModelTest.java b/tests/unit/src/com/android/settings/wifi/tether/WifiHotspotSpeedViewModelTest.java
index 3a1a927..f52478e 100644
--- a/tests/unit/src/com/android/settings/wifi/tether/WifiHotspotSpeedViewModelTest.java
+++ b/tests/unit/src/com/android/settings/wifi/tether/WifiHotspotSpeedViewModelTest.java
@@ -20,6 +20,9 @@
import static com.android.settings.wifi.repository.WifiHotspotRepository.SPEED_2GHZ_5GHZ;
import static com.android.settings.wifi.repository.WifiHotspotRepository.SPEED_5GHZ;
import static com.android.settings.wifi.repository.WifiHotspotRepository.SPEED_6GHZ;
+import static com.android.settings.wifi.tether.WifiHotspotSpeedViewModel.RES_SPEED_5G_SUMMARY;
+import static com.android.settings.wifi.tether.WifiHotspotSpeedViewModel.RES_SPEED_6G_SUMMARY;
+import static com.android.settings.wifi.tether.WifiHotspotSpeedViewModel.RES_SUMMARY_UNAVAILABLE;
import static com.google.common.truth.Truth.assertThat;
@@ -128,7 +131,9 @@
mViewModel.on6gAvailableChanged(true);
verify(mSpeedInfoMapData).setValue(mViewModel.mSpeedInfoMap);
- assertThat(mViewModel.mSpeedInfoMap.get(SPEED_6GHZ).mIsEnabled).isTrue();
+ WifiHotspotSpeedViewModel.SpeedInfo speedInfo = mViewModel.mSpeedInfoMap.get(SPEED_6GHZ);
+ assertThat(speedInfo.mIsEnabled).isTrue();
+ assertThat(speedInfo.mSummary).isEqualTo(mContext.getString(RES_SPEED_6G_SUMMARY));
}
@Test
@@ -139,7 +144,9 @@
mViewModel.on6gAvailableChanged(false);
verify(mSpeedInfoMapData).setValue(mViewModel.mSpeedInfoMap);
- assertThat(mViewModel.mSpeedInfoMap.get(SPEED_6GHZ).mIsEnabled).isFalse();
+ WifiHotspotSpeedViewModel.SpeedInfo speedInfo = mViewModel.mSpeedInfoMap.get(SPEED_6GHZ);
+ assertThat(speedInfo.mIsEnabled).isFalse();
+ assertThat(speedInfo.mSummary).isEqualTo(mContext.getString(RES_SUMMARY_UNAVAILABLE));
}
@Test
@@ -150,7 +157,9 @@
mViewModel.on5gAvailableChanged(true);
verify(mSpeedInfoMapData).setValue(mViewModel.mSpeedInfoMap);
- assertThat(mViewModel.mSpeedInfoMap.get(SPEED_5GHZ).mIsEnabled).isTrue();
+ WifiHotspotSpeedViewModel.SpeedInfo speedInfo = mViewModel.mSpeedInfoMap.get(SPEED_5GHZ);
+ assertThat(speedInfo.mIsEnabled).isTrue();
+ assertThat(speedInfo.mSummary).isEqualTo(mContext.getString(RES_SPEED_5G_SUMMARY));
}
@Test
@@ -161,7 +170,9 @@
mViewModel.on5gAvailableChanged(false);
verify(mSpeedInfoMapData).setValue(mViewModel.mSpeedInfoMap);
- assertThat(mViewModel.mSpeedInfoMap.get(SPEED_5GHZ).mIsEnabled).isFalse();
+ WifiHotspotSpeedViewModel.SpeedInfo speedInfo = mViewModel.mSpeedInfoMap.get(SPEED_5GHZ);
+ assertThat(speedInfo.mIsEnabled).isFalse();
+ assertThat(speedInfo.mSummary).isEqualTo(mContext.getString(RES_SUMMARY_UNAVAILABLE));
}
@Test
diff --git a/tests/unit/src/com/android/settings/wifi/tether/WifiTetherViewModelTest.java b/tests/unit/src/com/android/settings/wifi/tether/WifiTetherViewModelTest.java
index af1f62b..1c1473f 100644
--- a/tests/unit/src/com/android/settings/wifi/tether/WifiTetherViewModelTest.java
+++ b/tests/unit/src/com/android/settings/wifi/tether/WifiTetherViewModelTest.java
@@ -16,6 +16,9 @@
package com.android.settings.wifi.tether;
+import static com.android.settings.wifi.tether.WifiTetherViewModel.RES_INSTANT_HOTSPOT_SUMMARY_OFF;
+import static com.android.settings.wifi.tether.WifiTetherViewModel.RES_INSTANT_HOTSPOT_SUMMARY_ON;
+
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Mockito.verify;
@@ -23,12 +26,15 @@
import android.app.Application;
import android.net.wifi.SoftApConfiguration;
+import android.net.wifi.sharedconnectivity.app.SharedConnectivitySettingsState;
import androidx.lifecycle.MutableLiveData;
import androidx.test.annotation.UiThreadTest;
+import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.android.settings.testutils.FakeFeatureFactory;
+import com.android.settings.wifi.repository.SharedConnectivityRepository;
import com.android.settings.wifi.repository.WifiHotspotRepository;
import org.junit.Before;
@@ -36,6 +42,7 @@
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
+import org.mockito.Spy;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
@@ -45,8 +52,8 @@
public class WifiTetherViewModelTest {
@Rule
public final MockitoRule mMockitoRule = MockitoJUnit.rule();
- @Mock
- Application mApplication;
+ @Spy
+ Application mApplication = ApplicationProvider.getApplicationContext();
@Mock
Executor mExecutor;
@Mock
@@ -57,6 +64,12 @@
MutableLiveData<Integer> mSpeedType;
@Mock
private MutableLiveData<Boolean> mRestarting;
+ @Mock
+ private SharedConnectivityRepository mSharedConnectivityRepository;
+ @Mock
+ private MutableLiveData<SharedConnectivitySettingsState> mSettingsState;
+ @Mock
+ private MutableLiveData<String> mInstantHotspotSummary;
WifiTetherViewModel mViewModel;
@@ -70,8 +83,18 @@
when(mWifiHotspotRepository.getSecurityType()).thenReturn(mSecurityType);
when(mWifiHotspotRepository.getSpeedType()).thenReturn(mSpeedType);
when(mWifiHotspotRepository.getRestarting()).thenReturn(mRestarting);
+ when(featureFactory.getWifiFeatureProvider().getSharedConnectivityRepository())
+ .thenReturn(mSharedConnectivityRepository);
+ when(mSharedConnectivityRepository.isServiceAvailable()).thenReturn(true);
+ when(mSharedConnectivityRepository.getSettingsState()).thenReturn(mSettingsState);
mViewModel = new WifiTetherViewModel(mApplication);
+ mViewModel.mInstantHotspotSummary = mInstantHotspotSummary;
+ }
+
+ @Test
+ public void constructor_observeData() {
+ verify(mSettingsState).observeForever(mViewModel.mInstantHotspotStateObserver);
}
@Test
@@ -83,6 +106,7 @@
verify(mSecurityType).removeObserver(mViewModel.mSecurityTypeObserver);
verify(mSpeedType).removeObserver(mViewModel.mSpeedTypeObserver);
+ verify(mSettingsState).removeObserver(mViewModel.mInstantHotspotStateObserver);
}
@Test
@@ -141,4 +165,59 @@
public void getRestarting_shouldNotReturnNull() {
assertThat(mViewModel.getRestarting()).isNotNull();
}
+
+ @Test
+ public void isInstantHotspotFeatureAvailable_serviceAvailable_returnTrue() {
+ when(mSharedConnectivityRepository.isServiceAvailable()).thenReturn(true);
+
+ assertThat(mViewModel.isInstantHotspotFeatureAvailable()).isTrue();
+ }
+
+ @Test
+ public void isInstantHotspotFeatureAvailable_serviceNotAvailable_returnFalse() {
+ when(mSharedConnectivityRepository.isServiceAvailable()).thenReturn(false);
+
+ assertThat(mViewModel.isInstantHotspotFeatureAvailable()).isFalse();
+ }
+
+ @Test
+ public void getInstantHotspotSummary_isNotNull() {
+ assertThat(mViewModel.getInstantHotspotSummary()).isNotNull();
+ }
+
+ @Test
+ public void onInstantHotspotStateChanged_stageNull_summarySetValueNull() {
+ mViewModel.onInstantHotspotStateChanged(null);
+
+ verify(mInstantHotspotSummary).setValue(null);
+ }
+
+ @Test
+ public void onInstantHotspotStateChanged_stateEnabled_summarySetValueOn() {
+ SharedConnectivitySettingsState state = new SharedConnectivitySettingsState.Builder()
+ .setInstantTetherEnabled(true).build();
+
+ mViewModel.onInstantHotspotStateChanged(state);
+
+ verify(mInstantHotspotSummary)
+ .setValue(mApplication.getString(RES_INSTANT_HOTSPOT_SUMMARY_ON));
+ }
+
+ @Test
+ public void onInstantHotspotStateChanged_stateNotEnabled_recordVisibleSummaryOff() {
+ SharedConnectivitySettingsState state = new SharedConnectivitySettingsState.Builder()
+ .setInstantTetherEnabled(false).build();
+
+ mViewModel.onInstantHotspotStateChanged(state);
+
+ verify(mInstantHotspotSummary)
+ .setValue(mApplication.getString(RES_INSTANT_HOTSPOT_SUMMARY_OFF));
+ }
+
+ @Test
+ public void launchInstantHotspotSettings_launchSettingsByRepository() {
+ mViewModel.launchInstantHotspotSettings();
+
+ verify(mSharedConnectivityRepository).launchSettings();
+ }
}