[automerger skipped] RESTRICT AUTOMERGE: Catch exceptions from setLockCredential() am: 5f07aba150 -s ours am: 733d83d27b -s ours am: aa93927b87 -s ours am: 80ef1e0ff5 -s ours am: 7f97ac5ea7 -s ours am: 4643015447 -s ours am: f050fc11ca -s ours am: a78f35b23d -s ours am: 1679e7f685 -s ours am: 4471257a0b -s ours am: 6cc1240b9d -s ours
am skip reason: subject contains skip directive
Original change: https://googleplex-android-review.googlesource.com/c/platform/packages/apps/Settings/+/24298399
Change-Id: I06f9cd874393b2bb275db1f533bbcfa7fd86f643
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
diff --git a/Android.bp b/Android.bp
index 4d07913..71e0542 100644
--- a/Android.bp
+++ b/Android.bp
@@ -69,6 +69,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",
@@ -82,6 +83,7 @@
"net-utils-framework-common",
"app-usage-event-protos-lite",
"battery-event-protos-lite",
+ "power-anomaly-event-protos-lite",
"settings-contextual-card-protos-lite",
"settings-log-bridge-protos-lite",
"settings-telephony-protos-lite",
@@ -149,14 +151,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 df4ad39..80b481f 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -1577,6 +1577,19 @@
android:value="@string/menu_key_apps"/>
</activity-alias>
+ <activity android:name="Settings$UserAspectRatioAppListActivity"
+ android:exported="true"
+ android:label="@string/aspect_ratio_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$ManageDomainUrlsActivity"
android:exported="true"
@@ -2395,6 +2408,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>
@@ -4837,7 +4852,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">
@@ -4902,6 +4917,20 @@
<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/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_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_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_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_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..f1449f9
--- /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="?android:attr/colorAccent"
+ 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_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/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/battery_tips_card.xml b/res/layout/battery_tips_card.xml
new file mode 100644
index 0000000..d2edb51
--- /dev/null
+++ b/res/layout/battery_tips_card.xml
@@ -0,0 +1,125 @@
+<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"
+ android:orientation="vertical"
+ android:padding="24dp">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+
+ <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" />
+
+ <Space
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:layout_weight="1" />
+
+ <ImageButton
+ android:id="@+id/dismiss_button"
+ style="@style/Banner.Dismiss.SettingsLib"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical|end"
+ android:layout_marginEnd="0dp"
+ android:src="@drawable/ic_battery_tips_close_icon" />
+ </LinearLayout>
+
+ <TextView
+ android:id="@+id/title"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="8dp"
+ android:maxLines="2"
+ android:textAlignment="viewStart"
+ android:textAppearance="?android:attr/textAppearanceLarge"
+ android:textColor="?android:attr/textColorPrimary" />
+
+ <TextView
+ android:id="@+id/summary"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="8dp"
+ android:gravity="start"
+ android:maxLines="10"
+ android:textAlignment="viewStart"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:textColor="?android:attr/textColorSecondary" />
+
+ <com.google.android.material.button.MaterialButton
+ android:id="@+id/action_button"
+ style="@style/Widget.Material3.Button.OutlinedButton"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="end"
+ android:layout_marginTop="8dp"
+ android:text="@string/battery_tips_card_action_button"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:textColor="?android:attr/textColorPrimary"
+ android:textStyle="bold"
+ app:strokeColor="?android:attr/colorAccent"
+ app:strokeWidth="1dp" />
+ </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/values/config.xml b/res/values/config.xml
index 5ae0220..687fa15 100755
--- a/res/values/config.xml
+++ b/res/values/config.xml
@@ -548,6 +548,9 @@
<!-- Whether to show Smooth Display feature in Settings Options -->
<bool name="config_show_smooth_display">false</bool>
+ <!-- Whether to show Stay awake on fold feature in Settings Options -->
+ <bool name="config_stay_awake_on_fold">false</bool>
+
<!-- Whether to show emergency settings in top-level Settings -->
<bool name="config_show_emergency_settings">true</bool>
@@ -608,6 +611,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..fd582de 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>
@@ -364,6 +373,10 @@
<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>
+
<!-- Dimensions for Dream settings cards -->
<dimen name="dream_item_min_column_width">174dp</dimen>
<dimen name="dream_item_corner_radius">28dp</dimen>
@@ -397,6 +410,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..efd1791 100644
--- a/res/values/ids.xml
+++ b/res/values/ids.xml
@@ -40,4 +40,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 1ad45d5..040a91e 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -258,8 +258,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 +413,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] -->
@@ -863,7 +865,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] -->
@@ -1215,14 +1217,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>
@@ -1856,7 +1852,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] -->
@@ -2361,6 +2357,10 @@
<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 name to enable staying awake on fold [CHAR LIMIT=30] -->
+ <string name="stay_awake_on_fold_title">Stay unlocked on fold</string>
+ <!-- Display settings screen, setting summary to enable staying awake on fold [CHAR LIMIT=NONE] -->
+ <string name="stay_awake_on_fold_summary">Keep front display unlocked when folded until screen timeout</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] -->
@@ -2658,6 +2658,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>
@@ -2729,6 +2731,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>
@@ -3012,8 +3024,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] -->
@@ -3413,16 +3423,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>
@@ -3483,6 +3493,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] -->
@@ -5511,6 +5533,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>
@@ -6405,7 +6429,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>
@@ -7008,6 +7032,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_stay_awake_on_lock">
+ 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>
@@ -7022,6 +7049,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>
@@ -9622,6 +9650,24 @@
<!-- 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=NONE] -->
+ <string name="battery_tips_card_action_button" translatable="false">Optimize</string>
+
+ <!-- Feedback card message in battery tips card [CHAR LIMIT=NONE] -->
+ <string name="battery_tips_card_feedback_info" translatable="false">Is this message helpful?</string>
+
+ <!-- Title of battery tips: adaptive brightness [CHAR LIMIT=NONE] -->
+ <string name="battery_tips_adaptive_brightness_title" translatable="false">Turn on adaptive brightness to extend battery life</string>
+
+ <!-- Summary of battery tips: adaptive brightness [CHAR LIMIT=NONE] -->
+ <string name="battery_tips_adaptive_brightness_summary" translatable="false">It will help reduce your daily battery drain by 10%</string>
+
+ <!-- Title of battery tips: reduce screen timeout [CHAR LIMIT=NONE] -->
+ <string name="battery_tips_screen_timeout_title" translatable="false">Reduce screen timeout to extend battery life</string>
+
+ <!-- Summary of battery tips: reduce screen timeout [CHAR LIMIT=NONE] -->
+ <string name="battery_tips_screen_timeout_summary" translatable="false">It will help reduce your daily battery drain by 10%</string>
+
<!-- Filter title for battery unrestricted[CHAR_LIMIT=50]-->
<string name="filter_battery_unrestricted_title">Unrestricted</string>
@@ -9705,12 +9751,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>
@@ -10511,7 +10551,7 @@
<!-- Debugging developer settings: enable angle as system driver? [CHAR LIMIT=50] -->
<string name="enable_angle_as_system_driver">Enable ANGLE</string>
<!-- Debugging developer settings: enable angle as system driver summary [CHAR LIMIT=NONE] -->
- <string name="enable_angle_as_system_driver_summary">Enable ANGLE as system OpenGL ES driver</string>
+ <string name="enable_angle_as_system_driver_summary">Enable ANGLE as default OpenGL ES driver. Enabling it on incompatible devices may break some applications.</string>
<!--Dialog body text used to explain a reboot is required after changing ANGLE as system GLES driver setting-->
<string name="reboot_dialog_enable_angle_as_system_driver">A reboot is required to change the system OpenGL ES driver</string>
@@ -10527,8 +10567,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>
@@ -11987,7 +12025,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 -->
@@ -12061,6 +12099,33 @@
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">Choose an 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 suggested apps filter label -->
+ <string name="user_aspect_ratio_suggested_apps_label">Suggested apps</string>
+ <!-- [CHAR LIMIT=NONE] Filter label for apps that have user aspect ratio override applied -->
+ <string name="user_aspect_ratio_overridden_apps_label">Apps you have overridden</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] 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.</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..651ed9b 100644
--- a/res/xml/apps.xml
+++ b/res/xml/apps.xml
@@ -80,6 +80,18 @@
android:order="10"/>
<Preference
+ android:key="aspect_ratio_apps"
+ android:title="@string/aspect_ratio_title"
+ android:summary="@string/summary_placeholder"
+ android:order="14"
+ 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>
+
+ <Preference
android:key="hibernated_apps"
android:title="@string/unused_apps"
android:summary="@string/summary_placeholder"
@@ -105,7 +117,6 @@
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"/>
</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/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..f94ba70 100644
--- a/res/xml/display_settings.xml
+++ b/res/xml/display_settings.xml
@@ -48,6 +48,13 @@
settings:keywords="@string/keywords_ambient_display_screen"
settings:controller="com.android.settings.security.screenlock.LockScreenPreferenceController"/>
+ <SwitchPreference
+ android:key="stay_awake_on_fold"
+ android:title="@string/stay_awake_on_fold_title"
+ android:summary="@string/stay_awake_on_fold_summary"
+ settings:keywords="@string/keywords_stay_awake_on_lock"
+ settings:controller="com.android.settings.display.StayAwakeOnFoldPreferenceController"/>
+
<com.android.settingslib.RestrictedPreference
android:key="screen_timeout"
android:title="@string/screen_timeout"
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/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/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..fc921dd
--- /dev/null
+++ b/res/xml/user_aspect_ratio_details.xml
@@ -0,0 +1,59 @@
+<?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/aspect_ratio_title">
+
+ <com.android.settingslib.widget.ActionButtonsPreference
+ android:key="header_view" />
+
+ <com.android.settingslib.widget.SelectorWithWidgetPreference
+ android:key="app_default_pref"
+ android:title="@string/user_aspect_ratio_app_default"/>
+
+ <com.android.settingslib.widget.SelectorWithWidgetPreference
+ android:key="fullscreen_pref"
+ android:title="@string/user_aspect_ratio_fullscreen"/>
+
+ <com.android.settingslib.widget.SelectorWithWidgetPreference
+ android:key="half_screen_pref"
+ android:title="@string/user_aspect_ratio_half_screen"/>
+
+ <com.android.settingslib.widget.SelectorWithWidgetPreference
+ android:key="display_size_pref"
+ android:title="@string/user_aspect_ratio_device_size"/>
+
+ <com.android.settingslib.widget.SelectorWithWidgetPreference
+ android:key="16_9_pref"
+ android:title="@string/user_aspect_ratio_16_9"/>
+
+ <com.android.settingslib.widget.SelectorWithWidgetPreference
+ android:key="4_3_pref"
+ android:title="@string/user_aspect_ratio_4_3"/>
+
+ <com.android.settingslib.widget.SelectorWithWidgetPreference
+ android:key="3_2_pref"
+ android:title="@string/user_aspect_ratio_3_2"/>
+
+ <com.android.settingslib.widget.FooterPreference
+ android:title="@string/app_aspect_ratio_footer"
+ android:selectable="false"
+ settings:searchable="false"/>
+
+</PreferenceScreen>
\ No newline at end of file
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/src/com/android/settings/Settings.java b/src/com/android/settings/Settings.java
index a67aeaa..a1a7cda 100644
--- a/src/com/android/settings/Settings.java
+++ b/src/com/android/settings/Settings.java
@@ -56,6 +56,7 @@
/** Container for {@link FaceSettings} to use with a pre-defined task affinity. */
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 {
@@ -360,6 +361,8 @@
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 */ }
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 f9f4cdf..dda5b24 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;
@@ -708,9 +711,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 +737,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 +785,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());
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/AppDashboardFragment.java b/src/com/android/settings/applications/AppDashboardFragment.java
index 7e203b0..11f8405 100644
--- a/src/com/android/settings/applications/AppDashboardFragment.java
+++ b/src/com/android/settings/applications/AppDashboardFragment.java
@@ -66,7 +66,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);
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/UserAspectRatioAppsPreferenceController.java b/src/com/android/settings/applications/appcompat/UserAspectRatioAppsPreferenceController.java
new file mode 100644
index 0000000..ff68fb0
--- /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, 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..f8406f9
--- /dev/null
+++ b/src/com/android/settings/applications/appcompat/UserAspectRatioDetails.java
@@ -0,0 +1,224 @@
+/*
+ * 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.Bundle;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.appcompat.app.AlertDialog;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.settings.R;
+import com.android.settings.applications.AppInfoWithHeader;
+import com.android.settingslib.widget.ActionButtonsPreference;
+import com.android.settingslib.widget.SelectorWithWidgetPreference;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * App specific activity to show aspect ratio overrides
+ */
+public class UserAspectRatioDetails extends AppInfoWithHeader implements
+ SelectorWithWidgetPreference.OnClickListener {
+ private static final String TAG = UserAspectRatioDetails.class.getSimpleName();
+
+ 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<SelectorWithWidgetPreference> 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 SelectorWithWidgetPreference 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;
+ }
+ }
+
+ private void initPreferences() {
+ addPreferencesFromResource(R.xml.user_aspect_ratio_details);
+
+ ((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 SelectorWithWidgetPreference pref = findPreference(key);
+ if (pref == null) {
+ return;
+ }
+ if (!mUserAspectRatioManager.containsAspectRatioOption(aspectRatio)) {
+ pref.setVisible(false);
+ return;
+ }
+ pref.setTitle(mUserAspectRatioManager.getUserMinAspectRatioEntry(aspectRatio));
+ pref.setOnClickListener(this);
+ mAspectRatioPreferences.add(pref);
+ }
+
+ private void updateAllPreferences(@NonNull String selectedKey) {
+ for (SelectorWithWidgetPreference 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..c132fd0
--- /dev/null
+++ b/src/com/android/settings/applications/appcompat/UserAspectRatioManager.java
@@ -0,0 +1,224 @@
+/*
+ * 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.app.AppGlobals;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.IPackageManager;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+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.google.common.annotations.VisibleForTesting;
+
+import java.util.List;
+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 = false;
+ @VisibleForTesting
+ static final String KEY_ENABLE_USER_ASPECT_RATIO_SETTINGS =
+ "enable_app_compat_user_aspect_ratio_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 List<ResolveInfo> mInfoHasLauncherEntryList;
+ private final Map<Integer, String> mUserAspectRatioMap;
+
+ public UserAspectRatioManager(@NonNull Context context) {
+ mContext = context;
+ mIPm = AppGlobals.getPackageManager();
+ mInfoHasLauncherEntryList = context.getPackageManager().queryIntentActivities(
+ UserAspectRatioManager.LAUNCHER_ENTRY_INTENT, PackageManager.GET_META_DATA);
+ 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 containsAspectRatioOption(aspectRatio)
+ ? aspectRatio : PackageManager.USER_MIN_ASPECT_RATIO_UNSET;
+ }
+
+ /**
+ * @return corresponding string for {@link PackageManager.UserMinAspectRatio} value
+ */
+ @NonNull
+ public String getUserMinAspectRatioEntry(@PackageManager.UserMinAspectRatio int aspectRatio) {
+ if (!containsAspectRatioOption(aspectRatio)) {
+ return mUserAspectRatioMap.get(PackageManager.USER_MIN_ASPECT_RATIO_UNSET);
+ }
+ return mUserAspectRatioMap.get(aspectRatio);
+ }
+
+ /**
+ * @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);
+ }
+
+ /**
+ * Whether user aspect ratio option is specified in
+ * {@link R.array.config_userAspectRatioOverrideValues}
+ * and is enabled by device config
+ */
+ public boolean containsAspectRatioOption(@PackageManager.UserMinAspectRatio int option) {
+ if (option == PackageManager.USER_MIN_ASPECT_RATIO_FULLSCREEN
+ && !isFullscreenOptionEnabled()) {
+ 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 hasLauncherEntry = mInfoHasLauncherEntryList.stream()
+ .anyMatch(info -> info.activityInfo.packageName.equals(app.packageName));
+ return hasLauncherEntry;
+ }
+
+ /**
+ * Whether fullscreen option in per-app user aspect ratio settings is enabled
+ */
+ @VisibleForTesting
+ boolean isFullscreenOptionEnabled() {
+ final boolean isBuildTimeFlagEnabled = mContext.getResources().getBoolean(
+ com.android.internal.R.bool.config_appCompatUserAppAspectRatioFullscreenIsEnabled);
+ return isBuildTimeFlagEnabled && getValueFromDeviceConfig(
+ KEY_ENABLE_USER_ASPECT_RATIO_FULLSCREEN,
+ DEFAULT_VALUE_ENABLE_USER_ASPECT_RATIO_FULLSCREEN);
+ }
+
+ 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);
+ 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:
+ 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 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);
+ }
+ }
+
+ @VisibleForTesting
+ void addInfoHasLauncherEntry(@NonNull ResolveInfo infoHasLauncherEntry) {
+ mInfoHasLauncherEntryList.add(infoHasLauncherEntry);
+ }
+}
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/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/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/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/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 dbdb024..a62bd67 100644
--- a/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollEnrolling.java
+++ b/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollEnrolling.java
@@ -1101,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: {
diff --git a/src/com/android/settings/biometrics/fingerprint/FingerprintSettings.java b/src/com/android/settings/biometrics/fingerprint/FingerprintSettings.java
index e47e9a8..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";
@@ -534,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);
@@ -545,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/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..a08b3db
--- /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().findViewById(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/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/BluetoothDetailsProfilesController.java b/src/com/android/settings/bluetooth/BluetoothDetailsProfilesController.java
index 3472e39..1fd09a3 100644
--- a/src/com/android/settings/bluetooth/BluetoothDetailsProfilesController.java
+++ b/src/com/android/settings/bluetooth/BluetoothDetailsProfilesController.java
@@ -69,7 +69,7 @@
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_DEVICE_DETAIL_DEFAULT_VALUE = false;
+ private static final boolean LE_AUDIO_DEVICE_DETAIL_DEFAULT_VALUE = true;
private LocalBluetoothManager mManager;
private LocalBluetoothProfileManager mProfileManager;
@@ -89,7 +89,7 @@
mManager = manager;
mProfileManager = mManager.getProfileManager();
mCachedDevice = device;
- mAllOfCachedDevices = Utils.getAllOfCachedBluetoothDevices(mContext, mCachedDevice);
+ mAllOfCachedDevices = Utils.getAllOfCachedBluetoothDevices(mManager, mCachedDevice);
lifecycle.addObserver(this);
}
@@ -324,11 +324,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)) {
@@ -354,12 +359,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);
+ }
}
}
@@ -376,6 +385,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);
+ }
}
}
@@ -392,6 +407,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);
+ }
}
}
@@ -460,7 +481,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/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 05bc179..f9d083d 100644
--- a/src/com/android/settings/bluetooth/BluetoothFindBroadcastsFragment.java
+++ b/src/com/android/settings/bluetooth/BluetoothFindBroadcastsFragment.java
@@ -125,6 +125,10 @@
Log.w(TAG, "onSourceAdded: mSelectedPreference == null!");
return;
}
+ if (mLeBroadcastAssistant != null
+ && mLeBroadcastAssistant.isSearchInProgress()) {
+ mLeBroadcastAssistant.stopSearchingForSources();
+ }
getActivity().runOnUiThread(() -> updateListCategoryFromBroadcastMetadata(
mSelectedPreference.getBluetoothLeBroadcastMetadata(), true));
}
@@ -232,6 +236,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/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/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..0a0e208 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,8 @@
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.bluetooth.BluetoothUtils;
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
import com.android.settingslib.core.AbstractPreferenceController;
@@ -45,6 +52,7 @@
import com.android.settingslib.core.lifecycle.LifecycleObserver;
import com.android.settingslib.core.lifecycle.events.OnResume;
+import java.util.ArrayList;
import java.util.List;
/**
@@ -73,6 +81,9 @@
@VisibleForTesting
PreferenceCategory mPreferencesContainer;
+ @VisibleForTesting
+ Dialog mDialog;
+
public StylusDevicesController(Context context, InputDevice inputDevice,
CachedBluetoothDevice cachedBluetoothDevice, Lifecycle lifecycle) {
super(context);
@@ -100,8 +111,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,11 +124,17 @@
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;
}
@@ -155,7 +172,13 @@
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(),
@@ -229,6 +252,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}.
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..43337c8
--- /dev/null
+++ b/src/com/android/settings/connecteddevice/stylus/StylusFeatureProvider.java
@@ -0,0 +1,47 @@
+/*
+ * 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
+ */
+ @Nullable
+ List<Preference> getUsbFirmwareUpdatePreferences(Context context);
+}
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..dba569b
--- /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) {
+ 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..4a4dfa2
--- /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);
+
+ 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..0166250
--- /dev/null
+++ b/src/com/android/settings/connecteddevice/stylus/UsbStylusBroadcastReceiver.java
@@ -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.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);
+ 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/gateway/SettingsGateway.java b/src/com/android/settings/core/gateway/SettingsGateway.java
index 149d1f4..3100706 100644
--- a/src/com/android/settings/core/gateway/SettingsGateway.java
+++ b/src/com/android/settings/core/gateway/SettingsGateway.java
@@ -72,6 +72,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 +95,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;
@@ -265,6 +267,7 @@
AssistGestureSettings.class.getName(),
FaceSettings.class.getName(),
FingerprintSettings.FingerprintSettingsFragment.class.getName(),
+ FingerprintSettingsV2Fragment.class.getName(),
CombinedBiometricSettings.class.getName(),
CombinedBiometricProfileSettings.class.getName(),
SwipeToNotificationSettings.class.getName(),
@@ -371,7 +374,8 @@
NfcAndPaymentFragment.class.getName(),
ColorAndMotionFragment.class.getName(),
LongBackgroundTasksDetails.class.getName(),
- RegionalPreferencesEntriesFragment.class.getName()
+ RegionalPreferencesEntriesFragment.class.getName(),
+ BatteryInfoFragment.class.getName()
};
public static final String[] SETTINGS_FOR_RESTRICTED = {
diff --git a/src/com/android/settings/dashboard/DashboardFragment.java b/src/com/android/settings/dashboard/DashboardFragment.java
index f8a5d76..d4acfa1 100644
--- a/src/com/android/settings/dashboard/DashboardFragment.java
+++ b/src/com/android/settings/dashboard/DashboardFragment.java
@@ -25,11 +25,14 @@
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;
@@ -170,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 =
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 9545728..298ced0 100644
--- a/src/com/android/settings/development/BluetoothLeAudioDeviceDetailsPreferenceController.java
+++ b/src/com/android/settings/development/BluetoothLeAudioDeviceDetailsPreferenceController.java
@@ -40,7 +40,7 @@
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_DEVICE_DETAIL_DEFAULT_VALUE = false;
+ private static final boolean LE_AUDIO_DEVICE_DETAIL_DEFAULT_VALUE = true;
static int sLeAudioSupportedStateCache = BluetoothStatusCodes.ERROR_UNKNOWN;
@VisibleForTesting
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/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/display/StayAwakeOnFoldPreferenceController.java b/src/com/android/settings/display/StayAwakeOnFoldPreferenceController.java
new file mode 100644
index 0000000..9df48f3
--- /dev/null
+++ b/src/com/android/settings/display/StayAwakeOnFoldPreferenceController.java
@@ -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.display;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.provider.Settings;
+
+import com.android.settings.R;
+import com.android.settings.core.TogglePreferenceController;
+
+/**
+ * A preference controller for the "Stay unlocked on fold" setting.
+ *
+ * This preference controller allows users to control whether or not the device
+ * stays awake when it is folded. When this setting is enabled, the device will
+ * stay awake even if the device is folded.
+ *
+ * @link android.provider.Settings.System#STAY_AWAKE_ON_FOLD
+ */
+public class StayAwakeOnFoldPreferenceController extends TogglePreferenceController {
+
+ private final Resources mResources;
+
+ public StayAwakeOnFoldPreferenceController(Context context, String key) {
+ this(context, key, context.getResources());
+ }
+
+ public StayAwakeOnFoldPreferenceController(Context context, String key, Resources resources) {
+ super(context, key);
+ mResources = resources;
+ }
+
+ @Override
+ public int getAvailabilityStatus() {
+ return mResources.getBoolean(R.bool.config_stay_awake_on_fold) ? AVAILABLE
+ : UNSUPPORTED_ON_DEVICE;
+ }
+
+ @Override
+ public boolean isChecked() {
+ return Settings.System.getInt(
+ mContext.getContentResolver(),
+ Settings.System.STAY_AWAKE_ON_FOLD,
+ 0) == 1;
+ }
+
+ @Override
+ public boolean setChecked(boolean isChecked) {
+ final int stayUnlockedOnFold = isChecked ? 1 : 0;
+
+ return Settings.System.putInt(mContext.getContentResolver(),
+ Settings.System.STAY_AWAKE_ON_FOLD, stayUnlockedOnFold);
+ }
+
+ @Override
+ public int getSliceHighlightMenuRes() {
+ return R.string.menu_key_display;
+ }
+
+}
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..41ead68 100644
--- a/src/com/android/settings/fuelgauge/AdvancedPowerUsageDetail.java
+++ b/src/com/android/settings/fuelgauge/AdvancedPowerUsageDetail.java
@@ -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..258ded1 100644
--- a/src/com/android/settings/fuelgauge/PowerUsageFeatureProvider.java
+++ b/src/com/android/settings/fuelgauge/PowerUsageFeatureProvider.java
@@ -18,6 +18,7 @@
import android.content.Context;
import android.content.Intent;
+import android.os.Bundle;
import android.util.ArrayMap;
import android.util.SparseIntArray;
@@ -37,6 +38,16 @@
boolean isBatteryUsageEnabled();
/**
+ * Check whether the battery tips card is enabled in the battery usage page
+ */
+ boolean isBatteryTipsEnabled();
+
+ /**
+ * Check whether the feedback card is enabled in the battery tips card
+ */
+ boolean isBatteryTipsFeedbackEnabled();
+
+ /**
* Returns a threshold (in milliseconds) for the minimal screen on time in battery usage list
*/
double getBatteryUsageListScreenOnTimeThresholdInMs();
@@ -129,6 +140,16 @@
boolean delayHourlyJobWhenBooting();
/**
+ * Insert settings configuration data for anomaly detection
+ */
+ void insertSettingsData(Context context, double displayDrain);
+
+ /**
+ * Returns {@link Bundle} for settings anomaly detection result
+ */
+ Bundle 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..9b5bb5e 100644
--- a/src/com/android/settings/fuelgauge/PowerUsageFeatureProviderImpl.java
+++ b/src/com/android/settings/fuelgauge/PowerUsageFeatureProviderImpl.java
@@ -21,6 +21,7 @@
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
+import android.os.Bundle;
import android.os.Process;
import android.util.ArrayMap;
import android.util.ArraySet;
@@ -75,6 +76,16 @@
}
@Override
+ public boolean isBatteryTipsEnabled() {
+ return false;
+ }
+
+ @Override
+ public boolean isBatteryTipsFeedbackEnabled() {
+ return true;
+ }
+
+ @Override
public double getBatteryUsageListScreenOnTimeThresholdInMs() {
return 0;
}
@@ -161,6 +172,14 @@
}
@Override
+ public void insertSettingsData(Context context, double displayDrain) {}
+
+ @Override
+ public Bundle 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/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/BatteryChartPreferenceController.java b/src/com/android/settings/fuelgauge/batteryusage/BatteryChartPreferenceController.java
index 17d9c8a..b7e1885 100644
--- a/src/com/android/settings/fuelgauge/batteryusage/BatteryChartPreferenceController.java
+++ b/src/com/android/settings/fuelgauge/batteryusage/BatteryChartPreferenceController.java
@@ -98,6 +98,20 @@
void onScreenOnTimeUpdated(Long screenOnTime, String slotTimestamp);
}
+ /**
+ * A callback listener for the battery tips card is updated.
+ * This happens when battery tips card is ready.
+ */
+ public interface OnBatteryTipsUpdatedListener {
+ /**
+ * The callback function for the battery tips card is updated.
+ * @param title the title of the battery tip card
+ * @param summary the summary of the battery tip card
+ */
+ void onBatteryTipsUpdated(String title, String summary);
+ }
+
+
@VisibleForTesting
Context mPrefContext;
@VisibleForTesting
@@ -119,6 +133,7 @@
private List<BatteryChartViewModel> mHourlyViewModels;
private OnBatteryUsageUpdatedListener mOnBatteryUsageUpdatedListener;
private OnScreenOnTimeUpdatedListener mOnScreenOnTimeUpdatedListener;
+ private OnBatteryTipsUpdatedListener mOnBatteryTipsUpdatedListener;
private final SettingsActivity mActivity;
private final MetricsFeatureProvider mMetricsFeatureProvider;
@@ -209,6 +224,10 @@
mOnScreenOnTimeUpdatedListener = listener;
}
+ void setOnBatteryTipsUpdatedListener(OnBatteryTipsUpdatedListener listener) {
+ mOnBatteryTipsUpdatedListener = listener;
+ }
+
void setBatteryHistoryMap(
final Map<Long, Map<String, BatteryHistEntry>> batteryHistoryMap) {
Log.d(TAG, "setBatteryHistoryMap() " + (batteryHistoryMap == null ? "null"
@@ -344,6 +363,10 @@
}
mOnBatteryUsageUpdatedListener.onBatteryUsageUpdated(
slotUsageData, getSlotInformation(), isBatteryUsageMapNullOrEmpty());
+
+ if (mOnBatteryTipsUpdatedListener != null) {
+ mOnBatteryTipsUpdatedListener.onBatteryTipsUpdated(null, null);
+ }
}
return true;
}
diff --git a/src/com/android/settings/fuelgauge/batteryusage/BatteryChartView.java b/src/com/android/settings/fuelgauge/batteryusage/BatteryChartView.java
index 445a5d1..891e5e0 100644
--- a/src/com/android/settings/fuelgauge/batteryusage/BatteryChartView.java
+++ b/src/com/android/settings/fuelgauge/batteryusage/BatteryChartView.java
@@ -18,6 +18,7 @@
import static com.android.settings.Utils.formatPercentage;
import static com.android.settings.fuelgauge.batteryusage.BatteryChartViewModel.AxisLabelPosition.BETWEEN_TRAPEZOIDS;
+import static java.lang.Math.abs;
import static java.lang.Math.round;
import static java.util.Objects.requireNonNull;
@@ -61,6 +62,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 +75,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;
@@ -158,7 +162,12 @@
}
// Updates the indent configurations.
mIndent.top = mPercentageBounds[0].height();
- mIndent.right = mPercentageBounds[0].width() + mTextPadding;
+ final int textWidth = mPercentageBounds[0].width() + mTextPadding;
+ if (isRTL()) {
+ mIndent.left = textWidth;
+ } else {
+ mIndent.right = textWidth;
+ }
if (mViewModel != null) {
int maxTop = 0;
@@ -333,25 +342,27 @@
}
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 +371,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 +393,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 +402,7 @@
default:
axisLabelDisplayAreas = getAxisLabelDisplayAreas(
/* size= */ mViewModel.size(),
- /* baselineX= */ mDividerWidth * .5f,
+ /* baselineX= */ mIndent.left + mDividerWidth * .5f,
/* offsetX= */ mDividerWidth + unitWidth,
baselineY,
/* shiftFirstAndLast= */ true);
@@ -400,7 +411,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 +425,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 +519,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 +568,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);
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/BatteryTipsCardPreference.java b/src/com/android/settings/fuelgauge/batteryusage/BatteryTipsCardPreference.java
new file mode 100644
index 0000000..661d7c8
--- /dev/null
+++ b/src/com/android/settings/fuelgauge/batteryusage/BatteryTipsCardPreference.java
@@ -0,0 +1,99 @@
+/*
+ * 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.util.AttributeSet;
+import android.view.View;
+import android.widget.ImageButton;
+import android.widget.TextView;
+
+import androidx.preference.Preference;
+import androidx.preference.PreferenceViewHolder;
+
+import com.android.settings.R;
+import com.android.settings.fuelgauge.PowerUsageFeatureProvider;
+import com.android.settings.overlay.FeatureFactory;
+
+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";
+
+ private final PowerUsageFeatureProvider mPowerUsageFeatureProvider;
+
+ private MaterialButton mActionButton;
+ private ImageButton mDismissButton;
+ private ImageButton mThumbUpButton;
+ private ImageButton mThumbDownButton;
+ private CharSequence mTitle;
+ private CharSequence mSummary;
+
+ public BatteryTipsCardPreference(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ setLayoutResource(R.layout.battery_tips_card);
+ setSelectable(false);
+ mPowerUsageFeatureProvider = FeatureFactory.getFactory(context)
+ .getPowerUsageFeatureProvider(context);
+ }
+
+ @Override
+ public void setTitle(CharSequence title) {
+ mTitle = title;
+ notifyChanged();
+ }
+
+ @Override
+ public void setSummary(CharSequence summary) {
+ mSummary = summary;
+ notifyChanged();
+ }
+
+ @Override
+ public void onClick(View view) {
+ // TODO: replace with the settings anomaly obtained from detectSettingsAnomaly();
+ }
+
+ @Override
+ public void onBindViewHolder(PreferenceViewHolder view) {
+ super.onBindViewHolder(view);
+
+ ((TextView) view.findViewById(R.id.title)).setText(mTitle);
+ ((TextView) view.findViewById(R.id.summary)).setText(mSummary);
+
+ mActionButton = (MaterialButton) view.findViewById(R.id.action_button);
+ mActionButton.setOnClickListener(this);
+ mDismissButton = (ImageButton) view.findViewById(R.id.dismiss_button);
+ mDismissButton.setOnClickListener(this);
+
+ if (!mPowerUsageFeatureProvider.isBatteryTipsFeedbackEnabled()) {
+ return;
+ }
+ view.findViewById(R.id.tips_card)
+ .setBackgroundResource(R.drawable.battery_tips_half_rounded_top_bg);
+ view.findViewById(R.id.feedback_card).setVisibility(View.VISIBLE);
+
+ mThumbUpButton = (ImageButton) view.findViewById(R.id.thumb_up);
+ mThumbUpButton.setOnClickListener(this);
+ mThumbDownButton = (ImageButton) view.findViewById(R.id.thumb_down);
+ mThumbDownButton.setOnClickListener(this);
+ }
+}
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..bcedd4f
--- /dev/null
+++ b/src/com/android/settings/fuelgauge/batteryusage/BatteryTipsController.java
@@ -0,0 +1,77 @@
+/*
+ * 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 androidx.preference.PreferenceScreen;
+
+import com.android.settings.core.BasePreferenceController;
+import com.android.settings.fuelgauge.PowerUsageFeatureProvider;
+import com.android.settings.overlay.FeatureFactory;
+
+/** 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 PowerUsageFeatureProvider mPowerUsageFeatureProvider;
+
+ private Context mPrefContext;
+ private BatteryTipsCardPreference mCardPreference;
+
+ public BatteryTipsController(Context context) {
+ super(context, ROOT_PREFERENCE_KEY);
+ mPowerUsageFeatureProvider = FeatureFactory.getFactory(context)
+ .getPowerUsageFeatureProvider(context);
+ }
+
+ @Override
+ public int getAvailabilityStatus() {
+ return AVAILABLE;
+ }
+
+ @Override
+ public void displayPreference(PreferenceScreen screen) {
+ super.displayPreference(screen);
+ mPrefContext = screen.getContext();
+ mCardPreference = screen.findPreference(CARD_PREFERENCE_KEY);
+ }
+
+ /**
+ * Update the card visibility and contents.
+ * @param title a string not extend 2 lines.
+ * @param summary a string not extend 10 lines.
+ */
+ // TODO: replace parameters with SettingsAnomaly Data Proto
+ public void handleBatteryTipsCardUpdated(String title, String summary) {
+ if (!mPowerUsageFeatureProvider.isBatteryTipsEnabled()) {
+ mCardPreference.setVisible(false);
+ return;
+ }
+ if (title == null || summary == null) {
+ mCardPreference.setVisible(false);
+ return;
+ }
+ mCardPreference.setTitle(title);
+ mCardPreference.setSummary(summary);
+ mCardPreference.setVisible(true);
+ }
+
+}
diff --git a/src/com/android/settings/fuelgauge/batteryusage/BatteryUsageDataLoader.java b/src/com/android/settings/fuelgauge/batteryusage/BatteryUsageDataLoader.java
index fb1be3e..ae86095 100644
--- a/src/com/android/settings/fuelgauge/batteryusage/BatteryUsageDataLoader.java
+++ b/src/com/android/settings/fuelgauge/batteryusage/BatteryUsageDataLoader.java
@@ -23,6 +23,9 @@
import androidx.annotation.VisibleForTesting;
+import com.android.settings.fuelgauge.BatteryUsageHistoricalLogEntry.Action;
+import com.android.settings.fuelgauge.batteryusage.bugreport.BatteryUsageLogUtils;
+
import java.util.List;
import java.util.function.Supplier;
@@ -46,6 +49,7 @@
@VisibleForTesting
static void loadUsageData(final Context context, final boolean isFullChargeStart) {
+ BatteryUsageLogUtils.writeLog(context, Action.FETCH_USAGE_DATA, "");
final long start = System.currentTimeMillis();
final BatteryUsageStats batteryUsageStats = DataProcessor.getBatteryUsageStats(context);
final List<BatteryEntry> batteryEntryList =
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..3fc44cc 100644
--- a/src/com/android/settings/fuelgauge/batteryusage/ConvertUtils.java
+++ b/src/com/android/settings/fuelgauge/batteryusage/ConvertUtils.java
@@ -17,7 +17,6 @@
import android.annotation.IntDef;
import android.annotation.Nullable;
-import android.app.usage.IUsageStatsManager;
import android.app.usage.UsageEvents.Event;
import android.app.usage.UsageStatsManager;
import android.content.ContentValues;
@@ -27,7 +26,6 @@
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;
@@ -67,6 +65,12 @@
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() {
}
@@ -181,8 +185,7 @@
/** Converts to {@link AppUsageEvent} from {@link Event} */
@Nullable
public static AppUsageEvent convertToAppUsageEvent(
- Context context, final IUsageStatsManager usageStatsManager, final Event event,
- final long userId) {
+ Context context, final Event event, final long userId) {
final String packageName = event.getPackageName();
if (packageName == null) {
// See b/190609174: Event package names should never be null, but sometimes they are.
@@ -207,7 +210,7 @@
}
final String effectivePackageName =
- getEffectivePackageName(usageStatsManager, packageName, taskRootPackageName);
+ getEffectivePackageName(context, packageName, taskRootPackageName);
try {
final long uid = context
.getPackageManager()
@@ -323,9 +326,8 @@
*/
@VisibleForTesting
static String getEffectivePackageName(
- final IUsageStatsManager usageStatsManager, final String packageName,
- final String taskRootPackageName) {
- int usageSource = getUsageSource(usageStatsManager);
+ Context context, final String packageName, final String taskRootPackageName) {
+ final int usageSource = getUsageSource(context);
switch (usageSource) {
case UsageStatsManager.USAGE_SOURCE_TASK_ROOT_ACTIVITY:
return !TextUtils.isEmpty(taskRootPackageName)
@@ -370,18 +372,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) {
+ if (sUsageSource == EMPTY_USAGE_SOURCE) {
+ sUsageSource = DatabaseUtils.getUsageSource(context);
}
+ return sUsageSource;
}
private static AppUsageEventType getAppUsageEventType(final int eventType) {
diff --git a/src/com/android/settings/fuelgauge/batteryusage/DataProcessManager.java b/src/com/android/settings/fuelgauge/batteryusage/DataProcessManager.java
index 0f67e6a..1c851fd 100644
--- a/src/com/android/settings/fuelgauge/batteryusage/DataProcessManager.java
+++ b/src/com/android/settings/fuelgauge/batteryusage/DataProcessManager.java
@@ -397,8 +397,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() {
diff --git a/src/com/android/settings/fuelgauge/batteryusage/DataProcessor.java b/src/com/android/settings/fuelgauge/batteryusage/DataProcessor.java
index 6914c30..badc359 100644
--- a/src/com/android/settings/fuelgauge/batteryusage/DataProcessor.java
+++ b/src/com/android/settings/fuelgauge/batteryusage/DataProcessor.java
@@ -32,7 +32,6 @@
import android.os.BatteryUsageStatsQuery;
import android.os.Process;
import android.os.RemoteException;
-import android.os.ServiceManager;
import android.os.UidBatteryConsumer;
import android.os.UserBatteryConsumer;
import android.os.UserHandle;
@@ -78,8 +77,6 @@
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";
@@ -111,11 +108,6 @@
@VisibleForTesting
static Set<String> sTestSystemAppsPackageNames;
- @VisibleForTesting
- static IUsageStatsManager sUsageStatsManager =
- IUsageStatsManager.Stub.asInterface(
- ServiceManager.getService(Context.USAGE_STATS_SERVICE));
-
public static final String CURRENT_TIME_BATTERY_HISTORY_PLACEHOLDER =
"CURRENT_TIME_BATTERY_HISTORY_PLACEHOLDER";
@@ -271,7 +263,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 +297,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));
}
}
@@ -346,8 +338,7 @@
break;
}
final AppUsageEvent appUsageEvent =
- ConvertUtils.convertToAppUsageEvent(
- context, sUsageStatsManager, event, userId);
+ ConvertUtils.convertToAppUsageEvent(context, event, userId);
if (appUsageEvent != null) {
numEventsFetched++;
appUsageEventList.add(appUsageEvent);
@@ -661,8 +652,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,7 +693,7 @@
final AppUsageEvent firstEvent = usageEvents.get(0);
final long eventUserId = firstEvent.getUserId();
final String packageName = getEffectivePackageName(
- sUsageStatsManager,
+ context,
firstEvent.getPackageName(),
firstEvent.getTaskRootPackageName());
usageEvents.addAll(deviceEvents);
@@ -975,7 +966,7 @@
final long startTime = DatabaseUtils.getAppUsageStartTimestampOfUser(
context, userID, earliestTimestamp);
return loadAppUsageEventsForUserFromService(
- sUsageStatsManager, startTime, now, userID, callingPackage);
+ DatabaseUtils.sUsageStatsManager, startTime, now, userID, callingPackage);
}
@Nullable
diff --git a/src/com/android/settings/fuelgauge/batteryusage/DatabaseUtils.java b/src/com/android/settings/fuelgauge/batteryusage/DatabaseUtils.java
index 0435e45..ea1f3ed 100644
--- a/src/com/android/settings/fuelgauge/batteryusage/DatabaseUtils.java
+++ b/src/com/android/settings/fuelgauge/batteryusage/DatabaseUtils.java
@@ -15,6 +15,8 @@
*/
package com.android.settings.fuelgauge.batteryusage;
+import android.app.usage.IUsageStatsManager;
+import android.app.usage.UsageStatsManager;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
@@ -28,13 +30,17 @@
import android.os.BatteryUsageStats;
import android.os.Handler;
import android.os.Looper;
+import android.os.RemoteException;
+import android.os.ServiceManager;
import android.os.SystemClock;
import android.os.UserManager;
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;
@@ -61,6 +67,7 @@
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";
/** An authority name of the battery content provider. */
public static final String AUTHORITY = "com.android.settings.battery.usage.provider";
@@ -72,8 +79,6 @@
public static final String BATTERY_STATE_TABLE = "BatteryState";
/** 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 **/
@@ -112,6 +117,11 @@
@VisibleForTesting
static Supplier<Cursor> sFakeSupplier;
+ @VisibleForTesting
+ static IUsageStatsManager sUsageStatsManager =
+ IUsageStatsManager.Stub.asInterface(
+ ServiceManager.getService(Context.USAGE_STATS_SERVICE));
+
private DatabaseUtils() {
}
@@ -395,6 +405,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 +415,8 @@
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);
+ errorMessage = e.toString();
+ Log.e(TAG, "bulkInsert() data into database error:\n" + errorMessage);
}
} else {
// Inserts one fake data into battery provider.
@@ -424,11 +436,16 @@
+ isFullChargeStart);
} catch (Exception e) {
- Log.e(TAG, "insert() data into database error:\n" + e);
+ errorMessage = e.toString();
+ Log.e(TAG, "insert() data into database error:\n" + errorMessage);
}
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) {
@@ -459,6 +476,37 @@
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) {
+ 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 = sUsageStatsManager.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 recordDateTime(Context context, String preferenceKey) {
final SharedPreferences sharedPreferences = getSharedPreferences(context);
if (sharedPreferences != null) {
@@ -555,7 +603,7 @@
private static Map<Long, Map<String, BatteryHistEntry>> loadHistoryMapFromContentProvider(
Context context, Uri batteryStateUri) {
- context = DatabaseUtils.getParentContext(context);
+ context = getParentContext(context);
if (context == null) {
return null;
}
diff --git a/src/com/android/settings/fuelgauge/batteryusage/PeriodicJobManager.java b/src/com/android/settings/fuelgauge/batteryusage/PeriodicJobManager.java
index 3d78c00..8c0e66c 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;
@@ -76,8 +78,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, "triggerTime=" + utcToLocalTime);
+ 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..2bd0466 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";
@@ -39,6 +42,7 @@
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);
diff --git a/src/com/android/settings/fuelgauge/batteryusage/PowerUsageAdvanced.java b/src/com/android/settings/fuelgauge/batteryusage/PowerUsageAdvanced.java
index 7c4478e..9a8680e 100644
--- a/src/com/android/settings/fuelgauge/batteryusage/PowerUsageAdvanced.java
+++ b/src/com/android/settings/fuelgauge/batteryusage/PowerUsageAdvanced.java
@@ -34,6 +34,8 @@
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;
@@ -143,6 +145,16 @@
controllers.add(screenOnTimeController);
controllers.add(batteryUsageBreakdownController);
setBatteryChartPreferenceController();
+
+ final PowerUsageFeatureProvider powerUsageFeatureProvider =
+ FeatureFactory.getFactory(context).getPowerUsageFeatureProvider(context);
+ if (powerUsageFeatureProvider.isBatteryTipsEnabled()) {
+ BatteryTipsController batteryTipsController = new BatteryTipsController(context);
+ mBatteryChartPreferenceController.setOnBatteryTipsUpdatedListener(
+ batteryTipsController::handleBatteryTipsCardUpdated);
+ controllers.add(batteryTipsController);
+ }
+
return controllers;
}
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/protos/Android.bp b/src/com/android/settings/fuelgauge/protos/Android.bp
index 3af2aef..1f3cdd9 100644
--- a/src/com/android/settings/fuelgauge/protos/Android.bp
+++ b/src/com/android/settings/fuelgauge/protos/Android.bp
@@ -30,3 +30,11 @@
},
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/power_anomaly_event.proto b/src/com/android/settings/fuelgauge/protos/power_anomaly_event.proto
new file mode 100644
index 0000000..b4277c4
--- /dev/null
+++ b/src/com/android/settings/fuelgauge/protos/power_anomaly_event.proto
@@ -0,0 +1,37 @@
+syntax = "proto2";
+
+option java_multiple_files = true;
+option java_package = "com.android.settings.fuelgauge.batteryusage";
+option java_outer_classname = "PowerAnomalyEventProto";
+
+message PowerAnomalyEvent {
+ optional int64 timestamp = 1;
+ optional string type = 2; // e.g. settings, apps
+ optional string key = 3; // e.g. brightness, significant_increase
+ optional float score = 4;
+ oneof info {
+ WarningBannerInfo warning_banner_info = 5;
+ WarningItemInfo warning_item_info = 6;
+ }
+}
+
+message WarningBannerInfo {
+ optional string title_string = 1;
+ optional string description_string = 2;
+ optional string main_button_string = 3;
+ optional string main_button_action = 4;
+ optional string cancel_button_string = 5;
+ optional string cancel_button_action = 6;
+}
+
+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 main_button_action = 7;
+ optional string cancel_button_string = 8;
+ optional string cancel_button_action = 9;
+}
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..28ead89 100644
--- a/src/com/android/settings/inputmethod/ModifierKeysPickerDialogFragment.java
+++ b/src/com/android/settings/inputmethod/ModifierKeysPickerDialogFragment.java
@@ -21,7 +21,9 @@
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
+import android.app.settings.SettingsEnums;
import android.content.Context;
+import android.graphics.drawable.Drawable;
import android.hardware.input.InputManager;
import android.os.Bundle;
import android.text.Spannable;
@@ -39,11 +41,14 @@
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 com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
import java.util.ArrayList;
import java.util.Arrays;
@@ -60,6 +65,12 @@
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 MetricsFeatureProvider mMetricsFeatureProvider;
private List<int[]> mRemappableKeyList =
new ArrayList<>(Arrays.asList(
@@ -83,6 +94,9 @@
super.onCreateDialog(savedInstanceState);
mActivity = getActivity();
+ mMetricsFeatureProvider = FeatureFactory.getFactory(mActivity).getMetricsFeatureProvider();
+ 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 +111,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);
@@ -125,6 +143,7 @@
doneButton.setOnClickListener(v -> {
String selectedItem = modifierKeys.get(adapter.getCurrentItem());
Spannable itemSummary;
+ logMetricsForRemapping(selectedItem);
if (selectedItem.equals(mKeyDefaultName)) {
itemSummary = new SpannableString(
mActivity.getString(R.string.modifier_keys_default_summary));
@@ -175,6 +194,28 @@
return modifierKeyDialog;
}
+ private void logMetricsForRemapping(String selectedItem) {
+ if (mKeyDefaultName.equals("Caps lock")) {
+ mMetricsFeatureProvider.action(
+ mActivity, SettingsEnums.ACTION_FROM_CAPS_LOCK_TO, selectedItem);
+ }
+
+ if (mKeyDefaultName.equals("Ctrl")) {
+ mMetricsFeatureProvider.action(
+ mActivity, SettingsEnums.ACTION_FROM_CTRL_TO, selectedItem);
+ }
+
+ if (mKeyDefaultName.equals("Action key")) {
+ mMetricsFeatureProvider.action(
+ mActivity, SettingsEnums.ACTION_FROM_ACTION_KEY_TO, selectedItem);
+ }
+
+ if (mKeyDefaultName.equals("Alt")) {
+ mMetricsFeatureProvider.action(
+ mActivity, SettingsEnums.ACTION_FROM_ALT_TO, selectedItem);
+ }
+ }
+
private void setInitialFocusItem(
List<String> modifierKeys, ModifierKeyAdapter adapter) {
if (modifierKeys.indexOf(mKeyFocus) == -1) {
@@ -226,10 +267,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 +292,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..fea6e65 100644
--- a/src/com/android/settings/inputmethod/ModifierKeysResetDialogFragment.java
+++ b/src/com/android/settings/inputmethod/ModifierKeysResetDialogFragment.java
@@ -21,6 +21,7 @@
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
+import android.app.settings.SettingsEnums;
import android.hardware.input.InputManager;
import android.os.Bundle;
import android.view.LayoutInflater;
@@ -31,13 +32,18 @@
import androidx.fragment.app.DialogFragment;
import com.android.settings.R;
+import com.android.settings.overlay.FeatureFactory;
+import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
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";
private static final String MODIFIER_KEYS_ALT = "modifier_keys_alt";
+ private MetricsFeatureProvider mMetricsFeatureProvider;
+
private String[] mKeys = {
MODIFIER_KEYS_CAPS_LOCK,
MODIFIER_KEYS_CTRL,
@@ -51,6 +57,7 @@
super.onCreateDialog(savedInstanceState);
Activity activity = getActivity();
+ mMetricsFeatureProvider = FeatureFactory.getFactory(activity).getMetricsFeatureProvider();
InputManager inputManager = activity.getSystemService(InputManager.class);
View dialoglayout =
LayoutInflater.from(activity).inflate(R.layout.modifier_key_reset_dialog, null);
@@ -60,6 +67,7 @@
Button restoreButton = dialoglayout.findViewById(R.id.modifier_key_reset_restore_button);
restoreButton.setOnClickListener(v -> {
+ mMetricsFeatureProvider.action(activity, SettingsEnums.ACTION_CLEAR_REMAPPINGS);
inputManager.clearAllModifierKeyRemappings();
dismiss();
activity.recreate();
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..65b1c62 100644
--- a/src/com/android/settings/inputmethod/NewKeyboardLayoutPickerController.java
+++ b/src/com/android/settings/inputmethod/NewKeyboardLayoutPickerController.java
@@ -16,10 +16,12 @@
package com.android.settings.inputmethod;
+import android.app.settings.SettingsEnums;
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;
@@ -27,8 +29,11 @@
import androidx.preference.Preference;
import androidx.preference.PreferenceScreen;
+import com.android.settings.R;
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;
import com.android.settingslib.core.lifecycle.LifecycleObserver;
import com.android.settingslib.core.lifecycle.events.OnStart;
import com.android.settingslib.core.lifecycle.events.OnStop;
@@ -38,38 +43,47 @@
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;
+ private String mFinalSelectedLayout;
private String mLayout;
+ private MetricsFeatureProvider mMetricsFeatureProvider;
public NewKeyboardLayoutPickerController(Context context, String key) {
super(context, key);
mIm = context.getSystemService(InputManager.class);
mInputDeviceId = -1;
mPreferenceMap = new HashMap<>();
+ mMetricsFeatureProvider = FeatureFactory.getFactory(context).getMetricsFeatureProvider();
}
- 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();
+ mFinalSelectedLayout = mLayout;
mKeyboardLayouts = mIm.getKeyboardLayoutListForInputDevice(
- inputDeviceIdentifier, userId, imeInfo, imeSubtype);
+ mInputDeviceIdentifier, mUserId, mInputMethodInfo, mInputMethodSubtype);
+ parent.getActivity().setTitle(mTitle);
}
@Override
@@ -85,6 +99,11 @@
@Override
public void onStop() {
+ if (!mLayout.equals(mFinalSelectedLayout)) {
+ String change = "From:" + mLayout + ", to:" + mFinalSelectedLayout;
+ mMetricsFeatureProvider.action(
+ mContext, SettingsEnums.ACTION_PK_LAYOUT_CHANGED, change);
+ }
mIm.unregisterInputDeviceListener(this);
mInputDeviceId = -1;
}
@@ -115,6 +134,7 @@
}
setLayout(pref);
mPreviousSelection = preference.getKey();
+ mFinalSelectedLayout = pref.getTitle().toString();
return true;
}
@@ -162,4 +182,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..289d7c1 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,13 @@
(SwitchPreference) mKeyboardAssistanceCategory.findPreference(
SHOW_VIRTUAL_KEYBOARD_SWITCH));
+ FeatureFactory featureFactory = FeatureFactory.getFactory(getContext());
+ mMetricsFeatureProvider = featureFactory.getMetricsFeatureProvider();
+ 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 +123,12 @@
}
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 (intentFromWhere != -1) {
+ mMetricsFeatureProvider.action(
+ getContext(), SettingsEnums.ACTION_OPEN_PK_SETTINGS_FROM, intentFromWhere);
+ }
if (inputDeviceIdentifier != null) {
mAutoInputDeviceIdentifier = inputDeviceIdentifier;
}
@@ -244,9 +259,16 @@
});
}
category.addPreference(pref);
+ mMetricsFeatureProvider.action(
+ getContext(),
+ SettingsEnums.ACTION_USE_SPECIFIC_KEYBOARD,
+ hardKeyboardDeviceInfo.mDeviceName);
}
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..f0ee1fd 100644
--- a/src/com/android/settings/inputmethod/TouchGesturesButtonPreferenceController.java
+++ b/src/com/android/settings/inputmethod/TouchGesturesButtonPreferenceController.java
@@ -16,6 +16,7 @@
package com.android.settings.inputmethod;
+import android.app.settings.SettingsEnums;
import android.content.Context;
import android.util.FeatureFlagUtils;
@@ -23,6 +24,8 @@
import androidx.preference.PreferenceScreen;
import com.android.settings.core.BasePreferenceController;
+import com.android.settings.overlay.FeatureFactory;
+import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
import com.android.settingslib.widget.ButtonPreference;
public class TouchGesturesButtonPreferenceController extends BasePreferenceController {
@@ -33,9 +36,11 @@
private static final String GESTURE_DIALOG_TAG = "GESTURE_DIALOG_TAG";
private Fragment mParent;
+ private MetricsFeatureProvider mMetricsFeatureProvider;
public TouchGesturesButtonPreferenceController(Context context, String key) {
super(context, key);
+ mMetricsFeatureProvider = FeatureFactory.getFactory(context).getMetricsFeatureProvider();
}
public void setFragment(Fragment parent) {
@@ -63,12 +68,11 @@
@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() {
+ mMetricsFeatureProvider.action(mContext, SettingsEnums.ACTION_LEARN_TOUCHPAD_GESTURE_CLICK);
TrackpadGestureDialogFragment fragment = new TrackpadGestureDialogFragment();
fragment.setTargetFragment(mParent, 0);
fragment.show(mParent.getActivity().getSupportFragmentManager(), GESTURE_DIALOG_TAG);
diff --git a/src/com/android/settings/inputmethod/TrackpadBottomPreferenceController.java b/src/com/android/settings/inputmethod/TrackpadBottomPreferenceController.java
index 5133d04..1cf1f6f 100644
--- a/src/com/android/settings/inputmethod/TrackpadBottomPreferenceController.java
+++ b/src/com/android/settings/inputmethod/TrackpadBottomPreferenceController.java
@@ -16,16 +16,22 @@
package com.android.settings.inputmethod;
+import android.app.settings.SettingsEnums;
import android.content.Context;
import android.hardware.input.InputSettings;
import com.android.settings.R;
import com.android.settings.core.TogglePreferenceController;
+import com.android.settings.overlay.FeatureFactory;
+import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
public class TrackpadBottomPreferenceController extends TogglePreferenceController {
+ private MetricsFeatureProvider mMetricsFeatureProvider;
+
public TrackpadBottomPreferenceController(Context context, String key) {
super(context, key);
+ mMetricsFeatureProvider = FeatureFactory.getFactory(context).getMetricsFeatureProvider();
}
@Override
@@ -36,6 +42,8 @@
@Override
public boolean setChecked(boolean isChecked) {
InputSettings.setTouchpadRightClickZone(mContext, isChecked);
+ mMetricsFeatureProvider.action(
+ mContext, SettingsEnums.ACTION_GESTURE_BOTTOM_RIGHT_TAP_CHANGED, isChecked);
return true;
}
diff --git a/src/com/android/settings/inputmethod/TrackpadGoBackPreferenceController.java b/src/com/android/settings/inputmethod/TrackpadGoBackPreferenceController.java
index 017689d..11d7cf3 100644
--- a/src/com/android/settings/inputmethod/TrackpadGoBackPreferenceController.java
+++ b/src/com/android/settings/inputmethod/TrackpadGoBackPreferenceController.java
@@ -16,18 +16,24 @@
package com.android.settings.inputmethod;
+import android.app.settings.SettingsEnums;
import android.content.Context;
import android.provider.Settings;
import com.android.settings.R;
import com.android.settings.core.TogglePreferenceController;
+import com.android.settings.overlay.FeatureFactory;
+import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
public class TrackpadGoBackPreferenceController extends TogglePreferenceController {
private static final String SETTING_KEY = Settings.Secure.TRACKPAD_GESTURE_BACK_ENABLED;
+ private MetricsFeatureProvider mMetricsFeatureProvider;
+
public TrackpadGoBackPreferenceController(Context context, String key) {
super(context, key);
+ mMetricsFeatureProvider = FeatureFactory.getFactory(context).getMetricsFeatureProvider();
}
@Override
@@ -38,6 +44,8 @@
@Override
public boolean setChecked(boolean isChecked) {
Settings.Secure.putInt(mContext.getContentResolver(), SETTING_KEY, isChecked ? 1 : 0);
+ mMetricsFeatureProvider.action(
+ mContext, SettingsEnums.ACTION_GESTURE_GO_BACK_CHANGED, isChecked);
return true;
}
diff --git a/src/com/android/settings/inputmethod/TrackpadGoHomePreferenceController.java b/src/com/android/settings/inputmethod/TrackpadGoHomePreferenceController.java
index 18699e3..5027e2f 100644
--- a/src/com/android/settings/inputmethod/TrackpadGoHomePreferenceController.java
+++ b/src/com/android/settings/inputmethod/TrackpadGoHomePreferenceController.java
@@ -16,18 +16,24 @@
package com.android.settings.inputmethod;
+import android.app.settings.SettingsEnums;
import android.content.Context;
import android.provider.Settings;
import com.android.settings.R;
import com.android.settings.core.TogglePreferenceController;
+import com.android.settings.overlay.FeatureFactory;
+import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
public class TrackpadGoHomePreferenceController extends TogglePreferenceController {
private static final String SETTING_KEY = Settings.Secure.TRACKPAD_GESTURE_HOME_ENABLED;
+ private MetricsFeatureProvider mMetricsFeatureProvider;
+
public TrackpadGoHomePreferenceController(Context context, String key) {
super(context, key);
+ mMetricsFeatureProvider = FeatureFactory.getFactory(context).getMetricsFeatureProvider();
}
@Override
@@ -38,6 +44,8 @@
@Override
public boolean setChecked(boolean isChecked) {
Settings.Secure.putInt(mContext.getContentResolver(), SETTING_KEY, isChecked ? 1 : 0);
+ mMetricsFeatureProvider.action(
+ mContext, SettingsEnums.ACTION_GESTURE_GO_HOME_CHANGED, isChecked);
return true;
}
diff --git a/src/com/android/settings/inputmethod/TrackpadNotificationsPreferenceController.java b/src/com/android/settings/inputmethod/TrackpadNotificationsPreferenceController.java
index 21f04a3..0fb28d7 100644
--- a/src/com/android/settings/inputmethod/TrackpadNotificationsPreferenceController.java
+++ b/src/com/android/settings/inputmethod/TrackpadNotificationsPreferenceController.java
@@ -16,18 +16,24 @@
package com.android.settings.inputmethod;
+import android.app.settings.SettingsEnums;
import android.content.Context;
import android.provider.Settings;
import com.android.settings.R;
import com.android.settings.core.TogglePreferenceController;
+import com.android.settings.overlay.FeatureFactory;
+import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
public class TrackpadNotificationsPreferenceController extends TogglePreferenceController {
private static final String SETTING_KEY = Settings.Secure.TRACKPAD_GESTURE_NOTIFICATION_ENABLED;
+ private MetricsFeatureProvider mMetricsFeatureProvider;
+
public TrackpadNotificationsPreferenceController(Context context, String key) {
super(context, key);
+ mMetricsFeatureProvider = FeatureFactory.getFactory(context).getMetricsFeatureProvider();
}
@Override
@@ -38,6 +44,8 @@
@Override
public boolean setChecked(boolean isChecked) {
Settings.Secure.putInt(mContext.getContentResolver(), SETTING_KEY, isChecked ? 1 : 0);
+ mMetricsFeatureProvider.action(
+ mContext, SettingsEnums.ACTION_GESTURE_NOTIFICATION_CHANGED, isChecked);
return true;
}
diff --git a/src/com/android/settings/inputmethod/TrackpadPointerSpeedPreferenceController.java b/src/com/android/settings/inputmethod/TrackpadPointerSpeedPreferenceController.java
index 71b4119..58b4772 100644
--- a/src/com/android/settings/inputmethod/TrackpadPointerSpeedPreferenceController.java
+++ b/src/com/android/settings/inputmethod/TrackpadPointerSpeedPreferenceController.java
@@ -16,20 +16,25 @@
package com.android.settings.inputmethod;
+import android.app.settings.SettingsEnums;
import android.content.Context;
import android.hardware.input.InputSettings;
import androidx.preference.PreferenceScreen;
import com.android.settings.core.SliderPreferenceController;
+import com.android.settings.overlay.FeatureFactory;
import com.android.settings.widget.SeekBarPreference;
+import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
public class TrackpadPointerSpeedPreferenceController extends SliderPreferenceController {
private SeekBarPreference mPreference;
+ private MetricsFeatureProvider mMetricsFeatureProvider;
public TrackpadPointerSpeedPreferenceController(Context context, String key) {
super(context, key);
+ mMetricsFeatureProvider = FeatureFactory.getFactory(context).getMetricsFeatureProvider();
}
@Override
@@ -53,6 +58,8 @@
return false;
}
InputSettings.setTouchpadPointerSpeed(mContext, position);
+ mMetricsFeatureProvider.action(
+ mContext, SettingsEnums.ACTION_GESTURE_POINTER_SPEED_CHANGED, position);
return true;
}
diff --git a/src/com/android/settings/inputmethod/TrackpadRecentAppsPreferenceController.java b/src/com/android/settings/inputmethod/TrackpadRecentAppsPreferenceController.java
index eab2b33..878cbe3 100644
--- a/src/com/android/settings/inputmethod/TrackpadRecentAppsPreferenceController.java
+++ b/src/com/android/settings/inputmethod/TrackpadRecentAppsPreferenceController.java
@@ -16,18 +16,24 @@
package com.android.settings.inputmethod;
+import android.app.settings.SettingsEnums;
import android.content.Context;
import android.provider.Settings;
import com.android.settings.R;
import com.android.settings.core.TogglePreferenceController;
+import com.android.settings.overlay.FeatureFactory;
+import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
public class TrackpadRecentAppsPreferenceController extends TogglePreferenceController {
private static final String SETTING_KEY = Settings.Secure.TRACKPAD_GESTURE_OVERVIEW_ENABLED;
+ private MetricsFeatureProvider mMetricsFeatureProvider;
+
public TrackpadRecentAppsPreferenceController(Context context, String key) {
super(context, key);
+ mMetricsFeatureProvider = FeatureFactory.getFactory(context).getMetricsFeatureProvider();
}
@Override
@@ -38,6 +44,8 @@
@Override
public boolean setChecked(boolean isChecked) {
Settings.Secure.putInt(mContext.getContentResolver(), SETTING_KEY, isChecked ? 1 : 0);
+ mMetricsFeatureProvider.action(
+ mContext, SettingsEnums.ACTION_GESTURE_RECENT_APPS_CHANGED, isChecked);
return true;
}
diff --git a/src/com/android/settings/inputmethod/TrackpadReverseScrollingPreferenceController.java b/src/com/android/settings/inputmethod/TrackpadReverseScrollingPreferenceController.java
index 10d3013..2b74c74 100644
--- a/src/com/android/settings/inputmethod/TrackpadReverseScrollingPreferenceController.java
+++ b/src/com/android/settings/inputmethod/TrackpadReverseScrollingPreferenceController.java
@@ -16,16 +16,22 @@
package com.android.settings.inputmethod;
+import android.app.settings.SettingsEnums;
import android.content.Context;
import android.hardware.input.InputSettings;
import com.android.settings.R;
import com.android.settings.core.TogglePreferenceController;
+import com.android.settings.overlay.FeatureFactory;
+import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
public class TrackpadReverseScrollingPreferenceController extends TogglePreferenceController {
+ private MetricsFeatureProvider mMetricsFeatureProvider;
+
public TrackpadReverseScrollingPreferenceController(Context context, String key) {
super(context, key);
+ mMetricsFeatureProvider = FeatureFactory.getFactory(context).getMetricsFeatureProvider();
}
@Override
@@ -36,6 +42,8 @@
@Override
public boolean setChecked(boolean isChecked) {
InputSettings.setTouchpadNaturalScrolling(mContext, !isChecked);
+ mMetricsFeatureProvider.action(
+ mContext, SettingsEnums.ACTION_GESTURE_REVERSE_SCROLLING_CHANGED, isChecked);
return true;
}
diff --git a/src/com/android/settings/inputmethod/TrackpadSwitchAppsPreferenceController.java b/src/com/android/settings/inputmethod/TrackpadSwitchAppsPreferenceController.java
index 84de64e..cfca856 100644
--- a/src/com/android/settings/inputmethod/TrackpadSwitchAppsPreferenceController.java
+++ b/src/com/android/settings/inputmethod/TrackpadSwitchAppsPreferenceController.java
@@ -16,18 +16,24 @@
package com.android.settings.inputmethod;
+import android.app.settings.SettingsEnums;
import android.content.Context;
import android.provider.Settings;
import com.android.settings.R;
import com.android.settings.core.TogglePreferenceController;
+import com.android.settings.overlay.FeatureFactory;
+import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
public class TrackpadSwitchAppsPreferenceController extends TogglePreferenceController {
private static final String SETTING_KEY = Settings.Secure.TRACKPAD_GESTURE_QUICK_SWITCH_ENABLED;
+ private MetricsFeatureProvider mMetricsFeatureProvider;
+
public TrackpadSwitchAppsPreferenceController(Context context, String key) {
super(context, key);
+ mMetricsFeatureProvider = FeatureFactory.getFactory(context).getMetricsFeatureProvider();
}
@Override
@@ -38,6 +44,8 @@
@Override
public boolean setChecked(boolean isChecked) {
Settings.Secure.putInt(mContext.getContentResolver(), SETTING_KEY, isChecked ? 1 : 0);
+ mMetricsFeatureProvider.action(
+ mContext, SettingsEnums.ACTION_GESTURE_SWITCH_APPS_CHANGED, isChecked);
return true;
}
diff --git a/src/com/android/settings/inputmethod/TrackpadTapToClickPreferenceController.java b/src/com/android/settings/inputmethod/TrackpadTapToClickPreferenceController.java
index 8655307..9ee446b 100644
--- a/src/com/android/settings/inputmethod/TrackpadTapToClickPreferenceController.java
+++ b/src/com/android/settings/inputmethod/TrackpadTapToClickPreferenceController.java
@@ -16,16 +16,22 @@
package com.android.settings.inputmethod;
+import android.app.settings.SettingsEnums;
import android.content.Context;
import android.hardware.input.InputSettings;
import com.android.settings.R;
import com.android.settings.core.TogglePreferenceController;
+import com.android.settings.overlay.FeatureFactory;
+import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
public class TrackpadTapToClickPreferenceController extends TogglePreferenceController {
+ private MetricsFeatureProvider mMetricsFeatureProvider;
+
public TrackpadTapToClickPreferenceController(Context context, String key) {
super(context, key);
+ mMetricsFeatureProvider = FeatureFactory.getFactory(context).getMetricsFeatureProvider();
}
@Override
@@ -36,6 +42,8 @@
@Override
public boolean setChecked(boolean isChecked) {
InputSettings.setTouchpadTapToClick(mContext, isChecked);
+ mMetricsFeatureProvider.action(
+ mContext, SettingsEnums.ACTION_GESTURE_TAP_TO_CLICK_CHANGED, isChecked);
return true;
}
diff --git a/src/com/android/settings/localepicker/AppLocalePickerActivity.java b/src/com/android/settings/localepicker/AppLocalePickerActivity.java
index 092207b..d1e1137 100644
--- a/src/com/android/settings/localepicker/AppLocalePickerActivity.java
+++ b/src/com/android/settings/localepicker/AppLocalePickerActivity.java
@@ -18,6 +18,7 @@
import android.app.FragmentTransaction;
import android.app.LocaleManager;
+import android.app.settings.SettingsEnums;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Bundle;
@@ -37,15 +38,22 @@
import com.android.settings.applications.AppLocaleUtil;
import com.android.settings.applications.appinfo.AppLocaleDetails;
import com.android.settings.core.SettingsBaseActivity;
+import com.android.settings.overlay.FeatureFactory;
+import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
public class AppLocalePickerActivity extends SettingsBaseActivity
implements LocalePickerWithRegion.LocaleSelectedListener, MenuItem.OnActionExpandListener {
private static final String TAG = AppLocalePickerActivity.class.getSimpleName();
+ private static final int SIM_LOCALE = 1 << 0;
+ private static final int SYSTEM_LOCALE = 1 << 1;
+ private static final int APP_LOCALE = 1 << 2;
+ private static final int IME_LOCALE = 1 << 3;
private String mPackageName;
private LocalePickerWithRegion mLocalePickerWithRegion;
private AppLocaleDetails mAppLocaleDetails;
private View mAppLocaleDetailContainer;
+ private MetricsFeatureProvider mMetricsFeatureProvider;
@Override
public void onCreate(Bundle savedInstanceState) {
@@ -71,6 +79,7 @@
setTitle(R.string.app_locale_picker_title);
getActionBar().setDisplayHomeAsUpEnabled(true);
+ mMetricsFeatureProvider = FeatureFactory.getFactory(this).getMetricsFeatureProvider();
mLocalePickerWithRegion = LocalePickerWithRegion.createLanguagePicker(
this,
@@ -99,6 +108,7 @@
if (localeInfo == null || localeInfo.getLocale() == null || localeInfo.isSystemLocale()) {
setAppDefaultLocale("");
} else {
+ logLocaleSource(localeInfo);
setAppDefaultLocale(localeInfo.getLocale().toLanguageTag());
}
finish();
@@ -177,4 +187,32 @@
return false;
}
+
+ private void logLocaleSource(LocaleStore.LocaleInfo localeInfo) {
+ if (!localeInfo.isSuggested() || localeInfo.isAppCurrentLocale()) {
+ return;
+ }
+ int localeSource = 0;
+ if (hasSuggestionType(localeInfo,
+ LocaleStore.LocaleInfo.SUGGESTION_TYPE_SYSTEM_AVAILABLE_LANGUAGE)) {
+ localeSource |= SYSTEM_LOCALE;
+ }
+ if (hasSuggestionType(localeInfo,
+ LocaleStore.LocaleInfo.SUGGESTION_TYPE_OTHER_APP_LANGUAGE)) {
+ localeSource |= APP_LOCALE;
+ }
+ if (hasSuggestionType(localeInfo, LocaleStore.LocaleInfo.SUGGESTION_TYPE_IME_LANGUAGE)) {
+ localeSource |= IME_LOCALE;
+ }
+ if (hasSuggestionType(localeInfo, LocaleStore.LocaleInfo.SUGGESTION_TYPE_SIM)) {
+ localeSource |= SIM_LOCALE;
+ }
+ mMetricsFeatureProvider.action(this,
+ SettingsEnums.ACTION_CHANGE_APP_LANGUAGE_FROM_SUGGESTED, localeSource);
+ }
+
+ private static boolean hasSuggestionType(LocaleStore.LocaleInfo localeInfo,
+ int suggestionType) {
+ return localeInfo.isSuggestionOfType(suggestionType);
+ }
}
diff --git a/src/com/android/settings/localepicker/LocaleDialogFragment.java b/src/com/android/settings/localepicker/LocaleDialogFragment.java
index ad9e10f..6c37e38 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,22 @@
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);
+ mMetricsFeatureProvider.action(mContext, SettingsEnums.ACTION_CHANGE_LANGUAGE,
+ changed);
}
mShouldKeepDialog = false;
}
diff --git a/src/com/android/settings/localepicker/LocaleDragAndDropAdapter.java b/src/com/android/settings/localepicker/LocaleDragAndDropAdapter.java
index edd3026..6054c59 100644
--- a/src/com/android/settings/localepicker/LocaleDragAndDropAdapter.java
+++ b/src/com/android/settings/localepicker/LocaleDragAndDropAdapter.java
@@ -16,6 +16,7 @@
package com.android.settings.localepicker;
+import android.app.settings.SettingsEnums;
import android.content.Context;
import android.graphics.Canvas;
import android.os.Bundle;
@@ -30,6 +31,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;
@@ -37,6 +39,7 @@
import com.android.internal.app.LocalePicker;
import com.android.internal.app.LocaleStore;
import com.android.settings.R;
+import com.android.settings.overlay.FeatureFactory;
import com.android.settings.shortcut.ShortcutsUpdateTask;
import java.text.NumberFormat;
@@ -176,17 +179,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 +229,13 @@
Log.e(TAG, String.format(Locale.US,
"Negative position in onItemMove %d -> %d", fromPosition, toPosition));
}
+
+ if (fromPosition != toPosition) {
+ FeatureFactory.getFactory(mContext).getMetricsFeatureProvider()
+ .action(mContext, SettingsEnums.ACTION_REORDER_LANGUAGE,
+ mDragLocale.getLocale().toLanguageTag() + " move to " + toPosition);
+ }
+
notifyItemChanged(fromPosition); // to update the numbers
notifyItemChanged(toPosition);
notifyItemMoved(fromPosition, toPosition);
@@ -244,8 +270,13 @@
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()) {
+ FeatureFactory.getFactory(mContext).getMetricsFeatureProvider()
+ .action(mContext, SettingsEnums.ACTION_REMOVE_LANGUAGE,
+ localeInfo.getLocale().toLanguageTag());
mFeedItemList.remove(i);
}
}
@@ -381,10 +412,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/LocaleListEditor.java b/src/com/android/settings/localepicker/LocaleListEditor.java
index 7ec08f7..65563ad 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,9 +199,11 @@
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();
+ mMetricsFeatureProvider.action(getContext(), SettingsEnums.ACTION_ADD_LANGUAGE,
+ localeInfo.getLocale().toLanguageTag());
} else if (requestCode == DIALOG_CONFIRM_SYSTEM_DEFAULT) {
localeInfo = mAdapter.getFeedItemList().get(0);
if (resultCode == Activity.RESULT_OK) {
@@ -215,6 +216,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 +322,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 +368,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 +394,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/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..0cd12fe 100644
--- a/src/com/android/settings/network/SubscriptionUtil.java
+++ b/src/com/android/settings/network/SubscriptionUtil.java
@@ -23,6 +23,7 @@
import android.annotation.Nullable;
import android.content.Context;
+import android.content.SharedPreferences;
import android.os.ParcelUuid;
import android.provider.Settings;
import android.telephony.PhoneNumberUtils;
@@ -61,6 +62,10 @@
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 List<SubscriptionInfo> sAvailableResultsForTesting;
private static List<SubscriptionInfo> sActiveResultsForTesting;
@@ -265,20 +270,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 +298,14 @@
// 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 -> {
+ String cachedDisplayName = getDisplayNameFromSharedPreference(
+ context, info.subscriptionInfo.getSubscriptionId());
+ if (!TextUtils.isEmpty(cachedDisplayName)) {
+ Log.d(TAG, "use cached display name : " + cachedDisplayName);
+ info.uniqueName = cachedDisplayName;
+ return info;
+ }
+
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 +313,17 @@
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 "
+ + info.subscriptionInfo.getSubscriptionId());
+ saveDisplayNameToSharedPreference(
+ context, info.subscriptionInfo.getSubscriptionId(), info.uniqueName);
}
-
} else {
info.uniqueName = info.originalName;
}
@@ -371,6 +387,27 @@
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 String getDisplayNameFromSharedPreference(Context context, int subid) {
+ return getDisplayNameSharedPreferences(context).getString(SUB_ID + subid, "");
+ }
+
public static String getDisplayName(SubscriptionInfo info) {
final CharSequence name = info.getDisplayName();
if (name != null) {
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/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/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/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/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 a72bff4..3a1532b 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()));
mPasswordRestrictionView.setAccessibilityLiveRegion(ACCESSIBILITY_LIVE_REGION_POLITE);
@@ -562,6 +565,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) {
@@ -571,6 +576,7 @@
.setTitle(getString(R.string.unlock_set_unlock_launch_picker_title))
.setReturnCredentials(true)
.setRequestGatekeeperPasswordHandle(mRequestGatekeeperPassword)
+ .setRequestWriteRepairModePassword(mRequestWriteRepairModePassword)
.setUserId(mUserId)
.show();
}
@@ -627,11 +633,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
@@ -1010,7 +1038,10 @@
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();
@@ -1030,7 +1061,7 @@
(mAutoPinConfirmOption != null && mAutoPinConfirmOption.isChecked()),
mUserId);
- mSaveAndFinishWorker.start(mLockPatternUtils, mRequestGatekeeperPassword,
+ mSaveAndFinishWorker.start(mLockPatternUtils,
mChosenPassword, mCurrentCredential, mUserId);
}
@@ -1083,50 +1114,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 a2fd986..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;
@@ -563,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) {
@@ -576,6 +577,7 @@
.setTitle(getString(R.string.unlock_set_unlock_launch_picker_title))
.setReturnCredentials(true)
.setRequestGatekeeperPasswordHandle(mRequestGatekeeperPassword)
+ .setRequestWriteRepairModePassword(mRequestWriteRepairModePassword)
.setUserId(mUserId)
.show();
@@ -827,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();
@@ -843,7 +848,7 @@
profileCredential);
}
}
- mSaveAndFinishWorker.start(mLockPatternUtils, mRequestGatekeeperPassword,
+ mSaveAndFinishWorker.start(mLockPatternUtils,
mChosenPattern, mCurrentCredential, mUserId);
}
@@ -867,63 +872,4 @@
getActivity().finish();
}
}
-
- public static class SaveAndFinishWorker extends SaveChosenLockWorkerBase {
-
- private LockscreenCredential mChosenPattern;
- private LockscreenCredential mCurrentCredential;
- private boolean mLockVirgin;
-
- 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;
-
- mLockVirgin = !mUtils.isPatternEverChosen(mUserId);
-
- 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);
- }
-
- @Override
- protected void finish(Intent resultData) {
- if (mLockVirgin) {
- mUtils.setVisiblePatternEnabled(true, mUserId);
- }
-
- super.finish(resultData);
- }
- }
}
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..a4d349e 100644
--- a/src/com/android/settings/password/PasswordRequirementAdapter.java
+++ b/src/com/android/settings/password/PasswordRequirementAdapter.java
@@ -16,6 +16,7 @@
package com.android.settings.password;
+import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@@ -31,9 +32,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);
}
@@ -61,7 +65,12 @@
@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/regionalpreferences/NumberingSystemItemController.java b/src/com/android/settings/regionalpreferences/NumberingSystemItemController.java
index e3a8d23..2a99e99 100644
--- a/src/com/android/settings/regionalpreferences/NumberingSystemItemController.java
+++ b/src/com/android/settings/regionalpreferences/NumberingSystemItemController.java
@@ -153,7 +153,7 @@
private void handleLanguageSelect(Preference preference) {
String selectedLanguage = preference.getKey();
mMetricsFeatureProvider.action(mContext,
- SettingsEnums.ACTION_CHOOSE_LANGUAGE_FOR_NUMBERS_PREFERENCES);
+ SettingsEnums.ACTION_CHOOSE_LANGUAGE_FOR_NUMBERS_PREFERENCES, selectedLanguage);
final Bundle extra = new Bundle();
extra.putString(RegionalPreferencesEntriesFragment.ARG_KEY_REGIONAL_PREFERENCE,
ARG_VALUE_NUMBERING_SYSTEM_SELECT);
@@ -177,7 +177,8 @@
saveNumberingSystemToLocale(Locale.forLanguageTag(mSelectedLanguage),
numberingSystem);
mMetricsFeatureProvider.action(mContext,
- SettingsEnums.ACTION_SET_NUMBERS_PREFERENCES);
+ SettingsEnums.ACTION_SET_NUMBERS_PREFERENCES,
+ updatedLocale.getDisplayName() + ": " + numberingSystem);
// 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..432ce0e 100644
--- a/src/com/android/settings/regionalpreferences/RegionalPreferenceListBasePreferenceController.java
+++ b/src/com/android/settings/regionalpreferences/RegionalPreferenceListBasePreferenceController.java
@@ -59,6 +59,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 +68,10 @@
RegionalPreferencesDataUtils.savePreference(mContext, getExtensionTypes(),
item.equals(RegionalPreferencesDataUtils.DEFAULT_VALUE)
? null : item);
- mMetricsFeatureProvider.action(mContext, getMetricsActionKey());
+ mMetricsFeatureProvider.action(mContext, getMetricsActionKey(),
+ getPreferenceTitle(value) + " > " + getPreferenceTitle(item));
return true;
});
- String value = RegionalPreferencesDataUtils.getDefaultUnicodeExtensionData(mContext,
- getExtensionTypes());
pref.setSelected(!value.isEmpty() && item.equals(value));
}
}
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/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..3680715
--- /dev/null
+++ b/src/com/android/settings/spa/app/appcompat/UserAspectRatioAppPreference.kt
@@ -0,0 +1,80 @@
+/*
+ * 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.ExperimentalLifecycleComposeApi
+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
+
+@OptIn(ExperimentalLifecycleComposeApi::class)
+@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_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..ff90492
--- /dev/null
+++ b/src/com/android/settings/spa/app/appcompat/UserAspectRatioAppsPageProvider.kt
@@ -0,0 +1,213 @@
+/*
+ * 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.ExperimentalLifecycleComposeApi
+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.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_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, Build.MODEL)
+}
+
+@Composable
+fun UserAspectRatioAppList(
+ appList: @Composable AppListInput<UserAspectRatioAppListItemModel>.() -> Unit
+ = { AppList() },
+) {
+ AppListPage(
+ title = stringResource(R.string.aspect_ratio_title),
+ listModel = rememberContext(::UserAspectRatioAppListModel),
+ appList = appList,
+ header = {
+ Box(Modifier.padding(SettingsDimension.itemPadding)) {
+ SettingsBody(UserAspectRatioAppsPageProvider.getSummary())
+ }
+ }
+ )
+}
+
+data class UserAspectRatioAppListItemModel(
+ override val app: ApplicationInfo,
+ val override: 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.override != 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,
+ override = 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.override != USER_MIN_ASPECT_RATIO_UNSET })
+ else -> ({ it.canDisplay })
+ }
+ )
+
+ @OptIn(ExperimentalLifecycleComposeApi::class)
+ @Composable
+ override fun getSummary(option: Int, record: UserAspectRatioAppListItemModel) : State<String> =
+ remember(record.override) {
+ flow {
+ emit(userAspectRatioManager.getUserMinAspectRatioEntry(record.override))
+ }.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_overridden_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 d59a4f7..e6df933 100644
--- a/src/com/android/settings/spa/app/appinfo/AppInfoSettings.kt
+++ b/src/com/android/settings/spa/app/appinfo/AppInfoSettings.kt
@@ -35,6 +35,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
@@ -150,6 +151,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/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 a91bb6c..8cec2f4 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();
}
@@ -212,6 +211,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/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 3890ddf..098787c 100644
--- a/src/com/android/settings/wifi/calling/WifiCallingSettingsForSub.java
+++ b/src/com/android/settings/wifi/calling/WifiCallingSettingsForSub.java
@@ -201,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)
diff --git a/src/com/android/settings/wifi/details2/WifiDetailPreferenceController2.java b/src/com/android/settings/wifi/details2/WifiDetailPreferenceController2.java
index 4c5a4bf..2e1bc31 100644
--- a/src/com/android/settings/wifi/details2/WifiDetailPreferenceController2.java
+++ b/src/com/android/settings/wifi/details2/WifiDetailPreferenceController2.java
@@ -637,29 +637,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() {
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/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/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/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/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..31ff76c
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/applications/appcompat/UserAspectRatioDetailsTest.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.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 com.android.settingslib.widget.SelectorWithWidgetPreference;
+
+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 SelectorWithWidgetPreference 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 SelectorWithWidgetPreference(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/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/fingerprint/FingerprintEnrollEnrollingTest.java b/tests/robotests/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollEnrollingTest.java
index 0f12d1e..a23eded 100644
--- a/tests/robotests/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollEnrollingTest.java
+++ b/tests/robotests/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollEnrollingTest.java
@@ -148,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);
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/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..3c459de 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,7 @@
import androidx.test.core.app.ApplicationProvider;
import com.android.settings.R;
+import com.android.settings.dashboard.profileselector.UserAdapter;
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
import com.android.settingslib.core.lifecycle.Lifecycle;
@@ -59,7 +68,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 +90,8 @@
@Mock
private PackageManager mPm;
@Mock
+ private UserManager mUserManager;
+ @Mock
private RoleManager mRm;
@Mock
private Lifecycle mLifecycle;
@@ -87,7 +100,6 @@
@Mock
private BluetoothDevice mBluetoothDevice;
-
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
@@ -101,6 +113,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 +128,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 +243,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 +310,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 +325,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
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..5922016
--- /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()))
+ .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..ccaefb2
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/connecteddevice/stylus/UsbStylusBroadcastReceiverTest.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.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);
+ }
+}
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/StayAwakeOnFoldPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/display/StayAwakeOnFoldPreferenceControllerTest.java
new file mode 100644
index 0000000..c994818
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/display/StayAwakeOnFoldPreferenceControllerTest.java
@@ -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.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 android.provider.Settings;
+
+import com.android.settings.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 StayAwakeOnFoldPreferenceControllerTest {
+
+ @Mock
+ private Resources mResources;
+ private Context mContext;
+ private StayAwakeOnFoldPreferenceController mController;
+
+ @Before
+ public void setUp() {
+ mContext = RuntimeEnvironment.application;
+ mResources = Mockito.mock(Resources.class);
+ mController = new StayAwakeOnFoldPreferenceController(mContext, "key", mResources);
+ }
+
+ @Test
+ public void getAvailabilityStatus_withConfigNoShow_returnUnsupported() {
+ when(mResources.getBoolean(R.bool.config_stay_awake_on_fold)).thenReturn(false);
+
+ assertThat(mController.getAvailabilityStatus()).isEqualTo(UNSUPPORTED_ON_DEVICE);
+ }
+
+ @Test
+ public void getAvailabilityStatus_withConfigNoShow_returnAvailable() {
+ when(mResources.getBoolean(R.bool.config_stay_awake_on_fold)).thenReturn(true);
+
+ assertThat(mController.getAvailabilityStatus()).isEqualTo(AVAILABLE);
+ }
+
+ @Test
+ public void setChecked_enableStayAwakeOnFold_setChecked() {
+ mController.setChecked(true);
+
+ assertThat(isStayAwakeOnFoldEnabled())
+ .isTrue();
+ }
+
+ @Test
+ public void setChecked_disableStayAwakeOnFold_setUnchecked() {
+ mController.setChecked(false);
+
+ assertThat(isStayAwakeOnFoldEnabled())
+ .isFalse();
+ }
+
+ @Test
+ public void isChecked_enableStayAwakeOnFold_returnTrue() {
+ enableStayAwakeOnFoldPreference();
+
+ assertThat(mController.isChecked()).isTrue();
+ }
+
+ @Test
+ public void isChecked_disableStayAwakeOnFold_returnFalse() {
+ disableStayAwakeOnFoldPreference();
+
+ assertThat(mController.isChecked()).isFalse();
+ }
+
+ private void enableStayAwakeOnFoldPreference() {
+ Settings.System.putInt(
+ mContext.getContentResolver(),
+ Settings.System.STAY_AWAKE_ON_FOLD,
+ 1);
+ }
+
+ private void disableStayAwakeOnFoldPreference() {
+ Settings.System.putInt(
+ mContext.getContentResolver(),
+ Settings.System.STAY_AWAKE_ON_FOLD,
+ 0);
+ }
+
+ private boolean isStayAwakeOnFoldEnabled() {
+ return (Settings.System.getInt(
+ mContext.getContentResolver(),
+ Settings.System.STAY_AWAKE_ON_FOLD,
+ 0) == 1);
+ }
+}
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..c9591a5 100644
--- a/tests/robotests/src/com/android/settings/fuelgauge/PowerUsageFeatureProviderImplTest.java
+++ b/tests/robotests/src/com/android/settings/fuelgauge/PowerUsageFeatureProviderImplTest.java
@@ -68,6 +68,15 @@
}
@Test
+ public void testIsBatteryTipsEnabled_returnFalse() {
+ assertThat(mPowerFeatureProvider.isBatteryTipsEnabled()).isFalse();
+ }
+
+ @Test
+ public void testIsBatteryTipsFeedbackEnabled_returnTrue() {
+ assertThat(mPowerFeatureProvider.isBatteryTipsFeedbackEnabled()).isTrue();
+ }
+ @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/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/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/BatteryTipsCardPreferenceTest.java b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryTipsCardPreferenceTest.java
new file mode 100644
index 0000000..6f9a474
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryTipsCardPreferenceTest.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.batteryusage;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.spy;
+
+import android.content.Context;
+
+import com.android.settings.R;
+
+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 final class BatteryTipsCardPreferenceTest {
+
+ private Context mContext;
+ private BatteryTipsCardPreference mBatteryTipsCardPreference;
+
+ @Before
+ public void setUp() {
+ mContext = spy(RuntimeEnvironment.application);
+ mBatteryTipsCardPreference = new BatteryTipsCardPreference(mContext, /*attrs=*/ null);
+ }
+
+ @Test
+ public void constructor_returnExpectedResult() {
+ assertThat(mBatteryTipsCardPreference.getLayoutResource()).isEqualTo(
+ R.layout.battery_tips_card);
+ }
+}
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..3cbe8a4 100644
--- a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/ConvertUtilsTest.java
+++ b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/ConvertUtilsTest.java
@@ -25,7 +25,6 @@
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.when;
-import android.app.usage.IUsageStatsManager;
import android.app.usage.UsageEvents;
import android.app.usage.UsageEvents.Event;
import android.content.ContentValues;
@@ -35,7 +34,6 @@
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;
@@ -62,14 +60,13 @@
@Mock
private BatteryUsageStats mBatteryUsageStats;
@Mock
- private IUsageStatsManager mUsageStatsManager;
- @Mock
private BatteryEntry mMockBatteryEntry;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
mContext = spy(RuntimeEnvironment.application);
+ ConvertUtils.sUsageSource = ConvertUtils.EMPTY_USAGE_SOURCE;
when(mContext.getPackageManager()).thenReturn(mMockPackageManager);
}
@@ -302,7 +299,7 @@
final long userId = 2;
final AppUsageEvent appUsageEvent = ConvertUtils.convertToAppUsageEvent(
- mContext, mUsageStatsManager, event, userId);
+ mContext, event, userId);
assertThat(appUsageEvent.getTimestamp()).isEqualTo(101L);
assertThat(appUsageEvent.getType()).isEqualTo(AppUsageEventType.ACTIVITY_RESUMED);
assertThat(appUsageEvent.getPackageName()).isEqualTo("com.android.settings1");
@@ -322,8 +319,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, event, userId);
assertThat(appUsageEvent.getTimestamp()).isEqualTo(101L);
assertThat(appUsageEvent.getType()).isEqualTo(AppUsageEventType.DEVICE_SHUTDOWN);
assertThat(appUsageEvent.getPackageName()).isEqualTo("com.android.settings1");
@@ -338,8 +335,8 @@
final Event event = new Event();
event.mPackage = null;
- final AppUsageEvent appUsageEvent = ConvertUtils.convertToAppUsageEvent(
- mContext, mUsageStatsManager, event, /*userId=*/ 0);
+ final AppUsageEvent appUsageEvent =
+ ConvertUtils.convertToAppUsageEvent(mContext, event, /*userId=*/ 0);
assertThat(appUsageEvent).isNull();
}
@@ -354,8 +351,8 @@
.thenThrow(new PackageManager.NameNotFoundException());
final long userId = 1;
- final AppUsageEvent appUsageEvent = ConvertUtils.convertToAppUsageEvent(
- mContext, mUsageStatsManager, event, userId);
+ final AppUsageEvent appUsageEvent =
+ ConvertUtils.convertToAppUsageEvent(mContext, event, userId);
assertThat(appUsageEvent).isNull();
}
@@ -450,51 +447,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, 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, 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, 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, packageName, /*taskRootPackageName=*/ null))
.isEqualTo(packageName);
assertThat(ConvertUtils.getEffectivePackageName(
- mUsageStatsManager, packageName, /*taskRootPackageName=*/ ""))
+ mContext, 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..7f7fe43 100644
--- a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/DataProcessManagerTest.java
+++ b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/DataProcessManagerTest.java
@@ -72,7 +72,7 @@
MockitoAnnotations.initMocks(this);
mContext = spy(RuntimeEnvironment.application);
- DataProcessor.sUsageStatsManager = mUsageStatsManager;
+ DatabaseUtils.sUsageStatsManager = mUsageStatsManager;
doReturn(mContext).when(mContext).getApplicationContext();
doReturn(mUserManager)
.when(mContext)
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..8bed054 100644
--- a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/DataProcessorTest.java
+++ b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/DataProcessorTest.java
@@ -93,7 +93,7 @@
mPowerUsageFeatureProvider = mFeatureFactory.powerUsageFeatureProvider;
DataProcessor.sTestSystemAppsPackageNames = Set.of();
- DataProcessor.sUsageStatsManager = mUsageStatsManager;
+ DatabaseUtils.sUsageStatsManager = mUsageStatsManager;
doReturn(mIntent).when(mContext).registerReceiver(
isA(BroadcastReceiver.class), isA(IntentFilter.class));
doReturn(100).when(mIntent).getIntExtra(eq(BatteryManager.EXTRA_SCALE), anyInt());
@@ -249,7 +249,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
@@ -287,7 +287,8 @@
hourlyBatteryLevelsPerDay.add(
new BatteryLevelData.PeriodBatteryLevelData(new ArrayList<>(), new ArrayList<>()));
assertThat(DataProcessor.generateAppUsagePeriodMap(
- 0L, hourlyBatteryLevelsPerDay, new ArrayList<>(), new ArrayList<>())).isNull();
+ mContext, hourlyBatteryLevelsPerDay, new ArrayList<>(), new ArrayList<>()))
+ .isNull();
}
@Test
@@ -1644,7 +1645,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 +1669,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 +1681,7 @@
AppUsageEventType.DEVICE_SHUTDOWN, /*timestamp=*/ 2));
assertThat(DataProcessor.buildAppUsagePeriodList(
- appUsageEvents, new ArrayList<>(), 0, 3)).isNull();
+ mContext, appUsageEvents, new ArrayList<>(), 0, 3)).isNull();
}
@Test
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..24be769 100644
--- a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/DatabaseUtilsTest.java
+++ b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/DatabaseUtilsTest.java
@@ -16,6 +16,9 @@
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;
@@ -23,15 +26,19 @@
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.os.BatteryManager;
import android.os.BatteryUsageStats;
+import android.os.RemoteException;
import android.os.UserHandle;
import android.os.UserManager;
@@ -67,6 +74,7 @@
@Mock private BatteryEntry mMockBatteryEntry2;
@Mock private BatteryEntry mMockBatteryEntry3;
@Mock private Context mMockContext;
+ @Mock private IUsageStatsManager mUsageStatsManager;
@Before
public void setUp() {
@@ -77,6 +85,7 @@
doReturn(mPackageManager).when(mMockContext).getPackageManager();
doReturn(mPackageManager).when(mContext).getPackageManager();
DatabaseUtils.getSharedPreferences(mContext).edit().clear().apply();
+ DatabaseUtils.sUsageStatsManager = mUsageStatsManager;
}
@Test
@@ -423,6 +432,71 @@
}
@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))
+ .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))
+ .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))
+ .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);
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/inputmethod/TrackpadBottomPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/inputmethod/TrackpadBottomPreferenceControllerTest.java
index 1b061ec..3c51cf3 100644
--- a/tests/robotests/src/com/android/settings/inputmethod/TrackpadBottomPreferenceControllerTest.java
+++ b/tests/robotests/src/com/android/settings/inputmethod/TrackpadBottomPreferenceControllerTest.java
@@ -18,6 +18,11 @@
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.app.settings.SettingsEnums;
import android.content.Context;
import android.os.UserHandle;
import android.provider.Settings;
@@ -26,25 +31,33 @@
import com.android.settings.R;
import com.android.settings.core.BasePreferenceController;
+import com.android.settings.testutils.FakeFeatureFactory;
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.MockitoRule;
import org.robolectric.RobolectricTestRunner;
/** Tests for {@link TrackpadBottomPreferenceController} */
@RunWith(RobolectricTestRunner.class)
public class TrackpadBottomPreferenceControllerTest {
+ @Rule
+ public MockitoRule rule = MockitoJUnit.rule();
private static final String PREFERENCE_KEY = "trackpad_bottom_right_tap";
private static final String SETTING_KEY = Settings.System.TOUCHPAD_RIGHT_CLICK_ZONE;
private Context mContext;
private TrackpadBottomPreferenceController mController;
+ private FakeFeatureFactory mFeatureFactory;
@Before
public void setUp() {
mContext = ApplicationProvider.getApplicationContext();
+ mFeatureFactory = FakeFeatureFactory.setupForTest();
mController = new TrackpadBottomPreferenceController(mContext, PREFERENCE_KEY);
}
@@ -70,6 +83,10 @@
UserHandle.USER_CURRENT);
assertThat(result).isEqualTo(1);
+ verify(mFeatureFactory.metricsFeatureProvider).action(
+ any(),
+ eq(SettingsEnums.ACTION_GESTURE_BOTTOM_RIGHT_TAP_CHANGED),
+ eq(true));
}
@Test
@@ -83,6 +100,10 @@
UserHandle.USER_CURRENT);
assertThat(result).isEqualTo(0);
+ verify(mFeatureFactory.metricsFeatureProvider).action(
+ any(),
+ eq(SettingsEnums.ACTION_GESTURE_BOTTOM_RIGHT_TAP_CHANGED),
+ eq(false));
}
@Test
diff --git a/tests/robotests/src/com/android/settings/inputmethod/TrackpadGoBackPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/inputmethod/TrackpadGoBackPreferenceControllerTest.java
index 0e1705e..85d56ef 100644
--- a/tests/robotests/src/com/android/settings/inputmethod/TrackpadGoBackPreferenceControllerTest.java
+++ b/tests/robotests/src/com/android/settings/inputmethod/TrackpadGoBackPreferenceControllerTest.java
@@ -18,6 +18,11 @@
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.app.settings.SettingsEnums;
import android.content.Context;
import android.provider.Settings;
@@ -25,25 +30,33 @@
import com.android.settings.R;
import com.android.settings.core.BasePreferenceController;
+import com.android.settings.testutils.FakeFeatureFactory;
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.MockitoRule;
import org.robolectric.RobolectricTestRunner;
/** Tests for {@link TrackpadGoBackPreferenceController} */
@RunWith(RobolectricTestRunner.class)
public class TrackpadGoBackPreferenceControllerTest {
+ @Rule
+ public MockitoRule rule = MockitoJUnit.rule();
private static final String PREFERENCE_KEY = "gesture_go_back";
private static final String SETTING_KEY = Settings.Secure.TRACKPAD_GESTURE_BACK_ENABLED;
private Context mContext;
private TrackpadGoBackPreferenceController mController;
+ private FakeFeatureFactory mFeatureFactory;
@Before
public void setUp() {
mContext = ApplicationProvider.getApplicationContext();
+ mFeatureFactory = FakeFeatureFactory.setupForTest();
mController = new TrackpadGoBackPreferenceController(mContext, PREFERENCE_KEY);
}
@@ -65,6 +78,10 @@
int result = Settings.Secure.getInt(mContext.getContentResolver(), SETTING_KEY, 1);
assertThat(result).isEqualTo(1);
+ verify(mFeatureFactory.metricsFeatureProvider).action(
+ any(),
+ eq(SettingsEnums.ACTION_GESTURE_GO_BACK_CHANGED),
+ eq(true));
}
@Test
@@ -74,6 +91,10 @@
int result = Settings.Secure.getInt(mContext.getContentResolver(), SETTING_KEY, 1);
assertThat(result).isEqualTo(0);
+ verify(mFeatureFactory.metricsFeatureProvider).action(
+ any(),
+ eq(SettingsEnums.ACTION_GESTURE_GO_BACK_CHANGED),
+ eq(false));
}
@Test
diff --git a/tests/robotests/src/com/android/settings/inputmethod/TrackpadGoHomePreferenceControllerTest.java b/tests/robotests/src/com/android/settings/inputmethod/TrackpadGoHomePreferenceControllerTest.java
index 3289bcc..6b3b3f5 100644
--- a/tests/robotests/src/com/android/settings/inputmethod/TrackpadGoHomePreferenceControllerTest.java
+++ b/tests/robotests/src/com/android/settings/inputmethod/TrackpadGoHomePreferenceControllerTest.java
@@ -18,6 +18,11 @@
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.app.settings.SettingsEnums;
import android.content.Context;
import android.provider.Settings;
@@ -25,25 +30,33 @@
import com.android.settings.R;
import com.android.settings.core.BasePreferenceController;
+import com.android.settings.testutils.FakeFeatureFactory;
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.MockitoRule;
import org.robolectric.RobolectricTestRunner;
/** Tests for {@link TrackpadGoHomePreferenceController} */
@RunWith(RobolectricTestRunner.class)
public class TrackpadGoHomePreferenceControllerTest {
+ @Rule
+ public MockitoRule rule = MockitoJUnit.rule();
private static final String PREFERENCE_KEY = "gesture_go_home";
private static final String SETTING_KEY = Settings.Secure.TRACKPAD_GESTURE_HOME_ENABLED;
private Context mContext;
private TrackpadGoHomePreferenceController mController;
+ private FakeFeatureFactory mFeatureFactory;
@Before
public void setUp() {
mContext = ApplicationProvider.getApplicationContext();
+ mFeatureFactory = FakeFeatureFactory.setupForTest();
mController = new TrackpadGoHomePreferenceController(mContext, PREFERENCE_KEY);
}
@@ -65,6 +78,10 @@
int result = Settings.Secure.getInt(mContext.getContentResolver(), SETTING_KEY, 1);
assertThat(result).isEqualTo(1);
+ verify(mFeatureFactory.metricsFeatureProvider).action(
+ any(),
+ eq(SettingsEnums.ACTION_GESTURE_GO_HOME_CHANGED),
+ eq(true));
}
@Test
@@ -74,6 +91,10 @@
int result = Settings.Secure.getInt(mContext.getContentResolver(), SETTING_KEY, 1);
assertThat(result).isEqualTo(0);
+ verify(mFeatureFactory.metricsFeatureProvider).action(
+ any(),
+ eq(SettingsEnums.ACTION_GESTURE_GO_HOME_CHANGED),
+ eq(false));
}
@Test
diff --git a/tests/robotests/src/com/android/settings/inputmethod/TrackpadNotificationsPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/inputmethod/TrackpadNotificationsPreferenceControllerTest.java
index 3df1627..005bc9f 100644
--- a/tests/robotests/src/com/android/settings/inputmethod/TrackpadNotificationsPreferenceControllerTest.java
+++ b/tests/robotests/src/com/android/settings/inputmethod/TrackpadNotificationsPreferenceControllerTest.java
@@ -18,6 +18,11 @@
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.app.settings.SettingsEnums;
import android.content.Context;
import android.provider.Settings;
@@ -25,25 +30,33 @@
import com.android.settings.R;
import com.android.settings.core.BasePreferenceController;
+import com.android.settings.testutils.FakeFeatureFactory;
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.MockitoRule;
import org.robolectric.RobolectricTestRunner;
/** Tests for {@link TrackpadNotificationsPreferenceController} */
@RunWith(RobolectricTestRunner.class)
public class TrackpadNotificationsPreferenceControllerTest {
+ @Rule
+ public MockitoRule rule = MockitoJUnit.rule();
private static final String PREFERENCE_KEY = "gesture_notifications";
private static final String SETTING_KEY = Settings.Secure.TRACKPAD_GESTURE_NOTIFICATION_ENABLED;
private Context mContext;
private TrackpadNotificationsPreferenceController mController;
+ private FakeFeatureFactory mFeatureFactory;
@Before
public void setUp() {
mContext = ApplicationProvider.getApplicationContext();
+ mFeatureFactory = FakeFeatureFactory.setupForTest();
mController = new TrackpadNotificationsPreferenceController(mContext, PREFERENCE_KEY);
}
@@ -65,6 +78,10 @@
int result = Settings.Secure.getInt(mContext.getContentResolver(), SETTING_KEY, 1);
assertThat(result).isEqualTo(1);
+ verify(mFeatureFactory.metricsFeatureProvider).action(
+ any(),
+ eq(SettingsEnums.ACTION_GESTURE_NOTIFICATION_CHANGED),
+ eq(true));
}
@Test
@@ -74,6 +91,10 @@
int result = Settings.Secure.getInt(mContext.getContentResolver(), SETTING_KEY, 1);
assertThat(result).isEqualTo(0);
+ verify(mFeatureFactory.metricsFeatureProvider).action(
+ any(),
+ eq(SettingsEnums.ACTION_GESTURE_NOTIFICATION_CHANGED),
+ eq(false));
}
@Test
diff --git a/tests/robotests/src/com/android/settings/inputmethod/TrackpadPointerSpeedPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/inputmethod/TrackpadPointerSpeedPreferenceControllerTest.java
index daf1773..1cfda12 100644
--- a/tests/robotests/src/com/android/settings/inputmethod/TrackpadPointerSpeedPreferenceControllerTest.java
+++ b/tests/robotests/src/com/android/settings/inputmethod/TrackpadPointerSpeedPreferenceControllerTest.java
@@ -18,6 +18,11 @@
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.app.settings.SettingsEnums;
import android.content.Context;
import android.hardware.input.InputSettings;
import android.os.UserHandle;
@@ -26,15 +31,21 @@
import androidx.test.core.app.ApplicationProvider;
import com.android.settings.core.BasePreferenceController;
+import com.android.settings.testutils.FakeFeatureFactory;
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.MockitoRule;
import org.robolectric.RobolectricTestRunner;
/** Tests for {@link TrackpadPointerSpeedPreferenceController} */
@RunWith(RobolectricTestRunner.class)
public class TrackpadPointerSpeedPreferenceControllerTest {
+ @Rule
+ public MockitoRule rule = MockitoJUnit.rule();
private static final String PREFERENCE_KEY = "trackpad_pointer_speed";
private static final String SETTING_KEY = Settings.System.TOUCHPAD_POINTER_SPEED;
@@ -42,10 +53,12 @@
private Context mContext;
private TrackpadPointerSpeedPreferenceController mController;
private int mDefaultSpeed;
+ private FakeFeatureFactory mFeatureFactory;
@Before
public void setUp() {
mContext = ApplicationProvider.getApplicationContext();
+ mFeatureFactory = FakeFeatureFactory.setupForTest();
mController = new TrackpadPointerSpeedPreferenceController(mContext, PREFERENCE_KEY);
mDefaultSpeed = Settings.System.getIntForUser(
mContext.getContentResolver(),
@@ -85,6 +98,10 @@
assertThat(result).isTrue();
assertThat(mController.getSliderPosition()).isEqualTo(inputSpeed);
+ verify(mFeatureFactory.metricsFeatureProvider).action(
+ any(),
+ eq(SettingsEnums.ACTION_GESTURE_POINTER_SPEED_CHANGED),
+ eq(1));
}
@Test
diff --git a/tests/robotests/src/com/android/settings/inputmethod/TrackpadRecentAppsPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/inputmethod/TrackpadRecentAppsPreferenceControllerTest.java
index dbed542..2ef53a6 100644
--- a/tests/robotests/src/com/android/settings/inputmethod/TrackpadRecentAppsPreferenceControllerTest.java
+++ b/tests/robotests/src/com/android/settings/inputmethod/TrackpadRecentAppsPreferenceControllerTest.java
@@ -18,6 +18,11 @@
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.app.settings.SettingsEnums;
import android.content.Context;
import android.provider.Settings;
@@ -25,25 +30,33 @@
import com.android.settings.R;
import com.android.settings.core.BasePreferenceController;
+import com.android.settings.testutils.FakeFeatureFactory;
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.MockitoRule;
import org.robolectric.RobolectricTestRunner;
/** Tests for {@link TrackpadRecentAppsPreferenceController} */
@RunWith(RobolectricTestRunner.class)
public class TrackpadRecentAppsPreferenceControllerTest {
+ @Rule
+ public MockitoRule rule = MockitoJUnit.rule();
private static final String PREFERENCE_KEY = "gesture_recent_apps";
private static final String SETTING_KEY = Settings.Secure.TRACKPAD_GESTURE_OVERVIEW_ENABLED;
private Context mContext;
private TrackpadRecentAppsPreferenceController mController;
+ private FakeFeatureFactory mFeatureFactory;
@Before
public void setUp() {
mContext = ApplicationProvider.getApplicationContext();
+ mFeatureFactory = FakeFeatureFactory.setupForTest();
mController = new TrackpadRecentAppsPreferenceController(mContext, PREFERENCE_KEY);
}
@@ -65,6 +78,10 @@
int result = Settings.Secure.getInt(mContext.getContentResolver(), SETTING_KEY, 1);
assertThat(result).isEqualTo(1);
+ verify(mFeatureFactory.metricsFeatureProvider).action(
+ any(),
+ eq(SettingsEnums.ACTION_GESTURE_RECENT_APPS_CHANGED),
+ eq(true));
}
@Test
@@ -74,6 +91,10 @@
int result = Settings.Secure.getInt(mContext.getContentResolver(), SETTING_KEY, 1);
assertThat(result).isEqualTo(0);
+ verify(mFeatureFactory.metricsFeatureProvider).action(
+ any(),
+ eq(SettingsEnums.ACTION_GESTURE_RECENT_APPS_CHANGED),
+ eq(false));
}
@Test
diff --git a/tests/robotests/src/com/android/settings/inputmethod/TrackpadReverseScrollingPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/inputmethod/TrackpadReverseScrollingPreferenceControllerTest.java
index a99abb8..e74261e 100644
--- a/tests/robotests/src/com/android/settings/inputmethod/TrackpadReverseScrollingPreferenceControllerTest.java
+++ b/tests/robotests/src/com/android/settings/inputmethod/TrackpadReverseScrollingPreferenceControllerTest.java
@@ -16,9 +16,13 @@
package com.android.settings.inputmethod;
-
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.app.settings.SettingsEnums;
import android.content.Context;
import android.os.UserHandle;
import android.provider.Settings;
@@ -27,25 +31,33 @@
import com.android.settings.R;
import com.android.settings.core.BasePreferenceController;
+import com.android.settings.testutils.FakeFeatureFactory;
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.MockitoRule;
import org.robolectric.RobolectricTestRunner;
/** Tests for {@link TrackpadReverseScrollingPreferenceController} */
@RunWith(RobolectricTestRunner.class)
public class TrackpadReverseScrollingPreferenceControllerTest {
+ @Rule
+ public MockitoRule rule = MockitoJUnit.rule();
private static final String PREFERENCE_KEY = "trackpad_reverse_scrolling";
private static final String SETTING_KEY = Settings.System.TOUCHPAD_NATURAL_SCROLLING;
private Context mContext;
private TrackpadReverseScrollingPreferenceController mController;
+ private FakeFeatureFactory mFeatureFactory;
@Before
public void setUp() {
mContext = ApplicationProvider.getApplicationContext();
+ mFeatureFactory = FakeFeatureFactory.setupForTest();
mController = new TrackpadReverseScrollingPreferenceController(mContext, PREFERENCE_KEY);
}
@@ -71,6 +83,10 @@
UserHandle.USER_CURRENT);
assertThat(result).isEqualTo(0);
+ verify(mFeatureFactory.metricsFeatureProvider).action(
+ any(),
+ eq(SettingsEnums.ACTION_GESTURE_REVERSE_SCROLLING_CHANGED),
+ eq(true));
}
@Test
@@ -84,6 +100,10 @@
UserHandle.USER_CURRENT);
assertThat(result).isEqualTo(1);
+ verify(mFeatureFactory.metricsFeatureProvider).action(
+ any(),
+ eq(SettingsEnums.ACTION_GESTURE_REVERSE_SCROLLING_CHANGED),
+ eq(false));
}
@Test
diff --git a/tests/robotests/src/com/android/settings/inputmethod/TrackpadSwitchAppsPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/inputmethod/TrackpadSwitchAppsPreferenceControllerTest.java
index 3f16025..5e354d2 100644
--- a/tests/robotests/src/com/android/settings/inputmethod/TrackpadSwitchAppsPreferenceControllerTest.java
+++ b/tests/robotests/src/com/android/settings/inputmethod/TrackpadSwitchAppsPreferenceControllerTest.java
@@ -18,6 +18,11 @@
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.app.settings.SettingsEnums;
import android.content.Context;
import android.provider.Settings;
@@ -25,25 +30,33 @@
import com.android.settings.R;
import com.android.settings.core.BasePreferenceController;
+import com.android.settings.testutils.FakeFeatureFactory;
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.MockitoRule;
import org.robolectric.RobolectricTestRunner;
/** Tests for {@link TrackpadSwitchAppsPreferenceController} */
@RunWith(RobolectricTestRunner.class)
public class TrackpadSwitchAppsPreferenceControllerTest {
+ @Rule
+ public MockitoRule rule = MockitoJUnit.rule();
private static final String PREFERENCE_KEY = "gesture_switch_apps";
private static final String SETTING_KEY = Settings.Secure.TRACKPAD_GESTURE_QUICK_SWITCH_ENABLED;
private Context mContext;
private TrackpadSwitchAppsPreferenceController mController;
+ private FakeFeatureFactory mFeatureFactory;
@Before
public void setUp() {
mContext = ApplicationProvider.getApplicationContext();
+ mFeatureFactory = FakeFeatureFactory.setupForTest();
mController = new TrackpadSwitchAppsPreferenceController(mContext, PREFERENCE_KEY);
}
@@ -65,6 +78,10 @@
int result = Settings.Secure.getInt(mContext.getContentResolver(), SETTING_KEY, 1);
assertThat(result).isEqualTo(1);
+ verify(mFeatureFactory.metricsFeatureProvider).action(
+ any(),
+ eq(SettingsEnums.ACTION_GESTURE_SWITCH_APPS_CHANGED),
+ eq(true));
}
@Test
@@ -74,6 +91,10 @@
int result = Settings.Secure.getInt(mContext.getContentResolver(), SETTING_KEY, 1);
assertThat(result).isEqualTo(0);
+ verify(mFeatureFactory.metricsFeatureProvider).action(
+ any(),
+ eq(SettingsEnums.ACTION_GESTURE_SWITCH_APPS_CHANGED),
+ eq(false));
}
@Test
diff --git a/tests/robotests/src/com/android/settings/inputmethod/TrackpadTapToClickPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/inputmethod/TrackpadTapToClickPreferenceControllerTest.java
index b4b8921..3784cc7 100644
--- a/tests/robotests/src/com/android/settings/inputmethod/TrackpadTapToClickPreferenceControllerTest.java
+++ b/tests/robotests/src/com/android/settings/inputmethod/TrackpadTapToClickPreferenceControllerTest.java
@@ -18,6 +18,11 @@
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.app.settings.SettingsEnums;
import android.content.Context;
import android.os.UserHandle;
import android.provider.Settings;
@@ -26,25 +31,33 @@
import com.android.settings.R;
import com.android.settings.core.BasePreferenceController;
+import com.android.settings.testutils.FakeFeatureFactory;
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.MockitoRule;
import org.robolectric.RobolectricTestRunner;
/** Tests for {@link TrackpadTapToClickPreferenceController} */
@RunWith(RobolectricTestRunner.class)
public class TrackpadTapToClickPreferenceControllerTest {
+ @Rule
+ public MockitoRule rule = MockitoJUnit.rule();
private static final String PREFERENCE_KEY = "trackpad_tap_to_click";
private static final String SETTING_KEY = Settings.System.TOUCHPAD_TAP_TO_CLICK;
private Context mContext;
private TrackpadTapToClickPreferenceController mController;
+ private FakeFeatureFactory mFeatureFactory;
@Before
public void setUp() {
mContext = ApplicationProvider.getApplicationContext();
+ mFeatureFactory = FakeFeatureFactory.setupForTest();
mController = new TrackpadTapToClickPreferenceController(mContext, PREFERENCE_KEY);
}
@@ -70,6 +83,10 @@
UserHandle.USER_CURRENT);
assertThat(result).isEqualTo(1);
+ verify(mFeatureFactory.metricsFeatureProvider).action(
+ any(),
+ eq(SettingsEnums.ACTION_GESTURE_TAP_TO_CLICK_CHANGED),
+ eq(true));
}
@Test
@@ -83,6 +100,10 @@
UserHandle.USER_CURRENT);
assertThat(result).isEqualTo(0);
+ verify(mFeatureFactory.metricsFeatureProvider).action(
+ any(),
+ eq(SettingsEnums.ACTION_GESTURE_TAP_TO_CLICK_CHANGED),
+ eq(false));
}
@Test
diff --git a/tests/robotests/src/com/android/settings/localepicker/AppLocalePickerActivityTest.java b/tests/robotests/src/com/android/settings/localepicker/AppLocalePickerActivityTest.java
index 48caecd..8fb3a5d 100644
--- a/tests/robotests/src/com/android/settings/localepicker/AppLocalePickerActivityTest.java
+++ b/tests/robotests/src/com/android/settings/localepicker/AppLocalePickerActivityTest.java
@@ -18,6 +18,8 @@
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.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
@@ -27,6 +29,7 @@
import android.app.Activity;
import android.app.ApplicationPackageManager;
import android.app.LocaleConfig;
+import android.app.settings.SettingsEnums;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
@@ -45,6 +48,7 @@
import com.android.internal.app.LocaleStore;
import com.android.settings.applications.AppInfoBase;
import com.android.settings.applications.AppLocaleUtil;
+import com.android.settings.testutils.FakeFeatureFactory;
import org.junit.After;
import org.junit.Before;
@@ -79,6 +83,7 @@
public class AppLocalePickerActivityTest {
private static final String TEST_PACKAGE_NAME = "com.android.settings";
private static final Uri TEST_PACKAGE_URI = Uri.parse("package:" + TEST_PACKAGE_NAME);
+ private FakeFeatureFactory mFeatureFactory;
@Mock
LocaleStore.LocaleInfo mLocaleInfo;
@@ -99,6 +104,7 @@
when(mLocaleConfig.getStatus()).thenReturn(LocaleConfig.STATUS_SUCCESS);
when(mLocaleConfig.getSupportedLocales()).thenReturn(LocaleList.forLanguageTags("en-US"));
ReflectionHelpers.setStaticField(AppLocaleUtil.class, "sLocaleConfig", mLocaleConfig);
+ mFeatureFactory = FakeFeatureFactory.setupForTest();
}
@After
@@ -210,6 +216,37 @@
assertThat(controller.get().isFinishing()).isTrue();
}
+ @Test
+ public void onLocaleSelected_logLocaleSource() {
+ ActivityController<TestAppLocalePickerActivity> controller =
+ initActivityController(true);
+ LocaleList.setDefault(LocaleList.forLanguageTags("ja-JP,en-CA,en-US"));
+ Locale locale = new Locale("en", "US");
+ when(mLocaleInfo.getLocale()).thenReturn(locale);
+ when(mLocaleInfo.isSystemLocale()).thenReturn(false);
+ when(mLocaleInfo.isSuggested()).thenReturn(true);
+ when(mLocaleInfo.isSuggestionOfType(LocaleStore.LocaleInfo.SUGGESTION_TYPE_SIM)).thenReturn(
+ true);
+ when(mLocaleInfo.isSuggestionOfType(
+ LocaleStore.LocaleInfo.SUGGESTION_TYPE_SYSTEM_AVAILABLE_LANGUAGE)).thenReturn(
+ true);
+ when(mLocaleInfo.isSuggestionOfType(
+ LocaleStore.LocaleInfo.SUGGESTION_TYPE_OTHER_APP_LANGUAGE)).thenReturn(
+ true);
+ when(mLocaleInfo.isSuggestionOfType(
+ LocaleStore.LocaleInfo.SUGGESTION_TYPE_IME_LANGUAGE)).thenReturn(
+ true);
+
+ controller.create();
+ AppLocalePickerActivity mActivity = controller.get();
+ mActivity.onLocaleSelected(mLocaleInfo);
+
+ int localeSource = 15; // SIM_LOCALE | SYSTEM_LOCALE |IME_LOCALE|APP_LOCALE
+ verify(mFeatureFactory.metricsFeatureProvider).action(
+ any(), eq(SettingsEnums.ACTION_CHANGE_APP_LANGUAGE_FROM_SUGGESTED),
+ eq(localeSource));
+ }
+
private ActivityController<TestAppLocalePickerActivity> initActivityController(
boolean hasPackageName) {
Intent data = new Intent();
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..5ff2baf 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,35 @@
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, "mParent", mLocaleListEditor);
+ 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/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/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 3fe3322..feea768 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
@@ -397,24 +390,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;
@@ -454,8 +430,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);
@@ -492,8 +466,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);
@@ -525,38 +497,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/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/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/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/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/details2/WifiDetailPreferenceController2Test.java b/tests/robotests/src/com/android/settings/wifi/details2/WifiDetailPreferenceController2Test.java
index c86a023..dcd9b36 100644
--- a/tests/robotests/src/com/android/settings/wifi/details2/WifiDetailPreferenceController2Test.java
+++ b/tests/robotests/src/com/android/settings/wifi/details2/WifiDetailPreferenceController2Test.java
@@ -701,10 +701,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 +712,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 +735,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
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/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/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..342405a
--- /dev/null
+++ b/tests/spa_unit/src/com/android/settings/spa/app/appcompat/UserAspectRatioAppPreferenceTest.kt
@@ -0,0 +1,204 @@
+/*
+ * 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.ActivityInfo
+import android.content.pm.ApplicationInfo
+import android.content.pm.PackageManager
+import android.content.pm.ResolveInfo
+import android.os.Build
+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 android.provider.DeviceConfig.NAMESPACE_WINDOW_MANAGER
+import com.android.settings.R
+import com.android.settings.applications.appinfo.AppInfoDashboardFragment
+import com.android.settings.applications.appcompat.UserAspectRatioDetails
+import com.android.settings.applications.appcompat.UserAspectRatioManager
+import com.android.settings.spa.app.appinfo.AppInfoSettingsProvider
+import com.android.settings.testutils.TestDeviceConfig
+import com.android.settingslib.spa.testutils.delay
+import com.android.settingslib.spa.testutils.waitUntilExists
+import org.junit.After
+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.Mockito.eq
+import org.mockito.Mockito.mock
+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_user_aspect_ratio_settings")
+
+ private lateinit var userAspectRatioManager: UserAspectRatioManager
+
+ @Mock
+ private lateinit var packageManager: PackageManager
+
+ @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)
+ userAspectRatioManager = mock(UserAspectRatioManager::class.java)
+ }
+
+ @After
+ fun tearDown() {
+ aspectRatioEnabledConfig.reset()
+ mockSession.finishMocking()
+ }
+
+ @Test
+ fun whenConfigIsFalse_notDisplayed() {
+ setConfig(false)
+
+ setContent()
+
+ composeTestRule.onRoot().assertIsNotDisplayed()
+ }
+
+ @Test
+ fun whenCannotDisplayAspectRatioUi_notDisplayed() {
+ setContent()
+
+ composeTestRule.onRoot().assertIsNotDisplayed()
+ }
+
+ @Test
+ fun whenCanDisplayAspectRatioUiAndConfigFalse_notDisplayed() {
+ setConfig(false)
+ whenever(packageManager.queryIntentActivities(any(), anyInt()))
+ .thenReturn(listOf(RESOLVE_INFO))
+
+ setContent()
+
+ composeTestRule.onRoot().assertIsNotDisplayed()
+ }
+
+ @Test
+ fun whenCannotDisplayAspectRatioUiAndConfigTrue_notDisplayed() {
+ setConfig(true)
+
+ setContent()
+
+ composeTestRule.onRoot().assertIsNotDisplayed()
+ }
+
+ @Test
+ fun whenCanDisplayAspectRatioUiAndConfigTrue_Displayed() {
+ setConfig(true)
+ whenever(packageManager.queryIntentActivities(any(), anyInt()))
+ .thenReturn(listOf(RESOLVE_INFO))
+
+ setContent()
+
+ composeTestRule.onNode(
+ hasTextExactly(
+ context.getString(R.string.aspect_ratio_title),
+ context.getString(R.string.user_aspect_ratio_app_default)
+ ),
+ ).assertIsDisplayed().assertIsEnabled()
+ }
+
+ @Test
+ fun onClick_startActivity() {
+ setConfig(true)
+ whenever(packageManager.queryIntentActivities(any(), anyInt()))
+ .thenReturn(listOf(RESOLVE_INFO))
+
+ 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 companion object {
+ const val PACKAGE_NAME = "package.name"
+ const val UID = 123
+ val APP = ApplicationInfo().apply {
+ packageName = PACKAGE_NAME
+ uid = UID
+ }
+ private val RESOLVE_INFO = ResolveInfo().apply {
+ activityInfo = ActivityInfo().apply {
+ packageName = PACKAGE_NAME
+ }
+ }
+ }
+}
\ 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..0d2869c
--- /dev/null
+++ b/tests/spa_unit/src/com/android/settings/spa/app/appcompat/UserAspectRatioAppsPageProviderTest.kt
@@ -0,0 +1,196 @@
+/*
+ * 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_title))
+ .assertIsDisplayed()
+ }
+
+ @Test
+ fun injectEntry_summary() {
+ setInjectEntry()
+ composeTestRule.onNodeWithText(context.getString(R.string.aspect_ratio_summary, Build.MODEL))
+ .assertIsDisplayed()
+ }
+
+ @Test
+ fun injectEntry_onClick_navigate() {
+ setInjectEntry()
+ composeTestRule.onNodeWithText(context.getString(R.string.aspect_ratio_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_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(override: Int): State<String> {
+ val listModel = UserAspectRatioAppListModel(context)
+ lateinit var summaryState: State<String>
+ composeTestRule.setContent {
+ summaryState = listModel.getSummary(option = 0,
+ record = UserAspectRatioAppListItemModel(
+ app = APP,
+ override = override,
+ 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,
+ override = USER_MIN_ASPECT_RATIO_UNSET,
+ suggested = true,
+ canDisplay = true
+ )
+ private val APP_RECORD_NOT_DISPLAYED = UserAspectRatioAppListItemModel(
+ APP,
+ override = 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/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 8e81218..82a488d 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",
@@ -32,6 +33,7 @@
"platform-test-annotations",
"truth-prebuilt",
"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 +42,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..f4dcaf8
--- /dev/null
+++ b/tests/unit/src/com/android/settings/applications/appcompat/UserAspectRatioManagerTest.java
@@ -0,0 +1,219 @@
+/*
+ * 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_3_2;
+import static android.content.pm.PackageManager.USER_MIN_ASPECT_RATIO_4_3;
+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 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.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.content.pm.ActivityInfo;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.ResolveInfo;
+import android.content.res.Resources;
+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.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * To run this test: atest SettingsUnitTests:UserAspectRatioManagerTest
+ */
+@RunWith(AndroidJUnit4.class)
+public class UserAspectRatioManagerTest {
+
+ private Context mContext;
+ private Resources mResources;
+ private UserAspectRatioManager mUtils;
+ private String mOriginalSettingsFlag;
+ private String mOriginalFullscreenFlag;
+
+ @Before
+ public void setUp() {
+ mContext = spy(ApplicationProvider.getApplicationContext());
+ mResources = spy(mContext.getResources());
+ mUtils = spy(new UserAspectRatioManager(mContext));
+
+ when(mContext.getResources()).thenReturn(mResources);
+
+ 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";
+ addResolveInfoLauncherEntry(canDisplay.packageName);
+
+ assertTrue(mUtils.canDisplayAspectRatioUi(canDisplay));
+
+ final ApplicationInfo noLauncherEntry = new ApplicationInfo();
+ noLauncherEntry.packageName = "com.app.nolauncherentry";
+
+ 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());
+ }
+
+ @Test
+ public void testIsFullscreenOptionEnabled_settingsDisabled_returnFalse() {
+ setAspectRatioFullscreenBuildTimeFlagEnabled(false);
+ assertFalse(mUtils.isFullscreenOptionEnabled());
+ }
+
+ @Test
+ public void testIsFullscreenOptionEnabled_disabledBuildTimeFlag_returnFalse() {
+ setAspectRatioFullscreenBuildTimeFlagEnabled(false);
+ assertFalse(mUtils.isFullscreenOptionEnabled());
+ }
+
+ @Test
+ public void testIsFullscreenOptionEnabled_disabledRuntimeFlag_returnFalse() {
+ setAspectRatioFullscreenDeviceConfigEnabled("false" /* enabled */, false /*makeDefault */);
+ assertFalse(mUtils.isFullscreenOptionEnabled());
+ }
+
+ @Test
+ public void containsAspectRatioOption_fullscreen() {
+ assertTrue(mUtils.containsAspectRatioOption(USER_MIN_ASPECT_RATIO_FULLSCREEN));
+
+ when(mUtils.isFullscreenOptionEnabled()).thenReturn(false);
+ assertFalse(mUtils.containsAspectRatioOption(USER_MIN_ASPECT_RATIO_FULLSCREEN));
+ }
+
+ @Test
+ public void testGetUserMinAspectRatioEntry() {
+ // 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))
+ .isEqualTo(appDefault);
+ // should always return default if value does not correspond to anything
+ assertThat(mUtils.getUserMinAspectRatioEntry(-1))
+ .isEqualTo(appDefault);
+ // R.string.user_aspect_ratio_half_screen
+ assertThat(mUtils.getUserMinAspectRatioEntry(USER_MIN_ASPECT_RATIO_SPLIT_SCREEN))
+ .isEqualTo(ResourcesUtils.getResourcesString(mContext,
+ "user_aspect_ratio_half_screen"));
+ // R.string.user_aspect_ratio_3_2
+ assertThat(mUtils.getUserMinAspectRatioEntry(USER_MIN_ASPECT_RATIO_3_2))
+ .isEqualTo(ResourcesUtils.getResourcesString(mContext, "user_aspect_ratio_3_2"));
+ // R,string.user_aspect_ratio_4_3
+ assertThat(mUtils.getUserMinAspectRatioEntry(USER_MIN_ASPECT_RATIO_4_3))
+ .isEqualTo(ResourcesUtils.getResourcesString(mContext, "user_aspect_ratio_4_3"));
+ // R.string.user_aspect_ratio_16_9
+ assertThat(mUtils.getUserMinAspectRatioEntry(USER_MIN_ASPECT_RATIO_16_9))
+ .isEqualTo(ResourcesUtils.getResourcesString(mContext, "user_aspect_ratio_16_9"));
+ // R.string.user_aspect_ratio_fullscreen
+ assertThat(mUtils.getUserMinAspectRatioEntry(USER_MIN_ASPECT_RATIO_FULLSCREEN))
+ .isEqualTo(ResourcesUtils.getResourcesString(mContext,
+ "user_aspect_ratio_fullscreen"));
+ }
+
+ @Test
+ public void testGetUserMinAspectRatioEntry_fullscreenDisabled_shouldReturnDefault() {
+ setAspectRatioFullscreenBuildTimeFlagEnabled(false);
+ assertThat(mUtils.getUserMinAspectRatioEntry(USER_MIN_ASPECT_RATIO_FULLSCREEN))
+ .isEqualTo(ResourcesUtils.getResourcesString(mContext,
+ "user_aspect_ratio_app_default"));
+ }
+
+ 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);
+ }
+
+ private void addResolveInfoLauncherEntry(String packageName) {
+ final ResolveInfo resolveInfo = mock(ResolveInfo.class);
+ final ActivityInfo activityInfo = mock(ActivityInfo.class);
+ activityInfo.packageName = packageName;
+ resolveInfo.activityInfo = activityInfo;
+ mUtils.addInfoHasLauncherEntry(resolveInfo);
+ }
+}
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/network/SubscriptionUtilTest.java b/tests/unit/src/com/android/settings/network/SubscriptionUtilTest.java
index 63dca7e..f063042 100644
--- a/tests/unit/src/com/android/settings/network/SubscriptionUtilTest.java
+++ b/tests/unit/src/com/android/settings/network/SubscriptionUtilTest.java
@@ -16,26 +16,32 @@
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;
@@ -445,6 +451,35 @@
}
@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 isInactiveInsertedPSim_nullSubInfo_doesNotCrash() {
assertThat(SubscriptionUtil.isInactiveInsertedPSim(null)).isFalse();
}
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/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/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");
+ }
+}