Merge changes from topic "b294333850-MailMail" into main
* changes:
Adds group to DND settings if channel names same
aconfig flag for dnd app settings channels dedupe
diff --git a/aconfig/OWNERS b/aconfig/OWNERS
index 1131545..c26a190 100644
--- a/aconfig/OWNERS
+++ b/aconfig/OWNERS
@@ -1 +1,3 @@
per-file settings_accessibility_flag_declarations.aconfig = file:/src/com/android/settings/accessibility/OWNERS
+per-file settings_biometrics_integration_declarations.aconfig = file:platform/vendor/unbundled_google/packages/SettingsGoogle:/future/biometrics/OWNERS
+
diff --git a/aconfig/settings_voice_activation_apps_flag_declarations.aconfig b/aconfig/settings_voice_activation_apps_flag_declarations.aconfig
index dccc805..d98bc52 100644
--- a/aconfig/settings_voice_activation_apps_flag_declarations.aconfig
+++ b/aconfig/settings_voice_activation_apps_flag_declarations.aconfig
@@ -1,8 +1,8 @@
package: "com.android.settings.flags"
flag {
- name: "enable_voice_activation_apps_special_app_access"
- namespace: "voice_activation_apps"
- description: "Enable voice activation apps in Special app access"
+ name: "enable_voice_activation_apps_in_settings"
+ namespace: "permissions"
+ description: "Enable voice activation apps in Settings"
bug: "303727896"
}
\ No newline at end of file
diff --git a/res/layout/audio_sharing_device_item.xml b/res/layout/audio_sharing_device_item.xml
new file mode 100644
index 0000000..f8e7454
--- /dev/null
+++ b/res/layout/audio_sharing_device_item.xml
@@ -0,0 +1,32 @@
+<?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.
+ -->
+
+<FrameLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+
+ <Button
+ android:id="@+id/device_button"
+ android:overScrollMode="never"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textAlignment="center"
+ android:text=""/>
+
+</FrameLayout>
\ No newline at end of file
diff --git a/res/layout/dialog_audio_sharing.xml b/res/layout/dialog_audio_sharing.xml
new file mode 100644
index 0000000..9624c90
--- /dev/null
+++ b/res/layout/dialog_audio_sharing.xml
@@ -0,0 +1,49 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:padding="24dp"
+ android:orientation="vertical">
+
+ <TextView
+ style="@style/device_info_dialog_value"
+ android:id="@+id/share_audio_subtitle1"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textAlignment="center"
+ android:layout_gravity="center"/>
+
+ <TextView
+ style="@style/device_info_dialog_value"
+ android:id="@+id/share_audio_subtitle2"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textAlignment="center"
+ android:layout_gravity="center"/>
+
+ <com.android.internal.widget.RecyclerView
+ android:visibility="visible"
+ android:id="@+id/btn_list"
+ android:nestedScrollingEnabled="false"
+ android:overScrollMode="never"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"/>
+</LinearLayout>
\ No newline at end of file
diff --git a/res/layout/fingerprint_v2_rfps_enroll_enrolling.xml b/res/layout/fingerprint_v2_rfps_enroll_enrolling.xml
new file mode 100644
index 0000000..0b087d2
--- /dev/null
+++ b/res/layout/fingerprint_v2_rfps_enroll_enrolling.xml
@@ -0,0 +1,75 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<com.google.android.setupdesign.GlifLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ style="?attr/fingerprint_layout_theme"
+ android:id="@+id/setup_wizard_layout"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ >
+
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ style="@style/SudContentFrame"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:clipChildren="false"
+ android:clipToPadding="false"
+ android:orientation="vertical">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="1"
+ android:gravity="center"
+ android:orientation="vertical">
+
+ <com.google.android.setupdesign.view.FillContentLayout
+ android:layout_width="@dimen/fingerprint_progress_bar_max_size"
+ android:layout_height="@dimen/fingerprint_progress_bar_max_size"
+ android:layout_marginVertical="24dp"
+ android:paddingTop="0dp"
+ android:paddingBottom="0dp">
+
+ <com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.rfps.ui.widget.RFPSProgressBar
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/fingerprint_progress_bar"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@drawable/fp_illustration"
+ android:minHeight="@dimen/fingerprint_progress_bar_min_size"
+ android:progress="0" />
+
+ </com.google.android.setupdesign.view.FillContentLayout>
+
+ <TextView
+ android:id="@+id/text"
+ style="@style/TextAppearance.ErrorText"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_horizontal|bottom"
+ android:accessibilityLiveRegion="polite"
+ android:gravity="center"
+ android:visibility="invisible" />
+
+ </LinearLayout>
+
+</LinearLayout>
+
+</com.google.android.setupdesign.GlifLayout>
\ No newline at end of file
diff --git a/res/layout/preference_widget_qrcode.xml b/res/layout/preference_widget_qrcode.xml
new file mode 100644
index 0000000..7994fe1
--- /dev/null
+++ b/res/layout/preference_widget_qrcode.xml
@@ -0,0 +1,25 @@
+<?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.
+ -->
+
+<ImageButton xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/button_icon"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:minWidth="@dimen/two_target_min_width"
+ android:minHeight="@dimen/min_tap_target_size"
+ android:layout_gravity="center"
+ android:background="?android:attr/selectableItemBackground"/>
\ No newline at end of file
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 0dae688..02b7a4c 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -1212,7 +1212,7 @@
<string name="private_space_hide_title">Hide when locked</string>
<!-- Title for the hide Private Space setting. [CHAR LIMIT=60] -->
<string name="privatespace_hide_page_title">Hide Private Space when locked</string>
- <!-- Description for hide Private Space settings page. [CHAR LIMIT=60] -->
+ <!-- Description for hide Private Space settings page. [CHAR LIMIT=NONE] -->
<string name="privatespace_hide_page_summary">To stop other people knowing Private Space is on your device, you can hide it from your apps list</string>
<!-- Header in hide Private Space settings page to access Private Space when hidden. [CHAR LIMIT=60] -->
<string name="privatespace_access_header">Access Private Space when hidden</string>
@@ -3169,6 +3169,8 @@
<string name="apn_settings">APNs</string>
<!-- Screen title after user selects APNs setting option -->
<string name="apn_edit">Edit access point</string>
+ <!-- Screen title after user selects add APNs setting -->
+ <string name="apn_add">Add access point</string>
<!-- Edit access point label summary text when no value has been set -->
<string name="apn_not_set">Not set</string>
<!-- Edit access point label summary text when no value has been set for mvno value. [CHAR LIMIT=NONE]-->
@@ -5434,6 +5436,10 @@
<!-- Category title for battery background settings in power usage detail page [CHAR LIMIT=NONE] -->
<string name="manager_battery_usage_category_title">Manage battery usage</string>
+ <!-- Title for allow background usage [CHAR LIMIT=NONE] -->
+ <string name="manager_battery_usage_allow_background_usage_title">Allow background usage</string>
+ <!-- Summary for allow background usage [CHAR LIMIT=NONE] -->
+ <string name="manager_battery_usage_allow_background_usage_summary">Enable for real-time updates, disable to save battery</string>
<!-- Title for the battery unrestricted settings [CHAR_LIMIT=40] -->
<string name="manager_battery_usage_unrestricted_title">Unrestricted</string>
<!-- Title for the battery optimized settings [CHAR_LIMIT=40] -->
@@ -9514,6 +9520,13 @@
<!-- Label for showing apps that can manage external storage[CHAR LIMIT=45] -->
<string name="filter_manage_external_storage">Can access all files</string>
+ <!-- Voice Activation apps settings title [CHAR LIMIT=40] -->
+ <string name="voice_activation_apps_title">Voice activation apps</string>
+ <!-- Label for a setting which controls whether an app can be voice activated [CHAR LIMIT=NONE] -->
+ <string name="permit_voice_activation_apps">Allow voice activation</string>
+ <!-- Description for a setting which controls whether an app can be voice activated [CHAR LIMIT=NONE] -->
+ <string name ="allow_voice_activation_apps_description">Voice activation turns-on approved apps, hands-free, using voice command.\n\nUntill activated, none of these apps can directly access your microphone.Instead, this device uses built-in proteced adaptive sensing to turn-on aprroved apps for you.\n\n<a href="">More about protected adaptive sensing</a></string>
+
<!-- Manage full screen intent permission title [CHAR LIMIT=40] -->
<string name="full_screen_intent_title">Full screen notifications</string>
diff --git a/res/xml/bluetooth_audio_sharing.xml b/res/xml/bluetooth_audio_sharing.xml
index 86bb062..ca7137a 100644
--- a/res/xml/bluetooth_audio_sharing.xml
+++ b/res/xml/bluetooth_audio_sharing.xml
@@ -26,6 +26,12 @@
settings:controller="com.android.settings.connecteddevice.audiosharing.CallsAndAlarmsPreferenceController"
android:summary=""/>
+ <com.android.settings.connecteddevice.audiosharing.AudioSharingNamePreference
+ android:key="audio_sharing_stream_name"
+ android:title="Stream name"
+ android:summary="********"
+ settings:controller="com.android.settings.connecteddevice.audiosharing.AudioSharingNamePreferenceController"/>
+
<PreferenceCategory
android:key="audio_streams_settings_category"
android:title="@string/audio_sharing_streams_category_title"
@@ -38,5 +44,4 @@
android:icon="@drawable/ic_chevron_right_24dp" />
</PreferenceCategory>
-
</PreferenceScreen>
\ No newline at end of file
diff --git a/res/xml/power_background_usage_detail.xml b/res/xml/power_background_usage_detail.xml
new file mode 100644
index 0000000..fb089fd
--- /dev/null
+++ b/res/xml/power_background_usage_detail.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/manager_battery_usage_allow_background_usage_title">
+
+ <com.android.settingslib.widget.LayoutPreference
+ android:key="header_view"
+ android:layout="@layout/settings_entity_header"
+ android:selectable="false"/>
+
+ <com.android.settingslib.widget.MainSwitchPreference
+ android:key="allow_background_usage"
+ android:title="@string/manager_battery_usage_allow_background_usage_title"
+ settings:controller="com.android.settings.fuelgauge.AllowBackgroundPreferenceController"/>
+
+ <com.android.settingslib.widget.SelectorWithWidgetPreference
+ android:key="optimized_preference"
+ android:title="@string/manager_battery_usage_optimized_title"
+ android:summary="@string/manager_battery_usage_optimized_summary"
+ settings:controller="com.android.settings.fuelgauge.OptimizedPreferenceController"/>
+
+ <com.android.settingslib.widget.SelectorWithWidgetPreference
+ android:key="unrestricted_preference"
+ android:title="@string/manager_battery_usage_unrestricted_title"
+ android:summary="@string/manager_battery_usage_unrestricted_summary"
+ settings:controller="com.android.settings.fuelgauge.UnrestrictedPreferenceController"/>
+
+ <com.android.settingslib.widget.FooterPreference
+ android:key="app_usage_footer_preference"
+ android:title="@string/manager_battery_usage_footer"
+ android:selectable="false"
+ settings:searchable="false"/>
+</PreferenceScreen>
\ No newline at end of file
diff --git a/res/xml/power_usage_detail.xml b/res/xml/power_usage_detail.xml
index 7b92f99..f3b30b6 100644
--- a/res/xml/power_usage_detail.xml
+++ b/res/xml/power_usage_detail.xml
@@ -50,30 +50,11 @@
android:title="@string/manager_battery_usage_category_title"
android:key="manage_battery_usage_category">
- <com.android.settingslib.widget.SelectorWithWidgetPreference
- android:key="unrestricted_pref"
- android:summary="@string/manager_battery_usage_unrestricted_summary"
- android:title="@string/manager_battery_usage_unrestricted_title"
- settings:controller="com.android.settings.fuelgauge.UnrestrictedPreferenceController"/>
-
- <com.android.settingslib.widget.SelectorWithWidgetPreference
- android:key="optimized_pref"
- android:summary="@string/manager_battery_usage_optimized_summary"
- android:title="@string/manager_battery_usage_optimized_title"
- settings:controller="com.android.settings.fuelgauge.OptimizedPreferenceController"/>
-
- <com.android.settingslib.widget.SelectorWithWidgetPreference
- android:key="restricted_pref"
- android:summary="@string/manager_battery_usage_restricted_summary"
- android:title="@string/manager_battery_usage_restricted_title"
- settings:controller="com.android.settings.fuelgauge.RestrictedPreferenceController"/>
+ <com.android.settingslib.PrimarySwitchPreference
+ android:key="allow_background_usage"
+ android:title="@string/manager_battery_usage_allow_background_usage_title"
+ settings:controller="com.android.settings.fuelgauge.AllowBackgroundPreferenceController"/>
</PreferenceCategory>
- <com.android.settingslib.widget.FooterPreference
- android:key="app_usage_footer_preference"
- android:title="@string/manager_battery_usage_footer"
- android:selectable="false"
- settings:searchable="false"/>
-
</PreferenceScreen>
\ No newline at end of file
diff --git a/res/xml/special_access.xml b/res/xml/special_access.xml
index b3f3f7d..3f3d75d 100644
--- a/res/xml/special_access.xml
+++ b/res/xml/special_access.xml
@@ -100,6 +100,11 @@
settings:controller="com.android.settings.spa.app.specialaccess.UseFullScreenIntentPreferenceController" />
<Preference
+ android:key="voice_activation_apps"
+ android:title="@string/voice_activation_apps_title"
+ settings:controller="com.android.settings.spa.app.specialaccess.VoiceActivationAppsPreferenceController" />
+
+ <Preference
android:key="picture_in_picture"
android:title="@string/picture_in_picture_title"
android:order="-1100"
diff --git a/src/com/android/settings/SettingsActivityUtil.kt b/src/com/android/settings/SettingsActivityUtil.kt
index 65d26de..c23bc18 100644
--- a/src/com/android/settings/SettingsActivityUtil.kt
+++ b/src/com/android/settings/SettingsActivityUtil.kt
@@ -37,6 +37,7 @@
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.VoiceActivationAppsListProvider
import com.android.settings.spa.app.specialaccess.WifiControlAppListProvider
import com.android.settings.wifi.ChangeWifiStateDetails
@@ -65,6 +66,8 @@
WifiControlAppListProvider.getAppInfoRoutePrefix(),
NfcTagAppsSettingsProvider::class.qualifiedName to
NfcTagAppsSettingsProvider.getAppInfoRoutePrefix(),
+ VoiceActivationAppsListProvider::class.qualifiedName to
+ VoiceActivationAppsListProvider.getAppInfoRoutePrefix(),
)
@JvmStatic
diff --git a/src/com/android/settings/applications/manageapplications/ManageApplicationsUtil.kt b/src/com/android/settings/applications/manageapplications/ManageApplicationsUtil.kt
index 216ce47..a1ba5a8 100644
--- a/src/com/android/settings/applications/manageapplications/ManageApplicationsUtil.kt
+++ b/src/com/android/settings/applications/manageapplications/ManageApplicationsUtil.kt
@@ -67,6 +67,7 @@
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.TurnScreenOnAppsAppListProvider
import com.android.settings.spa.app.specialaccess.WifiControlAppListProvider
import com.android.settings.spa.app.storage.StorageAppListPageProvider
import com.android.settings.spa.notification.AppListNotificationsPageProvider
@@ -120,6 +121,7 @@
LIST_TYPE_MAIN -> AllAppListPageProvider.name
LIST_TYPE_NFC_TAG_APPS -> NfcTagAppsSettingsProvider.getAppListRoute()
LIST_TYPE_USER_ASPECT_RATIO_APPS -> UserAspectRatioAppsPageProvider.name
+ LIST_TYPE_TURN_SCREEN_ON -> TurnScreenOnAppsAppListProvider.getAppListRoute()
// TODO(b/292165031) enable once sorting is supported
//LIST_TYPE_STORAGE -> StorageAppListPageProvider.Apps.name
//LIST_TYPE_GAMES -> StorageAppListPageProvider.Games.name
diff --git a/src/com/android/settings/biometrics/fingerprint2/conversion/Util.kt b/src/com/android/settings/biometrics/fingerprint2/conversion/Util.kt
index 98b7ed0..58ef509 100644
--- a/src/com/android/settings/biometrics/fingerprint2/conversion/Util.kt
+++ b/src/com/android/settings/biometrics/fingerprint2/conversion/Util.kt
@@ -16,14 +16,61 @@
package com.android.settings.biometrics.fingerprint2.conversion
+import android.hardware.biometrics.BiometricFingerprintConstants.FINGERPRINT_ERROR_CANCELED
+import android.hardware.biometrics.BiometricFingerprintConstants.FINGERPRINT_ERROR_UNABLE_TO_PROCESS
import android.hardware.fingerprint.FingerprintManager
+import com.android.settings.R
import com.android.settings.biometrics.fingerprint2.shared.model.EnrollReason
+import com.android.settings.biometrics.fingerprint2.shared.model.FingerEnrollState
-class Util
-
-fun EnrollReason.toOriginalReason(): Int {
- return when (this) {
- EnrollReason.EnrollEnrolling -> FingerprintManager.ENROLL_ENROLL
- EnrollReason.FindSensor -> FingerprintManager.ENROLL_FIND_SENSOR
+object Util {
+ fun EnrollReason.toOriginalReason(): Int {
+ return when (this) {
+ EnrollReason.EnrollEnrolling -> FingerprintManager.ENROLL_ENROLL
+ EnrollReason.FindSensor -> FingerprintManager.ENROLL_FIND_SENSOR
+ }
}
+
+ fun Int.toEnrollError(isSetupWizard: Boolean): FingerEnrollState.EnrollError {
+ val errTitle =
+ when (this) {
+ FingerprintManager.FINGERPRINT_ERROR_TIMEOUT ->
+ R.string.security_settings_fingerprint_enroll_error_dialog_title
+ FingerprintManager.FINGERPRINT_ERROR_BAD_CALIBRATION ->
+ R.string.security_settings_fingerprint_bad_calibration_title
+ else -> R.string.security_settings_fingerprint_enroll_error_unable_to_process_dialog_title
+ }
+ val errString =
+ if (isSetupWizard) {
+ when (this) {
+ FingerprintManager.FINGERPRINT_ERROR_TIMEOUT ->
+ R.string.security_settings_fingerprint_enroll_error_dialog_title
+ FingerprintManager.FINGERPRINT_ERROR_BAD_CALIBRATION ->
+ R.string.security_settings_fingerprint_bad_calibration_title
+ else -> R.string.security_settings_fingerprint_enroll_error_unable_to_process_dialog_title
+ }
+ } else {
+ when (this) {
+ // This message happens when the underlying crypto layer
+ // decides to revoke the enrollment auth token
+ FingerprintManager.FINGERPRINT_ERROR_TIMEOUT ->
+ R.string.security_settings_fingerprint_enroll_error_timeout_dialog_message
+ FingerprintManager.FINGERPRINT_ERROR_BAD_CALIBRATION ->
+ R.string.security_settings_fingerprint_bad_calibration
+ FingerprintManager.FINGERPRINT_ERROR_UNABLE_TO_PROCESS ->
+ R.string.security_settings_fingerprint_enroll_error_unable_to_process_message
+ // There's nothing specific to tell the user about. Ask them to try again.
+ else -> R.string.security_settings_fingerprint_enroll_error_generic_dialog_message
+ }
+ }
+
+ return FingerEnrollState.EnrollError(
+ errTitle,
+ errString,
+ this == FINGERPRINT_ERROR_UNABLE_TO_PROCESS,
+ this == FINGERPRINT_ERROR_CANCELED,
+ )
+ }
+
}
+
diff --git a/src/com/android/settings/biometrics/fingerprint2/domain/interactor/FingerprintManagerInteractorImpl.kt b/src/com/android/settings/biometrics/fingerprint2/domain/interactor/FingerprintManagerInteractorImpl.kt
index 5c9232f..984d04c 100644
--- a/src/com/android/settings/biometrics/fingerprint2/domain/interactor/FingerprintManagerInteractorImpl.kt
+++ b/src/com/android/settings/biometrics/fingerprint2/domain/interactor/FingerprintManagerInteractorImpl.kt
@@ -24,12 +24,16 @@
import android.os.CancellationSignal
import android.util.Log
import com.android.settings.biometrics.GatekeeperPasswordProvider
-import com.android.settings.biometrics.fingerprint2.conversion.toOriginalReason
+import com.android.settings.biometrics.fingerprint2.conversion.Util.toEnrollError
+import com.android.settings.biometrics.fingerprint2.conversion.Util.toOriginalReason
+import com.android.settings.biometrics.fingerprint2.shared.data.repository.PressToAuthProvider
import com.android.settings.biometrics.fingerprint2.shared.domain.interactor.FingerprintManagerInteractor
import com.android.settings.biometrics.fingerprint2.shared.model.EnrollReason
-import com.android.settings.biometrics.fingerprint2.shared.model.FingerEnrollStateViewModel
-import com.android.settings.biometrics.fingerprint2.shared.model.FingerprintAuthAttemptViewModel
-import com.android.settings.biometrics.fingerprint2.shared.model.FingerprintViewModel
+import com.android.settings.biometrics.fingerprint2.shared.model.FingerprintFlow
+import com.android.settings.biometrics.fingerprint2.shared.model.FingerEnrollState
+import com.android.settings.biometrics.fingerprint2.shared.model.FingerprintAuthAttemptModel
+import com.android.settings.biometrics.fingerprint2.shared.model.FingerprintData
+import com.android.settings.biometrics.fingerprint2.shared.model.SetupWizard
import com.android.settings.password.ChooseLockSettingsHelper
import com.android.systemui.biometrics.shared.model.toFingerprintSensor
import kotlin.coroutines.resume
@@ -38,9 +42,12 @@
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.channels.onFailure
+import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.flow
+import kotlinx.coroutines.flow.update
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withContext
@@ -51,7 +58,8 @@
private val backgroundDispatcher: CoroutineDispatcher,
private val fingerprintManager: FingerprintManager,
private val gatekeeperPasswordProvider: GatekeeperPasswordProvider,
- private val pressToAuthProvider: () -> Boolean,
+ private val pressToAuthProvider: PressToAuthProvider,
+ private val fingerprintFlow: FingerprintFlow,
) : FingerprintManagerInteractor {
private val maxFingerprints =
@@ -60,6 +68,8 @@
)
private val applicationContext = applicationContext.applicationContext
+ private val enrollRequestOutstanding = MutableStateFlow(false)
+
override suspend fun generateChallenge(gateKeeperPasswordHandle: Long): Pair<Long, ByteArray> =
suspendCoroutine {
val callback = GenerateChallengeCallback { _, userId, challenge ->
@@ -75,11 +85,11 @@
fingerprintManager.generateChallenge(applicationContext.userId, callback)
}
- override val enrolledFingerprints: Flow<List<FingerprintViewModel>> = flow {
+ override val enrolledFingerprints: Flow<List<FingerprintData>> = flow {
emit(
fingerprintManager
.getEnrolledFingerprints(applicationContext.userId)
- .map { (FingerprintViewModel(it.name.toString(), it.biometricId, it.deviceId)) }
+ .map { (FingerprintData(it.name.toString(), it.biometricId, it.deviceId)) }
.toList()
)
}
@@ -103,28 +113,51 @@
override suspend fun enroll(
hardwareAuthToken: ByteArray?,
enrollReason: EnrollReason,
- ): Flow<FingerEnrollStateViewModel> = callbackFlow {
+ ): Flow<FingerEnrollState> = callbackFlow {
+ // TODO (b/308456120) Improve this logic
+ if (enrollRequestOutstanding.value) {
+ Log.d(TAG, "Outstanding enroll request, waiting 150ms")
+ delay(150)
+ if (enrollRequestOutstanding.value) {
+ Log.e(TAG, "Request still present, continuing")
+ }
+ }
+
+ enrollRequestOutstanding.update { true }
+
var streamEnded = false
+ var totalSteps: Int? = null
val enrollmentCallback =
object : FingerprintManager.EnrollmentCallback() {
override fun onEnrollmentProgress(remaining: Int) {
- trySend(FingerEnrollStateViewModel.EnrollProgress(remaining)).onFailure { error ->
+ // This is sort of an implementation detail, but unfortunately the API isn't
+ // very expressive. If anything we should look at changing the FingerprintManager API.
+ if (totalSteps == null) {
+ totalSteps = remaining + 1
+ }
+
+ trySend(FingerEnrollState.EnrollProgress(remaining, totalSteps!!)).onFailure {
+ error ->
Log.d(TAG, "onEnrollmentProgress($remaining) failed to send, due to $error")
}
+
if (remaining == 0) {
streamEnded = true
+ enrollRequestOutstanding.update { false }
}
}
override fun onEnrollmentHelp(helpMsgId: Int, helpString: CharSequence?) {
- trySend(FingerEnrollStateViewModel.EnrollHelp(helpMsgId, helpString.toString()))
+ trySend(FingerEnrollState.EnrollHelp(helpMsgId, helpString.toString()))
.onFailure { error -> Log.d(TAG, "onEnrollmentHelp failed to send, due to $error") }
}
override fun onEnrollmentError(errMsgId: Int, errString: CharSequence?) {
- trySend(FingerEnrollStateViewModel.EnrollError(errMsgId, errString.toString()))
+ trySend(errMsgId.toEnrollError(fingerprintFlow == SetupWizard))
.onFailure { error -> Log.d(TAG, "onEnrollmentError failed to send, due to $error") }
+ Log.d(TAG, "onEnrollmentError($errMsgId)")
streamEnded = true
+ enrollRequestOutstanding.update { false }
}
}
@@ -140,12 +173,13 @@
// If the stream has not been ended, and the user has stopped collecting the flow
// before it was over, send cancel.
if (!streamEnded) {
+ Log.e(TAG, "Cancel is sent from settings for enroll()")
cancellationSignal.cancel()
}
}
}
- override suspend fun removeFingerprint(fp: FingerprintViewModel): Boolean = suspendCoroutine {
+ override suspend fun removeFingerprint(fp: FingerprintData): Boolean = suspendCoroutine {
val callback =
object : RemovalCallback() {
override fun onRemovalError(
@@ -170,7 +204,7 @@
)
}
- override suspend fun renameFingerprint(fp: FingerprintViewModel, newName: String) {
+ override suspend fun renameFingerprint(fp: FingerprintData, newName: String) {
withContext(backgroundDispatcher) {
fingerprintManager.rename(fp.fingerId, applicationContext.userId, newName)
}
@@ -181,11 +215,11 @@
}
override suspend fun pressToAuthEnabled(): Boolean = suspendCancellableCoroutine {
- it.resume(pressToAuthProvider())
+ it.resume(pressToAuthProvider.isEnabled)
}
- override suspend fun authenticate(): FingerprintAuthAttemptViewModel =
- suspendCancellableCoroutine { c: CancellableContinuation<FingerprintAuthAttemptViewModel> ->
+ override suspend fun authenticate(): FingerprintAuthAttemptModel =
+ suspendCancellableCoroutine { c: CancellableContinuation<FingerprintAuthAttemptModel> ->
val authenticationCallback =
object : FingerprintManager.AuthenticationCallback() {
@@ -195,7 +229,7 @@
Log.d(TAG, "framework sent down onAuthError after finish")
return
}
- c.resume(FingerprintAuthAttemptViewModel.Error(errorCode, errString.toString()))
+ c.resume(FingerprintAuthAttemptModel.Error(errorCode, errString.toString()))
}
override fun onAuthenticationSucceeded(result: FingerprintManager.AuthenticationResult) {
@@ -204,7 +238,7 @@
Log.d(TAG, "framework sent down onAuthError after finish")
return
}
- c.resume(FingerprintAuthAttemptViewModel.Success(result.fingerprint?.biometricId ?: -1))
+ c.resume(FingerprintAuthAttemptModel.Success(result.fingerprint?.biometricId ?: -1))
}
}
diff --git a/src/com/android/settings/biometrics/fingerprint2/repository/PressToAuthProviderImpl.kt b/src/com/android/settings/biometrics/fingerprint2/repository/PressToAuthProviderImpl.kt
new file mode 100644
index 0000000..38c5335
--- /dev/null
+++ b/src/com/android/settings/biometrics/fingerprint2/repository/PressToAuthProviderImpl.kt
@@ -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.biometrics.fingerprint2.repository
+
+import android.content.Context
+import android.provider.Settings
+import com.android.settings.biometrics.fingerprint2.shared.data.repository.PressToAuthProvider
+
+class PressToAuthProviderImpl(val context: Context) : PressToAuthProvider {
+ override val isEnabled: Boolean
+ get() {
+ var toReturn: Int =
+ Settings.Secure.getIntForUser(
+ context.contentResolver,
+ Settings.Secure.SFPS_PERFORMANT_AUTH_ENABLED,
+ -1,
+ context.userId,
+ )
+ if (toReturn == -1) {
+ toReturn =
+ if (
+ context.resources.getBoolean(com.android.internal.R.bool.config_performantAuthDefault)
+ ) {
+ 1
+ } else {
+ 0
+ }
+ Settings.Secure.putIntForUser(
+ context.contentResolver,
+ Settings.Secure.SFPS_PERFORMANT_AUTH_ENABLED,
+ toReturn,
+ context.userId
+ )
+ }
+ return (toReturn == 1)
+ }
+}
diff --git a/src/com/android/settings/biometrics/fingerprint2/shared/model/FingerprintViewModel.kt b/src/com/android/settings/biometrics/fingerprint2/shared/data/repository/PressToAuthProvider.kt
similarity index 60%
copy from src/com/android/settings/biometrics/fingerprint2/shared/model/FingerprintViewModel.kt
copy to src/com/android/settings/biometrics/fingerprint2/shared/data/repository/PressToAuthProvider.kt
index db28e79..e776b9a 100644
--- a/src/com/android/settings/biometrics/fingerprint2/shared/model/FingerprintViewModel.kt
+++ b/src/com/android/settings/biometrics/fingerprint2/shared/data/repository/PressToAuthProvider.kt
@@ -14,21 +14,14 @@
* limitations under the License.
*/
-package com.android.settings.biometrics.fingerprint2.shared.model
+package com.android.settings.biometrics.fingerprint2.shared.data.repository
-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()
-}
+/**
+ * Interface that indicates if press to auth is on or off.
+ */
+interface PressToAuthProvider {
+ /**
+ * Indicates true if the PressToAuth feature is enabled, false otherwise.
+ */
+ val isEnabled: Boolean
+}
\ No newline at end of file
diff --git a/src/com/android/settings/biometrics/fingerprint2/shared/domain/interactor/FingerprintManagerInteractor.kt b/src/com/android/settings/biometrics/fingerprint2/shared/domain/interactor/FingerprintManagerInteractor.kt
index 7286715..94afa49 100644
--- a/src/com/android/settings/biometrics/fingerprint2/shared/domain/interactor/FingerprintManagerInteractor.kt
+++ b/src/com/android/settings/biometrics/fingerprint2/shared/domain/interactor/FingerprintManagerInteractor.kt
@@ -17,9 +17,9 @@
package com.android.settings.biometrics.fingerprint2.shared.domain.interactor
import com.android.settings.biometrics.fingerprint2.shared.model.EnrollReason
-import com.android.settings.biometrics.fingerprint2.shared.model.FingerprintAuthAttemptViewModel
-import com.android.settings.biometrics.fingerprint2.shared.model.FingerprintViewModel
-import com.android.settings.biometrics.fingerprint2.shared.model.FingerEnrollStateViewModel
+import com.android.settings.biometrics.fingerprint2.shared.model.FingerprintAuthAttemptModel
+import com.android.settings.biometrics.fingerprint2.shared.model.FingerprintData
+import com.android.settings.biometrics.fingerprint2.shared.model.FingerEnrollState
import com.android.systemui.biometrics.shared.model.FingerprintSensor
import kotlinx.coroutines.flow.Flow
@@ -31,7 +31,7 @@
*/
interface FingerprintManagerInteractor {
/** Returns the list of current fingerprints. */
- val enrolledFingerprints: Flow<List<FingerprintViewModel>>
+ val enrolledFingerprints: Flow<List<FingerprintData>>
/** Returns the max enrollable fingerprints, note during SUW this might be 1 */
val maxEnrollableFingerprints: Flow<Int>
@@ -43,7 +43,7 @@
val sensorPropertiesInternal: Flow<FingerprintSensor?>
/** Runs the authenticate flow */
- suspend fun authenticate(): FingerprintAuthAttemptViewModel
+ suspend fun authenticate(): FingerprintAuthAttemptModel
/**
* Generates a challenge with the provided [gateKeeperPasswordHandle] and on success returns a
@@ -56,22 +56,22 @@
/**
* Runs [FingerprintManager.enroll] with the [hardwareAuthToken] and [EnrollReason] for this
- * enrollment. Returning the [FingerEnrollStateViewModel] that represents this fingerprint
+ * enrollment. Returning the [FingerEnrollState] that represents this fingerprint
* enrollment state.
*/
suspend fun enroll(
- hardwareAuthToken: ByteArray?,
- enrollReason: EnrollReason,
- ): Flow<FingerEnrollStateViewModel>
+ hardwareAuthToken: ByteArray?,
+ enrollReason: EnrollReason,
+ ): Flow<FingerEnrollState>
/**
* Removes the given fingerprint, returning true if it was successfully removed and false
* otherwise
*/
- suspend fun removeFingerprint(fp: FingerprintViewModel): Boolean
+ suspend fun removeFingerprint(fp: FingerprintData): Boolean
/** Renames the given fingerprint if one exists */
- suspend fun renameFingerprint(fp: FingerprintViewModel, newName: String)
+ suspend fun renameFingerprint(fp: FingerprintData, newName: String)
/** Indicates if the device has side fingerprint */
suspend fun hasSideFps(): Boolean
diff --git a/src/com/android/settings/biometrics/fingerprint2/shared/model/EnrollReasonViewModel.kt b/src/com/android/settings/biometrics/fingerprint2/shared/model/EnrollReason.kt
similarity index 100%
rename from src/com/android/settings/biometrics/fingerprint2/shared/model/EnrollReasonViewModel.kt
rename to src/com/android/settings/biometrics/fingerprint2/shared/model/EnrollReason.kt
diff --git a/src/com/android/settings/biometrics/fingerprint2/shared/model/FingerEnrollStateViewModel.kt b/src/com/android/settings/biometrics/fingerprint2/shared/model/FingerEnrollState.kt
similarity index 73%
rename from src/com/android/settings/biometrics/fingerprint2/shared/model/FingerEnrollStateViewModel.kt
rename to src/com/android/settings/biometrics/fingerprint2/shared/model/FingerEnrollState.kt
index 179ac60..4766d59 100644
--- a/src/com/android/settings/biometrics/fingerprint2/shared/model/FingerEnrollStateViewModel.kt
+++ b/src/com/android/settings/biometrics/fingerprint2/shared/model/FingerEnrollState.kt
@@ -22,19 +22,28 @@
* Represents a fingerprint enrollment state. See [FingerprintManager.EnrollmentCallback] for more
* information
*/
-sealed class FingerEnrollStateViewModel {
- /** Represents enrollment step progress. */
+sealed class FingerEnrollState {
+ /**
+ * Represents an enrollment step progress.
+ *
+ * Progress is obtained by (totalStepsRequired - remainingSteps) / totalStepsRequired
+ */
data class EnrollProgress(
val remainingSteps: Int,
- ) : FingerEnrollStateViewModel()
+ val totalStepsRequired: Int,
+ ) : FingerEnrollState()
+
/** Represents that recoverable error has been encountered during enrollment. */
data class EnrollHelp(
@StringRes val helpMsgId: Int,
val helpString: String,
- ) : FingerEnrollStateViewModel()
+ ) : FingerEnrollState()
+
/** Represents that an unrecoverable error has been encountered and the operation is complete. */
data class EnrollError(
- @StringRes val errMsgId: Int,
- val errString: String,
- ) : FingerEnrollStateViewModel()
+ @StringRes val errTitle: Int,
+ @StringRes val errString: Int,
+ val shouldRetryEnrollment: Boolean,
+ val isCancelled: Boolean,
+ ) : FingerEnrollState()
}
diff --git a/src/com/android/settings/biometrics/fingerprint2/shared/model/FingerprintViewModel.kt b/src/com/android/settings/biometrics/fingerprint2/shared/model/FingerprintData.kt
similarity index 84%
rename from src/com/android/settings/biometrics/fingerprint2/shared/model/FingerprintViewModel.kt
rename to src/com/android/settings/biometrics/fingerprint2/shared/model/FingerprintData.kt
index db28e79..b2aa25c 100644
--- a/src/com/android/settings/biometrics/fingerprint2/shared/model/FingerprintViewModel.kt
+++ b/src/com/android/settings/biometrics/fingerprint2/shared/model/FingerprintData.kt
@@ -16,19 +16,19 @@
package com.android.settings.biometrics.fingerprint2.shared.model
-data class FingerprintViewModel(
+data class FingerprintData(
val name: String,
val fingerId: Int,
val deviceId: Long,
)
-sealed class FingerprintAuthAttemptViewModel {
+sealed class FingerprintAuthAttemptModel {
data class Success(
val fingerId: Int,
- ) : FingerprintAuthAttemptViewModel()
+ ) : FingerprintAuthAttemptModel()
data class Error(
val error: Int,
val message: String,
- ) : FingerprintAuthAttemptViewModel()
+ ) : FingerprintAuthAttemptModel()
}
diff --git a/src/com/android/settings/biometrics/fingerprint2/shared/model/FingerprintFlow.kt b/src/com/android/settings/biometrics/fingerprint2/shared/model/FingerprintFlow.kt
new file mode 100644
index 0000000..93c7577
--- /dev/null
+++ b/src/com/android/settings/biometrics/fingerprint2/shared/model/FingerprintFlow.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.biometrics.fingerprint2.shared.model
+
+/**
+ * The [FingerprintFlow] for fingerprint enrollment indicates information on how the flow should behave.
+ */
+sealed class FingerprintFlow
+
+/** The default enrollment experience, typically called from Settings */
+data object Default : FingerprintFlow()
+
+/** SetupWizard/Out of box experience (OOBE) enrollment type. */
+data object SetupWizard : FingerprintFlow()
+
+/** Unicorn enrollment type */
+data object Unicorn : FingerprintFlow()
+
+/** Flow to specify settings type */
+data object Settings : FingerprintFlow()
diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/activity/FingerprintEnrollmentV2Activity.kt b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/activity/FingerprintEnrollmentV2Activity.kt
index 58fcea6..de2a1ee 100644
--- a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/activity/FingerprintEnrollmentV2Activity.kt
+++ b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/activity/FingerprintEnrollmentV2Activity.kt
@@ -16,12 +16,9 @@
package com.android.settings.biometrics.fingerprint2.ui.enrollment.activity
-import android.annotation.ColorInt
import android.app.Activity
import android.content.Intent
-import android.content.res.ColorStateList
import android.content.res.Configuration
-import android.graphics.Color
import android.hardware.fingerprint.FingerprintManager
import android.os.Bundle
import android.provider.Settings
@@ -35,22 +32,27 @@
import com.android.internal.widget.LockPatternUtils
import com.android.settings.R
import com.android.settings.SetupWizardUtils
-import com.android.settings.Utils
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.RESULT_FINISHED
import com.android.settings.biometrics.GatekeeperPasswordProvider
import com.android.settings.biometrics.fingerprint2.domain.interactor.FingerprintManagerInteractorImpl
-import com.android.settings.biometrics.fingerprint2.shared.model.FingerprintViewModel
+import com.android.settings.biometrics.fingerprint2.shared.model.Default
+import com.android.settings.biometrics.fingerprint2.shared.model.SetupWizard
+import com.android.settings.biometrics.fingerprint2.repository.PressToAuthProviderImpl
import com.android.settings.biometrics.fingerprint2.ui.enrollment.fragment.FingerprintEnrollConfirmationV2Fragment
import com.android.settings.biometrics.fingerprint2.ui.enrollment.fragment.FingerprintEnrollEnrollingV2Fragment
import com.android.settings.biometrics.fingerprint2.ui.enrollment.fragment.FingerprintEnrollFindSensorV2Fragment
import com.android.settings.biometrics.fingerprint2.ui.enrollment.fragment.FingerprintEnrollIntroV2Fragment
+import com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.rfps.ui.fragment.RFPSEnrollFragment
+import com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.rfps.ui.viewmodel.RFPSViewModel
import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.AccessibilityViewModel
+import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.BackgroundViewModel
import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.Confirmation
import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.Education
import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.Enrollment
+import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintEnrollEnrollingViewModel
import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintEnrollFindSensorViewModel
import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintEnrollNavigationViewModel
import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintEnrollViewModel
@@ -65,8 +67,11 @@
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.systemui.biometrics.shared.model.FingerprintSensorType
+import com.google.android.setupcompat.util.WizardManagerHelper
import com.google.android.setupdesign.util.ThemeHelper
import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.launch
@@ -77,6 +82,7 @@
* children fragments.
*/
class FingerprintEnrollmentV2Activity : FragmentActivity() {
+ private lateinit var fingerprintEnrollEnrollingViewModel: FingerprintEnrollEnrollingViewModel
private lateinit var navigationViewModel: FingerprintEnrollNavigationViewModel
private lateinit var gatekeeperViewModel: FingerprintGatekeeperViewModel
private lateinit var fingerprintEnrollViewModel: FingerprintEnrollViewModel
@@ -84,6 +90,7 @@
private lateinit var foldStateViewModel: FoldStateViewModel
private lateinit var orientationStateViewModel: OrientationStateViewModel
private lateinit var fingerprintScrollViewModel: FingerprintScrollViewModel
+ private lateinit var backgroundViewModel: BackgroundViewModel
private val coroutineDispatcher = Dispatchers.Default
/** Result listener for ChooseLock activity flow. */
@@ -101,23 +108,22 @@
}
}
- override fun onAttachedToWindow() {
- window.statusBarColor = getBackgroundColor()
- super.onAttachedToWindow()
+ override fun onStop() {
+ super.onStop()
+ if (!isChangingConfigurations) {
+ backgroundViewModel.wentToBackground()
+ }
}
+ override fun onResume() {
+ super.onResume()
+ backgroundViewModel.inForeground()
+ }
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
foldStateViewModel.onConfigurationChange(newConfig)
}
- @ColorInt
- private fun getBackgroundColor(): Int {
- val stateList: ColorStateList? =
- Utils.getColorAttr(applicationContext, android.R.attr.windowBackground)
- return stateList?.defaultColor ?: Color.TRANSPARENT
- }
-
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?
@@ -137,39 +143,28 @@
val context = applicationContext
val fingerprintManager = context.getSystemService(FINGERPRINT_SERVICE) as FingerprintManager
+ val isAnySuw = WizardManagerHelper.isAnySetupWizard(intent)
+ val enrollType =
+ if (isAnySuw) {
+ SetupWizard
+ } else {
+ Default
+ }
+
+ backgroundViewModel =
+ ViewModelProvider(this, BackgroundViewModel.BackgroundViewModelFactory())[
+ BackgroundViewModel::class.java]
+
val interactor =
FingerprintManagerInteractorImpl(
context,
backgroundDispatcher,
fingerprintManager,
- GatekeeperPasswordProvider(LockPatternUtils(context))
- ) {
- var toReturn: Int =
- Settings.Secure.getIntForUser(
- context.contentResolver,
- Settings.Secure.SFPS_PERFORMANT_AUTH_ENABLED,
- -1,
- context.userId,
- )
- if (toReturn == -1) {
- toReturn =
- if (
- context.resources.getBoolean(com.android.internal.R.bool.config_performantAuthDefault)
- ) {
- 1
- } else {
- 0
- }
- Settings.Secure.putIntForUser(
- context.contentResolver,
- Settings.Secure.SFPS_PERFORMANT_AUTH_ENABLED,
- toReturn,
- context.userId
- )
- }
- toReturn == 1
- }
+ GatekeeperPasswordProvider(LockPatternUtils(context)),
+ PressToAuthProviderImpl(context),
+ enrollType,
+ )
var challenge: Long? = intent.getExtra(BiometricEnrollBase.EXTRA_KEY_CHALLENGE) as Long?
val token = intent.getByteArrayExtra(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN)
@@ -191,7 +186,8 @@
backgroundDispatcher,
interactor,
gatekeeperViewModel,
- gatekeeperInfo is GatekeeperInfo.GatekeeperPasswordInfo, /* canSkipConfirm */
+ gatekeeperInfo is GatekeeperInfo.GatekeeperPasswordInfo,
+ enrollType,
)
)[FingerprintEnrollNavigationViewModel::class.java]
@@ -207,7 +203,8 @@
this,
FingerprintEnrollViewModel.FingerprintEnrollViewModelFactory(
interactor,
- backgroundDispatcher
+ gatekeeperViewModel,
+ navigationViewModel,
)
)[FingerprintEnrollViewModel::class.java]
@@ -230,6 +227,16 @@
ViewModelProvider(this, OrientationStateViewModel.OrientationViewModelFactory(context))[
OrientationStateViewModel::class.java]
+ // Initialize FingerprintEnrollEnrollingViewModel
+ fingerprintEnrollEnrollingViewModel =
+ ViewModelProvider(
+ this,
+ FingerprintEnrollEnrollingViewModel.FingerprintEnrollEnrollingViewModelFactory(
+ fingerprintEnrollViewModel,
+ backgroundViewModel
+ )
+ )[FingerprintEnrollEnrollingViewModel::class.java]
+
// Initialize FingerprintEnrollFindSensorViewModel
ViewModelProvider(
this,
@@ -237,48 +244,65 @@
navigationViewModel,
fingerprintEnrollViewModel,
gatekeeperViewModel,
+ backgroundViewModel,
accessibilityViewModel,
foldStateViewModel,
orientationStateViewModel
)
)[FingerprintEnrollFindSensorViewModel::class.java]
+ // Initialize RFPS View Model
+ ViewModelProvider(
+ this,
+ RFPSViewModel.RFPSViewModelFactory(fingerprintEnrollEnrollingViewModel)
+ )[RFPSViewModel::class.java]
+
lifecycleScope.launch {
- navigationViewModel.navigationViewModel.filterNotNull().collect {
- Log.d(TAG, "navigationStep $it")
- val isForward = it.forward
- val currStep = it.currStep
- val theClass: Class<Fragment>? =
- when (currStep) {
- Confirmation -> FingerprintEnrollConfirmationV2Fragment::class.java as Class<Fragment>
- Education -> FingerprintEnrollFindSensorV2Fragment::class.java as Class<Fragment>
- Enrollment -> FingerprintEnrollEnrollingV2Fragment::class.java as Class<Fragment>
- Intro -> FingerprintEnrollIntroV2Fragment::class.java as Class<Fragment>
- else -> null
- }
-
- if (theClass != null) {
- supportFragmentManager.fragments.onEach { fragment ->
- supportFragmentManager.beginTransaction().remove(fragment).commit()
- }
- supportFragmentManager
- .beginTransaction()
- .setReorderingAllowed(true)
- .add(R.id.fragment_container_view, theClass, null)
- .commit()
- } else {
-
- if (currStep is Finish) {
- if (currStep.resultCode != null) {
- finishActivity(currStep.resultCode)
- } else {
- finish()
+ navigationViewModel.navigationViewModel
+ .filterNotNull()
+ .combine(fingerprintEnrollViewModel.sensorType) { nav, sensorType -> Pair(nav, sensorType) }
+ .collect { (nav, sensorType) ->
+ Log.d(TAG, "navigationStep $nav")
+ fingerprintEnrollViewModel.sensorTypeCached = sensorType
+ val isForward = nav.forward
+ val currStep = nav.currStep
+ val theClass: Class<Fragment>? =
+ when (currStep) {
+ Confirmation -> FingerprintEnrollConfirmationV2Fragment::class.java as Class<Fragment>
+ Education -> FingerprintEnrollFindSensorV2Fragment::class.java as Class<Fragment>
+ is Enrollment -> {
+ when (sensorType) {
+ FingerprintSensorType.REAR -> RFPSEnrollFragment::class.java as Class<Fragment>
+ else -> FingerprintEnrollEnrollingV2Fragment::class.java as Class<Fragment>
+ }
+ }
+ Intro -> FingerprintEnrollIntroV2Fragment::class.java as Class<Fragment>
+ else -> null
}
- } else if (currStep == LaunchConfirmDeviceCredential) {
- launchConfirmOrChooseLock(userId)
+
+ if (theClass != null) {
+ supportFragmentManager.fragments.onEach { fragment ->
+ supportFragmentManager.beginTransaction().remove(fragment).commit()
+ }
+
+ supportFragmentManager
+ .beginTransaction()
+ .setReorderingAllowed(true)
+ .add(R.id.fragment_container_view, theClass, null)
+ .commit()
+ } else {
+
+ if (currStep is Finish) {
+ if (currStep.resultCode != null) {
+ finishActivity(currStep.resultCode)
+ } else {
+ finish()
+ }
+ } else if (currStep == LaunchConfirmDeviceCredential) {
+ launchConfirmOrChooseLock(userId)
+ }
}
}
- }
}
val fromSettingsSummary =
diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/fragment/FingerprintEnrollFindSensorV2Fragment.kt b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/fragment/FingerprintEnrollFindSensorV2Fragment.kt
index 0afa613..bfd4264 100644
--- a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/fragment/FingerprintEnrollFindSensorV2Fragment.kt
+++ b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/fragment/FingerprintEnrollFindSensorV2Fragment.kt
@@ -30,6 +30,7 @@
import com.android.settings.biometrics.fingerprint.FingerprintErrorDialog
import com.android.settings.biometrics.fingerprint.FingerprintFindSensorAnimation
import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintEnrollFindSensorViewModel
+import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintEnrollViewModel
import com.android.systemui.biometrics.shared.model.FingerprintSensorType
import com.google.android.setupcompat.template.FooterBarMixin
import com.google.android.setupcompat.template.FooterButton
@@ -54,23 +55,8 @@
private var animation: FingerprintFindSensorAnimation? = null
private var contentLayoutId: Int = -1
- private lateinit var viewModel: FingerprintEnrollFindSensorViewModel
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- viewModel =
- ViewModelProvider(requireActivity())[FingerprintEnrollFindSensorViewModel::class.java]
- lifecycleScope.launch {
- viewModel.sensorType.collect {
- contentLayoutId =
- when (it) {
- FingerprintSensorType.UDFPS_OPTICAL,
- FingerprintSensorType.UDFPS_ULTRASONIC -> R.layout.udfps_enroll_find_sensor_layout
- FingerprintSensorType.POWER_BUTTON -> R.layout.sfps_enroll_find_sensor_layout
- else -> R.layout.fingerprint_v2_enroll_find_sensor
- }
- }
- }
+ private val viewModel: FingerprintEnrollFindSensorViewModel by lazy {
+ ViewModelProvider(requireActivity())[FingerprintEnrollFindSensorViewModel::class.java]
}
override fun onCreateView(
@@ -78,6 +64,18 @@
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
+
+ val sensorType =
+ ViewModelProvider(requireActivity())[FingerprintEnrollViewModel::class.java].sensorTypeCached
+
+ contentLayoutId =
+ when (sensorType) {
+ FingerprintSensorType.UDFPS_OPTICAL,
+ FingerprintSensorType.UDFPS_ULTRASONIC -> R.layout.udfps_enroll_find_sensor_layout
+ FingerprintSensorType.POWER_BUTTON -> R.layout.sfps_enroll_find_sensor_layout
+ else -> R.layout.fingerprint_v2_enroll_find_sensor
+ }
+
return inflater.inflate(contentLayoutId, container, false).also { it ->
val view = it!! as GlifLayout
@@ -106,7 +104,8 @@
}
lifecycleScope.launch {
viewModel.showRfpsAnimation.collect {
- animation = view.findViewById(R.id.fingerprint_sensor_location_animation)
+ animation =
+ view.findViewById(R.id.fingerprint_sensor_location_animation)
animation!!.startAnimation()
}
}
diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/fragment/FingerprintEnrollIntroV2Fragment.kt b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/fragment/FingerprintEnrollIntroV2Fragment.kt
index 898b158..b1ab301 100644
--- a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/fragment/FingerprintEnrollIntroV2Fragment.kt
+++ b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/fragment/FingerprintEnrollIntroV2Fragment.kt
@@ -36,11 +36,11 @@
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import com.android.settings.R
+import com.android.settings.biometrics.fingerprint2.shared.model.Unicorn
import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintEnrollNavigationViewModel
import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintEnrollViewModel
import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintGatekeeperViewModel
import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintScrollViewModel
-import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.Unicorn
import com.android.systemui.biometrics.shared.model.FingerprintSensorType
import com.google.android.setupcompat.template.FooterBarMixin
import com.google.android.setupcompat.template.FooterButton
@@ -120,7 +120,7 @@
viewLifecycleOwner.lifecycleScope.launch {
combine(
- navigationViewModel.enrollType,
+ navigationViewModel.fingerprintFlow,
fingerprintViewModel.sensorType,
) { enrollType, sensorType ->
Pair(enrollType, sensorType)
diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/README.md b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/README.md
new file mode 100644
index 0000000..dfb9598
--- /dev/null
+++ b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/README.md
@@ -0,0 +1,26 @@
+# Module enrollment
+
+### Fingerprint Settings Enrollment Modules
+
+This directory is responsible for containing the enrollment modules, each enrollment module is
+responsible for the actual enrolling portion of FingerprintEnrollment.
+The modules should be split out into udfps, rfps, and sfps.
+
+[comment]: <> This file structure print out has been generated with the tree command.
+
+```
+├── enrolling
+│ └── rfps
+│ ├── data
+│ ├── domain
+│ │ └── RFPSInteractor.kt
+│ ├── README.md
+│ └── ui
+│ ├── fragment
+│ │ └── RFPSEnrollFragment.kt
+│ ├── viewmodel
+│ │ └── RFPSViewModel.kt
+│ └── widget
+│ └── RFPSProgressIndicator.kt
+└── README.md
+```
\ No newline at end of file
diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/rfps/ui/fragment/RFPSEnrollFragment.kt b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/rfps/ui/fragment/RFPSEnrollFragment.kt
new file mode 100644
index 0000000..d8c2f5a
--- /dev/null
+++ b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/rfps/ui/fragment/RFPSEnrollFragment.kt
@@ -0,0 +1,252 @@
+/*
+ * 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.enrollment.modules.enrolling.rfps.ui.fragment
+
+import android.graphics.Color
+import android.os.Bundle
+import android.util.Log
+import android.view.LayoutInflater
+import android.view.MotionEvent
+import android.view.View
+import android.view.ViewGroup
+import android.view.animation.AnimationUtils
+import android.view.animation.Interpolator
+import android.widget.TextView
+import androidx.fragment.app.Fragment
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.ViewModelProvider
+import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
+import com.android.settings.R
+import com.android.settings.biometrics.fingerprint2.shared.model.FingerEnrollState
+import com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.rfps.ui.viewmodel.RFPSIconTouchViewModel
+import com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.rfps.ui.viewmodel.RFPSViewModel
+import com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.rfps.ui.widget.FingerprintErrorDialog
+import com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.rfps.ui.widget.IconTouchDialog
+import com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.rfps.ui.widget.RFPSProgressBar
+import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.BackgroundViewModel
+import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.OrientationStateViewModel
+import com.android.settings.core.instrumentation.InstrumentedDialogFragment
+import com.google.android.setupcompat.template.FooterBarMixin
+import com.google.android.setupcompat.template.FooterButton
+import com.google.android.setupdesign.GlifLayout
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.launch
+
+private const val TAG = "RFPSEnrollFragment"
+
+/** This fragment is responsible for taking care of rear fingerprint enrollment. */
+class RFPSEnrollFragment : Fragment(R.layout.fingerprint_v2_rfps_enroll_enrolling) {
+
+ private lateinit var linearOutSlowInInterpolator: Interpolator
+ private lateinit var fastOutLinearInInterpolator: Interpolator
+ private lateinit var textView: TextView
+ private lateinit var progressBar: RFPSProgressBar
+
+ private val iconTouchViewModel: RFPSIconTouchViewModel by lazy {
+ ViewModelProvider(requireActivity())[RFPSIconTouchViewModel::class.java]
+ }
+
+ private val orientationViewModel: OrientationStateViewModel by lazy {
+ ViewModelProvider(requireActivity())[OrientationStateViewModel::class.java]
+ }
+
+ private val rfpsViewModel: RFPSViewModel by lazy {
+ ViewModelProvider(requireActivity())[RFPSViewModel::class.java]
+ }
+
+ private val backgroundViewModel: BackgroundViewModel by lazy {
+ ViewModelProvider(requireActivity())[BackgroundViewModel::class.java]
+ }
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View? {
+ val view = super.onCreateView(inflater, container, savedInstanceState)!!
+ val fragment = this
+ val context = requireContext()
+ val glifLayout = view.requireViewById(R.id.setup_wizard_layout) as GlifLayout
+ glifLayout.setDescriptionText(R.string.security_settings_fingerprint_enroll_start_message)
+ glifLayout.setHeaderText(R.string.security_settings_fingerprint_enroll_repeat_title)
+
+ fastOutLinearInInterpolator =
+ AnimationUtils.loadInterpolator(context, android.R.interpolator.fast_out_linear_in)
+ linearOutSlowInInterpolator =
+ AnimationUtils.loadInterpolator(context, android.R.interpolator.linear_out_slow_in)
+
+ textView = view.requireViewById(R.id.text) as TextView
+ progressBar = view.requireViewById(R.id.fingerprint_progress_bar) as RFPSProgressBar
+
+ val footerBarMixin = glifLayout.getMixin(FooterBarMixin::class.java)
+ footerBarMixin.secondaryButton =
+ FooterButton.Builder(context)
+ .setText(R.string.security_settings_fingerprint_enroll_enrolling_skip)
+ .setListener { Log.e(TAG, "skip enrollment!") }
+ .setButtonType(FooterButton.ButtonType.SKIP)
+ .setTheme(com.google.android.setupdesign.R.style.SudGlifButton_Secondary)
+ .build()
+ footerBarMixin.buttonContainer.setBackgroundColor(Color.TRANSPARENT)
+
+ progressBar.setOnTouchListener { _, motionEvent ->
+ if (motionEvent.actionMasked == MotionEvent.ACTION_DOWN) {
+ iconTouchViewModel.userTouchedFingerprintIcon()
+ }
+ true
+ }
+
+ // On any orientation event, dismiss dialogs.
+ viewLifecycleOwner.lifecycleScope.launch {
+ orientationViewModel.orientation.collect { dismissDialogs() }
+ }
+
+ // Signal we are ready for enrollment.
+ rfpsViewModel.readyForEnrollment()
+
+ viewLifecycleOwner.lifecycleScope.launch {
+ repeatOnLifecycle(Lifecycle.State.RESUMED) {
+ // Icon animation update
+ viewLifecycleOwner.lifecycleScope.launch {
+ rfpsViewModel.shouldAnimateIcon.collect { animate ->
+ progressBar.updateIconAnimation(animate)
+ }
+ }
+
+ // Flow to show a dialog.
+ viewLifecycleOwner.lifecycleScope.launch {
+ iconTouchViewModel.shouldShowDialog.collectLatest { showDialog ->
+ if (showDialog) {
+ try {
+ IconTouchDialog.showInstance(fragment)
+ } catch (exception: Exception) {
+ Log.d(TAG, "Dialog dismissed due to $exception")
+ }
+ }
+ }
+ }
+ }
+ }
+
+ // If we go to the background, then finish enrollment. This should be permanent finish,
+ // and shouldn't be reset until we explicitly tell the view model we want to retry
+ // enrollment.
+ viewLifecycleOwner.lifecycleScope.launch {
+ backgroundViewModel.background
+ .filter { inBackground -> inBackground }
+ .collect { rfpsViewModel.stopEnrollment() }
+ }
+
+ viewLifecycleOwner.lifecycleScope.launch {
+ rfpsViewModel.progress.filterNotNull().collect { progress -> handleEnrollProgress(progress) }
+ }
+
+ viewLifecycleOwner.lifecycleScope.launch {
+ rfpsViewModel.helpMessage.filterNotNull().collect { help ->
+ textView.text = help.helpString
+ textView.visibility = View.VISIBLE
+ textView.translationY =
+ resources.getDimensionPixelSize(R.dimen.fingerprint_error_text_appear_distance).toFloat()
+ textView.alpha = 0f
+ textView
+ .animate()
+ .alpha(1f)
+ .translationY(0f)
+ .setDuration(200)
+ .setInterpolator(linearOutSlowInInterpolator)
+ .start()
+
+ }
+ }
+
+ viewLifecycleOwner.lifecycleScope.launch {
+ rfpsViewModel.errorMessage.filterNotNull().collect { error -> handleEnrollError(error) }
+ }
+ viewLifecycleOwner.lifecycleScope.launch {
+ rfpsViewModel.textViewIsVisible.collect {
+ textView.visibility = if (it) View.VISIBLE else View.INVISIBLE
+ }
+ }
+
+ viewLifecycleOwner.lifecycleScope.launch {
+ rfpsViewModel.clearHelpMessage.collect {
+ textView
+ .animate()
+ .alpha(0f)
+ .translationY(
+ resources
+ .getDimensionPixelSize(R.dimen.fingerprint_error_text_disappear_distance)
+ .toFloat()
+ )
+ .setDuration(100)
+ .setInterpolator(fastOutLinearInInterpolator)
+ .withEndAction { rfpsViewModel.setVisibility(false) }
+ .start()
+ }
+ }
+
+ viewLifecycleOwner.lifecycleScope.launch {
+ repeatOnLifecycle(Lifecycle.State.DESTROYED) {
+ rfpsViewModel.stopEnrollment()
+ dismissDialogs()
+ }
+ }
+ return view
+ }
+
+ private fun handleEnrollError(error: FingerEnrollState.EnrollError) {
+ val fragment = this
+ viewLifecycleOwner.lifecycleScope.launch {
+ try {
+ val shouldRestartEnrollment = FingerprintErrorDialog.showInstance(error, fragment)
+ } catch (exception: Exception) {
+ Log.e(TAG, "Exception occurred $exception")
+ }
+ onEnrollmentFailed()
+ }
+ }
+
+ private fun onEnrollmentFailed() {
+ rfpsViewModel.stopEnrollment()
+ }
+
+ private fun handleEnrollProgress(progress: FingerEnrollState.EnrollProgress) {
+ progressBar.updateProgress(
+ progress.remainingSteps.toFloat() / progress.totalStepsRequired.toFloat()
+ )
+
+ if (progress.remainingSteps == 0) {
+ performNextStepSuccess()
+ }
+ }
+
+ private fun performNextStepSuccess() {}
+
+ private fun dismissDialogs() {
+ 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.commitAllowingStateLoss()
+ }
+}
diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/rfps/ui/viewmodel/RFPSIconTouchViewModel.kt b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/rfps/ui/viewmodel/RFPSIconTouchViewModel.kt
new file mode 100644
index 0000000..c16e65c
--- /dev/null
+++ b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/rfps/ui/viewmodel/RFPSIconTouchViewModel.kt
@@ -0,0 +1,61 @@
+/*
+ * 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.enrollment.modules.enrolling.rfps.ui.viewmodel
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.ViewModelProvider
+import androidx.lifecycle.viewModelScope
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.shareIn
+import kotlinx.coroutines.flow.transform
+import kotlinx.coroutines.flow.update
+
+private const val touchesToShowDialog = 3
+/**
+ * This class is responsible for counting the number of touches on the fingerprint icon, and if this
+ * number reaches a threshold it will produce an action via [shouldShowDialog] to indicate the ui
+ * should show a dialog.
+ */
+class RFPSIconTouchViewModel : ViewModel() {
+
+ /** Keeps the number of times a user has touches the fingerprint icon. */
+ private val _touches: MutableStateFlow<Int> = MutableStateFlow(0)
+
+ /**
+ * Whether or not the UI should be showing the dialog. By making this SharingStarted.Eagerly
+ * the first event 0 % 3 == 0 will fire as soon as this view model is created, so it should
+ * be ignored and work as intended.
+ */
+ val shouldShowDialog: Flow<Boolean> =
+ _touches
+ .transform { numTouches -> emit((numTouches % touchesToShowDialog) == 0) }
+ .shareIn(viewModelScope, SharingStarted.Eagerly, 0)
+
+ /** Indicates a user has tapped on the fingerprint icon. */
+ fun userTouchedFingerprintIcon() {
+ _touches.update { _touches.value + 1 }
+ }
+
+ class RFPSIconTouchViewModelFactory : ViewModelProvider.Factory {
+ @Suppress("UNCHECKED_CAST")
+ override fun <T : ViewModel> create(modelClass: Class<T>): T {
+ return RFPSIconTouchViewModel() as T
+ }
+ }
+}
diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/rfps/ui/viewmodel/RFPSViewModel.kt b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/rfps/ui/viewmodel/RFPSViewModel.kt
new file mode 100644
index 0000000..58d604e
--- /dev/null
+++ b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/rfps/ui/viewmodel/RFPSViewModel.kt
@@ -0,0 +1,102 @@
+/*
+ * 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.enrollment.modules.enrolling.rfps.ui.viewmodel
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.ViewModelProvider
+import androidx.lifecycle.viewModelScope
+import com.android.settings.biometrics.fingerprint2.shared.model.FingerEnrollState
+import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintEnrollEnrollingViewModel
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.filterIsInstance
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.shareIn
+import kotlinx.coroutines.flow.transform
+import kotlinx.coroutines.flow.update
+
+/** View Model used by the rear fingerprint enrollment fragment. */
+class RFPSViewModel(
+ private val fingerprintEnrollViewModel: FingerprintEnrollEnrollingViewModel,
+) : ViewModel() {
+
+ /** Value to indicate if the text view is visible or not **/
+ private val _textViewIsVisible = MutableStateFlow<Boolean>(false)
+ val textViewIsVisible: Flow<Boolean> = _textViewIsVisible.asStateFlow()
+
+ /** Indicates if the icon should be animating or not */
+ val shouldAnimateIcon = fingerprintEnrollViewModel.enrollFlowShouldBeRunning
+
+ private val enrollFlow: Flow<FingerEnrollState?> = fingerprintEnrollViewModel.enrollFLow
+
+ /**
+ * Enroll progress message with a replay of size 1 allowing for new subscribers to get the most
+ * recent state (this is useful for things like screen rotation)
+ */
+ val progress: Flow<FingerEnrollState.EnrollProgress?> =
+ enrollFlow
+ .filterIsInstance<FingerEnrollState.EnrollProgress>()
+ .shareIn(viewModelScope, SharingStarted.Eagerly, 1)
+
+ /** Clear help message on enroll progress */
+ val clearHelpMessage: Flow<Boolean> = progress.map { it != null }
+
+ /** Enroll help message that is only displayed once */
+ val helpMessage: Flow<FingerEnrollState.EnrollHelp?> =
+ enrollFlow
+ .filterIsInstance<FingerEnrollState.EnrollHelp>()
+ .shareIn(viewModelScope, SharingStarted.Eagerly, 0).transform {
+ _textViewIsVisible.update { true }
+ }
+
+ /**
+ * The error message should only be shown once, for scenarios like screen rotations, we don't want
+ * to re-show the error message.
+ */
+ val errorMessage: Flow<FingerEnrollState.EnrollError?> =
+ enrollFlow
+ .filterIsInstance<FingerEnrollState.EnrollError>()
+ .shareIn(viewModelScope, SharingStarted.Eagerly, 0)
+
+ /** Indicates if the consumer is ready for enrollment */
+ fun readyForEnrollment() {
+ fingerprintEnrollViewModel.canEnroll()
+ }
+
+ /** Indicates if enrollment should stop */
+ fun stopEnrollment() {
+ fingerprintEnrollViewModel.stopEnroll()
+ }
+
+ fun setVisibility(isVisible: Boolean) {
+ _textViewIsVisible.update { isVisible }
+ }
+
+ class RFPSViewModelFactory(
+ private val fingerprintEnrollEnrollingViewModel: FingerprintEnrollEnrollingViewModel,
+ ) : ViewModelProvider.Factory {
+
+ @Suppress("UNCHECKED_CAST")
+ override fun <T : ViewModel> create(
+ modelClass: Class<T>,
+ ): T {
+ return RFPSViewModel(fingerprintEnrollEnrollingViewModel) as T
+ }
+ }
+}
diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/rfps/ui/widget/FingerprintErrorDialog.kt b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/rfps/ui/widget/FingerprintErrorDialog.kt
new file mode 100644
index 0000000..b9c628e
--- /dev/null
+++ b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/rfps/ui/widget/FingerprintErrorDialog.kt
@@ -0,0 +1,124 @@
+/*
+ * 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.enrollment.modules.enrolling.rfps.ui.widget
+
+import android.app.AlertDialog
+import android.app.Dialog
+import android.app.settings.SettingsEnums
+import android.content.DialogInterface
+import android.os.Bundle
+import android.util.Log
+import androidx.fragment.app.Fragment
+import com.android.settings.R
+import com.android.settings.biometrics.fingerprint2.shared.model.FingerEnrollState
+import com.android.settings.core.instrumentation.InstrumentedDialogFragment
+import kotlin.coroutines.resume
+import kotlinx.coroutines.suspendCancellableCoroutine
+
+private const val TAG = "FingerprintErrorDialog"
+
+/** A Dialog used for fingerprint enrollment when an error occurs. */
+class FingerprintErrorDialog : InstrumentedDialogFragment() {
+ private lateinit var onContinue: DialogInterface.OnClickListener
+ private lateinit var onTryAgain: DialogInterface.OnClickListener
+ private 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 errorString = requireArguments().getInt(KEY_MESSAGE)
+ val errorTitle = requireArguments().getInt(KEY_TITLE)
+ val builder = AlertDialog.Builder(requireContext())
+ val shouldShowTryAgain = requireArguments().getBoolean(KEY_SHOULD_TRY_AGAIN)
+ builder.setTitle(errorTitle).setMessage(errorString).setCancelable(false)
+
+ if (shouldShowTryAgain) {
+ builder
+ .setPositiveButton(R.string.security_settings_fingerprint_enroll_dialog_try_again) {
+ dialog,
+ which ->
+ dialog.dismiss()
+ onTryAgain.onClick(dialog, which)
+ }
+ .setNegativeButton(R.string.security_settings_fingerprint_enroll_dialog_ok) { dialog, which
+ ->
+ dialog.dismiss()
+ onContinue.onClick(dialog, which)
+ }
+ } else {
+ builder.setPositiveButton(R.string.security_settings_fingerprint_enroll_dialog_ok) {
+ dialog,
+ which ->
+ dialog.dismiss()
+ onContinue.onClick(dialog, which)
+ }
+ }
+
+ val dialog = builder.create()
+ dialog.setCanceledOnTouchOutside(false)
+ return dialog
+ }
+
+ override fun getMetricsCategory(): Int {
+ return SettingsEnums.DIALOG_FINGERPINT_ERROR
+ }
+
+ companion object {
+ private const val KEY_MESSAGE = "fingerprint_message"
+ private const val KEY_TITLE = "fingerprint_title"
+ private const val KEY_SHOULD_TRY_AGAIN = "should_try_again"
+
+ suspend fun showInstance(
+ error: FingerEnrollState.EnrollError,
+ fragment: Fragment,
+ ) = suspendCancellableCoroutine { continuation ->
+ val dialog = FingerprintErrorDialog()
+ dialog.onTryAgain = DialogInterface.OnClickListener { _, _ -> continuation.resume(true) }
+
+ dialog.onContinue = DialogInterface.OnClickListener { _, _ -> continuation.resume(false) }
+
+ dialog.onCancelListener =
+ DialogInterface.OnCancelListener {
+ Log.d(TAG, "onCancelListener clicked $dialog")
+ continuation.resume(null)
+ }
+
+ continuation.invokeOnCancellation { Log.d(TAG, "invokeOnCancellation $dialog") }
+
+ val bundle = Bundle()
+ bundle.putInt(
+ KEY_TITLE,
+ error.errTitle,
+ )
+ bundle.putInt(
+ KEY_MESSAGE,
+ error.errString,
+ )
+ bundle.putBoolean(
+ KEY_SHOULD_TRY_AGAIN,
+ error.shouldRetryEnrollment,
+ )
+ dialog.arguments = bundle
+ Log.d(TAG, "showing dialog $dialog")
+ dialog.show(fragment.parentFragmentManager, FingerprintErrorDialog::class.java.toString())
+ }
+ }
+}
diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/rfps/ui/widget/IconTouchDialog.kt b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/rfps/ui/widget/IconTouchDialog.kt
new file mode 100644
index 0000000..c086343
--- /dev/null
+++ b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/rfps/ui/widget/IconTouchDialog.kt
@@ -0,0 +1,73 @@
+/*
+ * 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.enrollment.modules.enrolling.rfps.ui.widget
+
+import android.app.AlertDialog
+import android.app.Dialog
+import android.app.settings.SettingsEnums
+import android.content.DialogInterface
+import android.os.Bundle
+import android.util.Log
+import androidx.fragment.app.Fragment
+import com.android.settings.R
+import com.android.settings.core.instrumentation.InstrumentedDialogFragment
+import kotlin.coroutines.resume
+import kotlinx.coroutines.suspendCancellableCoroutine
+
+private const val TAG = "IconTouchDialog"
+
+/** Dialog shown when the user taps the Progress bar a certain amount of times. */
+class IconTouchDialog : InstrumentedDialogFragment() {
+ lateinit var onDismissListener: 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 {
+ val builder: AlertDialog.Builder = AlertDialog.Builder(activity, R.style.Theme_AlertDialog)
+ builder
+ .setTitle(R.string.security_settings_fingerprint_enroll_touch_dialog_title)
+ .setMessage(R.string.security_settings_fingerprint_enroll_touch_dialog_message)
+ .setPositiveButton(R.string.security_settings_fingerprint_enroll_dialog_ok) { dialog, which ->
+ dialog.dismiss()
+ onDismissListener.onClick(dialog, which)
+ }
+ .setOnCancelListener { onCancelListener.onCancel(it) }
+ return builder.create()
+ }
+
+ override fun getMetricsCategory(): Int {
+ return SettingsEnums.DIALOG_FINGERPRINT_ICON_TOUCH
+ }
+
+ companion object {
+ suspend fun showInstance(fragment: Fragment) = suspendCancellableCoroutine { continuation ->
+ val dialog = IconTouchDialog()
+ dialog.onDismissListener =
+ DialogInterface.OnClickListener { _, _ -> continuation.resume("Done") }
+ dialog.onCancelListener =
+ DialogInterface.OnCancelListener { _ -> continuation.resume("OnCancel") }
+
+ continuation.invokeOnCancellation { Log.d(TAG, "invokeOnCancellation $dialog") }
+
+ dialog.show(fragment.parentFragmentManager, IconTouchDialog::class.java.toString())
+ }
+ }
+}
diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/rfps/ui/widget/RFPSProgressBar.kt b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/rfps/ui/widget/RFPSProgressBar.kt
new file mode 100644
index 0000000..fe62681
--- /dev/null
+++ b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/rfps/ui/widget/RFPSProgressBar.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.biometrics.fingerprint2.ui.enrollment.modules.enrolling.rfps.ui.widget
+
+import android.animation.ObjectAnimator
+import android.content.Context
+import android.graphics.PorterDuff
+import android.graphics.drawable.Animatable2
+import android.graphics.drawable.AnimatedVectorDrawable
+import android.graphics.drawable.Drawable
+import android.graphics.drawable.LayerDrawable
+import android.util.AttributeSet
+import android.view.animation.AnimationUtils
+import android.view.animation.Interpolator
+import com.android.settings.R
+import com.android.settings.widget.RingProgressBar
+
+/** Progress bar for rear fingerprint enrollment. */
+class RFPSProgressBar(context: Context, attributeSet: AttributeSet) :
+ RingProgressBar(context, attributeSet) {
+
+ private val fastOutSlowInInterpolator: Interpolator
+
+ private val iconAnimationDrawable: AnimatedVectorDrawable
+ private val iconBackgroundBlinksDrawable: AnimatedVectorDrawable
+
+ private val maxProgress: Int
+
+ private var progressAnimation: ObjectAnimator? = null
+
+ private var shouldAnimateInternal: Boolean = true
+
+ init {
+ val fingerprintDrawable = background as LayerDrawable
+ iconAnimationDrawable =
+ fingerprintDrawable.findDrawableByLayerId(R.id.fingerprint_animation)
+ as AnimatedVectorDrawable
+ iconBackgroundBlinksDrawable =
+ fingerprintDrawable.findDrawableByLayerId(R.id.fingerprint_background)
+ as AnimatedVectorDrawable
+
+ fastOutSlowInInterpolator =
+ AnimationUtils.loadInterpolator(context, android.R.interpolator.fast_out_slow_in)
+
+ iconAnimationDrawable.registerAnimationCallback(
+ object : Animatable2.AnimationCallback() {
+ override fun onAnimationEnd(drawable: Drawable?) {
+ super.onAnimationEnd(drawable)
+ if (shouldAnimateInternal) {
+ animateIconAnimationInternal()
+ }
+ }
+ }
+ )
+ animateIconAnimationInternal()
+
+ progressBackgroundTintMode = PorterDuff.Mode.SRC
+
+ val attributes =
+ context.obtainStyledAttributes(R.style.RingProgressBarStyle, intArrayOf(android.R.attr.max))
+
+ maxProgress = attributes.getInt(0, -1)
+
+ attributes.recycle()
+ }
+
+ /** Indicates if the progress animation should be running */
+ fun updateIconAnimation(shouldAnimate: Boolean) {
+ if (shouldAnimate && !shouldAnimateInternal) {
+ animateIconAnimationInternal()
+ }
+
+ shouldAnimateInternal = shouldAnimate
+ }
+
+ /** This function should only be called when actual progress has been made. */
+ fun updateProgress(percentComplete: Float) {
+ val progress = maxProgress - (percentComplete.coerceIn(0.0f, 100.0f) * maxProgress).toInt()
+ iconBackgroundBlinksDrawable.start()
+
+ progressAnimation?.isRunning?.let { progressAnimation!!.cancel() }
+
+ progressAnimation = ObjectAnimator.ofInt(this, "progress", getProgress(), progress)
+
+ progressAnimation?.interpolator = fastOutSlowInInterpolator
+ progressAnimation?.setDuration(250)
+ progressAnimation?.start()
+ }
+
+ private fun animateIconAnimationInternal() {
+ iconAnimationDrawable.start()
+ }
+}
diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/viewmodel/BackgroundViewModel.kt b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/viewmodel/BackgroundViewModel.kt
new file mode 100644
index 0000000..2b53a53
--- /dev/null
+++ b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/viewmodel/BackgroundViewModel.kt
@@ -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.biometrics.fingerprint2.ui.enrollment.viewmodel
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.ViewModelProvider
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.update
+
+/** A class for determining if the application is in the background or not. */
+class BackgroundViewModel : ViewModel() {
+
+ private val _background = MutableStateFlow(false)
+ /** When true, the application is in background, else false */
+ val background = _background.asStateFlow()
+
+ /** Indicates that the application has been put in the background. */
+ fun wentToBackground() {
+ _background.update { true }
+ }
+
+ /** Indicates that the application has been brought to the foreground. */
+ fun inForeground() {
+ _background.update { false }
+ }
+
+ class BackgroundViewModelFactory : ViewModelProvider.Factory {
+ @Suppress("UNCHECKED_CAST")
+ override fun <T : ViewModel> create(modelClass: Class<T>): T {
+ return BackgroundViewModel() as T
+ }
+ }
+}
diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/viewmodel/FingerprintEnrollEnrollingViewModel.kt b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/viewmodel/FingerprintEnrollEnrollingViewModel.kt
new file mode 100644
index 0000000..7ab315e
--- /dev/null
+++ b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/viewmodel/FingerprintEnrollEnrollingViewModel.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.enrollment.viewmodel
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.ViewModelProvider
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.transformLatest
+import kotlinx.coroutines.flow.update
+
+/**
+ * This class is a wrapper around the [FingerprintEnrollViewModel] and decides when
+ * the user should or should not be enrolling.
+ */
+class FingerprintEnrollEnrollingViewModel(
+ private val fingerprintEnrollViewModel: FingerprintEnrollViewModel,
+ backgroundViewModel: BackgroundViewModel,
+) : ViewModel() {
+
+ private val _didTryEnrollment = MutableStateFlow(false)
+ private val _userDidEnroll = MutableStateFlow(false)
+ /** Indicates if the enrollment flow should be running. */
+ val enrollFlowShouldBeRunning: Flow<Boolean> =
+ _userDidEnroll.combine(backgroundViewModel.background) { shouldEnroll, isInBackground ->
+ if (isInBackground) {
+ false
+ } else {
+ shouldEnroll
+ }
+ }
+
+ /**
+ * Used to indicate the consumer of the view model is ready for an enrollment. Note that this does
+ * not necessarily try an enrollment.
+ */
+ fun canEnroll() {
+ // Update _consumerShouldEnroll after updating the other values.
+ if (!_didTryEnrollment.value) {
+ _didTryEnrollment.update { true }
+ _userDidEnroll.update { true }
+ }
+ }
+
+ /** Used to indicate to stop the enrollment. */
+ fun stopEnroll() {
+ _userDidEnroll.update { false }
+ }
+
+ /** Collects the enrollment flow based on [enrollFlowShouldBeRunning] */
+ val enrollFLow =
+ enrollFlowShouldBeRunning.transformLatest {
+ if (it) {
+ fingerprintEnrollViewModel.enrollFlow.collect { event -> emit(event) }
+ }
+ }
+
+ class FingerprintEnrollEnrollingViewModelFactory(
+ private val fingerprintEnrollViewModel: FingerprintEnrollViewModel,
+ private val backgroundViewModel: BackgroundViewModel
+ ) : ViewModelProvider.Factory {
+ @Suppress("UNCHECKED_CAST")
+ override fun <T : ViewModel> create(
+ modelClass: Class<T>,
+ ): T {
+ return FingerprintEnrollEnrollingViewModel(fingerprintEnrollViewModel, backgroundViewModel)
+ as T
+ }
+ }
+}
diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/viewmodel/FingerprintEnrollFindSensorViewModel.kt b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/viewmodel/FingerprintEnrollFindSensorViewModel.kt
index 90aefc8..7722a46 100644
--- a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/viewmodel/FingerprintEnrollFindSensorViewModel.kt
+++ b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/viewmodel/FingerprintEnrollFindSensorViewModel.kt
@@ -16,12 +16,11 @@
package com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel
-import android.hardware.fingerprint.FingerprintManager
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
-import com.android.settings.biometrics.fingerprint2.shared.model.EnrollReason
-import com.android.settings.biometrics.fingerprint2.shared.model.FingerEnrollStateViewModel
+import com.android.settings.biometrics.fingerprint2.shared.model.FingerEnrollState
+import com.android.settings.biometrics.fingerprint2.shared.model.SetupWizard
import com.android.systemui.biometrics.shared.model.FingerprintSensorType
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
@@ -40,6 +39,7 @@
private val navigationViewModel: FingerprintEnrollNavigationViewModel,
private val fingerprintEnrollViewModel: FingerprintEnrollViewModel,
private val gatekeeperViewModel: FingerprintGatekeeperViewModel,
+ backgroundViewModel: BackgroundViewModel,
accessibilityViewModel: AccessibilityViewModel,
foldStateViewModel: FoldStateViewModel,
orientationStateViewModel: OrientationStateViewModel
@@ -88,6 +88,14 @@
/** Represents the stream of showing error dialog. */
val showErrorDialog = _showErrorDialog.filterNotNull()
+ private var _didTryEducation = false
+ private var _education: MutableStateFlow<Boolean> = MutableStateFlow(false)
+ /** Indicates if the education flow should be running. */
+ private val educationFlowShouldBeRunning: Flow<Boolean> =
+ _education.combine(backgroundViewModel.background) { shouldRunEducation, isInBackground ->
+ !isInBackground && shouldRunEducation
+ }
+
init {
// Start or end enroll flow
viewModelScope.launch {
@@ -107,40 +115,58 @@
}
.collect { token ->
if (token != null) {
- fingerprintEnrollViewModel.startEnroll(token, EnrollReason.FindSensor)
+ canStartEducation()
} else {
- fingerprintEnrollViewModel.stopEnroll()
+ stopEducation()
}
}
}
// Enroll progress flow
viewModelScope.launch {
- combine(
- navigationViewModel.enrollType,
- fingerprintEnrollViewModel.enrollFlow.filterNotNull()
- ) { enrollType, enrollFlow ->
- Pair(enrollType, enrollFlow)
- }
- .collect { (enrollType, enrollFlow) ->
- when (enrollFlow) {
- // TODO: Cancel the enroll() when EnrollProgress is received instead of proceeding to
- // Enrolling page. Otherwise Enrolling page will receive the EnrollError.
- is FingerEnrollStateViewModel.EnrollProgress -> proceedToEnrolling()
- is FingerEnrollStateViewModel.EnrollError -> {
- val errMsgId = enrollFlow.errMsgId
- if (errMsgId == FingerprintManager.FINGERPRINT_ERROR_CANCELED) {
- proceedToEnrolling()
- } else {
- _showErrorDialog.update { Pair(errMsgId, enrollType == SetupWizard) }
+ educationFlowShouldBeRunning.collect {
+ // Only collect the flow when we should be running.
+ if (it) {
+ combine(
+ navigationViewModel.fingerprintFlow,
+ fingerprintEnrollViewModel.educationEnrollFlow.filterNotNull(),
+ ) { enrollType, educationFlow ->
+ Pair(enrollType, educationFlow)
+ }
+ .collect { (enrollType, educationFlow) ->
+ when (educationFlow) {
+ // TODO: Cancel the enroll() when EnrollProgress is received instead of proceeding
+ // to
+ // Enrolling page. Otherwise Enrolling page will receive the EnrollError.
+ is FingerEnrollState.EnrollProgress -> proceedToEnrolling()
+ is FingerEnrollState.EnrollError -> {
+ if (educationFlow.isCancelled) {
+ proceedToEnrolling()
+ } else {
+ _showErrorDialog.update { Pair(educationFlow.errString, enrollType == SetupWizard) }
+ }
+ }
+ is FingerEnrollState.EnrollHelp -> {}
}
}
- is FingerEnrollStateViewModel.EnrollHelp -> {}
- }
}
+ }
}
}
+ /** Indicates if education can begin */
+ private fun canStartEducation() {
+ if (!_didTryEducation) {
+ _didTryEducation = true
+ _education.update { true }
+ }
+ }
+
+ /** Indicates that education has finished */
+ private fun stopEducation() {
+ _education.update { false }
+ }
+
/** Proceed to EnrollEnrolling page. */
fun proceedToEnrolling() {
navigationViewModel.nextStep()
@@ -150,6 +176,7 @@
private val navigationViewModel: FingerprintEnrollNavigationViewModel,
private val fingerprintEnrollViewModel: FingerprintEnrollViewModel,
private val gatekeeperViewModel: FingerprintGatekeeperViewModel,
+ private val backgroundViewModel: BackgroundViewModel,
private val accessibilityViewModel: AccessibilityViewModel,
private val foldStateViewModel: FoldStateViewModel,
private val orientationStateViewModel: OrientationStateViewModel
@@ -160,6 +187,7 @@
navigationViewModel,
fingerprintEnrollViewModel,
gatekeeperViewModel,
+ backgroundViewModel,
accessibilityViewModel,
foldStateViewModel,
orientationStateViewModel
diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/viewmodel/FingerprintEnrollViewModel.kt b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/viewmodel/FingerprintEnrollViewModel.kt
index 392d205..c7a1071 100644
--- a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/viewmodel/FingerprintEnrollViewModel.kt
+++ b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/viewmodel/FingerprintEnrollViewModel.kt
@@ -17,32 +17,41 @@
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
+import androidx.lifecycle.viewModelScope
import com.android.settings.biometrics.fingerprint2.shared.domain.interactor.FingerprintManagerInteractor
import com.android.settings.biometrics.fingerprint2.shared.model.EnrollReason
-import com.android.settings.biometrics.fingerprint2.shared.model.FingerEnrollStateViewModel
+import com.android.settings.biometrics.fingerprint2.shared.model.FingerEnrollState
import com.android.systemui.biometrics.shared.model.FingerprintSensorType
-import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.filterNotNull
-import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.flow.transformLatest
-import kotlinx.coroutines.flow.update
-
-private const val TAG = "FingerprintEnrollViewModel"
/** Represents all of the fingerprint information needed for a fingerprint enrollment process. */
class FingerprintEnrollViewModel(
private val fingerprintManagerInteractor: FingerprintManagerInteractor,
- backgroundDispatcher: CoroutineDispatcher,
+ gatekeeperViewModel: FingerprintGatekeeperViewModel,
+ navigationViewModel: FingerprintEnrollNavigationViewModel,
) : ViewModel() {
- private var _enrollReason: MutableStateFlow<EnrollReason> =
- MutableStateFlow(EnrollReason.FindSensor)
- private var _hardwareAuthToken: MutableStateFlow<ByteArray?> = MutableStateFlow(null)
- private var _consumerShouldEnroll: MutableStateFlow<Boolean> = MutableStateFlow(false)
+ /**
+ * Cached value of [FingerprintSensorType]
+ *
+ * This is typically used by fragments that change their layout/behavior based on this
+ * information. This value should be set before any fragment is created.
+ */
+ var sensorTypeCached: FingerprintSensorType? = null
+ private var _enrollReason: Flow<EnrollReason?> =
+ navigationViewModel.navigationViewModel.map {
+ when (it.currStep) {
+ is Enrollment -> EnrollReason.EnrollEnrolling
+ is Education -> EnrollReason.FindSensor
+ else -> null
+ }
+ }
/** Represents the stream of [FingerprintSensorType] */
val sensorType: Flow<FingerprintSensorType> =
@@ -51,47 +60,68 @@
/**
* A flow that contains a [FingerprintEnrollViewModel] which contains the relevant information for
* an enrollment process
+ *
+ * This flow should be the only flow which calls enroll().
*/
- val enrollFlow: Flow<FingerEnrollStateViewModel> =
- combine(_consumerShouldEnroll, _hardwareAuthToken, _enrollReason) {
- consumerShouldEnroll,
- hardwareAuthToken,
- enrollReason ->
- Triple(consumerShouldEnroll, hardwareAuthToken, enrollReason)
+ val _enrollFlow: Flow<FingerEnrollState> =
+ combine(gatekeeperViewModel.gatekeeperInfo, _enrollReason) { hardwareAuthToken, enrollReason,
+ ->
+ Pair(hardwareAuthToken, enrollReason)
}
.transformLatest {
- // transformLatest() instead of transform() is used here for cancelling previous enroll()
- // whenever |consumerShouldEnroll| is changed. Otherwise the latest value will be suspended
- // since enroll() is an infinite callback flow.
- (consumerShouldEnroll, hardwareAuthToken, enrollReason) ->
- if (consumerShouldEnroll && hardwareAuthToken != null) {
- fingerprintManagerInteractor.enroll(hardwareAuthToken, enrollReason).collect { emit(it) }
+ /** [transformLatest] is used as we want to make sure to cancel previous API call. */
+ (hardwareAuthToken, enrollReason) ->
+ if (hardwareAuthToken is GatekeeperInfo.GatekeeperPasswordInfo && enrollReason != null) {
+ fingerprintManagerInteractor.enroll(hardwareAuthToken.token, enrollReason).collect {
+ emit(it)
+ }
}
}
- .flowOn(backgroundDispatcher)
+ .shareIn(viewModelScope, SharingStarted.WhileSubscribed(), 0)
- /** Used to indicate the consumer of the view model is ready for an enrollment. */
- fun startEnroll(hardwareAuthToken: ByteArray?, enrollReason: EnrollReason) {
- _enrollReason.update { enrollReason }
- _hardwareAuthToken.update { hardwareAuthToken }
- // Update _consumerShouldEnroll after updating the other values.
- _consumerShouldEnroll.update { true }
- }
+ /**
+ * This flow will kick off education when
+ * 1) There is an active subscriber to this flow
+ * 2) shouldEnroll is true and we are on the FindSensor step
+ */
+ val educationEnrollFlow: Flow<FingerEnrollState?> =
+ _enrollReason.filterNotNull().transformLatest { enrollReason ->
+ if (enrollReason == EnrollReason.FindSensor) {
+ _enrollFlow.collect { event -> emit(event) }
+ } else {
+ emit(null)
+ }
+ }
- /** Used to indicate to stop the enrollment. */
- fun stopEnroll() {
- _consumerShouldEnroll.update { false }
- }
+ /**
+ * This flow will kick off enrollment when
+ * 1) There is an active subscriber to this flow
+ * 2) shouldEnroll is true and we are on the EnrollEnrolling step
+ */
+ val enrollFlow: Flow<FingerEnrollState?> =
+ _enrollReason.filterNotNull().transformLatest { enrollReason ->
+ if (enrollReason == EnrollReason.EnrollEnrolling) {
+ _enrollFlow.collect { event -> emit(event) }
+ } else {
+ emit(null)
+ }
+ }
class FingerprintEnrollViewModelFactory(
val interactor: FingerprintManagerInteractor,
- val backgroundDispatcher: CoroutineDispatcher
+ val gatekeeperViewModel: FingerprintGatekeeperViewModel,
+ val navigationViewModel: FingerprintEnrollNavigationViewModel,
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(
modelClass: Class<T>,
): T {
- return FingerprintEnrollViewModel(interactor, backgroundDispatcher) as T
+ return FingerprintEnrollViewModel(
+ interactor,
+ gatekeeperViewModel,
+ navigationViewModel,
+ )
+ as T
}
}
}
diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/viewmodel/FingerprintEnrolllNavigationViewModel.kt b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/viewmodel/FingerprintEnrolllNavigationViewModel.kt
index 97c8271..2e5dce0 100644
--- a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/viewmodel/FingerprintEnrolllNavigationViewModel.kt
+++ b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/viewmodel/FingerprintEnrolllNavigationViewModel.kt
@@ -21,31 +21,20 @@
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import com.android.settings.biometrics.fingerprint2.shared.domain.interactor.FingerprintManagerInteractor
+import com.android.settings.biometrics.fingerprint2.shared.model.FingerprintFlow
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
private const val TAG = "FingerprintEnrollNavigationViewModel"
/**
- * The [EnrollType] for fingerprint enrollment indicates information on how the flow should behave.
- */
-sealed class EnrollType
-
-/** The default enrollment experience, typically called from Settings */
-object Default : EnrollType()
-
-/** SetupWizard/Out of box experience (OOBE) enrollment type. */
-object SetupWizard : EnrollType()
-
-/** Unicorn enrollment type */
-object Unicorn : EnrollType()
-
-/**
* This class is responsible for sending a [NavigationStep] which indicates where the user is in the
* Fingerprint Enrollment flow
*/
@@ -53,31 +42,26 @@
private val dispatcher: CoroutineDispatcher,
private val fingerprintManagerInteractor: FingerprintManagerInteractor,
private val gatekeeperViewModel: FingerprintGatekeeperViewModel,
- private val canSkipConfirm: Boolean
+ private val firstStep: NextStepViewModel,
+ private val navState: NavState,
+ private val theFingerprintFlow: FingerprintFlow,
) : ViewModel() {
private class InternalNavigationStep(
lastStep: NextStepViewModel,
nextStep: NextStepViewModel,
forward: Boolean,
- var canNavigate: Boolean
+ var canNavigate: Boolean,
) : NavigationStep(lastStep, nextStep, forward)
- private var _enrollType = MutableStateFlow<EnrollType?>(Default)
+ private var _fingerprintFlow = MutableStateFlow<FingerprintFlow?>(theFingerprintFlow)
- /** A flow that indicates the [EnrollType] */
- val enrollType: Flow<EnrollType?> = _enrollType.asStateFlow()
-
- private var navState = NavState(canSkipConfirm)
+ /** A flow that indicates the [FingerprintFlow] */
+ val fingerprintFlow: Flow<FingerprintFlow?> = _fingerprintFlow.asStateFlow()
private val _navigationStep =
MutableStateFlow(
- InternalNavigationStep(
- PlaceHolderState,
- Start.next(navState),
- forward = false,
- canNavigate = true
- )
+ InternalNavigationStep(PlaceHolderState, firstStep, forward = false, canNavigate = true)
)
init {
@@ -96,6 +80,10 @@
*/
val navigationViewModel: Flow<NavigationStep> = _navigationStep.asStateFlow()
+ /** This action indicates that the UI should actually update the navigation to the given step. */
+ val navigationAction: Flow<NavigationStep?> =
+ _navigationStep.shareIn(viewModelScope, SharingStarted.Lazily, 0)
+
/** Used to start the next step of Fingerprint Enrollment. */
fun nextStep() {
viewModelScope.launch {
@@ -130,6 +118,7 @@
private val fingerprintManagerInteractor: FingerprintManagerInteractor,
private val fingerprintGatekeeperViewModel: FingerprintGatekeeperViewModel,
private val canSkipConfirm: Boolean,
+ private val fingerprintFlow: FingerprintFlow,
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
@@ -137,11 +126,14 @@
modelClass: Class<T>,
): T {
+ val navState = NavState(canSkipConfirm)
return FingerprintEnrollNavigationViewModel(
backgroundDispatcher,
fingerprintManagerInteractor,
fingerprintGatekeeperViewModel,
- canSkipConfirm,
+ Start.next(navState),
+ navState,
+ fingerprintFlow,
)
as T
}
diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/viewmodel/NextStepViewModel.kt b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/viewmodel/NextStepViewModel.kt
index e99b8f9..b68f6d6 100644
--- a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/viewmodel/NextStepViewModel.kt
+++ b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/viewmodel/NextStepViewModel.kt
@@ -57,7 +57,7 @@
* This state is the initial state for the current step, and will be used to determine if the user
* needs to [LaunchConfirmDeviceCredential] if not, it will go to [Intro]
*/
-object Start : NextStepViewModel() {
+data object Start : NextStepViewModel() {
override fun next(state: NavState): NextStepViewModel =
if (state.confirmedDevice) Intro else LaunchConfirmDeviceCredential
@@ -71,19 +71,19 @@
}
/** State for the FingerprintEnrollment introduction */
-object Intro : NextStepViewModel() {
+data object Intro : NextStepViewModel() {
override fun next(state: NavState): NextStepViewModel = Education
override fun prev(state: NavState): NextStepViewModel = Finish(null)
}
/** State for the FingerprintEnrollment education */
-object Education : NextStepViewModel() {
+data object Education : NextStepViewModel() {
override fun next(state: NavState): NextStepViewModel = Enrollment
override fun prev(state: NavState): NextStepViewModel = Intro
}
/** State for the FingerprintEnrollment enrollment */
-object Enrollment : NextStepViewModel() {
+data object Enrollment : NextStepViewModel() {
override fun next(state: NavState): NextStepViewModel = Confirmation
override fun prev(state: NavState): NextStepViewModel = Education
}
diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/settings/binder/FingerprintSettingsViewBinder.kt b/src/com/android/settings/biometrics/fingerprint2/ui/settings/binder/FingerprintSettingsViewBinder.kt
index e66b4cd..debdfb8 100644
--- a/src/com/android/settings/biometrics/fingerprint2/ui/settings/binder/FingerprintSettingsViewBinder.kt
+++ b/src/com/android/settings/biometrics/fingerprint2/ui/settings/binder/FingerprintSettingsViewBinder.kt
@@ -19,8 +19,8 @@
import android.hardware.fingerprint.FingerprintManager
import android.util.Log
import androidx.lifecycle.LifecycleCoroutineScope
-import com.android.settings.biometrics.fingerprint2.shared.model.FingerprintAuthAttemptViewModel
-import com.android.settings.biometrics.fingerprint2.shared.model.FingerprintViewModel
+import com.android.settings.biometrics.fingerprint2.shared.model.FingerprintAuthAttemptModel
+import com.android.settings.biometrics.fingerprint2.shared.model.FingerprintData
import com.android.settings.biometrics.fingerprint2.ui.settings.binder.FingerprintSettingsViewBinder.FingerprintView
import com.android.settings.biometrics.fingerprint2.ui.settings.viewmodel.EnrollAdditionalFingerprint
import com.android.settings.biometrics.fingerprint2.ui.settings.viewmodel.EnrollFirstFingerprint
@@ -66,21 +66,21 @@
/** Indicates what result should be set for the returning callee */
fun setResultExternal(resultCode: Int)
/** Indicates the settings UI should be shown */
- fun showSettings(enrolledFingerprints: List<FingerprintViewModel>)
+ fun showSettings(enrolledFingerprints: List<FingerprintData>)
/** Updates the add fingerprints preference */
fun updateAddFingerprintsPreference(canEnroll: Boolean, maxFingerprints: Int)
/** Updates the sfps fingerprints preference */
fun updateSfpsPreference(isSfpsPrefVisible: Boolean)
/** Indicates that a user has been locked out */
- fun userLockout(authAttemptViewModel: FingerprintAuthAttemptViewModel.Error)
+ fun userLockout(authAttemptViewModel: FingerprintAuthAttemptModel.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
+ suspend fun askUserToDeleteDialog(fingerprintViewModel: FingerprintData): Boolean
/** Indicates a user should be asked to renae ma dialog */
suspend fun askUserToRenameDialog(
- fingerprintViewModel: FingerprintViewModel
- ): Pair<FingerprintViewModel, String>?
+ fingerprintViewModel: FingerprintData
+ ): Pair<FingerprintData, String>?
}
fun bind(
@@ -131,10 +131,10 @@
lifecycleScope.launch {
viewModel.authFlow.filterNotNull().collect {
when (it) {
- is FingerprintAuthAttemptViewModel.Success -> {
+ is FingerprintAuthAttemptModel.Success -> {
view.highlightPref(it.fingerId)
}
- is FingerprintAuthAttemptViewModel.Error -> {
+ is FingerprintAuthAttemptModel.Error -> {
if (it.error == FingerprintManager.FINGERPRINT_ERROR_LOCKOUT) {
view.userLockout(it)
}
diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/settings/fragment/FingerprintDeletionDialog.kt b/src/com/android/settings/biometrics/fingerprint2/ui/settings/fragment/FingerprintDeletionDialog.kt
index 32b50c5..71a22eb 100644
--- a/src/com/android/settings/biometrics/fingerprint2/ui/settings/fragment/FingerprintDeletionDialog.kt
+++ b/src/com/android/settings/biometrics/fingerprint2/ui/settings/fragment/FingerprintDeletionDialog.kt
@@ -26,7 +26,7 @@
import android.os.UserManager
import androidx.appcompat.app.AlertDialog
import com.android.settings.R
-import com.android.settings.biometrics.fingerprint2.shared.model.FingerprintViewModel
+import com.android.settings.biometrics.fingerprint2.shared.model.FingerprintData
import com.android.settings.core.instrumentation.InstrumentedDialogFragment
import kotlin.coroutines.resume
import kotlinx.coroutines.suspendCancellableCoroutine
@@ -34,7 +34,7 @@
private const val KEY_IS_LAST_FINGERPRINT = "IS_LAST_FINGERPRINT"
class FingerprintDeletionDialog : InstrumentedDialogFragment() {
- private lateinit var fingerprintViewModel: FingerprintViewModel
+ private lateinit var fingerprintViewModel: FingerprintData
private var isLastFingerprint: Boolean = false
private lateinit var alertDialog: AlertDialog
lateinit var onClickListener: DialogInterface.OnClickListener
@@ -51,7 +51,7 @@
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)
+ fingerprintViewModel = FingerprintData(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)
@@ -95,9 +95,9 @@
companion object {
private const val KEY_FINGERPRINT = "fingerprint"
suspend fun showInstance(
- fp: FingerprintViewModel,
- lastFingerprint: Boolean,
- target: FingerprintSettingsV2Fragment,
+ fp: FingerprintData,
+ lastFingerprint: Boolean,
+ target: FingerprintSettingsV2Fragment,
) = suspendCancellableCoroutine { continuation ->
val dialog = FingerprintDeletionDialog()
dialog.onClickListener = DialogInterface.OnClickListener { _, _ -> continuation.resume(true) }
diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/settings/fragment/FingerprintSettingsPreference.kt b/src/com/android/settings/biometrics/fingerprint2/ui/settings/fragment/FingerprintSettingsPreference.kt
index b1e5097..ea26946 100644
--- a/src/com/android/settings/biometrics/fingerprint2/ui/settings/fragment/FingerprintSettingsPreference.kt
+++ b/src/com/android/settings/biometrics/fingerprint2/ui/settings/fragment/FingerprintSettingsPreference.kt
@@ -22,7 +22,7 @@
import androidx.lifecycle.lifecycleScope
import androidx.preference.PreferenceViewHolder
import com.android.settings.R
-import com.android.settings.biometrics.fingerprint2.shared.model.FingerprintViewModel
+import com.android.settings.biometrics.fingerprint2.shared.model.FingerprintData
import com.android.settingslib.widget.TwoTargetPreference
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
@@ -30,10 +30,10 @@
private const val TAG = "FingerprintSettingsPreference"
class FingerprintSettingsPreference(
- context: Context,
- val fingerprintViewModel: FingerprintViewModel,
- val fragment: FingerprintSettingsV2Fragment,
- val isLastFingerprint: Boolean
+ context: Context,
+ val fingerprintViewModel: FingerprintData,
+ val fragment: FingerprintSettingsV2Fragment,
+ val isLastFingerprint: Boolean
) : TwoTargetPreference(context) {
private lateinit var myView: View
@@ -79,7 +79,7 @@
return FingerprintDeletionDialog.showInstance(fingerprintViewModel, isLastFingerprint, fragment)
}
- suspend fun askUserToRenameDialog(): Pair<FingerprintViewModel, String>? {
+ suspend fun askUserToRenameDialog(): Pair<FingerprintData, String>? {
return FingerprintSettingsRenameDialog.showInstance(fingerprintViewModel, fragment)
}
}
diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/settings/fragment/FingerprintSettingsRenameDialog.kt b/src/com/android/settings/biometrics/fingerprint2/ui/settings/fragment/FingerprintSettingsRenameDialog.kt
index 9bde0b0..ff469f1 100644
--- a/src/com/android/settings/biometrics/fingerprint2/ui/settings/fragment/FingerprintSettingsRenameDialog.kt
+++ b/src/com/android/settings/biometrics/fingerprint2/ui/settings/fragment/FingerprintSettingsRenameDialog.kt
@@ -27,7 +27,7 @@
import android.widget.ImeAwareEditText
import androidx.appcompat.app.AlertDialog
import com.android.settings.R
-import com.android.settings.biometrics.fingerprint2.shared.model.FingerprintViewModel
+import com.android.settings.biometrics.fingerprint2.shared.model.FingerprintData
import com.android.settings.core.instrumentation.InstrumentedDialogFragment
import kotlin.coroutines.resume
import kotlinx.coroutines.suspendCancellableCoroutine
@@ -46,7 +46,7 @@
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 fingerprintViewModel = FingerprintData(fp.name.toString(), fp.biometricId, fp.deviceId)
val context = requireContext()
val alertDialog =
@@ -101,7 +101,7 @@
companion object {
private const val KEY_FINGERPRINT = "fingerprint"
- suspend fun showInstance(fp: FingerprintViewModel, target: FingerprintSettingsV2Fragment) =
+ suspend fun showInstance(fp: FingerprintData, target: FingerprintSettingsV2Fragment) =
suspendCancellableCoroutine { continuation ->
val dialog = FingerprintSettingsRenameDialog()
val onClick =
diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/settings/fragment/FingerprintSettingsV2Fragment.kt b/src/com/android/settings/biometrics/fingerprint2/ui/settings/fragment/FingerprintSettingsV2Fragment.kt
index c818566..c22a5a7 100644
--- a/src/com/android/settings/biometrics/fingerprint2/ui/settings/fragment/FingerprintSettingsV2Fragment.kt
+++ b/src/com/android/settings/biometrics/fingerprint2/ui/settings/fragment/FingerprintSettingsV2Fragment.kt
@@ -46,8 +46,10 @@
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.shared.model.FingerprintAuthAttemptViewModel
-import com.android.settings.biometrics.fingerprint2.shared.model.FingerprintViewModel
+import com.android.settings.biometrics.fingerprint2.repository.PressToAuthProviderImpl
+import com.android.settings.biometrics.fingerprint2.shared.model.FingerprintAuthAttemptModel
+import com.android.settings.biometrics.fingerprint2.shared.model.FingerprintData
+import com.android.settings.biometrics.fingerprint2.shared.model.Settings
import com.android.settings.biometrics.fingerprint2.ui.settings.binder.FingerprintSettingsViewBinder
import com.android.settings.biometrics.fingerprint2.ui.settings.viewmodel.FingerprintSettingsNavigationViewModel
import com.android.settings.biometrics.fingerprint2.ui.settings.viewmodel.FingerprintSettingsViewModel
@@ -142,7 +144,7 @@
}
}
- override fun userLockout(authAttemptViewModel: FingerprintAuthAttemptViewModel.Error) {
+ override fun userLockout(authAttemptViewModel: FingerprintAuthAttemptModel.Error) {
Toast.makeText(activity, authAttemptViewModel.message, Toast.LENGTH_SHORT).show()
}
@@ -186,40 +188,46 @@
val backgroundDispatcher = Dispatchers.IO
val activity = requireActivity()
val userHandle = activity.user.identifier
+ // Note that SUW should not be launching FingerprintSettings
+ val isAnySuw = Settings
+
+ val pressToAuthProvider = {
+ 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 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
- }
+ GatekeeperPasswordProvider(LockPatternUtils(context.applicationContext)),
+ PressToAuthProviderImpl(context),
+ isAnySuw
+ )
val token = intent.getByteArrayExtra(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN)
val challenge = intent.getLongExtra(BiometricEnrollBase.EXTRA_KEY_CHALLENGE, -1L)
@@ -292,18 +300,18 @@
}
/** Used to indicate that preference has been clicked */
- fun onPrefClicked(fingerprintViewModel: FingerprintViewModel) {
+ fun onPrefClicked(fingerprintViewModel: FingerprintData) {
Log.d(TAG, "onPrefClicked(${fingerprintViewModel})")
settingsViewModel.onPrefClicked(fingerprintViewModel)
}
/** Used to indicate that a delete pref has been clicked */
- fun onDeletePrefClicked(fingerprintViewModel: FingerprintViewModel) {
+ fun onDeletePrefClicked(fingerprintViewModel: FingerprintData) {
Log.d(TAG, "onDeletePrefClicked(${fingerprintViewModel})")
settingsViewModel.onDeleteClicked(fingerprintViewModel)
}
- override fun showSettings(enrolledFingerprints: List<FingerprintViewModel>) {
+ override fun showSettings(enrolledFingerprints: List<FingerprintData>) {
val category =
this@FingerprintSettingsV2Fragment.findPreference(KEY_FINGERPRINTS_ENROLLED_CATEGORY)
as PreferenceCategory?
@@ -422,7 +430,7 @@
}
}
- override suspend fun askUserToDeleteDialog(fingerprintViewModel: FingerprintViewModel): Boolean {
+ override suspend fun askUserToDeleteDialog(fingerprintViewModel: FingerprintData): Boolean {
Log.d(TAG, "showing delete dialog for (${fingerprintViewModel})")
try {
@@ -446,8 +454,8 @@
}
override suspend fun askUserToRenameDialog(
- fingerprintViewModel: FingerprintViewModel
- ): Pair<FingerprintViewModel, String>? {
+ fingerprintViewModel: FingerprintData
+ ): Pair<FingerprintData, String>? {
Log.d(TAG, "showing rename dialog for (${fingerprintViewModel})")
try {
val toReturn =
diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/settings/viewmodel/FingerprintSettingsViewModel.kt b/src/com/android/settings/biometrics/fingerprint2/ui/settings/viewmodel/FingerprintSettingsViewModel.kt
index fa1e5e1..164f79f 100644
--- a/src/com/android/settings/biometrics/fingerprint2/ui/settings/viewmodel/FingerprintSettingsViewModel.kt
+++ b/src/com/android/settings/biometrics/fingerprint2/ui/settings/viewmodel/FingerprintSettingsViewModel.kt
@@ -22,8 +22,8 @@
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import com.android.settings.biometrics.fingerprint2.shared.domain.interactor.FingerprintManagerInteractor
-import com.android.settings.biometrics.fingerprint2.shared.model.FingerprintAuthAttemptViewModel
-import com.android.settings.biometrics.fingerprint2.shared.model.FingerprintViewModel
+import com.android.settings.biometrics.fingerprint2.shared.model.FingerprintAuthAttemptModel
+import com.android.settings.biometrics.fingerprint2.shared.model.FingerprintData
import com.android.systemui.biometrics.shared.model.FingerprintSensorType
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.Flow
@@ -53,11 +53,11 @@
private val backgroundDispatcher: CoroutineDispatcher,
private val navigationViewModel: FingerprintSettingsNavigationViewModel,
) : ViewModel() {
- private val _enrolledFingerprints: MutableStateFlow<List<FingerprintViewModel>?> =
+ private val _enrolledFingerprints: MutableStateFlow<List<FingerprintData>?> =
MutableStateFlow(null)
/** Represents the stream of enrolled fingerprints. */
- val enrolledFingerprints: Flow<List<FingerprintViewModel>> =
+ val enrolledFingerprints: Flow<List<FingerprintData>> =
_enrolledFingerprints.asStateFlow().filterNotNull().filterOnlyWhenSettingsIsShown()
/** Represents the stream of the information of "Add Fingerprint" preference. */
@@ -95,10 +95,10 @@
private val _sensorNullOrEmpty: Flow<Boolean> =
fingerprintManagerInteractor.sensorPropertiesInternal.map { it == null }
- private val _isLockedOut: MutableStateFlow<FingerprintAuthAttemptViewModel.Error?> =
+ private val _isLockedOut: MutableStateFlow<FingerprintAuthAttemptModel.Error?> =
MutableStateFlow(null)
- private val _authSucceeded: MutableSharedFlow<FingerprintAuthAttemptViewModel.Success?> =
+ private val _authSucceeded: MutableSharedFlow<FingerprintAuthAttemptModel.Success?> =
MutableSharedFlow()
private val _attemptsSoFar: MutableStateFlow<Int> = MutableStateFlow(0)
@@ -164,7 +164,7 @@
.distinctUntilChanged()
/** Represents a consistent stream of authentication attempts. */
- val authFlow: Flow<FingerprintAuthAttemptViewModel> =
+ val authFlow: Flow<FingerprintAuthAttemptModel> =
canAuthenticate
.transformLatest {
try {
@@ -173,11 +173,11 @@
Log.d(TAG, "canAuthenticate authing")
attemptingAuth()
when (val authAttempt = fingerprintManagerInteractor.authenticate()) {
- is FingerprintAuthAttemptViewModel.Success -> {
+ is FingerprintAuthAttemptModel.Success -> {
onAuthSuccess(authAttempt)
emit(authAttempt)
}
- is FingerprintAuthAttemptViewModel.Error -> {
+ is FingerprintAuthAttemptModel.Error -> {
if (authAttempt.error == FingerprintManager.FINGERPRINT_ERROR_LOCKOUT) {
lockout(authAttempt)
emit(authAttempt)
@@ -219,7 +219,7 @@
}
/** The fingerprint delete button has been clicked. */
- fun onDeleteClicked(fingerprintViewModel: FingerprintViewModel) {
+ fun onDeleteClicked(fingerprintViewModel: FingerprintData) {
viewModelScope.launch {
if (_isShowingDialog.value == null || navigationViewModel.nextStep.value != ShowSettings) {
_isShowingDialog.tryEmit(PreferenceViewModel.DeleteDialog(fingerprintViewModel))
@@ -230,7 +230,7 @@
}
/** The rename fingerprint dialog has been clicked. */
- fun onPrefClicked(fingerprintViewModel: FingerprintViewModel) {
+ fun onPrefClicked(fingerprintViewModel: FingerprintData) {
viewModelScope.launch {
if (_isShowingDialog.value == null || navigationViewModel.nextStep.value != ShowSettings) {
_isShowingDialog.tryEmit(PreferenceViewModel.RenameDialog(fingerprintViewModel))
@@ -241,7 +241,7 @@
}
/** A request to delete a fingerprint */
- fun deleteFingerprint(fp: FingerprintViewModel) {
+ fun deleteFingerprint(fp: FingerprintData) {
viewModelScope.launch(backgroundDispatcher) {
if (fingerprintManagerInteractor.removeFingerprint(fp)) {
updateEnrolledFingerprints()
@@ -250,7 +250,7 @@
}
/** A request to rename a fingerprint */
- fun renameFingerprint(fp: FingerprintViewModel, newName: String) {
+ fun renameFingerprint(fp: FingerprintData, newName: String) {
viewModelScope.launch {
fingerprintManagerInteractor.renameFingerprint(fp, newName)
updateEnrolledFingerprints()
@@ -261,12 +261,12 @@
_attemptsSoFar.update { it + 1 }
}
- private suspend fun onAuthSuccess(success: FingerprintAuthAttemptViewModel.Success) {
+ private suspend fun onAuthSuccess(success: FingerprintAuthAttemptModel.Success) {
_authSucceeded.emit(success)
_attemptsSoFar.update { 0 }
}
- private fun lockout(attemptViewModel: FingerprintAuthAttemptViewModel.Error) {
+ private fun lockout(attemptViewModel: FingerprintAuthAttemptModel.Error) {
_isLockedOut.update { attemptViewModel }
}
diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/settings/viewmodel/PreferenceViewModel.kt b/src/com/android/settings/biometrics/fingerprint2/ui/settings/viewmodel/PreferenceViewModel.kt
index 4c33f7f..181da4e 100644
--- a/src/com/android/settings/biometrics/fingerprint2/ui/settings/viewmodel/PreferenceViewModel.kt
+++ b/src/com/android/settings/biometrics/fingerprint2/ui/settings/viewmodel/PreferenceViewModel.kt
@@ -16,15 +16,15 @@
package com.android.settings.biometrics.fingerprint2.ui.settings.viewmodel
-import com.android.settings.biometrics.fingerprint2.shared.model.FingerprintViewModel
+import com.android.settings.biometrics.fingerprint2.shared.model.FingerprintData
/** Classed use to represent a Dialogs state. */
sealed class PreferenceViewModel {
data class RenameDialog(
- val fingerprintViewModel: FingerprintViewModel,
+ val fingerprintViewModel: FingerprintData,
) : PreferenceViewModel()
data class DeleteDialog(
- val fingerprintViewModel: FingerprintViewModel,
+ val fingerprintViewModel: FingerprintData,
) : PreferenceViewModel()
}
diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDashboardFragment.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDashboardFragment.java
index b9ef8f4..b3b7a2c 100644
--- a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDashboardFragment.java
+++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDashboardFragment.java
@@ -75,6 +75,8 @@
mMainSwitchBar = activity.getSwitchBar();
mMainSwitchBar.setTitle(getText(R.string.audio_sharing_switch_title));
mSwitchBarController = new AudioSharingSwitchBarController(activity, mMainSwitchBar);
+ mSwitchBarController.init(this);
+ getSettingsLifecycle().addObserver(mSwitchBarController);
mMainSwitchBar.show();
}
}
diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDeviceAdapter.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDeviceAdapter.java
new file mode 100644
index 0000000..6d5b693
--- /dev/null
+++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDeviceAdapter.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.connecteddevice.audiosharing;
+
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Button;
+
+import com.android.internal.widget.RecyclerView;
+import com.android.settings.R;
+
+import java.util.ArrayList;
+
+public class AudioSharingDeviceAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
+
+ private static final String TAG = "AudioSharingDeviceAdapter";
+ private final ArrayList<String> mDevices;
+ private final OnClickListener mOnClickListener;
+
+ public AudioSharingDeviceAdapter(ArrayList<String> devices, OnClickListener listener) {
+ mDevices = devices;
+ mOnClickListener = listener;
+ }
+
+ private class AudioSharingDeviceViewHolder extends RecyclerView.ViewHolder {
+ private final Button mButtonView;
+
+ AudioSharingDeviceViewHolder(View view) {
+ super(view);
+ mButtonView = view.findViewById(R.id.device_button);
+ }
+
+ public void bindView(int position) {
+ if (mButtonView != null) {
+ mButtonView.setText(mDevices.get(position));
+ mButtonView.setOnClickListener(v -> mOnClickListener.onClick(position));
+ } else {
+ Log.w(TAG, "bind view skipped due to button view is null");
+ }
+ }
+ }
+
+ @Override
+ public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+ View view =
+ LayoutInflater.from(parent.getContext())
+ .inflate(R.layout.audio_sharing_device_item, parent, false);
+ return new AudioSharingDeviceViewHolder(view);
+ }
+
+ @Override
+ public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
+ ((AudioSharingDeviceViewHolder) holder).bindView(position);
+ }
+
+ @Override
+ public int getItemCount() {
+ return mDevices.size();
+ }
+
+ public interface OnClickListener {
+ /** Called when an item has been clicked. */
+ void onClick(int position);
+ }
+}
diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDialogFragment.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDialogFragment.java
new file mode 100644
index 0000000..5b99907
--- /dev/null
+++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDialogFragment.java
@@ -0,0 +1,89 @@
+/*
+ * 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.audiosharing;
+
+import android.app.Dialog;
+import android.app.settings.SettingsEnums;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.TextView;
+
+import androidx.appcompat.app.AlertDialog;
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentManager;
+
+import com.android.internal.widget.LinearLayoutManager;
+import com.android.internal.widget.RecyclerView;
+import com.android.settings.R;
+import com.android.settings.core.instrumentation.InstrumentedDialogFragment;
+import com.android.settings.flags.Flags;
+
+import java.util.ArrayList;
+
+public class AudioSharingDialogFragment extends InstrumentedDialogFragment {
+ private static final String TAG = "AudioSharingDialog";
+
+ private View mRootView;
+
+ @Override
+ public int getMetricsCategory() {
+ return SettingsEnums.DIALOG_START_AUDIO_SHARING;
+ }
+
+ /**
+ * Display the {@link AudioSharingDialogFragment} dialog.
+ *
+ * @param host The Fragment this dialog will be hosted.
+ */
+ public static void show(Fragment host) {
+ if (!Flags.enableLeAudioSharing()) return;
+ final FragmentManager manager = host.getChildFragmentManager();
+ if (manager.findFragmentByTag(TAG) == null) {
+ final AudioSharingDialogFragment dialog = new AudioSharingDialogFragment();
+ dialog.show(manager, TAG);
+ }
+ }
+
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ final AlertDialog.Builder builder =
+ new AlertDialog.Builder(getActivity()).setTitle("Share audio");
+ mRootView =
+ LayoutInflater.from(builder.getContext())
+ .inflate(R.layout.dialog_audio_sharing, /* parent= */ null);
+ // TODO: use real subtitle according to device count.
+ TextView subTitle1 = mRootView.findViewById(R.id.share_audio_subtitle1);
+ TextView subTitle2 = mRootView.findViewById(R.id.share_audio_subtitle2);
+ subTitle1.setText("2 devices connected");
+ subTitle2.setText("placeholder");
+ RecyclerView recyclerView = mRootView.findViewById(R.id.btn_list);
+ // TODO: use real audio sharing device list.
+ ArrayList<String> devices = new ArrayList<>();
+ devices.add("Buds 1");
+ devices.add("Buds 2");
+ recyclerView.setAdapter(
+ new AudioSharingDeviceAdapter(
+ devices,
+ (int position) -> {
+ // TODO: add on click callback.
+ }));
+ recyclerView.setLayoutManager(
+ new LinearLayoutManager(getActivity(), LinearLayoutManager.VERTICAL, false));
+ return builder.setView(mRootView).create();
+ }
+}
diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingNamePreference.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingNamePreference.java
new file mode 100644
index 0000000..387ab7e
--- /dev/null
+++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingNamePreference.java
@@ -0,0 +1,64 @@
+/*
+ * 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.audiosharing;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.widget.ImageButton;
+
+import androidx.preference.PreferenceViewHolder;
+
+import com.android.settings.R;
+import com.android.settings.widget.ValidatedEditTextPreference;
+
+public class AudioSharingNamePreference extends ValidatedEditTextPreference {
+ private static final String TAG = "AudioSharingNamePreference";
+
+ public AudioSharingNamePreference(
+ Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ initialize();
+ }
+
+ public AudioSharingNamePreference(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ initialize();
+ }
+
+ public AudioSharingNamePreference(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ initialize();
+ }
+
+ public AudioSharingNamePreference(Context context) {
+ super(context);
+ initialize();
+ }
+
+ private void initialize() {
+ setLayoutResource(
+ com.android.settingslib.widget.preference.twotarget.R.layout.preference_two_target);
+ setWidgetLayoutResource(R.layout.preference_widget_qrcode);
+ }
+
+ @Override
+ public void onBindViewHolder(PreferenceViewHolder holder) {
+ super.onBindViewHolder(holder);
+ final ImageButton shareButton = (ImageButton) holder.findViewById(R.id.button_icon);
+ shareButton.setImageDrawable(getContext().getDrawable(R.drawable.ic_qrcode_24dp));
+ }
+}
diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingNamePreferenceController.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingNamePreferenceController.java
new file mode 100644
index 0000000..18c9bfd
--- /dev/null
+++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingNamePreferenceController.java
@@ -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.connecteddevice.audiosharing;
+
+import android.content.Context;
+
+import androidx.annotation.NonNull;
+import androidx.lifecycle.DefaultLifecycleObserver;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.preference.Preference;
+import androidx.preference.PreferenceScreen;
+
+import com.android.settings.core.BasePreferenceController;
+import com.android.settings.flags.Flags;
+import com.android.settings.widget.ValidatedEditTextPreference;
+
+public class AudioSharingNamePreferenceController extends BasePreferenceController
+ implements ValidatedEditTextPreference.Validator,
+ Preference.OnPreferenceChangeListener,
+ DefaultLifecycleObserver {
+
+ private static final String TAG = "AudioSharingNamePreferenceController";
+
+ private static final String PREF_KEY = "audio_sharing_stream_name";
+
+ protected Preference mPreference;
+
+ private AudioSharingNameTextValidator mAudioSharingNameTextValidator;
+
+ public AudioSharingNamePreferenceController(Context context) {
+ super(context, PREF_KEY);
+ mAudioSharingNameTextValidator = new AudioSharingNameTextValidator();
+ }
+
+ @Override
+ public int getAvailabilityStatus() {
+ return Flags.enableLeAudioSharing() ? AVAILABLE : UNSUPPORTED_ON_DEVICE;
+ }
+
+ @Override
+ public String getPreferenceKey() {
+ return PREF_KEY;
+ }
+
+ @Override
+ public boolean onPreferenceChange(Preference preference, Object newValue) {
+ // TODO: update broadcast when name is changed.
+ return true;
+ }
+
+ @Override
+ public void displayPreference(PreferenceScreen screen) {
+ super.displayPreference(screen);
+ mPreference = screen.findPreference(getPreferenceKey());
+ }
+
+ @Override
+ public boolean isTextValid(String value) {
+ return mAudioSharingNameTextValidator.isTextValid(value);
+ }
+
+ @Override
+ public void onStart(@NonNull LifecycleOwner owner) {
+ // TODO
+ }
+
+ @Override
+ public void onStop(@NonNull LifecycleOwner owner) {
+ // TODO
+ }
+}
diff --git a/src/com/android/settings/biometrics/fingerprint2/shared/model/FingerprintViewModel.kt b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingNameTextValidator.java
similarity index 60%
copy from src/com/android/settings/biometrics/fingerprint2/shared/model/FingerprintViewModel.kt
copy to src/com/android/settings/connecteddevice/audiosharing/AudioSharingNameTextValidator.java
index db28e79..9492961 100644
--- a/src/com/android/settings/biometrics/fingerprint2/shared/model/FingerprintViewModel.kt
+++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingNameTextValidator.java
@@ -14,21 +14,14 @@
* limitations under the License.
*/
-package com.android.settings.biometrics.fingerprint2.shared.model
+package com.android.settings.connecteddevice.audiosharing;
-data class FingerprintViewModel(
- val name: String,
- val fingerId: Int,
- val deviceId: Long,
-)
+import com.android.settings.widget.ValidatedEditTextPreference;
-sealed class FingerprintAuthAttemptViewModel {
- data class Success(
- val fingerId: Int,
- ) : FingerprintAuthAttemptViewModel()
-
- data class Error(
- val error: Int,
- val message: String,
- ) : FingerprintAuthAttemptViewModel()
+public class AudioSharingNameTextValidator implements ValidatedEditTextPreference.Validator {
+ @Override
+ public boolean isTextValid(String value) {
+ // TODO: Add validate rule if applicable.
+ return true;
+ }
}
diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingSwitchBarController.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingSwitchBarController.java
index 1400720..a375a3c 100644
--- a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingSwitchBarController.java
+++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingSwitchBarController.java
@@ -17,24 +17,30 @@
package com.android.settings.connecteddevice.audiosharing;
import android.content.Context;
+import android.util.Log;
import android.widget.Switch;
import androidx.annotation.NonNull;
import androidx.lifecycle.DefaultLifecycleObserver;
import androidx.lifecycle.LifecycleOwner;
+import com.android.settings.core.BasePreferenceController;
+import com.android.settings.dashboard.DashboardFragment;
+import com.android.settings.flags.Flags;
import com.android.settings.widget.SettingsMainSwitchBar;
import com.android.settingslib.widget.OnMainSwitchChangeListener;
-public class AudioSharingSwitchBarController
+public class AudioSharingSwitchBarController extends BasePreferenceController
implements DefaultLifecycleObserver, OnMainSwitchChangeListener {
-
private static final String TAG = "AudioSharingSwitchBarCtl";
+ private static final String PREF_KEY = "audio_sharing_main_switch";
private final Context mContext;
private final SettingsMainSwitchBar mSwitchBar;
+ private DashboardFragment mFragment;
AudioSharingSwitchBarController(Context context, SettingsMainSwitchBar switchBar) {
+ super(context, PREF_KEY);
mContext = context;
mSwitchBar = switchBar;
mSwitchBar.setChecked(false);
@@ -54,11 +60,32 @@
public void onSwitchChanged(Switch switchView, boolean isChecked) {
// Filter out unnecessary callbacks when switch is disabled.
if (!switchView.isEnabled()) return;
-
if (isChecked) {
- // TODO: start sharing
+ startAudioSharing();
} else {
// TODO: stop sharing
}
}
+
+ @Override
+ public int getAvailabilityStatus() {
+ return Flags.enableLeAudioSharing() ? AVAILABLE : UNSUPPORTED_ON_DEVICE;
+ }
+
+ /**
+ * Initialize the controller.
+ *
+ * @param fragment The fragment to host the {@link AudioSharingSwitchBarController} dialog.
+ */
+ public void init(DashboardFragment fragment) {
+ this.mFragment = fragment;
+ }
+
+ private void startAudioSharing() {
+ if (mFragment != null) {
+ AudioSharingDialogFragment.show(mFragment);
+ } else {
+ Log.w(TAG, "Dialog fail to show due to null fragment.");
+ }
+ }
}
diff --git a/src/com/android/settings/datausage/CellDataPreference.java b/src/com/android/settings/datausage/CellDataPreference.java
index 9374217..3bd3ecc 100644
--- a/src/com/android/settings/datausage/CellDataPreference.java
+++ b/src/com/android/settings/datausage/CellDataPreference.java
@@ -26,11 +26,10 @@
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
-import android.widget.Checkable;
+import android.widget.CompoundButton;
import androidx.annotation.VisibleForTesting;
import androidx.appcompat.app.AlertDialog.Builder;
-import androidx.core.content.res.TypedArrayUtils;
import androidx.preference.PreferenceViewHolder;
import com.android.settings.R;
@@ -51,12 +50,10 @@
public int mSubId = SubscriptionManager.INVALID_SUBSCRIPTION_ID;
public boolean mChecked;
public boolean mMultiSimDialog;
- private MobileDataEnabledListener mDataStateListener;
+ private final MobileDataEnabledListener mDataStateListener;
public CellDataPreference(Context context, AttributeSet attrs) {
- super(context, attrs, TypedArrayUtils.getAttr(context,
- androidx.preference.R.attr.switchPreferenceStyle,
- android.R.attr.switchPreferenceStyle));
+ super(context, attrs, androidx.preference.R.attr.switchPreferenceCompatStyle);
mDataStateListener = new MobileDataEnabledListener(context, this);
}
@@ -170,9 +167,10 @@
@Override
public void onBindViewHolder(PreferenceViewHolder holder) {
super.onBindViewHolder(holder);
- final View switchView = holder.findViewById(android.R.id.switch_widget);
+ final CompoundButton switchView =
+ (CompoundButton) holder.findViewById(androidx.preference.R.id.switchWidget);
switchView.setClickable(false);
- ((Checkable) switchView).setChecked(mChecked);
+ switchView.setChecked(mChecked);
}
@Override
diff --git a/src/com/android/settings/datausage/ChartDataUsagePreference.java b/src/com/android/settings/datausage/ChartDataUsagePreference.java
index e5a7307..e8e2109 100644
--- a/src/com/android/settings/datausage/ChartDataUsagePreference.java
+++ b/src/com/android/settings/datausage/ChartDataUsagePreference.java
@@ -112,9 +112,6 @@
// increment by current bucket total
totalData += data.getUsage();
- if (points.size() == 1) {
- points.put(toInt(startTime - mStart) - 1, -1);
- }
points.put(toInt(startTime - mStart + 1), (int) (totalData / RESOLUTION));
points.put(toInt(endTime - mStart), (int) (totalData / RESOLUTION));
}
diff --git a/src/com/android/settings/fuelgauge/AdvancedPowerUsageDetail.java b/src/com/android/settings/fuelgauge/AdvancedPowerUsageDetail.java
index 82917d2..5262ba9 100644
--- a/src/com/android/settings/fuelgauge/AdvancedPowerUsageDetail.java
+++ b/src/com/android/settings/fuelgauge/AdvancedPowerUsageDetail.java
@@ -27,10 +27,13 @@
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.os.UserHandle;
+import android.text.TextUtils;
import android.util.Log;
import android.view.View;
+import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
+import androidx.preference.Preference;
import com.android.settings.R;
import com.android.settings.SettingsActivity;
@@ -45,14 +48,12 @@
import com.android.settings.fuelgauge.batteryusage.BatteryEntry;
import com.android.settings.overlay.FeatureFactory;
import com.android.settings.widget.EntityHeaderController;
-import com.android.settingslib.HelpUtils;
+import com.android.settingslib.PrimarySwitchPreference;
import com.android.settingslib.applications.AppUtils;
import com.android.settingslib.applications.ApplicationsState;
import com.android.settingslib.core.AbstractPreferenceController;
import com.android.settingslib.core.instrumentation.Instrumentable;
-import com.android.settingslib.widget.FooterPreference;
import com.android.settingslib.widget.LayoutPreference;
-import com.android.settingslib.widget.SelectorWithWidgetPreference;
import java.util.ArrayList;
import java.util.List;
@@ -67,8 +68,8 @@
*/
public class AdvancedPowerUsageDetail extends DashboardFragment implements
ButtonActionDialogFragment.AppButtonsDialogListener,
- SelectorWithWidgetPreference.OnClickListener {
-
+ Preference.OnPreferenceClickListener,
+ Preference.OnPreferenceChangeListener {
public static final String TAG = "AdvancedPowerDetail";
public static final String EXTRA_UID = "extra_uid";
public static final String EXTRA_PACKAGE_NAME = "extra_package_name";
@@ -85,19 +86,16 @@
public static final String EXTRA_POWER_USAGE_AMOUNT = "extra_power_usage_amount";
private static final String KEY_PREF_HEADER = "header_view";
- private static final String KEY_PREF_UNRESTRICTED = "unrestricted_pref";
- private static final String KEY_PREF_OPTIMIZED = "optimized_pref";
- private static final String KEY_PREF_RESTRICTED = "restricted_pref";
- private static final String KEY_FOOTER_PREFERENCE = "app_usage_footer_preference";
- private static final String PACKAGE_NAME_NONE = "none";
-
- private static final String HEADER_SUMMARY_FORMAT = "%s\n(%s)";
+ private static final String KEY_ALLOW_BACKGROUND_USAGE = "allow_background_usage";
private static final int REQUEST_UNINSTALL = 0;
private static final int REQUEST_REMOVE_DEVICE_ADMIN = 1;
private final ExecutorService mExecutor = Executors.newSingleThreadExecutor();
+ private AppButtonsPreferenceController mAppButtonsPreferenceController;
+ private PowerUsageTimeController mPowerUsageTimeController;
+
@VisibleForTesting
LayoutPreference mHeaderPreference;
@VisibleForTesting
@@ -107,13 +105,7 @@
@VisibleForTesting
BatteryOptimizeUtils mBatteryOptimizeUtils;
@VisibleForTesting
- FooterPreference mFooterPreference;
- @VisibleForTesting
- SelectorWithWidgetPreference mRestrictedPreference;
- @VisibleForTesting
- SelectorWithWidgetPreference mOptimizePreference;
- @VisibleForTesting
- SelectorWithWidgetPreference mUnrestrictedPreference;
+ PrimarySwitchPreference mAllowBackgroundUsagePreference;
@VisibleForTesting
@BatteryOptimizeUtils.OptimizationMode
int mOptimizationMode = BatteryOptimizeUtils.MODE_UNKNOWN;
@@ -122,9 +114,6 @@
@VisibleForTesting
StringBuilder mLogStringBuilder;
- private AppButtonsPreferenceController mAppButtonsPreferenceController;
- private PowerUsageTimeController mPowerUsageTimeController;
-
// A wrapper class to carry LaunchBatteryDetailPage required arguments.
private static final class LaunchBatteryDetailPageArgs {
private String mUsagePercent;
@@ -209,7 +198,7 @@
args.putString(EXTRA_ANOMALY_HINT_PREF_KEY, launchArgs.mAnomalyHintPrefKey);
args.putString(EXTRA_ANOMALY_HINT_TEXT, launchArgs.mAnomalyHintText);
final int userId = launchArgs.mIsUserEntry ? ActivityManager.getCurrentUser()
- : UserHandle.getUserId(launchArgs.mUid);
+ : UserHandle.getUserId(launchArgs.mUid);
new SubSettingLauncher(context)
.setDestination(AdvancedPowerUsageDetail.class.getName())
@@ -257,7 +246,7 @@
super.onCreate(icicle);
final String packageName = getArguments().getString(EXTRA_PACKAGE_NAME);
- onCreateForTriState(packageName);
+ onCreateBackgroundUsageState(packageName);
mHeaderPreference = findPreference(KEY_PREF_HEADER);
if (packageName != null) {
@@ -271,10 +260,10 @@
initHeader();
mOptimizationMode = mBatteryOptimizeUtils.getAppOptimizationMode();
- initPreferenceForTriState(getContext());
+ initFooter();
mExecutor.execute(() -> {
- String packageName =
- getLoggingPackageName(getContext(), mBatteryOptimizeUtils.getPackageName());
+ final String packageName = BatteryUtils
+ .getLoggingPackageName(getContext(), mBatteryOptimizeUtils.getPackageName());
FeatureFactory.getFeatureFactory().getMetricsFeatureProvider()
.action(
getContext(),
@@ -288,11 +277,10 @@
public void onPause() {
super.onPause();
- final int selectedPreference = getSelectedPreference();
-
notifyBackupManager();
- mLogStringBuilder.append(", onPause mode = ").append(selectedPreference);
- logMetricCategory(selectedPreference);
+ final int currentOptimizeMode = mBatteryOptimizeUtils.getAppOptimizationMode();
+ mLogStringBuilder.append(", onPause mode = ").append(currentOptimizeMode);
+ logMetricCategory(currentOptimizeMode);
mExecutor.execute(() -> {
BatteryOptimizeLogUtils.writeLog(
@@ -302,7 +290,7 @@
mBatteryOptimizeUtils.getPackageName(), UserHandle.myUserId()),
mLogStringBuilder.toString());
});
- Log.d(TAG, "Leave with mode: " + selectedPreference);
+ Log.d(TAG, "Leave with mode: " + currentOptimizeMode);
}
@VisibleForTesting
@@ -353,33 +341,28 @@
}
@VisibleForTesting
- void initPreferenceForTriState(Context context) {
+ void initFooter() {
final String stateString;
- final String footerString;
+ final String detailInfoString;
+ final Context context = getContext();
if (mBatteryOptimizeUtils.isDisabledForOptimizeModeOnly()) {
// Present optimized only string when the package name is invalid.
stateString = context.getString(R.string.manager_battery_usage_optimized_only);
- footerString = context.getString(
- R.string.manager_battery_usage_footer_limited, stateString);
+ detailInfoString =
+ context.getString(R.string.manager_battery_usage_footer_limited, stateString);
} else if (mBatteryOptimizeUtils.isSystemOrDefaultApp()) {
// Present unrestricted only string when the package is system or default active app.
stateString = context.getString(R.string.manager_battery_usage_unrestricted_only);
- footerString = context.getString(
- R.string.manager_battery_usage_footer_limited, stateString);
+ detailInfoString =
+ context.getString(R.string.manager_battery_usage_footer_limited, stateString);
} else {
// Present default string to normal app.
- footerString = context.getString(R.string.manager_battery_usage_footer);
+ detailInfoString =
+ context.getString(
+ R.string.manager_battery_usage_allow_background_usage_summary);
}
- mFooterPreference.setTitle(footerString);
- final Intent helpIntent = HelpUtils.getHelpIntent(context, context.getString(
- R.string.help_url_app_usage_settings), /*backupContext=*/ "");
- if (helpIntent != null) {
- mFooterPreference.setLearnMoreAction(v ->
- startActivityForResult(helpIntent, /*requestCode=*/ 0));
- mFooterPreference.setLearnMoreText(
- context.getString(R.string.manager_battery_usage_link_a11y));
- }
+ mAllowBackgroundUsagePreference.setSummary(detailInfoString);
}
@Override
@@ -412,9 +395,7 @@
controllers.add(mPowerUsageTimeController);
}
controllers.add(mAppButtonsPreferenceController);
- controllers.add(new UnrestrictedPreferenceController(context, uid, packageName));
- controllers.add(new OptimizedPreferenceController(context, uid, packageName));
- controllers.add(new RestrictedPreferenceController(context, uid, packageName));
+ controllers.add(new AllowBackgroundPreferenceController(context, uid, packageName));
return controllers;
}
@@ -435,34 +416,45 @@
}
@Override
- public void onRadioButtonClicked(SelectorWithWidgetPreference selected) {
- final String selectedKey = selected.getKey();
- updatePreferenceState(mUnrestrictedPreference, selectedKey);
- updatePreferenceState(mOptimizePreference, selectedKey);
- updatePreferenceState(mRestrictedPreference, selectedKey);
- mBatteryOptimizeUtils.setAppUsageState(getSelectedPreference(), Action.APPLY);
+ public boolean onPreferenceClick(Preference preference) {
+ if (!(preference instanceof PrimarySwitchPreference)
+ || !TextUtils.equals(preference.getKey(), KEY_ALLOW_BACKGROUND_USAGE)) {
+ return false;
+ }
+ PowerBackgroundUsageDetail.startPowerBackgroundUsageDetailPage(
+ getContext(), getArguments());
+ return true;
}
- private void updatePreferenceState(SelectorWithWidgetPreference preference,
- String selectedKey) {
- preference.setChecked(selectedKey.equals(preference.getKey()));
+ @Override
+ public boolean onPreferenceChange(@NonNull Preference preference, Object newValue) {
+ if (!(preference instanceof PrimarySwitchPreference)
+ || !TextUtils.equals(preference.getKey(), KEY_ALLOW_BACKGROUND_USAGE)) {
+ return false;
+ }
+ if (newValue instanceof Boolean) {
+ final boolean isAllowBackgroundUsage = (boolean) newValue;
+ mBatteryOptimizeUtils.setAppUsageState(
+ isAllowBackgroundUsage
+ ? BatteryOptimizeUtils.MODE_OPTIMIZED
+ : BatteryOptimizeUtils.MODE_RESTRICTED,
+ Action.APPLY);
+ }
+ return true;
}
- private void logMetricCategory(int selectedKey) {
- if (selectedKey == mOptimizationMode) {
+ private void logMetricCategory(int currentOptimizeMode) {
+ if (currentOptimizeMode == mOptimizationMode) {
return;
}
-
int metricCategory = 0;
- switch (selectedKey) {
+ switch (currentOptimizeMode) {
case BatteryOptimizeUtils.MODE_UNRESTRICTED:
- metricCategory = SettingsEnums.ACTION_APP_BATTERY_USAGE_UNRESTRICTED;
- break;
case BatteryOptimizeUtils.MODE_OPTIMIZED:
- metricCategory = SettingsEnums.ACTION_APP_BATTERY_USAGE_OPTIMIZED;
+ metricCategory = SettingsEnums.ACTION_APP_BATTERY_USAGE_ALLOW_BACKGROUND;
break;
case BatteryOptimizeUtils.MODE_RESTRICTED:
- metricCategory = SettingsEnums.ACTION_APP_BATTERY_USAGE_RESTRICTED;
+ metricCategory = SettingsEnums.ACTION_APP_BATTERY_USAGE_DISABLE_BACKGROUND;
break;
}
if (metricCategory == 0) {
@@ -470,8 +462,8 @@
}
int finalMetricCategory = metricCategory;
mExecutor.execute(() -> {
- String packageName =
- getLoggingPackageName(getContext(), mBatteryOptimizeUtils.getPackageName());
+ String packageName = BatteryUtils
+ .getLoggingPackageName(getContext(), mBatteryOptimizeUtils.getPackageName());
FeatureFactory.getFeatureFactory().getMetricsFeatureProvider()
.action(
/* attribution */ SettingsEnums.OPEN_APP_BATTERY_USAGE,
@@ -482,33 +474,15 @@
});
}
- private void onCreateForTriState(String packageName) {
- mUnrestrictedPreference = findPreference(KEY_PREF_UNRESTRICTED);
- mOptimizePreference = findPreference(KEY_PREF_OPTIMIZED);
- mRestrictedPreference = findPreference(KEY_PREF_RESTRICTED);
- mFooterPreference = findPreference(KEY_FOOTER_PREFERENCE);
- mUnrestrictedPreference.setOnClickListener(this);
- mOptimizePreference.setOnClickListener(this);
- mRestrictedPreference.setOnClickListener(this);
-
- mBatteryOptimizeUtils = new BatteryOptimizeUtils(
- getContext(), getArguments().getInt(EXTRA_UID), packageName);
- }
-
- private int getSelectedPreference() {
- if (mRestrictedPreference.isChecked()) {
- return BatteryOptimizeUtils.MODE_RESTRICTED;
- } else if (mUnrestrictedPreference.isChecked()) {
- return BatteryOptimizeUtils.MODE_UNRESTRICTED;
- } else if (mOptimizePreference.isChecked()) {
- return BatteryOptimizeUtils.MODE_OPTIMIZED;
- } else {
- return BatteryOptimizeUtils.MODE_UNKNOWN;
+ private void onCreateBackgroundUsageState(String packageName) {
+ mAllowBackgroundUsagePreference = findPreference(KEY_ALLOW_BACKGROUND_USAGE);
+ if (mAllowBackgroundUsagePreference != null) {
+ mAllowBackgroundUsagePreference.setOnPreferenceClickListener(this);
+ mAllowBackgroundUsagePreference.setOnPreferenceChangeListener(this);
}
- }
- private static String getLoggingPackageName(Context context, String originalPackingName) {
- return BatteryUtils.isAppInstalledFromGooglePlayStore(context, originalPackingName)
- ? originalPackingName : PACKAGE_NAME_NONE;
+ mBatteryOptimizeUtils =
+ new BatteryOptimizeUtils(
+ getContext(), getArguments().getInt(EXTRA_UID), packageName);
}
}
diff --git a/src/com/android/settings/fuelgauge/AllowBackgroundPreferenceController.java b/src/com/android/settings/fuelgauge/AllowBackgroundPreferenceController.java
new file mode 100644
index 0000000..d722bad
--- /dev/null
+++ b/src/com/android/settings/fuelgauge/AllowBackgroundPreferenceController.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.fuelgauge;
+
+import android.content.Context;
+
+import androidx.annotation.VisibleForTesting;
+import androidx.preference.Preference;
+
+import com.android.settings.core.PreferenceControllerMixin;
+import com.android.settingslib.PrimarySwitchPreference;
+import com.android.settingslib.core.AbstractPreferenceController;
+import com.android.settingslib.widget.MainSwitchPreference;
+
+/** Controller to update the app background usage state */
+public class AllowBackgroundPreferenceController extends AbstractPreferenceController
+ implements PreferenceControllerMixin {
+
+ private static final String TAG = "AllowBackgroundPreferenceController";
+
+ @VisibleForTesting static final String KEY_ALLOW_BACKGROUND_USAGE = "allow_background_usage";
+
+ @VisibleForTesting BatteryOptimizeUtils mBatteryOptimizeUtils;
+
+ public AllowBackgroundPreferenceController(Context context, int uid, String packageName) {
+ super(context);
+ mBatteryOptimizeUtils = new BatteryOptimizeUtils(context, uid, packageName);
+ }
+
+ private void setChecked(Preference preference, boolean checked) {
+ if (preference instanceof PrimarySwitchPreference) {
+ ((PrimarySwitchPreference) preference).setChecked(checked);
+ } else if (preference instanceof MainSwitchPreference) {
+ ((MainSwitchPreference) preference).setChecked(checked);
+ }
+ }
+
+ @Override
+ public void updateState(Preference preference) {
+ preference.setEnabled(mBatteryOptimizeUtils.isOptimizeModeMutable());
+
+ final boolean isAllowBackground = mBatteryOptimizeUtils.getAppOptimizationMode()
+ != BatteryOptimizeUtils.MODE_RESTRICTED;
+ setChecked(preference, isAllowBackground);
+ }
+
+ @Override
+ public boolean isAvailable() {
+ return true;
+ }
+
+ @Override
+ public String getPreferenceKey() {
+ return KEY_ALLOW_BACKGROUND_USAGE;
+ }
+
+ @Override
+ public boolean handlePreferenceTreeClick(Preference preference) {
+ return getPreferenceKey().equals(preference.getKey());
+ }
+}
diff --git a/src/com/android/settings/fuelgauge/BatteryOptimizeUtils.java b/src/com/android/settings/fuelgauge/BatteryOptimizeUtils.java
index 7b3a6ad..003f771 100644
--- a/src/com/android/settings/fuelgauge/BatteryOptimizeUtils.java
+++ b/src/com/android/settings/fuelgauge/BatteryOptimizeUtils.java
@@ -147,6 +147,22 @@
}
/**
+ * Return {@code true} if the optimization mode of this package can be changed
+ */
+ public boolean isOptimizeModeMutable() {
+ return !isDisabledForOptimizeModeOnly() && !isSystemOrDefaultApp();
+ }
+
+ /**
+ * Return {@code true} if the optimization mode is mutable and current state is not restricted
+ */
+ public boolean isSelectorPreferenceEnabled() {
+ // Enable the preference if apps are not set into restricted mode, otherwise disable it
+ return isOptimizeModeMutable()
+ && getAppOptimizationMode() != BatteryOptimizeUtils.MODE_RESTRICTED;
+ }
+
+ /**
* Gets the list of installed applications.
*/
public static ArraySet<ApplicationInfo> getInstalledApplications(
diff --git a/src/com/android/settings/fuelgauge/BatteryUtils.java b/src/com/android/settings/fuelgauge/BatteryUtils.java
index 3b958ae..c38af07 100644
--- a/src/com/android/settings/fuelgauge/BatteryUtils.java
+++ b/src/com/android/settings/fuelgauge/BatteryUtils.java
@@ -88,6 +88,7 @@
public static final String BYPASS_DOCK_DEFENDER_ACTION = "battery.dock.defender.bypass";
private static final String GOOGLE_PLAY_STORE_PACKAGE = "com.android.vending";
+ private static final String PACKAGE_NAME_NONE = "none";
@Retention(RetentionPolicy.SOURCE)
@IntDef({StatusType.SCREEN_USAGE,
@@ -140,6 +141,12 @@
FeatureFactory.getFeatureFactory().getPowerUsageFeatureProvider();
}
+ /** For test to reset single instance. */
+ @VisibleForTesting
+ public void reset() {
+ sInstance = null;
+ }
+
public long getProcessTimeMs(@StatusType int type, @Nullable BatteryStats.Uid uid,
int which) {
if (uid == null) {
@@ -616,6 +623,12 @@
&& GOOGLE_PLAY_STORE_PACKAGE.equals(installSourceInfo.getInitiatingPackageName());
}
+ /** Gets the logging package name. */
+ public static String getLoggingPackageName(Context context, String originalPackingName) {
+ return BatteryUtils.isAppInstalledFromGooglePlayStore(context, originalPackingName)
+ ? originalPackingName : PACKAGE_NAME_NONE;
+ }
+
/** Gets the latest sticky battery intent from the Android system. */
public static Intent getBatteryIntent(Context context) {
return com.android.settingslib.fuelgauge.BatteryUtils.getBatteryIntent(context);
diff --git a/src/com/android/settings/fuelgauge/OptimizedPreferenceController.java b/src/com/android/settings/fuelgauge/OptimizedPreferenceController.java
index ca75b0e..3fed00c 100644
--- a/src/com/android/settings/fuelgauge/OptimizedPreferenceController.java
+++ b/src/com/android/settings/fuelgauge/OptimizedPreferenceController.java
@@ -17,7 +17,6 @@
package com.android.settings.fuelgauge;
import android.content.Context;
-import android.util.Log;
import androidx.annotation.VisibleForTesting;
import androidx.preference.Preference;
@@ -31,8 +30,10 @@
private static final String TAG = "OPTIMIZED_PREF";
- @VisibleForTesting String KEY_OPTIMIZED_PREF = "optimized_pref";
- @VisibleForTesting BatteryOptimizeUtils mBatteryOptimizeUtils;
+ @VisibleForTesting
+ static final String KEY_OPTIMIZED_PREF = "optimized_preference";
+ @VisibleForTesting
+ BatteryOptimizeUtils mBatteryOptimizeUtils;
public OptimizedPreferenceController(Context context, int uid, String packageName) {
super(context);
@@ -46,24 +47,12 @@
@Override
public void updateState(Preference preference) {
- if (mBatteryOptimizeUtils.isDisabledForOptimizeModeOnly()) {
- Log.d(TAG, "disable preference for " + mBatteryOptimizeUtils.getPackageName());
- preference.setEnabled(true);
- ((SelectorWithWidgetPreference) preference).setChecked(true);
- return;
- }
+ preference.setEnabled(mBatteryOptimizeUtils.isSelectorPreferenceEnabled());
- if (mBatteryOptimizeUtils.getAppOptimizationMode()
- == BatteryOptimizeUtils.MODE_OPTIMIZED) {
- Log.d(TAG, "is optimized states");
- ((SelectorWithWidgetPreference) preference).setChecked(true);
- } else {
- ((SelectorWithWidgetPreference) preference).setChecked(false);
- if (mBatteryOptimizeUtils.isSystemOrDefaultApp()) {
- Log.d(TAG, "is system or default app, disable pref");
- preference.setEnabled(false);
- }
- }
+ final boolean isOptimized = mBatteryOptimizeUtils.isDisabledForOptimizeModeOnly()
+ || mBatteryOptimizeUtils.getAppOptimizationMode()
+ == BatteryOptimizeUtils.MODE_OPTIMIZED;
+ ((SelectorWithWidgetPreference) preference).setChecked(isOptimized);
}
@Override
diff --git a/src/com/android/settings/fuelgauge/PowerBackgroundUsageDetail.java b/src/com/android/settings/fuelgauge/PowerBackgroundUsageDetail.java
new file mode 100644
index 0000000..3bf4562
--- /dev/null
+++ b/src/com/android/settings/fuelgauge/PowerBackgroundUsageDetail.java
@@ -0,0 +1,351 @@
+/*
+ * 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.android.settings.fuelgauge.BatteryOptimizeHistoricalLogEntry.Action;
+
+import android.app.Activity;
+import android.app.backup.BackupManager;
+import android.app.settings.SettingsEnums;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.UserHandle;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.View;
+import android.widget.Switch;
+
+import androidx.annotation.VisibleForTesting;
+
+import com.android.settings.R;
+import com.android.settings.core.SubSettingLauncher;
+import com.android.settings.dashboard.DashboardFragment;
+import com.android.settings.overlay.FeatureFactory;
+import com.android.settings.widget.EntityHeaderController;
+import com.android.settingslib.HelpUtils;
+import com.android.settingslib.applications.AppUtils;
+import com.android.settingslib.applications.ApplicationsState;
+import com.android.settingslib.core.AbstractPreferenceController;
+import com.android.settingslib.widget.FooterPreference;
+import com.android.settingslib.widget.LayoutPreference;
+import com.android.settingslib.widget.MainSwitchPreference;
+import com.android.settingslib.widget.OnMainSwitchChangeListener;
+import com.android.settingslib.widget.SelectorWithWidgetPreference;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+/**
+ * Allow background usage fragment for each app
+ */
+public class PowerBackgroundUsageDetail extends DashboardFragment implements
+ SelectorWithWidgetPreference.OnClickListener,
+ OnMainSwitchChangeListener {
+ private static final String TAG = "PowerBackgroundUsageDetail";
+
+ public static final String EXTRA_UID = "extra_uid";
+ public static final String EXTRA_PACKAGE_NAME = "extra_package_name";
+ public static final String EXTRA_LABEL = "extra_label";
+ public static final String EXTRA_POWER_USAGE_AMOUNT = "extra_power_usage_amount";
+ public static final String EXTRA_ICON_ID = "extra_icon_id";
+ private static final String KEY_PREF_HEADER = "header_view";
+ private static final String KEY_PREF_UNRESTRICTED = "unrestricted_preference";
+ private static final String KEY_PREF_OPTIMIZED = "optimized_preference";
+ private static final String KEY_ALLOW_BACKGROUND_USAGE = "allow_background_usage";
+ private static final String KEY_FOOTER_PREFERENCE = "app_usage_footer_preference";
+
+ private final ExecutorService mExecutor = Executors.newSingleThreadExecutor();
+
+ @VisibleForTesting
+ LayoutPreference mHeaderPreference;
+ @VisibleForTesting
+ ApplicationsState mState;
+ @VisibleForTesting
+ ApplicationsState.AppEntry mAppEntry;
+ @VisibleForTesting
+ BatteryOptimizeUtils mBatteryOptimizeUtils;
+ @VisibleForTesting
+ SelectorWithWidgetPreference mOptimizePreference;
+ @VisibleForTesting
+ SelectorWithWidgetPreference mUnrestrictedPreference;
+ @VisibleForTesting
+ MainSwitchPreference mMainSwitchPreference;
+ @VisibleForTesting
+ FooterPreference mFooterPreference;
+ @VisibleForTesting
+ BackupManager mBackupManager;
+ @VisibleForTesting
+ StringBuilder mLogStringBuilder;
+ @VisibleForTesting
+ @BatteryOptimizeUtils.OptimizationMode
+ int mOptimizationMode = BatteryOptimizeUtils.MODE_UNKNOWN;
+
+ @Override
+ public void onAttach(Activity activity) {
+ super.onAttach(activity);
+
+ mState = ApplicationsState.getInstance(getActivity().getApplication());
+ }
+
+ @Override
+ public void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+
+ final String packageName = getArguments().getString(EXTRA_PACKAGE_NAME);
+ onCreateBackgroundUsageState(packageName);
+ mHeaderPreference = findPreference(KEY_PREF_HEADER);
+
+ if (packageName != null) {
+ mAppEntry = mState.getEntry(packageName, UserHandle.myUserId());
+ }
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ initHeader();
+ mOptimizationMode = mBatteryOptimizeUtils.getAppOptimizationMode();
+ initFooter();
+ mExecutor.execute(() -> {
+ String packageName = BatteryUtils
+ .getLoggingPackageName(getContext(), mBatteryOptimizeUtils.getPackageName());
+ FeatureFactory.getFeatureFactory().getMetricsFeatureProvider()
+ .action(
+ getContext(),
+ SettingsEnums.OPEN_APP_BATTERY_USAGE,
+ packageName);
+ });
+ mLogStringBuilder = new StringBuilder("onResume mode = ").append(mOptimizationMode);
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+
+ notifyBackupManager();
+ final int currentOptimizeMode = mBatteryOptimizeUtils.getAppOptimizationMode();
+ mLogStringBuilder.append(", onPause mode = ").append(currentOptimizeMode);
+ logMetricCategory(currentOptimizeMode);
+
+ mExecutor.execute(() -> {
+ BatteryOptimizeLogUtils.writeLog(
+ getContext().getApplicationContext(),
+ Action.LEAVE,
+ BatteryOptimizeLogUtils.getPackageNameWithUserId(
+ mBatteryOptimizeUtils.getPackageName(), UserHandle.myUserId()),
+ mLogStringBuilder.toString());
+ });
+ Log.d(TAG, "Leave with mode: " + currentOptimizeMode);
+ }
+
+ @Override
+ public void onRadioButtonClicked(SelectorWithWidgetPreference selected) {
+ final String selectedKey = selected == null ? null : selected.getKey();
+ updateSelectorPreferenceState(mUnrestrictedPreference, selectedKey);
+ updateSelectorPreferenceState(mOptimizePreference, selectedKey);
+ mBatteryOptimizeUtils.setAppUsageState(getSelectedPreference(), Action.APPLY);
+ }
+
+ @Override
+ public void onSwitchChanged(Switch switchView, boolean isChecked) {
+ mMainSwitchPreference.setChecked(isChecked);
+ updateSelectorPreference(isChecked);
+ }
+
+ @Override
+ public int getMetricsCategory() {
+ return SettingsEnums.FUELGAUGE_POWER_USAGE_MANAGE_BACKGROUND;
+ }
+
+ @Override
+ protected List<AbstractPreferenceController> createPreferenceControllers(Context context) {
+ final List<AbstractPreferenceController> controllers = new ArrayList<>();
+ final Bundle bundle = getArguments();
+ final int uid = bundle.getInt(EXTRA_UID, 0);
+ final String packageName = bundle.getString(EXTRA_PACKAGE_NAME);
+
+ controllers.add(new AllowBackgroundPreferenceController(context, uid, packageName));
+ controllers.add(new OptimizedPreferenceController(context, uid, packageName));
+ controllers.add(new UnrestrictedPreferenceController(context, uid, packageName));
+
+ return controllers;
+ }
+
+ @Override
+ protected int getPreferenceScreenResId() {
+ return R.xml.power_background_usage_detail;
+ }
+
+ @Override
+ protected String getLogTag() {
+ return TAG;
+ }
+
+ @VisibleForTesting
+ void updateSelectorPreference(boolean isEnabled) {
+ mOptimizePreference.setEnabled(isEnabled);
+ mUnrestrictedPreference.setEnabled(isEnabled);
+ onRadioButtonClicked(isEnabled ? mOptimizePreference : null);
+ }
+
+ @VisibleForTesting
+ void notifyBackupManager() {
+ if (mOptimizationMode != mBatteryOptimizeUtils.getAppOptimizationMode()) {
+ final BackupManager backupManager = mBackupManager != null
+ ? mBackupManager : new BackupManager(getContext());
+ backupManager.dataChanged();
+ }
+ }
+
+ @VisibleForTesting
+ int getSelectedPreference() {
+ if (!mMainSwitchPreference.isChecked()) {
+ return BatteryOptimizeUtils.MODE_RESTRICTED;
+ } else if (mUnrestrictedPreference.isChecked()) {
+ return BatteryOptimizeUtils.MODE_UNRESTRICTED;
+ } else if (mOptimizePreference.isChecked()) {
+ return BatteryOptimizeUtils.MODE_OPTIMIZED;
+ } else {
+ return BatteryOptimizeUtils.MODE_UNKNOWN;
+ }
+ }
+
+ static void startPowerBackgroundUsageDetailPage(
+ Context context, Bundle args) {
+ new SubSettingLauncher(context)
+ .setDestination(PowerBackgroundUsageDetail.class.getName())
+ .setArguments(args)
+ .setSourceMetricsCategory(SettingsEnums.FUELGAUGE_POWER_USAGE_MANAGE_BACKGROUND)
+ .launch();
+ }
+
+ @VisibleForTesting
+ void initHeader() {
+ final View appSnippet = mHeaderPreference.findViewById(R.id.entity_header);
+ final Activity context = getActivity();
+ final Bundle bundle = getArguments();
+ EntityHeaderController controller = EntityHeaderController
+ .newInstance(context, this, appSnippet)
+ .setButtonActions(EntityHeaderController.ActionType.ACTION_NONE,
+ EntityHeaderController.ActionType.ACTION_NONE);
+
+ if (mAppEntry == null) {
+ controller.setLabel(bundle.getString(EXTRA_LABEL));
+
+ final int iconId = bundle.getInt(EXTRA_ICON_ID, 0);
+ if (iconId == 0) {
+ controller.setIcon(context.getPackageManager().getDefaultActivityIcon());
+ } else {
+ controller.setIcon(context.getDrawable(bundle.getInt(EXTRA_ICON_ID)));
+ }
+ } else {
+ mState.ensureIcon(mAppEntry);
+ controller.setLabel(mAppEntry);
+ controller.setIcon(mAppEntry);
+ controller.setIsInstantApp(AppUtils.isInstant(mAppEntry.info));
+ }
+
+ controller.done(true /* rebindActions */);
+ }
+
+ @VisibleForTesting
+ void initFooter() {
+ final String stateString;
+ final String footerString;
+ final Context context = getContext();
+
+ if (mBatteryOptimizeUtils.isDisabledForOptimizeModeOnly()) {
+ // Present optimized only string when the package name is invalid.
+ stateString = context.getString(R.string.manager_battery_usage_optimized_only);
+ footerString = context.getString(
+ R.string.manager_battery_usage_footer_limited, stateString);
+ } else if (mBatteryOptimizeUtils.isSystemOrDefaultApp()) {
+ // Present unrestricted only string when the package is system or default active app.
+ stateString = context.getString(R.string.manager_battery_usage_unrestricted_only);
+ footerString = context.getString(
+ R.string.manager_battery_usage_footer_limited, stateString);
+ } else {
+ // Present default string to normal app.
+ footerString = context.getString(R.string.manager_battery_usage_footer);
+ }
+ mFooterPreference.setTitle(footerString);
+ final Intent helpIntent = HelpUtils.getHelpIntent(context, context.getString(
+ R.string.help_url_app_usage_settings), /*backupContext=*/ "");
+ if (helpIntent != null) {
+ mFooterPreference.setLearnMoreAction(v ->
+ startActivityForResult(helpIntent, /*requestCode=*/ 0));
+ mFooterPreference.setLearnMoreText(
+ context.getString(R.string.manager_battery_usage_link_a11y));
+ }
+ }
+
+ private void onCreateBackgroundUsageState(String packageName) {
+ mOptimizePreference = findPreference(KEY_PREF_OPTIMIZED);
+ mUnrestrictedPreference = findPreference(KEY_PREF_UNRESTRICTED);
+ mMainSwitchPreference = findPreference(KEY_ALLOW_BACKGROUND_USAGE);
+ mFooterPreference = findPreference(KEY_FOOTER_PREFERENCE);
+
+ mOptimizePreference.setOnClickListener(this);
+ mUnrestrictedPreference.setOnClickListener(this);
+ mMainSwitchPreference.addOnSwitchChangeListener(this);
+
+ mBatteryOptimizeUtils = new BatteryOptimizeUtils(
+ getContext(), getArguments().getInt(EXTRA_UID), packageName);
+ }
+
+ private void updateSelectorPreferenceState(SelectorWithWidgetPreference preference,
+ String selectedKey) {
+ preference.setChecked(TextUtils.equals(selectedKey, preference.getKey()));
+ }
+
+ private void logMetricCategory(int currentOptimizeMode) {
+ if (currentOptimizeMode == mOptimizationMode) {
+ return;
+ }
+ int metricCategory = 0;
+ switch (currentOptimizeMode) {
+ case BatteryOptimizeUtils.MODE_UNRESTRICTED:
+ metricCategory = SettingsEnums.ACTION_APP_BATTERY_USAGE_UNRESTRICTED;
+ break;
+ case BatteryOptimizeUtils.MODE_OPTIMIZED:
+ metricCategory = SettingsEnums.ACTION_APP_BATTERY_USAGE_OPTIMIZED;
+ break;
+ case BatteryOptimizeUtils.MODE_RESTRICTED:
+ metricCategory = SettingsEnums.ACTION_APP_BATTERY_USAGE_RESTRICTED;
+ break;
+ }
+ if (metricCategory == 0) {
+ return;
+ }
+ int finalMetricCategory = metricCategory;
+ mExecutor.execute(() -> {
+ String packageName = BatteryUtils
+ .getLoggingPackageName(getContext(), mBatteryOptimizeUtils.getPackageName());
+ FeatureFactory.getFeatureFactory().getMetricsFeatureProvider()
+ .action(
+ /* attribution */ SettingsEnums.OPEN_APP_BATTERY_USAGE,
+ /* action */ finalMetricCategory,
+ /* pageId */ SettingsEnums.OPEN_APP_BATTERY_USAGE,
+ packageName,
+ getArguments().getInt(EXTRA_POWER_USAGE_AMOUNT));
+ });
+ }
+}
diff --git a/src/com/android/settings/fuelgauge/RestrictedPreferenceController.java b/src/com/android/settings/fuelgauge/RestrictedPreferenceController.java
deleted file mode 100644
index 7db77f1..0000000
--- a/src/com/android/settings/fuelgauge/RestrictedPreferenceController.java
+++ /dev/null
@@ -1,80 +0,0 @@
-/*
- * 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.fuelgauge;
-
-import android.content.Context;
-import android.util.Log;
-
-import androidx.annotation.VisibleForTesting;
-import androidx.preference.Preference;
-
-import com.android.settings.core.PreferenceControllerMixin;
-import com.android.settingslib.core.AbstractPreferenceController;
-import com.android.settingslib.widget.SelectorWithWidgetPreference;
-
-public class RestrictedPreferenceController extends AbstractPreferenceController
- implements PreferenceControllerMixin {
-
- private static final String TAG = "RESTRICTED_PREF";
-
- @VisibleForTesting String KEY_RESTRICTED_PREF = "restricted_pref";
- @VisibleForTesting BatteryOptimizeUtils mBatteryOptimizeUtils;
-
- public RestrictedPreferenceController(Context context, int uid, String packageName) {
- super(context);
- mBatteryOptimizeUtils = new BatteryOptimizeUtils(context, uid, packageName);
- }
-
- @Override
- public void updateState(Preference preference) {
-
- if (mBatteryOptimizeUtils.isDisabledForOptimizeModeOnly()) {
- Log.d(TAG, "disable preference for " + mBatteryOptimizeUtils.getPackageName());
- preference.setEnabled(false);
- return;
- } else {
- preference.setEnabled(true);
- }
-
- if (mBatteryOptimizeUtils.getAppOptimizationMode()
- == BatteryOptimizeUtils.MODE_RESTRICTED) {
- Log.d(TAG, "is restricted states");
- ((SelectorWithWidgetPreference) preference).setChecked(true);
- } else {
- ((SelectorWithWidgetPreference) preference).setChecked(false);
- if (mBatteryOptimizeUtils.isSystemOrDefaultApp()) {
- Log.d(TAG, "is system or default app, disable pref");
- preference.setEnabled(false);
- }
- }
- }
-
- @Override
- public boolean isAvailable() {
- return true;
- }
-
- @Override
- public String getPreferenceKey() {
- return KEY_RESTRICTED_PREF;
- }
-
- @Override
- public boolean handlePreferenceTreeClick(Preference preference) {
- return getPreferenceKey().equals(preference.getKey());
- }
-}
diff --git a/src/com/android/settings/fuelgauge/UnrestrictedPreferenceController.java b/src/com/android/settings/fuelgauge/UnrestrictedPreferenceController.java
index 4578723..b06b7e2 100644
--- a/src/com/android/settings/fuelgauge/UnrestrictedPreferenceController.java
+++ b/src/com/android/settings/fuelgauge/UnrestrictedPreferenceController.java
@@ -17,7 +17,6 @@
package com.android.settings.fuelgauge;
import android.content.Context;
-import android.util.Log;
import androidx.annotation.VisibleForTesting;
import androidx.preference.Preference;
@@ -31,7 +30,9 @@
private static final String TAG = "UNRESTRICTED_PREF";
- @VisibleForTesting String KEY_UNRESTRICTED_PREF = "unrestricted_pref";
+ @VisibleForTesting
+ static final String KEY_UNRESTRICTED_PREF = "unrestricted_preference";
+
@VisibleForTesting BatteryOptimizeUtils mBatteryOptimizeUtils;
public UnrestrictedPreferenceController(Context context, int uid, String packageName) {
@@ -41,26 +42,11 @@
@Override
public void updateState(Preference preference) {
+ preference.setEnabled(mBatteryOptimizeUtils.isSelectorPreferenceEnabled());
- if (mBatteryOptimizeUtils.isDisabledForOptimizeModeOnly()) {
- Log.d(TAG, "disable preference for " + mBatteryOptimizeUtils.getPackageName());
- preference.setEnabled(false);
- return;
- } else {
- preference.setEnabled(true);
- }
-
- if (mBatteryOptimizeUtils.getAppOptimizationMode()
- == BatteryOptimizeUtils.MODE_UNRESTRICTED) {
- Log.d(TAG, "is unrestricted states");
- ((SelectorWithWidgetPreference) preference).setChecked(true);
- } else {
- ((SelectorWithWidgetPreference) preference).setChecked(false);
- if (mBatteryOptimizeUtils.isSystemOrDefaultApp()) {
- Log.d(TAG, "is system or default app, disable pref");
- preference.setEnabled(false);
- }
- }
+ final boolean isUnrestricted = mBatteryOptimizeUtils.getAppOptimizationMode()
+ == BatteryOptimizeUtils.MODE_UNRESTRICTED;
+ ((SelectorWithWidgetPreference) preference).setChecked(isUnrestricted);
}
@Override
diff --git a/src/com/android/settings/fuelgauge/batteryusage/BatteryDiffEntry.java b/src/com/android/settings/fuelgauge/batteryusage/BatteryDiffEntry.java
index 9d7b629..a7deaf9 100644
--- a/src/com/android/settings/fuelgauge/batteryusage/BatteryDiffEntry.java
+++ b/src/com/android/settings/fuelgauge/batteryusage/BatteryDiffEntry.java
@@ -45,6 +45,9 @@
// Caches app label and icon to improve loading performance.
static final Map<String, BatteryEntry.NameAndIcon> sResourceCache = new HashMap<>();
+ // Caches package name and uid to improve loading performance.
+ static final Map<String, Integer> sPackageNameAndUidCache = new HashMap<>();
+
// Whether a specific item is valid to launch restriction page?
@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
static final Map<String, Boolean> sValidForRestriction = new HashMap<>();
@@ -289,10 +292,20 @@
return false;
}
- final int uid = BatteryUtils.getInstance(mContext).getPackageUid(packageName);
+ final int uid = getPackageUid(packageName);
return uid == BatteryUtils.UID_REMOVED_APPS || uid == BatteryUtils.UID_NULL;
}
+ private int getPackageUid(String packageName) {
+ if (sPackageNameAndUidCache.containsKey(packageName)) {
+ return sPackageNameAndUidCache.get(packageName);
+ }
+
+ int uid = BatteryUtils.getInstance(mContext).getPackageUid(packageName);
+ sPackageNameAndUidCache.put(packageName, uid);
+ return uid;
+ }
+
void loadLabelAndIcon() {
if (mIsLoaded) {
return;
@@ -498,10 +511,11 @@
return builder.toString();
}
- /** Clears app icon and label cache data. */
+ /** Clears all cache data. */
public static void clearCache() {
sResourceCache.clear();
sValidForRestriction.clear();
+ sPackageNameAndUidCache.clear();
}
private Drawable getBadgeIconForUser(Drawable icon) {
diff --git a/src/com/android/settings/fuelgauge/batteryusage/db/BatteryEventDao.java b/src/com/android/settings/fuelgauge/batteryusage/db/BatteryEventDao.java
index 0a6de71..91ff4e8 100644
--- a/src/com/android/settings/fuelgauge/batteryusage/db/BatteryEventDao.java
+++ b/src/com/android/settings/fuelgauge/batteryusage/db/BatteryEventDao.java
@@ -43,7 +43,7 @@
/** Gets the {@link Cursor} of all recorded data after a specific timestamp. */
@Query("SELECT * FROM BatteryEventEntity"
- + " WHERE timestamp > :timestamp AND batteryEventType IN (:batteryEventTypes)"
+ + " WHERE timestamp >= :timestamp AND batteryEventType IN (:batteryEventTypes)"
+ " ORDER BY timestamp DESC")
Cursor getAllAfter(long timestamp, List<Integer> batteryEventTypes);
diff --git a/src/com/android/settings/fuelgauge/datasaver/DynamicDenylistManager.java b/src/com/android/settings/fuelgauge/datasaver/DynamicDenylistManager.java
new file mode 100644
index 0000000..be72e56
--- /dev/null
+++ b/src/com/android/settings/fuelgauge/datasaver/DynamicDenylistManager.java
@@ -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.fuelgauge.datasaver;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.net.NetworkPolicyManager;
+
+import androidx.annotation.VisibleForTesting;
+
+/** A class to dynamically manage per apps {@link NetworkPolicyManager} POLICY_ flags. */
+public final class DynamicDenylistManager {
+
+ private static final String TAG = "DynamicDenylistManager";
+ private static final String PREF_KEY_MANUAL_DENY = "manual_denylist_preference";
+ private static final String PREF_KEY_DYNAMIC_DENY = "dynamic_denylist_preference";
+
+ private final Context mContext;
+ private final NetworkPolicyManager mNetworkPolicyManager;
+
+ private static DynamicDenylistManager sInstance;
+
+ /** @return a DynamicDenylistManager object */
+ public static DynamicDenylistManager getInstance(Context context) {
+ synchronized (DynamicDenylistManager.class) {
+ if (sInstance == null) {
+ sInstance = new DynamicDenylistManager(context);
+ }
+ return sInstance;
+ }
+ }
+
+ DynamicDenylistManager(Context context) {
+ mContext = context.getApplicationContext();
+ mNetworkPolicyManager = NetworkPolicyManager.from(mContext);
+ }
+
+ /** Update the target uid policy in {@link #getManualDenylistPref()}. */
+ public void updateManualDenylist(String uid, int policy) {
+ if (policy != NetworkPolicyManager.POLICY_REJECT_METERED_BACKGROUND) {
+ getManualDenylistPref().edit().remove(uid).apply();
+ } else {
+ getManualDenylistPref().edit().putInt(uid, policy).apply();
+ }
+ }
+
+ /** Return true if the target uid is in {@link #getManualDenylistPref()}. */
+ public boolean isInManualDenylist(String uid) {
+ return getManualDenylistPref().contains(uid);
+ }
+
+ /** Clear all data in {@link #getManualDenylistPref()} */
+ public void clearManualDenylistPref() {
+ getManualDenylistPref().edit().clear().apply();
+ }
+
+ /** Clear all data in {@link #getDynamicDenylistPref()} */
+ public void clearDynamicDenylistPref() {
+ getDynamicDenylistPref().edit().clear().apply();
+ }
+
+ @VisibleForTesting
+ SharedPreferences getManualDenylistPref() {
+ return mContext.getSharedPreferences(PREF_KEY_MANUAL_DENY, Context.MODE_PRIVATE);
+ }
+
+ @VisibleForTesting
+ SharedPreferences getDynamicDenylistPref() {
+ return mContext.getSharedPreferences(PREF_KEY_DYNAMIC_DENY, Context.MODE_PRIVATE);
+ }
+}
diff --git a/src/com/android/settings/localepicker/LocaleDragAndDropAdapter.java b/src/com/android/settings/localepicker/LocaleDragAndDropAdapter.java
index bfe0749..43fc9cf 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;
@@ -38,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;
@@ -225,6 +227,12 @@
"Negative position in onItemMove %d -> %d", fromPosition, toPosition));
}
+ if (fromPosition != toPosition) {
+ FeatureFactory.getFeatureFactory().getMetricsFeatureProvider()
+ .action(mContext, SettingsEnums.ACTION_REORDER_LANGUAGE,
+ mDragLocale.getLocale().toLanguageTag() + " move to " + toPosition);
+ }
+
notifyItemChanged(fromPosition); // to update the numbers
notifyItemChanged(toPosition);
notifyItemMoved(fromPosition, toPosition);
@@ -263,6 +271,9 @@
for (int i = itemCount - 1; i >= 0; i--) {
localeInfo = mFeedItemList.get(i);
if (localeInfo.getChecked()) {
+ FeatureFactory.getFeatureFactory().getMetricsFeatureProvider()
+ .action(mContext, SettingsEnums.ACTION_REMOVE_LANGUAGE,
+ localeInfo.getLocale().toLanguageTag());
mFeedItemList.remove(i);
}
}
diff --git a/src/com/android/settings/localepicker/LocaleHelperPreferenceController.java b/src/com/android/settings/localepicker/LocaleHelperPreferenceController.java
index a639c9d..b962b9e 100644
--- a/src/com/android/settings/localepicker/LocaleHelperPreferenceController.java
+++ b/src/com/android/settings/localepicker/LocaleHelperPreferenceController.java
@@ -16,6 +16,7 @@
package com.android.settings.localepicker;
+import android.app.settings.SettingsEnums;
import android.content.Context;
import android.content.Intent;
import android.util.Log;
@@ -24,8 +25,10 @@
import androidx.preference.PreferenceScreen;
import com.android.settings.R;
+import com.android.settings.overlay.FeatureFactory;
import com.android.settingslib.HelpUtils;
import com.android.settingslib.core.AbstractPreferenceController;
+import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
import com.android.settingslib.widget.FooterPreference;
/**
@@ -36,8 +39,11 @@
private static final String KEY_FOOTER_LANGUAGE_PICKER = "footer_languages_picker";
+ private final MetricsFeatureProvider mMetricsFeatureProvider;
+
public LocaleHelperPreferenceController(Context context) {
super(context);
+ mMetricsFeatureProvider = FeatureFactory.getFeatureFactory().getMetricsFeatureProvider();
}
@Override
@@ -72,6 +78,7 @@
mContext.getString(R.string.link_locale_picker_footer_learn_more),
mContext.getClass().getName());
if (intent != null) {
+ mMetricsFeatureProvider.action(mContext, SettingsEnums.ACTION_LANGUAGES_LEARN_MORE);
mContext.startActivity(intent);
} else {
Log.w(TAG, "HelpIntent is null");
diff --git a/src/com/android/settings/localepicker/LocaleListEditor.java b/src/com/android/settings/localepicker/LocaleListEditor.java
index bdda549..28f066a 100644
--- a/src/com/android/settings/localepicker/LocaleListEditor.java
+++ b/src/com/android/settings/localepicker/LocaleListEditor.java
@@ -224,6 +224,8 @@
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) {
diff --git a/src/com/android/settings/network/EnableMultiSimSidecar.java b/src/com/android/settings/network/EnableMultiSimSidecar.java
index aefd55f..6bc38ef 100644
--- a/src/com/android/settings/network/EnableMultiSimSidecar.java
+++ b/src/com/android/settings/network/EnableMultiSimSidecar.java
@@ -171,8 +171,11 @@
}
int activePorts = 0;
for (UiccSlotInfo slotInfo : slotsInfo) {
+ if (slotInfo == null) {
+ continue;
+ }
for (UiccPortInfo portInfo : slotInfo.getPorts()) {
- if (slotInfo != null && portInfo.isActive()) {
+ if (portInfo.isActive()) {
activePorts++;
}
}
@@ -189,8 +192,11 @@
}
Set<Integer> activeRemovableLogicalSlotIds = new ArraySet<>();
for (UiccSlotInfo info : infos) {
- for (UiccPortInfo portInfo :info.getPorts()) {
- if (info != null && portInfo.isActive() && info.isRemovable()) {
+ if (info == null) {
+ continue;
+ }
+ for (UiccPortInfo portInfo : info.getPorts()) {
+ if (portInfo.isActive() && info.isRemovable()) {
activeRemovableLogicalSlotIds.add(portInfo.getLogicalSlotIndex());
}
}
diff --git a/src/com/android/settings/network/MobileNetworkListFragment.java b/src/com/android/settings/network/MobileNetworkListFragment.java
deleted file mode 100644
index 3de05af..0000000
--- a/src/com/android/settings/network/MobileNetworkListFragment.java
+++ /dev/null
@@ -1,73 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.settings.network;
-
-import android.app.settings.SettingsEnums;
-import android.content.Context;
-import android.os.UserManager;
-
-import androidx.recyclerview.widget.RecyclerView;
-
-import com.android.settings.R;
-import com.android.settings.dashboard.DashboardFragment;
-import com.android.settings.network.telephony.MobileNetworkUtils;
-import com.android.settings.search.BaseSearchIndexProvider;
-import com.android.settingslib.search.SearchIndexable;
-
-@SearchIndexable(forTarget = SearchIndexable.ALL & ~SearchIndexable.ARC)
-public class MobileNetworkListFragment extends DashboardFragment {
- private static final String LOG_TAG = "NetworkListFragment";
-
- private static final String KEY_ADD_SIM = "add_sim";
-
- @Override
- public void onResume() {
- super.onResume();
- // Disable the animation of the preference list
- final RecyclerView prefListView = getListView();
- if (prefListView != null) {
- prefListView.setItemAnimator(null);
- }
-
- findPreference(KEY_ADD_SIM).setVisible(MobileNetworkUtils.showEuiccSettings(getContext()));
- }
-
- @Override
- protected int getPreferenceScreenResId() {
- return R.xml.network_provider_sims_list;
- }
-
- @Override
- protected String getLogTag() {
- return LOG_TAG;
- }
-
- @Override
- public int getMetricsCategory() {
- return SettingsEnums.MOBILE_NETWORK_LIST;
- }
-
- public static final BaseSearchIndexProvider SEARCH_INDEX_DATA_PROVIDER =
- new BaseSearchIndexProvider(R.xml.network_provider_sims_list) {
-
- @Override
- protected boolean isPageSearchEnabled(Context context) {
- return SubscriptionUtil.isSimHardwareVisible(context) &&
- context.getSystemService(UserManager.class).isAdminUser();
- }
- };
-}
diff --git a/src/com/android/settings/network/MobileNetworkListFragment.kt b/src/com/android/settings/network/MobileNetworkListFragment.kt
new file mode 100644
index 0000000..5000afd
--- /dev/null
+++ b/src/com/android/settings/network/MobileNetworkListFragment.kt
@@ -0,0 +1,83 @@
+/*
+ * 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
+
+import android.app.settings.SettingsEnums
+import android.content.Context
+import android.os.Bundle
+import android.provider.Settings
+import android.view.View
+import androidx.annotation.VisibleForTesting
+import androidx.preference.Preference
+import com.android.settings.R
+import com.android.settings.SettingsPreferenceFragment
+import com.android.settings.dashboard.DashboardFragment
+import com.android.settings.network.telephony.MobileNetworkUtils
+import com.android.settings.search.BaseSearchIndexProvider
+import com.android.settings.utils.observeSettingsGlobalBoolean
+import com.android.settingslib.search.SearchIndexable
+import com.android.settingslib.spaprivileged.framework.common.userManager
+
+@SearchIndexable(forTarget = SearchIndexable.ALL and SearchIndexable.ARC.inv())
+class MobileNetworkListFragment : DashboardFragment() {
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ observeAirplaneModeAndFinishIfOn()
+ }
+
+ override fun onResume() {
+ super.onResume()
+ // Disable the animation of the preference list
+ listView.itemAnimator = null
+
+ findPreference<Preference>(KEY_ADD_SIM)!!.isVisible =
+ MobileNetworkUtils.showEuiccSettings(context)
+ }
+
+ override fun getPreferenceScreenResId() = R.xml.network_provider_sims_list
+
+ override fun getLogTag() = LOG_TAG
+
+ override fun getMetricsCategory() = SettingsEnums.MOBILE_NETWORK_LIST
+
+ companion object {
+ private const val LOG_TAG = "NetworkListFragment"
+ private const val KEY_ADD_SIM = "add_sim"
+
+ @JvmStatic
+ fun SettingsPreferenceFragment.observeAirplaneModeAndFinishIfOn() {
+ requireContext().observeSettingsGlobalBoolean(
+ name = Settings.Global.AIRPLANE_MODE_ON,
+ lifecycle = viewLifecycleOwner.lifecycle,
+ ) { isAirplaneModeOn: Boolean ->
+ if (isAirplaneModeOn) {
+ finish()
+ }
+ }
+ }
+
+ @JvmField
+ val SEARCH_INDEX_DATA_PROVIDER = SearchIndexProvider()
+
+ @VisibleForTesting
+ class SearchIndexProvider : BaseSearchIndexProvider(R.xml.network_provider_sims_list) {
+ public override fun isPageSearchEnabled(context: Context): Boolean =
+ SubscriptionUtil.isSimHardwareVisible(context) &&
+ context.userManager.isAdminUser
+ }
+ }
+}
diff --git a/src/com/android/settings/network/MobileNetworkRepository.java b/src/com/android/settings/network/MobileNetworkRepository.java
index 54c571b..f69b63f 100644
--- a/src/com/android/settings/network/MobileNetworkRepository.java
+++ b/src/com/android/settings/network/MobileNetworkRepository.java
@@ -378,11 +378,11 @@
return mMobileNetworkInfoDao.queryMobileNetworkInfoBySubId(subId);
}
- private void getUiccInfoBySubscriptionInfo(UiccSlotInfo[] uiccSlotInfos,
+ private void getUiccInfoBySubscriptionInfo(@NonNull UiccSlotInfo[] uiccSlotInfos,
SubscriptionInfo subInfo) {
for (int i = 0; i < uiccSlotInfos.length; i++) {
UiccSlotInfo curSlotInfo = uiccSlotInfos[i];
- if (curSlotInfo.getCardStateInfo() == CARD_STATE_INFO_PRESENT) {
+ if (curSlotInfo != null && curSlotInfo.getCardStateInfo() == CARD_STATE_INFO_PRESENT) {
final int index = i;
mIsEuicc = curSlotInfo.getIsEuicc();
mCardState = curSlotInfo.getCardStateInfo();
diff --git a/src/com/android/settings/network/NetworkProviderSettings.java b/src/com/android/settings/network/NetworkProviderSettings.java
index bc271ca..b4de224 100644
--- a/src/com/android/settings/network/NetworkProviderSettings.java
+++ b/src/com/android/settings/network/NetworkProviderSettings.java
@@ -175,10 +175,6 @@
private static final String EXTRA_START_CONNECT_SSID = "wifi_start_connect_ssid";
private String mOpenSsid;
- private static boolean isVerboseLoggingEnabled() {
- return WifiPickerTracker.isVerboseLoggingEnabled();
- }
-
private boolean mIsViewLoading;
@VisibleForTesting
final Runnable mRemoveLoadingRunnable = () -> {
@@ -840,7 +836,7 @@
}
final int wifiState = mWifiPickerTracker.getWifiState();
- if (isVerboseLoggingEnabled()) {
+ if (mWifiPickerTracker.isVerboseLoggingEnabled()) {
Log.i(TAG, "onWifiStateChanged called with wifi state: " + wifiState);
}
diff --git a/src/com/android/settings/network/UiccSlotUtil.java b/src/com/android/settings/network/UiccSlotUtil.java
index 49a1a85..e329c74 100644
--- a/src/com/android/settings/network/UiccSlotUtil.java
+++ b/src/com/android/settings/network/UiccSlotUtil.java
@@ -301,7 +301,8 @@
}
if (slotId == INVALID_PHYSICAL_SLOT_ID) {
for (int i = 0; i < slots.length; i++) {
- if (slots[i].isRemovable()
+ if (slots[i] != null
+ && slots[i].isRemovable()
&& !slots[i].getIsEuicc()
&& !slots[i].getPorts().stream().findFirst().get().isActive()
&& slots[i].getCardStateInfo() != UiccSlotInfo.CARD_STATE_INFO_ERROR
@@ -310,8 +311,9 @@
}
}
} else {
- if (slotId >= slots.length || !slots[slotId].isRemovable()) {
- throw new UiccSlotsException("The given slotId is not a removable slot: " + slotId);
+ if (slotId >= slots.length || slots[slotId] == null || !slots[slotId].isRemovable()) {
+ Log.d(TAG, "The given slotId is not a removable slot: " + slotId);
+ return INVALID_PHYSICAL_SLOT_ID;
}
if (!slots[slotId].getPorts().stream().findFirst().get().isActive()) {
return slotId;
diff --git a/src/com/android/settings/network/apn/ApnEditPageProvider.kt b/src/com/android/settings/network/apn/ApnEditPageProvider.kt
index 0e23a0e..81da0bf 100644
--- a/src/com/android/settings/network/apn/ApnEditPageProvider.kt
+++ b/src/com/android/settings/network/apn/ApnEditPageProvider.kt
@@ -96,7 +96,7 @@
getNetworkTypeSelectedOptionsState(apnData.networkType)
}
RegularScaffold(
- title = stringResource(id = R.string.apn_edit),
+ title = if(apnDataInit.newApn) stringResource(id = R.string.apn_add) else stringResource(id = R.string.apn_edit),
actions = {
IconButton(onClick = {
validateAndSaveApnData(
diff --git a/src/com/android/settings/network/telephony/MobileNetworkSettings.java b/src/com/android/settings/network/telephony/MobileNetworkSettings.java
index afc1b7e..7e290b8 100644
--- a/src/com/android/settings/network/telephony/MobileNetworkSettings.java
+++ b/src/com/android/settings/network/telephony/MobileNetworkSettings.java
@@ -16,6 +16,8 @@
package com.android.settings.network.telephony;
+import static com.android.settings.network.MobileNetworkListFragment.observeAirplaneModeAndFinishIfOn;
+
import android.app.Activity;
import android.app.settings.SettingsEnums;
import android.content.Context;
@@ -31,7 +33,10 @@
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
+import android.view.View;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.preference.Preference;
@@ -327,6 +332,12 @@
}
@Override
+ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ observeAirplaneModeAndFinishIfOn(this);
+ }
+
+ @Override
public void onResume() {
super.onResume();
mMobileNetworkRepository.addRegister(this, this, mSubId);
@@ -361,11 +372,6 @@
super.onPause();
}
- @Override
- public void onDestroy() {
- super.onDestroy();
- }
-
@VisibleForTesting
void onRestoreInstance(Bundle icicle) {
if (icicle != null) {
diff --git a/src/com/android/settings/regionalpreferences/FirstDayOfWeekItemListController.java b/src/com/android/settings/regionalpreferences/FirstDayOfWeekItemListController.java
index 03a59de..d509d2e 100644
--- a/src/com/android/settings/regionalpreferences/FirstDayOfWeekItemListController.java
+++ b/src/com/android/settings/regionalpreferences/FirstDayOfWeekItemListController.java
@@ -16,6 +16,7 @@
package com.android.settings.regionalpreferences;
+import android.app.settings.SettingsEnums;
import android.content.Context;
import com.android.settings.R;
@@ -57,4 +58,9 @@
protected String[] getUnitValues() {
return mContext.getResources().getStringArray(R.array.first_day_of_week);
}
+
+ @Override
+ protected int getMetricsActionKey() {
+ return SettingsEnums.ACTION_SET_FIRST_DAY_OF_WEEK;
+ }
}
diff --git a/src/com/android/settings/regionalpreferences/RegionalPreferenceListBasePreferenceController.java b/src/com/android/settings/regionalpreferences/RegionalPreferenceListBasePreferenceController.java
index 2f2bf76..ac0e7ee 100644
--- a/src/com/android/settings/regionalpreferences/RegionalPreferenceListBasePreferenceController.java
+++ b/src/com/android/settings/regionalpreferences/RegionalPreferenceListBasePreferenceController.java
@@ -22,16 +22,20 @@
import androidx.preference.PreferenceScreen;
import com.android.settings.core.BasePreferenceController;
+import com.android.settings.overlay.FeatureFactory;
import com.android.settings.widget.TickButtonPreference;
+import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
/** A base controller for handling all regional preferences controllers. */
public abstract class RegionalPreferenceListBasePreferenceController extends
BasePreferenceController {
+ private final MetricsFeatureProvider mMetricsFeatureProvider;
private PreferenceCategory mPreferenceCategory;
public RegionalPreferenceListBasePreferenceController(Context context, String preferenceKey) {
super(context, preferenceKey);
+ mMetricsFeatureProvider = FeatureFactory.getFeatureFactory().getMetricsFeatureProvider();
}
@Override
@@ -61,6 +65,8 @@
RegionalPreferencesDataUtils.savePreference(mContext, getExtensionTypes(),
item.equals(RegionalPreferencesDataUtils.DEFAULT_VALUE)
? null : item);
+ mMetricsFeatureProvider.action(mContext, getMetricsActionKey(),
+ getPreferenceTitle(value) + " > " + getPreferenceTitle(item));
return true;
});
pref.setSelected(!value.isEmpty() && item.equals(value));
@@ -90,4 +96,8 @@
protected abstract String getExtensionTypes();
protected abstract String[] getUnitValues();
+
+ protected abstract int getMetricsActionKey();
+
+
}
diff --git a/src/com/android/settings/regionalpreferences/TemperatureUnitListController.java b/src/com/android/settings/regionalpreferences/TemperatureUnitListController.java
index c51ca71..91ab1a2 100644
--- a/src/com/android/settings/regionalpreferences/TemperatureUnitListController.java
+++ b/src/com/android/settings/regionalpreferences/TemperatureUnitListController.java
@@ -16,6 +16,7 @@
package com.android.settings.regionalpreferences;
+import android.app.settings.SettingsEnums;
import android.content.Context;
import com.android.settings.R;
@@ -55,4 +56,9 @@
protected String[] getUnitValues() {
return mContext.getResources().getStringArray(R.array.temperature_units);
}
+
+ @Override
+ protected int getMetricsActionKey() {
+ return SettingsEnums.ACTION_SET_TEMPERATURE_UNIT;
+ }
}
diff --git a/src/com/android/settings/spa/SettingsSpaEnvironment.kt b/src/com/android/settings/spa/SettingsSpaEnvironment.kt
index 40cc9a2..6c5dfab 100644
--- a/src/com/android/settings/spa/SettingsSpaEnvironment.kt
+++ b/src/com/android/settings/spa/SettingsSpaEnvironment.kt
@@ -35,7 +35,9 @@
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.TurnScreenOnAppsAppListProvider
import com.android.settings.spa.app.specialaccess.UseFullScreenIntentAppListProvider
+import com.android.settings.spa.app.specialaccess.VoiceActivationAppsListProvider
import com.android.settings.spa.app.specialaccess.WifiControlAppListProvider
import com.android.settings.spa.app.storage.StorageAppListPageProvider
import com.android.settings.spa.core.instrumentation.SpaLogProvider
@@ -66,8 +68,10 @@
PictureInPictureListProvider,
InstallUnknownAppsListProvider,
AlarmsAndRemindersAppListProvider,
+ VoiceActivationAppsListProvider,
WifiControlAppListProvider,
NfcTagAppsSettingsProvider,
+ TurnScreenOnAppsAppListProvider,
)
}
diff --git a/src/com/android/settings/spa/app/appinfo/AppButtons.kt b/src/com/android/settings/spa/app/appinfo/AppButtons.kt
index 3200b81..307ff11 100644
--- a/src/com/android/settings/spa/app/appinfo/AppButtons.kt
+++ b/src/com/android/settings/spa/app/appinfo/AppButtons.kt
@@ -17,6 +17,8 @@
package com.android.settings.spa.app.appinfo
import android.content.pm.ApplicationInfo
+import android.content.pm.FeatureFlags
+import android.content.pm.FeatureFlagsImpl
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.lifecycle.compose.collectAsStateWithLifecycle
@@ -25,16 +27,22 @@
import com.android.settingslib.spa.widget.button.ActionButtons
@Composable
-fun AppButtons(packageInfoPresenter: PackageInfoPresenter) {
+/**
+ * @param featureFlags can be overridden in tests
+ */
+fun AppButtons(packageInfoPresenter: PackageInfoPresenter, featureFlags: FeatureFlags = FeatureFlagsImpl()) {
if (remember(packageInfoPresenter) { packageInfoPresenter.isMainlineModule() }) return
- val presenter = remember { AppButtonsPresenter(packageInfoPresenter) }
+ val presenter = remember { AppButtonsPresenter(packageInfoPresenter, featureFlags) }
ActionButtons(actionButtons = presenter.getActionButtons())
}
private fun PackageInfoPresenter.isMainlineModule(): Boolean =
AppUtils.isMainlineModule(userPackageManager, packageName)
-private class AppButtonsPresenter(private val packageInfoPresenter: PackageInfoPresenter) {
+private class AppButtonsPresenter(
+ private val packageInfoPresenter: PackageInfoPresenter,
+ private val featureFlags: FeatureFlags
+) {
private val appLaunchButton = AppLaunchButton(packageInfoPresenter)
private val appInstallButton = AppInstallButton(packageInfoPresenter)
private val appDisableButton = AppDisableButton(packageInfoPresenter)
@@ -50,7 +58,7 @@
@Composable
private fun getActionButtons(app: ApplicationInfo): List<ActionButton> = listOfNotNull(
- appLaunchButton.getActionButton(app),
+ if (featureFlags.archiving()) null else appLaunchButton.getActionButton(app),
appInstallButton.getActionButton(app),
appDisableButton.getActionButton(app),
appUninstallButton.getActionButton(app),
diff --git a/src/com/android/settings/spa/app/appinfo/AppInfoSettings.kt b/src/com/android/settings/spa/app/appinfo/AppInfoSettings.kt
index a9d16ae..3b7f579 100644
--- a/src/com/android/settings/spa/app/appinfo/AppInfoSettings.kt
+++ b/src/com/android/settings/spa/app/appinfo/AppInfoSettings.kt
@@ -18,6 +18,8 @@
import android.app.settings.SettingsEnums
import android.content.pm.ApplicationInfo
+import android.content.pm.FeatureFlags
+import android.content.pm.FeatureFlagsImpl
import android.os.Bundle
import android.os.UserHandle
import android.util.FeatureFlagUtils
@@ -30,6 +32,7 @@
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavType
import androidx.navigation.navArgument
+import com.android.settings.flags.Flags
import com.android.settings.R
import com.android.settings.applications.AppInfoBase
import com.android.settings.applications.appinfo.AppInfoDashboardFragment
@@ -40,6 +43,7 @@
import com.android.settings.spa.app.specialaccess.InstallUnknownAppsListProvider
import com.android.settings.spa.app.specialaccess.ModifySystemSettingsAppListProvider
import com.android.settings.spa.app.specialaccess.PictureInPictureListProvider
+import com.android.settings.spa.app.specialaccess.VoiceActivationAppsListProvider
import com.android.settingslib.spa.framework.common.SettingsPageProvider
import com.android.settingslib.spa.framework.compose.LifecycleEffect
import com.android.settingslib.spa.framework.compose.navigator
@@ -119,9 +123,11 @@
LifecycleEffect(onStart = { packageInfoPresenter.reloadPackageInfo() })
val packageInfo = packageInfoPresenter.flow.collectAsStateWithLifecycle().value ?: return
val app = checkNotNull(packageInfo.applicationInfo)
+ val featureFlags: FeatureFlags = FeatureFlagsImpl()
RegularScaffold(
title = stringResource(R.string.application_info_label),
actions = {
+ if (featureFlags.archiving()) TopBarAppLaunchButton(packageInfoPresenter, app)
AppInfoSettingsMoreOptions(packageInfoPresenter, app)
}
) {
@@ -156,6 +162,9 @@
InstallUnknownAppsListProvider.InfoPageEntryItem(app)
InteractAcrossProfilesDetailsPreference(app)
AlarmsAndRemindersAppListProvider.InfoPageEntryItem(app)
+ if (Flags.enableVoiceActivationAppsInSettings()) {
+ VoiceActivationAppsListProvider.InfoPageEntryItem(app)
+ }
}
Category(title = stringResource(R.string.app_install_details_group_title)) {
diff --git a/src/com/android/settings/spa/app/appinfo/TopBarAppLaunchButton.kt b/src/com/android/settings/spa/app/appinfo/TopBarAppLaunchButton.kt
new file mode 100644
index 0000000..92ad139
--- /dev/null
+++ b/src/com/android/settings/spa/app/appinfo/TopBarAppLaunchButton.kt
@@ -0,0 +1,59 @@
+/*
+ * 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.appinfo
+
+import android.content.ActivityNotFoundException
+import android.content.Intent
+import android.content.pm.ApplicationInfo
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.outlined.Launch
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.res.stringResource
+import com.android.settings.R
+import com.android.settingslib.spaprivileged.model.app.userHandle
+
+@Composable
+fun TopBarAppLaunchButton(packageInfoPresenter: PackageInfoPresenter, app: ApplicationInfo) {
+ val intent = packageInfoPresenter.launchIntent(app = app) ?: return
+ IconButton({ launchButtonAction(intent, app, packageInfoPresenter) }) {
+ Icon(
+ imageVector = Icons.AutoMirrored.Outlined.Launch,
+ contentDescription = stringResource(R.string.launch_instant_app),
+ )
+ }
+}
+
+private fun PackageInfoPresenter.launchIntent(
+ app: ApplicationInfo
+): Intent? {
+ return userPackageManager.getLaunchIntentForPackage(app.packageName)
+}
+
+private fun launchButtonAction(
+ intent: Intent,
+ app: ApplicationInfo,
+ packageInfoPresenter: PackageInfoPresenter
+) {
+ try {
+ packageInfoPresenter.context.startActivityAsUser(intent, app.userHandle)
+ } catch (_: ActivityNotFoundException) {
+ // Only happens after package changes like uninstall, and before page auto refresh or
+ // close, so ignore this exception is safe.
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/settings/spa/app/specialaccess/SpecialAppAccess.kt b/src/com/android/settings/spa/app/specialaccess/SpecialAppAccess.kt
index b40e32b..da97444 100644
--- a/src/com/android/settings/spa/app/specialaccess/SpecialAppAccess.kt
+++ b/src/com/android/settings/spa/app/specialaccess/SpecialAppAccess.kt
@@ -66,7 +66,9 @@
PictureInPictureListProvider,
InstallUnknownAppsListProvider,
AlarmsAndRemindersAppListProvider,
+ VoiceActivationAppsListProvider,
WifiControlAppListProvider,
+ TurnScreenOnAppsAppListProvider,
)
.map { it.buildAppListInjectEntry().setLink(fromPage = owner).build() }
}
diff --git a/src/com/android/settings/spa/app/specialaccess/TurnScreenOnApps.kt b/src/com/android/settings/spa/app/specialaccess/TurnScreenOnApps.kt
new file mode 100644
index 0000000..262acb7
--- /dev/null
+++ b/src/com/android/settings/spa/app/specialaccess/TurnScreenOnApps.kt
@@ -0,0 +1,54 @@
+/*
+ * 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.Manifest
+import android.app.AppOpsManager
+import android.app.settings.SettingsEnums
+import android.content.Context
+import com.android.settings.R
+import com.android.settings.overlay.FeatureFactory.Companion.featureFactory
+import com.android.settingslib.spaprivileged.template.app.AppOpPermissionListModel
+import com.android.settingslib.spaprivileged.template.app.AppOpPermissionRecord
+import com.android.settingslib.spaprivileged.template.app.TogglePermissionAppListProvider
+
+object TurnScreenOnAppsAppListProvider : TogglePermissionAppListProvider {
+ override val permissionType = "TurnScreenOnApps"
+ override fun createModel(context: Context) = TurnScreenOnAppsListModel(context)
+}
+
+class TurnScreenOnAppsListModel(context: Context) : AppOpPermissionListModel(context) {
+ override val pageTitleResId = com.android.settingslib.R.string.turn_screen_on_title
+ override val switchTitleResId = com.android.settingslib.R.string.allow_turn_screen_on
+ override val footerResId = com.android.settingslib.R.string.allow_turn_screen_on_description
+ override val appOp = AppOpsManager.OP_TURN_SCREEN_ON
+ override val permission = Manifest.permission.TURN_SCREEN_ON
+ override val setModeByUid = true
+
+ override fun setAllowed(record: AppOpPermissionRecord, newAllowed: Boolean) {
+ super.setAllowed(record, newAllowed)
+ logPermissionChange(newAllowed)
+ }
+
+ private fun logPermissionChange(newAllowed: Boolean) {
+ featureFactory.metricsFeatureProvider.action(
+ context,
+ SettingsEnums.SETTINGS_MANAGE_TURN_SCREEN_ON,
+ if (newAllowed) 1 else 0
+ )
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/settings/spa/app/specialaccess/VoiceActivationApps.kt b/src/com/android/settings/spa/app/specialaccess/VoiceActivationApps.kt
new file mode 100644
index 0000000..de5f3b7
--- /dev/null
+++ b/src/com/android/settings/spa/app/specialaccess/VoiceActivationApps.kt
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.spa.app.specialaccess
+
+import android.Manifest
+import android.app.AppOpsManager
+import android.app.settings.SettingsEnums
+import android.content.Context
+import android.content.res.Resources
+import com.android.settings.R
+import com.android.settings.overlay.FeatureFactory
+import com.android.settingslib.spaprivileged.template.app.AppOpPermissionListModel
+import com.android.settingslib.spaprivileged.template.app.AppOpPermissionRecord
+import com.android.settingslib.spaprivileged.template.app.TogglePermissionAppListProvider
+
+
+object VoiceActivationAppsListProvider : TogglePermissionAppListProvider {
+ override val permissionType = "VoiceActivationApps"
+ override fun createModel(context: Context) = VoiceActivationAppsListModel(context)
+}
+
+class VoiceActivationAppsListModel(context: Context) : AppOpPermissionListModel(context) {
+ override val pageTitleResId = R.string.voice_activation_apps_title
+ override val switchTitleResId = R.string.permit_voice_activation_apps
+ override val footerResId = R.string.allow_voice_activation_apps_description
+ override val appOp = AppOpsManager.OP_RECEIVE_SANDBOX_TRIGGER_AUDIO
+ override val permission = Manifest.permission.RECEIVE_SANDBOX_TRIGGER_AUDIO
+ override val setModeByUid = true
+
+ override fun setAllowed(record: AppOpPermissionRecord, newAllowed: Boolean) {
+ super.setAllowed(record, newAllowed)
+ logPermissionChange(newAllowed)
+ }
+
+ private fun logPermissionChange(newAllowed: Boolean) {
+ val category = when {
+ newAllowed -> SettingsEnums.APP_SPECIAL_PERMISSION_RECEIVE_SANDBOX_TRIGGER_AUDIO_ALLOW
+ else -> SettingsEnums.APP_SPECIAL_PERMISSION_RECEIVE_SANDBOX_TRIGGER_AUDIO_DENY
+ }
+ /**
+ * Leave the package string empty as we should not log the package names for the collected
+ * metrics.
+ */
+ FeatureFactory.featureFactory.metricsFeatureProvider.action(context, category, "")
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/settings/spa/app/specialaccess/VoiceActivationAppsPreferenceController.kt b/src/com/android/settings/spa/app/specialaccess/VoiceActivationAppsPreferenceController.kt
new file mode 100644
index 0000000..27d4b4b
--- /dev/null
+++ b/src/com/android/settings/spa/app/specialaccess/VoiceActivationAppsPreferenceController.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.app.specialaccess
+
+import android.content.Context
+import androidx.preference.Preference
+import com.android.settings.core.BasePreferenceController
+import com.android.settings.flags.Flags
+import com.android.settings.spa.SpaActivity.Companion.startSpaActivity
+
+class VoiceActivationAppsPreferenceController(context: Context, preferenceKey: String) :
+ BasePreferenceController(context, preferenceKey) {
+ override fun getAvailabilityStatus() =
+ if (Flags.enableVoiceActivationAppsInSettings()) AVAILABLE
+ else CONDITIONALLY_UNAVAILABLE
+
+ override fun handlePreferenceTreeClick(preference: Preference): Boolean {
+ if (preference.key == mPreferenceKey) {
+ mContext.startSpaActivity(VoiceActivationAppsListProvider.getAppListRoute())
+ return true
+ }
+ return false
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/settings/utils/SettingsGlobalBooleanDelegate.kt b/src/com/android/settings/utils/SettingsGlobalBooleanDelegate.kt
new file mode 100644
index 0000000..fdfbdb4
--- /dev/null
+++ b/src/com/android/settings/utils/SettingsGlobalBooleanDelegate.kt
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.utils
+
+import android.content.ContentResolver
+import android.content.Context
+import android.database.ContentObserver
+import android.os.Handler
+import android.provider.Settings
+import androidx.lifecycle.DefaultLifecycleObserver
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleOwner
+import kotlin.properties.ReadWriteProperty
+import kotlin.reflect.KProperty
+
+fun Context.observeSettingsGlobalBoolean(
+ name: String,
+ lifecycle: Lifecycle,
+ onChange: (newValue: Boolean) -> Unit,
+) {
+ val field by settingsGlobalBoolean(name)
+ val contentObserver = object : ContentObserver(Handler.getMain()) {
+ override fun onChange(selfChange: Boolean) {
+ onChange(field)
+ }
+ }
+ val uri = Settings.Global.getUriFor(name)
+ lifecycle.addObserver(object : DefaultLifecycleObserver {
+ override fun onStart(owner: LifecycleOwner) {
+ contentResolver.registerContentObserver(uri, false, contentObserver)
+ onChange(field)
+ }
+
+ override fun onStop(owner: LifecycleOwner) {
+ contentResolver.unregisterContentObserver(contentObserver)
+ }
+ })
+}
+
+fun Context.settingsGlobalBoolean(name: String): ReadWriteProperty<Any?, Boolean> =
+ SettingsGlobalBooleanDelegate(this, name)
+
+private class SettingsGlobalBooleanDelegate(context: Context, private val name: String) :
+ ReadWriteProperty<Any?, Boolean> {
+
+ private val contentResolver: ContentResolver = context.contentResolver
+
+ override fun getValue(thisRef: Any?, property: KProperty<*>): Boolean =
+ Settings.Global.getInt(contentResolver, name, 0) != 0
+
+ override fun setValue(thisRef: Any?, property: KProperty<*>, value: Boolean) {
+ Settings.Global.putInt(contentResolver, name, if (value) 1 else 0)
+ }
+}
diff --git a/src/com/android/settings/wifi/WifiEntryPreference.java b/src/com/android/settings/wifi/WifiEntryPreference.java
index c78f28c..e1add15 100644
--- a/src/com/android/settings/wifi/WifiEntryPreference.java
+++ b/src/com/android/settings/wifi/WifiEntryPreference.java
@@ -38,7 +38,6 @@
import com.android.settingslib.RestrictedPreference;
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;
@@ -108,7 +107,7 @@
@Override
public void onBindViewHolder(final PreferenceViewHolder view) {
super.onBindViewHolder(view);
- if (BaseWifiTracker.isVerboseLoggingEnabled()) {
+ if (mWifiEntry.isVerboseSummaryEnabled()) {
TextView summary = (TextView) view.findViewById(android.R.id.summary);
if (summary != null) {
summary.setMaxLines(100);
diff --git a/tests/robotests/src/com/android/settings/biometrics/fingerprint2/fragment/FingerprintEnrollIntroFragmentTest.kt b/tests/robotests/src/com/android/settings/biometrics/fingerprint2/fragment/FingerprintEnrollIntroFragmentTest.kt
index cea6676..024f346 100644
--- a/tests/robotests/src/com/android/settings/biometrics/fingerprint2/fragment/FingerprintEnrollIntroFragmentTest.kt
+++ b/tests/robotests/src/com/android/settings/biometrics/fingerprint2/fragment/FingerprintEnrollIntroFragmentTest.kt
@@ -33,12 +33,15 @@
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.runner.AndroidJUnit4
import com.android.settings.R
+import com.android.settings.biometrics.fingerprint2.shared.model.Default
import com.android.settings.biometrics.fingerprint2.ui.enrollment.fragment.FingerprintEnrollIntroV2Fragment
import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintEnrollNavigationViewModel
import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintEnrollViewModel
import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintGatekeeperViewModel
import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintScrollViewModel
import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.GatekeeperInfo
+import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.Intro
+import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.NavState
import com.android.settings.testutils2.FakeFingerprintManagerInteractor
import com.google.android.setupdesign.GlifLayout
import com.google.android.setupdesign.template.RequireScrollMixin
@@ -65,9 +68,12 @@
backgroundDispatcher,
interactor,
gatekeeperViewModel,
- canSkipConfirm = true,
+ Intro,
+ NavState(true),
+ Default,
)
- private var fingerprintViewModel = FingerprintEnrollViewModel(interactor, backgroundDispatcher)
+ private var fingerprintViewModel =
+ FingerprintEnrollViewModel(interactor, gatekeeperViewModel, navigationViewModel)
private var fingerprintScrollViewModel = FingerprintScrollViewModel()
@Before
diff --git a/tests/robotests/src/com/android/settings/fuelgauge/AdvancedPowerUsageDetailTest.java b/tests/robotests/src/com/android/settings/fuelgauge/AdvancedPowerUsageDetailTest.java
index 9131051..80486cb 100644
--- a/tests/robotests/src/com/android/settings/fuelgauge/AdvancedPowerUsageDetailTest.java
+++ b/tests/robotests/src/com/android/settings/fuelgauge/AdvancedPowerUsageDetailTest.java
@@ -17,6 +17,7 @@
package com.android.settings.fuelgauge;
import static com.android.settings.SettingsActivity.EXTRA_SHOW_FRAGMENT_ARGUMENTS;
+import static com.android.settings.fuelgauge.BatteryOptimizeHistoricalLogEntry.Action;
import static com.google.common.truth.Truth.assertThat;
@@ -53,13 +54,12 @@
import com.android.settings.testutils.shadow.ShadowActivityManager;
import com.android.settings.testutils.shadow.ShadowEntityHeaderController;
import com.android.settings.widget.EntityHeaderController;
+import com.android.settingslib.PrimarySwitchPreference;
import com.android.settingslib.applications.AppUtils;
import com.android.settingslib.applications.ApplicationsState;
import com.android.settingslib.applications.instantapps.InstantAppDataProvider;
import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
-import com.android.settingslib.widget.FooterPreference;
import com.android.settingslib.widget.LayoutPreference;
-import com.android.settingslib.widget.SelectorWithWidgetPreference;
import org.junit.After;
import org.junit.Before;
@@ -95,9 +95,7 @@
private static final long FOREGROUND_SERVICE_TIME_MS = 444;
private static final long FOREGROUND_TIME_MS =
FOREGROUND_ACTIVITY_TIME_MS + FOREGROUND_SERVICE_TIME_MS;
- private static final String KEY_PREF_UNRESTRICTED = "unrestricted_pref";
- private static final String KEY_PREF_OPTIMIZED = "optimized_pref";
- private static final String KEY_PREF_RESTRICTED = "restricted_pref";
+ private static final String KEY_ALLOW_BACKGROUND_USAGE = "allow_background_usage";
@Mock(answer = Answers.RETURNS_DEEP_STUBS)
private FragmentActivity mActivity;
@@ -127,10 +125,7 @@
private BackupManager mBackupManager;
private Context mContext;
- private FooterPreference mFooterPreference;
- private SelectorWithWidgetPreference mRestrictedPreference;
- private SelectorWithWidgetPreference mOptimizePreference;
- private SelectorWithWidgetPreference mUnrestrictedPreference;
+ private PrimarySwitchPreference mAllowBackgroundUsagePreference;
private AdvancedPowerUsageDetail mFragment;
private SettingsActivity mTestActivity;
private FakeFeatureFactory mFeatureFactory;
@@ -198,14 +193,9 @@
nullable(UserHandle.class));
doAnswer(callable).when(mActivity).startActivity(captor.capture());
- mFooterPreference = new FooterPreference(mContext);
- mRestrictedPreference = new SelectorWithWidgetPreference(mContext);
- mOptimizePreference = new SelectorWithWidgetPreference(mContext);
- mUnrestrictedPreference = new SelectorWithWidgetPreference(mContext);
- mFragment.mFooterPreference = mFooterPreference;
- mFragment.mRestrictedPreference = mRestrictedPreference;
- mFragment.mOptimizePreference = mOptimizePreference;
- mFragment.mUnrestrictedPreference = mUnrestrictedPreference;
+ mAllowBackgroundUsagePreference = new PrimarySwitchPreference(mContext);
+ mAllowBackgroundUsagePreference.setKey(KEY_ALLOW_BACKGROUND_USAGE);
+ mFragment.mAllowBackgroundUsagePreference = mAllowBackgroundUsagePreference;
}
@After
@@ -307,70 +297,60 @@
}
@Test
- public void initPreferenceForTriState_isValidPackageName_hasCorrectString() {
+ public void initFooter_isValidPackageName_hasCorrectString() {
when(mBatteryOptimizeUtils.isDisabledForOptimizeModeOnly()).thenReturn(true);
- mFragment.initPreferenceForTriState(mContext);
+ mFragment.initFooter();
- assertThat(mFooterPreference.getTitle().toString())
+ assertThat(mAllowBackgroundUsagePreference.getSummary().toString())
.isEqualTo("This app requires optimized battery usage.");
}
@Test
- public void initPreferenceForTriState_isSystemOrDefaultApp_hasCorrectString() {
+ public void initFooter_isSystemOrDefaultApp_hasCorrectString() {
when(mBatteryOptimizeUtils.isDisabledForOptimizeModeOnly()).thenReturn(false);
when(mBatteryOptimizeUtils.isSystemOrDefaultApp()).thenReturn(true);
- mFragment.initPreferenceForTriState(mContext);
+ mFragment.initFooter();
- assertThat(mFooterPreference.getTitle()
- .toString()).isEqualTo("This app requires unrestricted battery usage.");
+ assertThat(mAllowBackgroundUsagePreference.getSummary().toString())
+ .isEqualTo("This app requires unrestricted battery usage.");
}
@Test
- public void initPreferenceForTriState_hasCorrectString() {
+ public void initFooter_hasCorrectString() {
when(mBatteryOptimizeUtils.isDisabledForOptimizeModeOnly()).thenReturn(false);
when(mBatteryOptimizeUtils.isSystemOrDefaultApp()).thenReturn(false);
- mFragment.initPreferenceForTriState(mContext);
+ mFragment.initFooter();
- assertThat(mFooterPreference.getTitle().toString())
- .isEqualTo("Changing how an app uses your battery can affect its performance.");
- }
-
- @Test
- public void onRadioButtonClicked_clickOptimizePref_optimizePreferenceChecked() {
- mOptimizePreference.setKey(KEY_PREF_OPTIMIZED);
- mRestrictedPreference.setKey(KEY_PREF_RESTRICTED);
- mUnrestrictedPreference.setKey(KEY_PREF_UNRESTRICTED);
- mFragment.onRadioButtonClicked(mOptimizePreference);
-
- assertThat(mOptimizePreference.isChecked()).isTrue();
- assertThat(mRestrictedPreference.isChecked()).isFalse();
- assertThat(mUnrestrictedPreference.isChecked()).isFalse();
+ assertThat(mAllowBackgroundUsagePreference.getSummary().toString())
+ .isEqualTo("Enable for real-time updates, disable to save battery");
}
@Test
public void onPause_optimizationModeChanged_logPreference()
throws PackageManager.NameNotFoundException, InterruptedException {
final String packageName = "testPackageName";
- final int mode = BatteryOptimizeUtils.MODE_RESTRICTED;
- mFragment.mOptimizationMode = mode;
- when(mBatteryOptimizeUtils.getAppOptimizationMode()).thenReturn(mode);
+ final int restrictedMode = BatteryOptimizeUtils.MODE_RESTRICTED;
+ final int optimizedMode = BatteryOptimizeUtils.MODE_OPTIMIZED;
+ mFragment.mOptimizationMode = restrictedMode;
+ when(mBatteryOptimizeUtils.getAppOptimizationMode()).thenReturn(restrictedMode);
when(mBatteryOptimizeUtils.getPackageName()).thenReturn(packageName);
when(mContext.getPackageManager()).thenReturn(mPackageManager);
when(mPackageManager.getInstallSourceInfo(anyString())).thenReturn(mInstallSourceInfo);
when(mInstallSourceInfo.getInitiatingPackageName()).thenReturn("com.android.vending");
- mOptimizePreference.setKey(KEY_PREF_OPTIMIZED);
- mFragment.onRadioButtonClicked(mOptimizePreference);
+ mFragment.onPreferenceChange(mAllowBackgroundUsagePreference, true);
+ verify(mBatteryOptimizeUtils).setAppUsageState(optimizedMode, Action.APPLY);
+ when(mBatteryOptimizeUtils.getAppOptimizationMode()).thenReturn(optimizedMode);
mFragment.onPause();
TimeUnit.SECONDS.sleep(1);
verify(mMetricsFeatureProvider)
.action(
SettingsEnums.OPEN_APP_BATTERY_USAGE,
- SettingsEnums.ACTION_APP_BATTERY_USAGE_OPTIMIZED,
+ SettingsEnums.ACTION_APP_BATTERY_USAGE_ALLOW_BACKGROUND,
SettingsEnums.OPEN_APP_BATTERY_USAGE,
packageName,
/* consumed battery */ 0);
@@ -379,15 +359,20 @@
@Test
public void onPause_optimizationModeIsNotChanged_notInvokeLogging()
throws PackageManager.NameNotFoundException, InterruptedException {
- final int mode = BatteryOptimizeUtils.MODE_OPTIMIZED;
- mFragment.mOptimizationMode = mode;
- when(mBatteryOptimizeUtils.getAppOptimizationMode()).thenReturn(mode);
+ final int restrictedMode = BatteryOptimizeUtils.MODE_RESTRICTED;
+ final int optimizedMode = BatteryOptimizeUtils.MODE_OPTIMIZED;
+ mFragment.mOptimizationMode = restrictedMode;
+ when(mBatteryOptimizeUtils.getAppOptimizationMode()).thenReturn(restrictedMode);
when(mContext.getPackageManager()).thenReturn(mPackageManager);
when(mPackageManager.getInstallSourceInfo(anyString())).thenReturn(mInstallSourceInfo);
when(mInstallSourceInfo.getInitiatingPackageName()).thenReturn("com.android.vending");
- mOptimizePreference.setKey(KEY_PREF_OPTIMIZED);
- mFragment.onRadioButtonClicked(mOptimizePreference);
+ mFragment.onPreferenceChange(mAllowBackgroundUsagePreference, true);
+ verify(mBatteryOptimizeUtils).setAppUsageState(optimizedMode, Action.APPLY);
+ when(mBatteryOptimizeUtils.getAppOptimizationMode()).thenReturn(optimizedMode);
+ mFragment.onPreferenceChange(mAllowBackgroundUsagePreference, false);
+ verify(mBatteryOptimizeUtils).setAppUsageState(restrictedMode, Action.APPLY);
+ when(mBatteryOptimizeUtils.getAppOptimizationMode()).thenReturn(restrictedMode);
mFragment.onPause();
TimeUnit.SECONDS.sleep(1);
diff --git a/tests/robotests/src/com/android/settings/fuelgauge/AllowBackgroundPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/fuelgauge/AllowBackgroundPreferenceControllerTest.java
new file mode 100644
index 0000000..be80e1e
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/fuelgauge/AllowBackgroundPreferenceControllerTest.java
@@ -0,0 +1,150 @@
+/*
+ * 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 static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.content.pm.PackageManager;
+
+import com.android.settingslib.widget.MainSwitchPreference;
+
+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 AllowBackgroundPreferenceControllerTest {
+ private static final int UID = 12345;
+ private static final String PACKAGE_NAME = "com.android.app";
+
+ private AllowBackgroundPreferenceController mController;
+ private MainSwitchPreference mMainSwitchPreference;
+ private BatteryOptimizeUtils mBatteryOptimizeUtils;
+
+ @Mock private PackageManager mMockPackageManager;
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+
+ Context context = spy(RuntimeEnvironment.application);
+ BatteryUtils.getInstance(context).reset();
+ doReturn(UID)
+ .when(mMockPackageManager)
+ .getPackageUid(PACKAGE_NAME, PackageManager.GET_META_DATA);
+
+ mController = new AllowBackgroundPreferenceController(context, UID, PACKAGE_NAME);
+ mMainSwitchPreference = new MainSwitchPreference(RuntimeEnvironment.application);
+ mBatteryOptimizeUtils = spy(new BatteryOptimizeUtils(context, UID, PACKAGE_NAME));
+ mController.mBatteryOptimizeUtils = mBatteryOptimizeUtils;
+ }
+
+ @Test
+ public void testUpdateState_isValidPackage_prefEnabled() {
+ when(mBatteryOptimizeUtils.isDisabledForOptimizeModeOnly()).thenReturn(false);
+ when(mBatteryOptimizeUtils.isSystemOrDefaultApp()).thenReturn(false);
+
+ mController.updateState(mMainSwitchPreference);
+
+ assertThat(mBatteryOptimizeUtils.isOptimizeModeMutable()).isTrue();
+ assertThat(mMainSwitchPreference.isEnabled()).isTrue();
+ }
+
+ @Test
+ public void testUpdateState_invalidPackage_prefDisabled() {
+ when(mBatteryOptimizeUtils.isDisabledForOptimizeModeOnly()).thenReturn(true);
+ when(mBatteryOptimizeUtils.isSystemOrDefaultApp()).thenReturn(false);
+
+ mController.updateState(mMainSwitchPreference);
+
+ assertThat(mBatteryOptimizeUtils.isOptimizeModeMutable()).isFalse();
+ assertThat(mMainSwitchPreference.isEnabled()).isFalse();
+ }
+
+ @Test
+ public void testUpdateState_isSystemOrDefaultAppAndRestrictedStates_prefChecked() {
+ when(mBatteryOptimizeUtils.isDisabledForOptimizeModeOnly()).thenReturn(false);
+ when(mBatteryOptimizeUtils.isSystemOrDefaultApp()).thenReturn(true);
+ when(mBatteryOptimizeUtils.getAppOptimizationMode()).thenReturn(
+ BatteryOptimizeUtils.MODE_RESTRICTED);
+
+ mController.updateState(mMainSwitchPreference);
+
+ assertThat(mMainSwitchPreference.isEnabled()).isFalse();
+ assertThat(mMainSwitchPreference.isChecked()).isFalse();
+ }
+
+ @Test
+ public void testUpdateState_isSystemOrDefaultApp_prefUnchecked() {
+ when(mBatteryOptimizeUtils.isDisabledForOptimizeModeOnly()).thenReturn(false);
+ when(mBatteryOptimizeUtils.isSystemOrDefaultApp()).thenReturn(true);
+ when(mBatteryOptimizeUtils.getAppOptimizationMode()).thenReturn(
+ BatteryOptimizeUtils.MODE_OPTIMIZED);
+
+ mController.updateState(mMainSwitchPreference);
+
+ assertThat(mMainSwitchPreference.isEnabled()).isFalse();
+ assertThat(mMainSwitchPreference.isChecked()).isTrue();
+ }
+
+ @Test
+ public void testUpdateState_isRestrictedStates_prefChecked() {
+ when(mBatteryOptimizeUtils.isOptimizeModeMutable()).thenReturn(true);
+ when(mBatteryOptimizeUtils.getAppOptimizationMode()).thenReturn(
+ BatteryOptimizeUtils.MODE_RESTRICTED);
+
+ mController.updateState(mMainSwitchPreference);
+
+ assertThat(mMainSwitchPreference.isEnabled()).isTrue();
+ assertThat(mMainSwitchPreference.isChecked()).isFalse();
+ }
+
+ @Test
+ public void testUpdateState_prefUnchecked() {
+ when(mBatteryOptimizeUtils.isOptimizeModeMutable()).thenReturn(true);
+ when(mBatteryOptimizeUtils.getAppOptimizationMode()).thenReturn(
+ BatteryOptimizeUtils.MODE_OPTIMIZED);
+
+ mController.updateState(mMainSwitchPreference);
+
+ assertThat(mMainSwitchPreference.isEnabled()).isTrue();
+ assertThat(mMainSwitchPreference.isChecked()).isTrue();
+ }
+
+ @Test
+ public void testHandlePreferenceTreeClick_samePrefKey_verifyAction() {
+ mMainSwitchPreference.setKey(
+ AllowBackgroundPreferenceController.KEY_ALLOW_BACKGROUND_USAGE);
+ mController.handlePreferenceTreeClick(mMainSwitchPreference);
+
+ assertThat(mController.handlePreferenceTreeClick(mMainSwitchPreference)).isTrue();
+ }
+
+ @Test
+ public void testHandlePreferenceTreeClick_incorrectPrefKey_noAction() {
+ assertThat(mController.handlePreferenceTreeClick(mMainSwitchPreference)).isFalse();
+ }
+}
diff --git a/tests/robotests/src/com/android/settings/fuelgauge/BatteryBackupHelperTest.java b/tests/robotests/src/com/android/settings/fuelgauge/BatteryBackupHelperTest.java
index b8c72ee..350d2ef 100644
--- a/tests/robotests/src/com/android/settings/fuelgauge/BatteryBackupHelperTest.java
+++ b/tests/robotests/src/com/android/settings/fuelgauge/BatteryBackupHelperTest.java
@@ -121,6 +121,7 @@
mContext = spy(RuntimeEnvironment.application);
mStringWriter = new StringWriter();
mPrintWriter = new PrintWriter(mStringWriter);
+ BatteryUtils.getInstance(mContext).reset();
doReturn(mContext).when(mContext).getApplicationContext();
doReturn(mAppOpsManager).when(mContext).getSystemService(AppOpsManager.class);
doReturn(mUserManager).when(mContext).getSystemService(UserManager.class);
diff --git a/tests/robotests/src/com/android/settings/fuelgauge/BatterySettingsMigrateCheckerTest.java b/tests/robotests/src/com/android/settings/fuelgauge/BatterySettingsMigrateCheckerTest.java
index e2058e7..bab19e5 100644
--- a/tests/robotests/src/com/android/settings/fuelgauge/BatterySettingsMigrateCheckerTest.java
+++ b/tests/robotests/src/com/android/settings/fuelgauge/BatterySettingsMigrateCheckerTest.java
@@ -31,10 +31,8 @@
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.UserHandle;
-import android.os.UserManager;
import com.android.settings.TestUtils;
-import com.android.settings.fuelgauge.BatteryOptimizeHistoricalLogEntry;
import com.android.settings.fuelgauge.batterysaver.BatterySaverScheduleRadioButtonsController;
import org.junit.After;
@@ -75,6 +73,7 @@
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
mContext = spy(RuntimeEnvironment.application);
+ BatteryUtils.getInstance(mContext).reset();
doReturn(mContext).when(mContext).getApplicationContext();
doReturn(mPackageManager).when(mContext).getPackageManager();
doReturn(UID).when(mPackageManager)
diff --git a/tests/robotests/src/com/android/settings/fuelgauge/OptimizedPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/fuelgauge/OptimizedPreferenceControllerTest.java
index 71bb998..bfed149 100644
--- a/tests/robotests/src/com/android/settings/fuelgauge/OptimizedPreferenceControllerTest.java
+++ b/tests/robotests/src/com/android/settings/fuelgauge/OptimizedPreferenceControllerTest.java
@@ -18,8 +18,13 @@
import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.when;
+import android.content.Context;
+import android.content.pm.PackageManager;
+
import com.android.settingslib.widget.SelectorWithWidgetPreference;
import org.junit.Before;
@@ -37,34 +42,41 @@
private OptimizedPreferenceController mController;
private SelectorWithWidgetPreference mPreference;
+ private BatteryOptimizeUtils mBatteryOptimizeUtils;
- @Mock BatteryOptimizeUtils mockBatteryOptimizeUtils;
+ @Mock PackageManager mMockPackageManager;
@Before
- public void setUp() {
+ public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
- mController = new OptimizedPreferenceController(
- RuntimeEnvironment.application, UID, PACKAGE_NAME);
+ Context context = spy(RuntimeEnvironment.application);
+ BatteryUtils.getInstance(context).reset();
+ doReturn(UID)
+ .when(mMockPackageManager)
+ .getPackageUid(PACKAGE_NAME, PackageManager.GET_META_DATA);
+
+ mController = new OptimizedPreferenceController(context, UID, PACKAGE_NAME);
mPreference = new SelectorWithWidgetPreference(RuntimeEnvironment.application);
- mController.mBatteryOptimizeUtils = mockBatteryOptimizeUtils;
+ mBatteryOptimizeUtils = spy(new BatteryOptimizeUtils(context, UID, PACKAGE_NAME));
+ mController.mBatteryOptimizeUtils = mBatteryOptimizeUtils;
}
@Test
public void testUpdateState_invalidPackage_prefEnabled() {
- when(mockBatteryOptimizeUtils.isDisabledForOptimizeModeOnly()).thenReturn(true);
+ when(mBatteryOptimizeUtils.isDisabledForOptimizeModeOnly()).thenReturn(true);
mController.updateState(mPreference);
- assertThat(mPreference.isEnabled()).isTrue();
+ assertThat(mPreference.isEnabled()).isFalse();
assertThat(mPreference.isChecked()).isTrue();
}
@Test
public void testUpdateState_isSystemOrDefaultAppAndOptimizeStates_prefChecked() {
- when(mockBatteryOptimizeUtils.isDisabledForOptimizeModeOnly()).thenReturn(false);
- when(mockBatteryOptimizeUtils.isSystemOrDefaultApp()).thenReturn(true);
- when(mockBatteryOptimizeUtils.getAppOptimizationMode()).thenReturn(
+ when(mBatteryOptimizeUtils.isDisabledForOptimizeModeOnly()).thenReturn(false);
+ when(mBatteryOptimizeUtils.isSystemOrDefaultApp()).thenReturn(true);
+ when(mBatteryOptimizeUtils.getAppOptimizationMode()).thenReturn(
BatteryOptimizeUtils.MODE_OPTIMIZED);
mController.updateState(mPreference);
@@ -74,8 +86,8 @@
@Test
public void testUpdateState_isSystemOrDefaultApp_prefUnchecked() {
- when(mockBatteryOptimizeUtils.isDisabledForOptimizeModeOnly()).thenReturn(false);
- when(mockBatteryOptimizeUtils.isSystemOrDefaultApp()).thenReturn(true);
+ when(mBatteryOptimizeUtils.isDisabledForOptimizeModeOnly()).thenReturn(false);
+ when(mBatteryOptimizeUtils.isSystemOrDefaultApp()).thenReturn(true);
mController.updateState(mPreference);
@@ -85,8 +97,8 @@
@Test
public void testUpdateState_isOptimizedStates_prefChecked() {
- when(mockBatteryOptimizeUtils.isDisabledForOptimizeModeOnly()).thenReturn(false);
- when(mockBatteryOptimizeUtils.getAppOptimizationMode()).thenReturn(
+ when(mBatteryOptimizeUtils.isDisabledForOptimizeModeOnly()).thenReturn(false);
+ when(mBatteryOptimizeUtils.getAppOptimizationMode()).thenReturn(
BatteryOptimizeUtils.MODE_OPTIMIZED);
mController.updateState(mPreference);
@@ -96,7 +108,7 @@
@Test
public void testUpdateState_prefUnchecked() {
- when(mockBatteryOptimizeUtils.isDisabledForOptimizeModeOnly()).thenReturn(false);
+ when(mBatteryOptimizeUtils.isDisabledForOptimizeModeOnly()).thenReturn(false);
mController.updateState(mPreference);
diff --git a/tests/robotests/src/com/android/settings/fuelgauge/PowerBackgroundUsageDetailTest.java b/tests/robotests/src/com/android/settings/fuelgauge/PowerBackgroundUsageDetailTest.java
new file mode 100644
index 0000000..e6caf78
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/fuelgauge/PowerBackgroundUsageDetailTest.java
@@ -0,0 +1,285 @@
+/*
+ * 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.android.settings.SettingsActivity.EXTRA_SHOW_FRAGMENT_ARGUMENTS;
+import static com.android.settings.fuelgauge.BatteryOptimizeHistoricalLogEntry.Action;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.nullable;
+import static org.mockito.Mockito.doAnswer;
+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;
+
+import android.app.AppOpsManager;
+import android.app.backup.BackupManager;
+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.UserHandle;
+import android.widget.Switch;
+
+import androidx.fragment.app.FragmentActivity;
+import androidx.loader.app.LoaderManager;
+
+import com.android.settings.SettingsActivity;
+import com.android.settings.fuelgauge.batteryusage.BatteryEntry;
+import com.android.settings.testutils.shadow.ShadowActivityManager;
+import com.android.settings.testutils.shadow.ShadowEntityHeaderController;
+import com.android.settings.widget.EntityHeaderController;
+import com.android.settingslib.applications.AppUtils;
+import com.android.settingslib.applications.ApplicationsState;
+import com.android.settingslib.applications.instantapps.InstantAppDataProvider;
+import com.android.settingslib.widget.FooterPreference;
+import com.android.settingslib.widget.LayoutPreference;
+import com.android.settingslib.widget.MainSwitchPreference;
+import com.android.settingslib.widget.SelectorWithWidgetPreference;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Answers;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.mockito.stubbing.Answer;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Config;
+import org.robolectric.util.ReflectionHelpers;
+
+@RunWith(RobolectricTestRunner.class)
+@Config(shadows = {
+ ShadowEntityHeaderController.class,
+ ShadowActivityManager.class,
+ com.android.settings.testutils.shadow.ShadowFragment.class,
+})
+public class PowerBackgroundUsageDetailTest {
+ private static final String APP_LABEL = "app label";
+ private static final String SUMMARY = "summary";
+ private static final int ICON_ID = 123;
+ private static final int UID = 1;
+ private static final String KEY_PREF_UNRESTRICTED = "unrestricted_preference";
+ private static final String KEY_PREF_OPTIMIZED = "optimized_preference";
+ private static final String KEY_ALLOW_BACKGROUND_USAGE = "allow_background_usage";
+
+ private Context mContext;
+ private PowerBackgroundUsageDetail mFragment;
+ private FooterPreference mFooterPreference;
+ private MainSwitchPreference mMainSwitchPreference;
+ private SelectorWithWidgetPreference mOptimizePreference;
+ private SelectorWithWidgetPreference mUnrestrictedPreference;
+ private SettingsActivity mTestActivity;
+
+ @Mock(answer = Answers.RETURNS_DEEP_STUBS)
+ private FragmentActivity mActivity;
+ @Mock
+ private EntityHeaderController mEntityHeaderController;
+ @Mock
+ private BatteryOptimizeUtils mBatteryOptimizeUtils;
+ @Mock
+ private LayoutPreference mHeaderPreference;
+ @Mock
+ private ApplicationsState mState;
+ @Mock
+ private Bundle mBundle;
+ @Mock
+ private LoaderManager mLoaderManager;
+ @Mock
+ private ApplicationsState.AppEntry mAppEntry;
+ @Mock
+ private BatteryEntry mBatteryEntry;
+ @Mock
+ private BackupManager mBackupManager;
+ @Mock
+ private PackageManager mPackageManager;
+ @Mock
+ private AppOpsManager mAppOpsManager;
+ @Mock
+ private Switch mMockSwitch;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+
+ mContext = spy(RuntimeEnvironment.application);
+ when(mContext.getPackageName()).thenReturn("foo");
+
+ mFragment = spy(new PowerBackgroundUsageDetail());
+ doReturn(mContext).when(mFragment).getContext();
+ doReturn(mActivity).when(mFragment).getActivity();
+ doReturn(SUMMARY).when(mFragment).getString(anyInt());
+ doReturn(APP_LABEL).when(mBundle).getString(nullable(String.class));
+ when(mFragment.getArguments()).thenReturn(mBundle);
+ doReturn(mLoaderManager).when(mFragment).getLoaderManager();
+
+ ShadowEntityHeaderController.setUseMock(mEntityHeaderController);
+ doReturn(mEntityHeaderController).when(mEntityHeaderController)
+ .setButtonActions(anyInt(), anyInt());
+ doReturn(mEntityHeaderController).when(mEntityHeaderController)
+ .setIcon(nullable(Drawable.class));
+ doReturn(mEntityHeaderController).when(mEntityHeaderController).setIcon(nullable(
+ ApplicationsState.AppEntry.class));
+ doReturn(mEntityHeaderController).when(mEntityHeaderController)
+ .setLabel(nullable(String.class));
+ doReturn(mEntityHeaderController).when(mEntityHeaderController)
+ .setLabel(nullable(String.class));
+ doReturn(mEntityHeaderController).when(mEntityHeaderController)
+ .setLabel(nullable(ApplicationsState.AppEntry.class));
+ doReturn(mEntityHeaderController).when(mEntityHeaderController)
+ .setSummary(nullable(String.class));
+
+ when(mBatteryEntry.getUid()).thenReturn(UID);
+ when(mBatteryEntry.getLabel()).thenReturn(APP_LABEL);
+ mBatteryEntry.mIconId = ICON_ID;
+
+ mFragment.mHeaderPreference = mHeaderPreference;
+ mFragment.mState = mState;
+ mFragment.mBatteryOptimizeUtils = mBatteryOptimizeUtils;
+ mFragment.mBackupManager = mBackupManager;
+ mAppEntry.info = mock(ApplicationInfo.class);
+
+ mTestActivity = spy(new SettingsActivity());
+ doReturn(mPackageManager).when(mTestActivity).getPackageManager();
+ doReturn(mPackageManager).when(mActivity).getPackageManager();
+ doReturn(mAppOpsManager).when(mTestActivity).getSystemService(Context.APP_OPS_SERVICE);
+
+ final ArgumentCaptor<Intent> captor = ArgumentCaptor.forClass(Intent.class);
+
+ Answer<Void> callable = invocation -> {
+ mBundle = captor.getValue().getBundleExtra(EXTRA_SHOW_FRAGMENT_ARGUMENTS);
+ System.out.println("mBundle = " + mBundle);
+ return null;
+ };
+ doAnswer(callable).when(mActivity).startActivityAsUser(captor.capture(),
+ nullable(UserHandle.class));
+ doAnswer(callable).when(mActivity).startActivity(captor.capture());
+
+ mFooterPreference = spy(new FooterPreference(mContext));
+ mMainSwitchPreference = spy(new MainSwitchPreference(mContext));
+ mMainSwitchPreference.setKey(KEY_ALLOW_BACKGROUND_USAGE);
+ mOptimizePreference = spy(new SelectorWithWidgetPreference(mContext));
+ mOptimizePreference.setKey(KEY_PREF_OPTIMIZED);
+ mUnrestrictedPreference = spy(new SelectorWithWidgetPreference(mContext));
+ mUnrestrictedPreference.setKey(KEY_PREF_UNRESTRICTED);
+ mFragment.mFooterPreference = mFooterPreference;
+ mFragment.mMainSwitchPreference = mMainSwitchPreference;
+ mFragment.mOptimizePreference = mOptimizePreference;
+ mFragment.mUnrestrictedPreference = mUnrestrictedPreference;
+ }
+
+ @After
+ public void reset() {
+ ShadowEntityHeaderController.reset();
+ }
+
+ @Test
+ public void initHeader_NoAppEntry_BuildByBundle() {
+ mFragment.mAppEntry = null;
+ mFragment.initHeader();
+
+ verify(mEntityHeaderController).setIcon(nullable(Drawable.class));
+ verify(mEntityHeaderController).setLabel(APP_LABEL);
+ }
+
+ @Test
+ public void initHeader_HasAppEntry_BuildByAppEntry() {
+ ReflectionHelpers.setStaticField(AppUtils.class, "sInstantAppDataProvider",
+ new InstantAppDataProvider() {
+ @Override
+ public boolean isInstantApp(ApplicationInfo info) {
+ return false;
+ }
+ });
+ mFragment.mAppEntry = mAppEntry;
+ mFragment.initHeader();
+
+ verify(mEntityHeaderController).setIcon(mAppEntry);
+ verify(mEntityHeaderController).setLabel(mAppEntry);
+ verify(mEntityHeaderController).setIsInstantApp(false);
+ }
+
+ @Test
+ public void initHeader_HasAppEntry_InstantApp() {
+ ReflectionHelpers.setStaticField(AppUtils.class, "sInstantAppDataProvider",
+ new InstantAppDataProvider() {
+ @Override
+ public boolean isInstantApp(ApplicationInfo info) {
+ return true;
+ }
+ });
+ mFragment.mAppEntry = mAppEntry;
+ mFragment.initHeader();
+
+ verify(mEntityHeaderController).setIcon(mAppEntry);
+ verify(mEntityHeaderController).setLabel(mAppEntry);
+ verify(mEntityHeaderController).setIsInstantApp(true);
+ }
+
+ @Test
+ public void initFooter_hasCorrectString() {
+ when(mBatteryOptimizeUtils.isDisabledForOptimizeModeOnly()).thenReturn(false);
+ when(mBatteryOptimizeUtils.isSystemOrDefaultApp()).thenReturn(false);
+
+ mFragment.initFooter();
+
+ assertThat(mFooterPreference.getTitle().toString())
+ .isEqualTo("Changing how an app uses your battery can affect its performance.");
+ }
+
+ @Test
+ public void onSwitchChanged_fromUnrestrictedModeSetDisabled_becomeRestrictedMode() {
+ final int restrictedMode = BatteryOptimizeUtils.MODE_RESTRICTED;
+ final int optimizedMode = BatteryOptimizeUtils.MODE_OPTIMIZED;
+ mFragment.mOptimizationMode = optimizedMode;
+
+ mFragment.onSwitchChanged(mMockSwitch, /*isChecked=*/ false);
+
+ verify(mOptimizePreference).setEnabled(false);
+ verify(mUnrestrictedPreference).setEnabled(false);
+ verify(mFragment).onRadioButtonClicked(null);
+ verify(mMainSwitchPreference).setChecked(false);
+ assertThat(mFragment.getSelectedPreference()).isEqualTo(restrictedMode);
+ verify(mBatteryOptimizeUtils).setAppUsageState(restrictedMode, Action.APPLY);
+ }
+
+ @Test
+ public void onSwitchChanged_fromRestrictedModeSetEnabled_becomeOptimizedMode() {
+ final int restrictedMode = BatteryOptimizeUtils.MODE_RESTRICTED;
+ final int optimizedMode = BatteryOptimizeUtils.MODE_OPTIMIZED;
+ mFragment.mOptimizationMode = restrictedMode;
+
+ mFragment.onSwitchChanged(mMockSwitch, /*isChecked=*/ true);
+
+ verify(mOptimizePreference).setEnabled(true);
+ verify(mUnrestrictedPreference).setEnabled(true);
+ verify(mFragment).onRadioButtonClicked(mOptimizePreference);
+ verify(mMainSwitchPreference).setChecked(true);
+ verify(mOptimizePreference).setChecked(true);
+ assertThat(mFragment.getSelectedPreference()).isEqualTo(optimizedMode);
+ verify(mBatteryOptimizeUtils).setAppUsageState(optimizedMode, Action.APPLY);
+ }
+}
diff --git a/tests/robotests/src/com/android/settings/fuelgauge/RestrictedPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/fuelgauge/RestrictedPreferenceControllerTest.java
deleted file mode 100644
index bcddbc2..0000000
--- a/tests/robotests/src/com/android/settings/fuelgauge/RestrictedPreferenceControllerTest.java
+++ /dev/null
@@ -1,126 +0,0 @@
-/*
- * 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.fuelgauge;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.mockito.Mockito.when;
-
-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.RuntimeEnvironment;
-
-@RunWith(RobolectricTestRunner.class)
-public class RestrictedPreferenceControllerTest {
- private static final int UID = 12345;
- private static final String PACKAGE_NAME = "com.android.app";
-
- private RestrictedPreferenceController mController;
- private SelectorWithWidgetPreference mPreference;
-
- @Mock BatteryOptimizeUtils mockBatteryOptimizeUtils;
-
- @Before
- public void setUp() {
- MockitoAnnotations.initMocks(this);
-
- mController = new RestrictedPreferenceController(
- RuntimeEnvironment.application, UID, PACKAGE_NAME);
- mPreference = new SelectorWithWidgetPreference(RuntimeEnvironment.application);
- mController.mBatteryOptimizeUtils = mockBatteryOptimizeUtils;
- }
-
- @Test
- public void testUpdateState_isValidPackage_prefEnabled() {
- when(mockBatteryOptimizeUtils.isDisabledForOptimizeModeOnly()).thenReturn(false);
-
- mController.updateState(mPreference);
-
- assertThat(mPreference.isEnabled()).isTrue();
- }
-
- @Test
- public void testUpdateState_invalidPackage_prefDisabled() {
- when(mockBatteryOptimizeUtils.isDisabledForOptimizeModeOnly()).thenReturn(true);
-
- mController.updateState(mPreference);
-
- assertThat(mPreference.isEnabled()).isFalse();
- }
-
- @Test
- public void testUpdateState_isSystemOrDefaultAppAndRestrictedStates_prefChecked() {
- when(mockBatteryOptimizeUtils.isDisabledForOptimizeModeOnly()).thenReturn(false);
- when(mockBatteryOptimizeUtils.isSystemOrDefaultApp()).thenReturn(true);
- when(mockBatteryOptimizeUtils.getAppOptimizationMode()).thenReturn(
- BatteryOptimizeUtils.MODE_RESTRICTED);
-
- mController.updateState(mPreference);
-
- assertThat(mPreference.isChecked()).isTrue();
- }
-
- @Test
- public void testUpdateState_isSystemOrDefaultApp_prefUnchecked() {
- when(mockBatteryOptimizeUtils.isDisabledForOptimizeModeOnly()).thenReturn(false);
- when(mockBatteryOptimizeUtils.isSystemOrDefaultApp()).thenReturn(true);
-
- mController.updateState(mPreference);
-
- assertThat(mPreference.isChecked()).isFalse();
- assertThat(mPreference.isEnabled()).isFalse();
- }
-
- @Test
- public void testUpdateState_isRestrictedStates_prefChecked() {
- when(mockBatteryOptimizeUtils.isDisabledForOptimizeModeOnly()).thenReturn(false);
- when(mockBatteryOptimizeUtils.getAppOptimizationMode()).thenReturn(
- BatteryOptimizeUtils.MODE_RESTRICTED);
-
- mController.updateState(mPreference);
-
- assertThat(mPreference.isChecked()).isTrue();
- }
-
- @Test
- public void testUpdateState_prefUnchecked() {
- when(mockBatteryOptimizeUtils.isDisabledForOptimizeModeOnly()).thenReturn(false);
-
- mController.updateState(mPreference);
-
- assertThat(mPreference.isChecked()).isFalse();
- }
-
- @Test
- public void testHandlePreferenceTreeClick_samePrefKey_verifyAction() {
- mPreference.setKey(mController.KEY_RESTRICTED_PREF);
- mController.handlePreferenceTreeClick(mPreference);
-
- assertThat(mController.handlePreferenceTreeClick(mPreference)).isTrue();
- }
-
- @Test
- public void testHandlePreferenceTreeClick_incorrectPrefKey_noAction() {
- assertThat(mController.handlePreferenceTreeClick(mPreference)).isFalse();
- }
-}
diff --git a/tests/robotests/src/com/android/settings/fuelgauge/UnrestrictedPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/fuelgauge/UnrestrictedPreferenceControllerTest.java
index 9bed9ba..5489196 100644
--- a/tests/robotests/src/com/android/settings/fuelgauge/UnrestrictedPreferenceControllerTest.java
+++ b/tests/robotests/src/com/android/settings/fuelgauge/UnrestrictedPreferenceControllerTest.java
@@ -18,8 +18,13 @@
import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.when;
+import android.content.Context;
+import android.content.pm.PackageManager;
+
import com.android.settingslib.widget.SelectorWithWidgetPreference;
import org.junit.Before;
@@ -37,42 +42,53 @@
private UnrestrictedPreferenceController mController;
private SelectorWithWidgetPreference mPreference;
+ private BatteryOptimizeUtils mBatteryOptimizeUtils;
- @Mock BatteryOptimizeUtils mockBatteryOptimizeUtils;
+ @Mock PackageManager mMockPackageManager;
@Before
- public void setUp() {
+ public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
- mController = new UnrestrictedPreferenceController(
- RuntimeEnvironment.application, UID, PACKAGE_NAME);
+ Context context = spy(RuntimeEnvironment.application);
+ BatteryUtils.getInstance(context).reset();
+ doReturn(UID)
+ .when(mMockPackageManager)
+ .getPackageUid(PACKAGE_NAME, PackageManager.GET_META_DATA);
+
+ mController = new UnrestrictedPreferenceController(context, UID, PACKAGE_NAME);
mPreference = new SelectorWithWidgetPreference(RuntimeEnvironment.application);
- mController.mBatteryOptimizeUtils = mockBatteryOptimizeUtils;
+ mBatteryOptimizeUtils = spy(new BatteryOptimizeUtils(context, UID, PACKAGE_NAME));
+ mController.mBatteryOptimizeUtils = mBatteryOptimizeUtils;
}
@Test
public void testUpdateState_isValidPackage_prefEnabled() {
- when(mockBatteryOptimizeUtils.isDisabledForOptimizeModeOnly()).thenReturn(false);
+ when(mBatteryOptimizeUtils.isDisabledForOptimizeModeOnly()).thenReturn(false);
+ when(mBatteryOptimizeUtils.isSystemOrDefaultApp()).thenReturn(false);
mController.updateState(mPreference);
+ assertThat(mBatteryOptimizeUtils.isOptimizeModeMutable()).isTrue();
assertThat(mPreference.isEnabled()).isTrue();
}
@Test
public void testUpdateState_invalidPackage_prefDisabled() {
- when(mockBatteryOptimizeUtils.isDisabledForOptimizeModeOnly()).thenReturn(true);
+ when(mBatteryOptimizeUtils.isDisabledForOptimizeModeOnly()).thenReturn(true);
+ when(mBatteryOptimizeUtils.isSystemOrDefaultApp()).thenReturn(false);
mController.updateState(mPreference);
+ assertThat(mBatteryOptimizeUtils.isOptimizeModeMutable()).isFalse();
assertThat(mPreference.isEnabled()).isFalse();
}
@Test
public void testUpdateState_isSystemOrDefaultAppAndUnrestrictedStates_prefChecked() {
- when(mockBatteryOptimizeUtils.isDisabledForOptimizeModeOnly()).thenReturn(false);
- when(mockBatteryOptimizeUtils.isSystemOrDefaultApp()).thenReturn(true);
- when(mockBatteryOptimizeUtils.getAppOptimizationMode()).thenReturn(
+ when(mBatteryOptimizeUtils.isDisabledForOptimizeModeOnly()).thenReturn(false);
+ when(mBatteryOptimizeUtils.isSystemOrDefaultApp()).thenReturn(true);
+ when(mBatteryOptimizeUtils.getAppOptimizationMode()).thenReturn(
BatteryOptimizeUtils.MODE_UNRESTRICTED);
mController.updateState(mPreference);
@@ -82,32 +98,38 @@
@Test
public void testUpdateState_isSystemOrDefaultApp_prefUnchecked() {
- when(mockBatteryOptimizeUtils.isDisabledForOptimizeModeOnly()).thenReturn(false);
- when(mockBatteryOptimizeUtils.isSystemOrDefaultApp()).thenReturn(true);
+ when(mBatteryOptimizeUtils.isDisabledForOptimizeModeOnly()).thenReturn(false);
+ when(mBatteryOptimizeUtils.isSystemOrDefaultApp()).thenReturn(true);
+ when(mBatteryOptimizeUtils.getAppOptimizationMode()).thenReturn(
+ BatteryOptimizeUtils.MODE_OPTIMIZED);
mController.updateState(mPreference);
- assertThat(mPreference.isChecked()).isFalse();
assertThat(mPreference.isEnabled()).isFalse();
+ assertThat(mPreference.isChecked()).isFalse();
}
@Test
public void testUpdateState_isUnrestrictedStates_prefChecked() {
- when(mockBatteryOptimizeUtils.isDisabledForOptimizeModeOnly()).thenReturn(false);
- when(mockBatteryOptimizeUtils.getAppOptimizationMode()).thenReturn(
+ when(mBatteryOptimizeUtils.isOptimizeModeMutable()).thenReturn(true);
+ when(mBatteryOptimizeUtils.getAppOptimizationMode()).thenReturn(
BatteryOptimizeUtils.MODE_UNRESTRICTED);
mController.updateState(mPreference);
+ assertThat(mPreference.isEnabled()).isTrue();
assertThat(mPreference.isChecked()).isTrue();
}
@Test
public void testUpdateState_prefUnchecked() {
- when(mockBatteryOptimizeUtils.isDisabledForOptimizeModeOnly()).thenReturn(false);
+ when(mBatteryOptimizeUtils.isOptimizeModeMutable()).thenReturn(true);
+ when(mBatteryOptimizeUtils.getAppOptimizationMode()).thenReturn(
+ BatteryOptimizeUtils.MODE_OPTIMIZED);
mController.updateState(mPreference);
+ assertThat(mPreference.isEnabled()).isTrue();
assertThat(mPreference.isChecked()).isFalse();
}
diff --git a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryDiffEntryTest.java b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryDiffEntryTest.java
index ae726b7..bbba294 100644
--- a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryDiffEntryTest.java
+++ b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryDiffEntryTest.java
@@ -82,6 +82,7 @@
MockitoAnnotations.initMocks(this);
ShadowUserHandle.reset();
mContext = spy(RuntimeEnvironment.application);
+ BatteryUtils.getInstance(mContext).reset();
doReturn(mContext).when(mContext).getApplicationContext();
doReturn(mMockUserManager).when(mContext).getSystemService(UserManager.class);
doReturn(mMockPackageManager).when(mContext).getPackageManager();
@@ -351,16 +352,18 @@
}
@Test
- public void testClearCache_clearDataForResourcesAndFlags() {
+ public void testClearCache_clearDataForAllCaches() {
BatteryDiffEntry.sResourceCache.put(
"fake application key",
new BatteryEntry.NameAndIcon("app label", null, /* iconId= */ 0));
BatteryDiffEntry.sValidForRestriction.put("fake application key", Boolean.valueOf(false));
+ BatteryDiffEntry.sPackageNameAndUidCache.put(PACKAGE_NAME, UID);
BatteryDiffEntry.clearCache();
assertThat(BatteryDiffEntry.sResourceCache).isEmpty();
assertThat(BatteryDiffEntry.sValidForRestriction).isEmpty();
+ assertThat(BatteryDiffEntry.sPackageNameAndUidCache).isEmpty();
}
@Test
@@ -444,7 +447,11 @@
final BatteryDiffEntry entry = createBatteryDiffEntry(10, new BatteryHistEntry(values));
assertThat(entry.isSystemEntry()).isFalse();
+ assertThat(BatteryDiffEntry.sPackageNameAndUidCache.containsKey(PACKAGE_NAME)).isFalse();
assertThat(entry.isUninstalledEntry()).isFalse();
+ assertThat(BatteryDiffEntry.sPackageNameAndUidCache.containsKey(PACKAGE_NAME)).isTrue();
+ assertThat(BatteryDiffEntry.sPackageNameAndUidCache.get(PACKAGE_NAME)).isEqualTo(UID);
+
}
@Test
@@ -456,14 +463,13 @@
final BatteryDiffEntry entry = createBatteryDiffEntry(10, new BatteryHistEntry(values));
assertThat(entry.isSystemEntry()).isFalse();
+ assertThat(BatteryDiffEntry.sPackageNameAndUidCache.containsKey(PACKAGE_NAME)).isFalse();
assertThat(entry.isUninstalledEntry()).isFalse();
+ assertThat(BatteryDiffEntry.sPackageNameAndUidCache.containsKey(PACKAGE_NAME)).isFalse();
}
@Test
public void testIsUninstalledEntry_uninstalledApp_returnTrue() throws Exception {
- doReturn(BatteryUtils.UID_NULL)
- .when(mMockPackageManager)
- .getPackageUid(PACKAGE_NAME, PackageManager.GET_META_DATA);
final ContentValues values =
getContentValuesWithType(ConvertUtils.CONSUMER_TYPE_UID_BATTERY);
values.put(BatteryHistEntry.KEY_UID, UNINSTALLED_UID);
@@ -471,7 +477,11 @@
final BatteryDiffEntry entry = createBatteryDiffEntry(10, new BatteryHistEntry(values));
assertThat(entry.isSystemEntry()).isFalse();
+ assertThat(BatteryDiffEntry.sPackageNameAndUidCache.containsKey(UNINSTALLED_PACKAGE_NAME))
+ .isFalse();
assertThat(entry.isUninstalledEntry()).isTrue();
+ assertThat(BatteryDiffEntry.sPackageNameAndUidCache.get(UNINSTALLED_PACKAGE_NAME))
+ .isEqualTo(BatteryUtils.UID_NULL);
}
@Test
@@ -592,7 +602,7 @@
final BatteryHistEntry batteryHistEntry = new BatteryHistEntry(values);
doReturn(drawable).when(mMockPackageManager).getDefaultActivityIcon();
doReturn(null).when(mMockPackageManager).getApplicationInfo("com.a.b.c", 0);
- doReturn(new String[] {"com.a.b.c"}).when(mMockPackageManager).getPackagesForUid(1001);
+ doReturn(new String[]{"com.a.b.c"}).when(mMockPackageManager).getPackagesForUid(1001);
return createBatteryDiffEntry(10, batteryHistEntry);
}
diff --git a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/db/BatteryEventDaoTest.java b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/db/BatteryEventDaoTest.java
index 8462867..3c3e3c3 100644
--- a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/db/BatteryEventDaoTest.java
+++ b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/db/BatteryEventDaoTest.java
@@ -149,4 +149,31 @@
mBatteryEventDao.clearAll();
assertThat(mBatteryEventDao.getAll()).isEmpty();
}
+
+ @Test
+ public void getAllAfter_filterTimestamp_returnExpectedResult() {
+ mBatteryEventDao.insert(BatteryEventEntity.newBuilder()
+ .setTimestamp(100L)
+ .setBatteryEventType(1)
+ .setBatteryLevel(66)
+ .build());
+ mBatteryEventDao.insert(BatteryEventEntity.newBuilder()
+ .setTimestamp(200L)
+ .setBatteryEventType(1)
+ .setBatteryLevel(88)
+ .build());
+
+ final Cursor cursor = mBatteryEventDao.getAllAfter(200L, List.of(1));
+ assertThat(cursor.getCount()).isEqualTo(1);
+ cursor.moveToFirst();
+ assertThat(cursor.getLong(cursor.getColumnIndex(KEY_TIMESTAMP)))
+ .isEqualTo(200L);
+ assertThat(cursor.getInt(cursor.getColumnIndex(KEY_BATTERY_EVENT_TYPE)))
+ .isEqualTo(1);
+ assertThat(cursor.getInt(cursor.getColumnIndex(KEY_BATTERY_LEVEL)))
+ .isEqualTo(88);
+
+ mBatteryEventDao.clearAll();
+ assertThat(mBatteryEventDao.getAll()).isEmpty();
+ }
}
diff --git a/tests/robotests/src/com/android/settings/fuelgauge/datasaver/DynamicDenylistManagerTest.java b/tests/robotests/src/com/android/settings/fuelgauge/datasaver/DynamicDenylistManagerTest.java
new file mode 100644
index 0000000..cdf1514
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/fuelgauge/datasaver/DynamicDenylistManagerTest.java
@@ -0,0 +1,156 @@
+/*
+ * 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.datasaver;
+
+import static android.net.NetworkPolicyManager.POLICY_NONE;
+import static android.net.NetworkPolicyManager.POLICY_REJECT_METERED_BACKGROUND;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+
+@RunWith(RobolectricTestRunner.class)
+public class DynamicDenylistManagerTest {
+
+ private static final String FAKE_UID_1 = "package_uid_1";
+ private static final String FAKE_UID_2 = "package_uid_2";
+
+ private SharedPreferences mManualDenyListPref;
+ private SharedPreferences mDynamicDenyListPref;
+ private DynamicDenylistManager mDynamicDenylistManager;
+ private Context mContext;
+
+ @Before
+ public void setUp() {
+ mContext = RuntimeEnvironment.application.getApplicationContext();
+ mDynamicDenylistManager = new DynamicDenylistManager(mContext);
+ mManualDenyListPref = mDynamicDenylistManager.getManualDenylistPref();
+ mDynamicDenyListPref = mDynamicDenylistManager.getDynamicDenylistPref();
+ }
+
+ @After
+ public void tearDown() {
+ mDynamicDenylistManager.clearManualDenylistPref();
+ mDynamicDenylistManager.clearDynamicDenylistPref();
+ }
+
+ @Test
+ public void getManualDenylistPref_isEmpty() {
+ assertThat(mManualDenyListPref.getAll()).isEmpty();
+ }
+
+ @Test
+ public void getDynamicDenylistPref_isEmpty() {
+ assertThat(mDynamicDenyListPref.getAll()).isEmpty();
+ }
+
+ @Test
+ public void getManualDenylistPref_initiated_containsExpectedValue() {
+ mManualDenyListPref.edit().putInt(FAKE_UID_1, POLICY_REJECT_METERED_BACKGROUND).apply();
+
+ assertThat(mManualDenyListPref.getAll().size()).isEqualTo(1);
+ assertTrue(mManualDenyListPref.contains(FAKE_UID_1));
+ }
+
+ @Test
+ public void getDynamicDenylistPref_initiated_containsExpectedValue() {
+ mDynamicDenyListPref.edit().putInt(FAKE_UID_1, POLICY_REJECT_METERED_BACKGROUND).apply();
+
+ assertThat(mDynamicDenyListPref.getAll()).hasSize(1);
+ assertTrue(mDynamicDenyListPref.contains(FAKE_UID_1));
+ }
+
+ @Test
+ public void updateManualDenylist_policyReject_addsUid() {
+ mDynamicDenylistManager.updateManualDenylist(FAKE_UID_1, POLICY_REJECT_METERED_BACKGROUND);
+
+ assertThat(mManualDenyListPref.getAll()).hasSize(1);
+ assertTrue(mManualDenyListPref.contains(FAKE_UID_1));
+ }
+
+ @Test
+ public void updateManualDenylist_policyNone_removesUid() {
+ mManualDenyListPref.edit().putInt(FAKE_UID_1, POLICY_REJECT_METERED_BACKGROUND).apply();
+ assertTrue(mManualDenyListPref.contains(FAKE_UID_1));
+
+ mDynamicDenylistManager.updateManualDenylist(FAKE_UID_1, POLICY_NONE);
+
+ assertThat(mManualDenyListPref.getAll()).isEmpty();
+ }
+
+ @Test
+ public void updateManualDenylist_samePolicy_doNothing() {
+ mManualDenyListPref.edit().putInt(FAKE_UID_1, POLICY_REJECT_METERED_BACKGROUND).apply();
+ assertTrue(mManualDenyListPref.contains(FAKE_UID_1));
+
+ mDynamicDenylistManager.updateManualDenylist(FAKE_UID_1, POLICY_REJECT_METERED_BACKGROUND);
+
+ assertThat(mManualDenyListPref.getAll()).hasSize(1);
+ }
+
+ @Test
+ public void isManualDenylist_returnsFalse() {
+ assertFalse(mDynamicDenylistManager.isInManualDenylist(FAKE_UID_1));
+ }
+
+ @Test
+ public void isManualDenylist_incorrectUid_returnsFalse() {
+ mManualDenyListPref.edit().putInt(FAKE_UID_2, POLICY_REJECT_METERED_BACKGROUND).apply();
+
+ assertFalse(mDynamicDenylistManager.isInManualDenylist(FAKE_UID_1));
+ }
+
+ @Test
+ public void isManualDenylist_initiated_returnsTrue() {
+ mManualDenyListPref.edit().putInt(FAKE_UID_1, POLICY_REJECT_METERED_BACKGROUND).apply();
+
+ assertTrue(mDynamicDenylistManager.isInManualDenylist(FAKE_UID_1));
+ }
+
+ @Test
+ public void clearManualDenylistPref_isEmpty() {
+ mManualDenyListPref.edit().putInt(FAKE_UID_1, POLICY_REJECT_METERED_BACKGROUND).apply();
+ assertThat(mManualDenyListPref.getAll()).hasSize(1);
+ assertTrue(mManualDenyListPref.contains(FAKE_UID_1));
+
+ mDynamicDenylistManager.clearManualDenylistPref();
+
+ assertThat(mManualDenyListPref.getAll()).isEmpty();
+ }
+
+ @Test
+ public void clearDynamicDenylistPref_isEmpty() {
+ mDynamicDenyListPref.edit().putInt(FAKE_UID_1, POLICY_REJECT_METERED_BACKGROUND).apply();
+ assertThat(mDynamicDenyListPref.getAll()).hasSize(1);
+ assertTrue(mDynamicDenyListPref.contains(FAKE_UID_1));
+
+ mDynamicDenylistManager.clearDynamicDenylistPref();
+
+ assertThat(mDynamicDenyListPref.getAll()).isEmpty();
+ }
+}
diff --git a/tests/robotests/src/com/android/settings/network/MobileNetworkListFragmentTest.java b/tests/robotests/src/com/android/settings/network/MobileNetworkListFragmentTest.java
deleted file mode 100644
index 2e04ea7..0000000
--- a/tests/robotests/src/com/android/settings/network/MobileNetworkListFragmentTest.java
+++ /dev/null
@@ -1,88 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.settings.network;
-
-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.os.UserManager;
-
-import com.android.settings.R;
-import com.android.settings.search.BaseSearchIndexProvider;
-
-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.util.ReflectionHelpers;
-
-@RunWith(RobolectricTestRunner.class)
-public class MobileNetworkListFragmentTest {
- @Mock
- private Context mContext;
- @Mock
- private Resources mResources;
- @Mock
- private UserManager mUserManager;
-
- private MobileNetworkListFragment mFragment;
-
- @Before
- public void setUp() {
- MockitoAnnotations.initMocks(this);
- mFragment = new MobileNetworkListFragment();
- }
-
- @Test
- public void isPageSearchEnabled_adminUser_shouldReturnTrue() {
- when(mContext.getSystemService(UserManager.class)).thenReturn(mUserManager);
- when(mUserManager.isAdminUser()).thenReturn(true);
- final BaseSearchIndexProvider provider =
- (BaseSearchIndexProvider) mFragment.SEARCH_INDEX_DATA_PROVIDER;
-
- when(mContext.getResources()).thenReturn(mResources);
- when(mResources.getBoolean(R.bool.config_show_sim_info)).thenReturn(true);
-
- final Object obj = ReflectionHelpers.callInstanceMethod(provider, "isPageSearchEnabled",
- ReflectionHelpers.ClassParameter.from(Context.class, mContext));
- final boolean isEnabled = (Boolean) obj;
-
- assertThat(isEnabled).isTrue();
- }
-
- @Test
- public void isPageSearchEnabled_nonAdminUser_shouldReturnFalse() {
- when(mContext.getSystemService(UserManager.class)).thenReturn(mUserManager);
- when(mUserManager.isAdminUser()).thenReturn(false);
- final BaseSearchIndexProvider provider =
- (BaseSearchIndexProvider) mFragment.SEARCH_INDEX_DATA_PROVIDER;
-
- when(mContext.getResources()).thenReturn(mResources);
- when(mResources.getBoolean(R.bool.config_show_sim_info)).thenReturn(true);
-
- final Object obj = ReflectionHelpers.callInstanceMethod(provider, "isPageSearchEnabled",
- ReflectionHelpers.ClassParameter.from(Context.class, mContext));
- final boolean isEnabled = (Boolean) obj;
-
- assertThat(isEnabled).isFalse();
- }
-}
diff --git a/tests/shared/src/com/android/settings/testutils2/FakeFingerprintManagerInteractor.kt b/tests/shared/src/com/android/settings/testutils2/FakeFingerprintManagerInteractor.kt
index ad943f2..dd8658c 100644
--- a/tests/shared/src/com/android/settings/testutils2/FakeFingerprintManagerInteractor.kt
+++ b/tests/shared/src/com/android/settings/testutils2/FakeFingerprintManagerInteractor.kt
@@ -18,9 +18,9 @@
import com.android.settings.biometrics.fingerprint2.shared.domain.interactor.FingerprintManagerInteractor
import com.android.settings.biometrics.fingerprint2.shared.model.EnrollReason
-import com.android.settings.biometrics.fingerprint2.shared.model.FingerEnrollStateViewModel
-import com.android.settings.biometrics.fingerprint2.shared.model.FingerprintAuthAttemptViewModel
-import com.android.settings.biometrics.fingerprint2.shared.model.FingerprintViewModel
+import com.android.settings.biometrics.fingerprint2.shared.model.FingerEnrollState
+import com.android.settings.biometrics.fingerprint2.shared.model.FingerprintAuthAttemptModel
+import com.android.settings.biometrics.fingerprint2.shared.model.FingerprintData
import com.android.systemui.biometrics.shared.model.FingerprintSensor
import com.android.systemui.biometrics.shared.model.FingerprintSensorType
import com.android.systemui.biometrics.shared.model.SensorStrength
@@ -32,10 +32,11 @@
class FakeFingerprintManagerInteractor : FingerprintManagerInteractor {
var enrollableFingerprints: Int = 5
- var enrolledFingerprintsInternal: MutableList<FingerprintViewModel> = mutableListOf()
+ var enrolledFingerprintsInternal: MutableList<FingerprintData> = mutableListOf()
var challengeToGenerate: Pair<Long, ByteArray> = Pair(-1L, byteArrayOf())
- var authenticateAttempt = FingerprintAuthAttemptViewModel.Success(1)
- val enrollStateViewModel = FingerEnrollStateViewModel.EnrollProgress(1)
+ var authenticateAttempt = FingerprintAuthAttemptModel.Success(1)
+ var enrollStateViewModel: List<FingerEnrollState> =
+ listOf(FingerEnrollState.EnrollProgress(5, 5))
var pressToAuthEnabled = true
var sensorProp =
@@ -46,7 +47,7 @@
FingerprintSensorType.POWER_BUTTON
)
- override suspend fun authenticate(): FingerprintAuthAttemptViewModel {
+ override suspend fun authenticate(): FingerprintAuthAttemptModel {
return authenticateAttempt
}
@@ -54,7 +55,7 @@
return challengeToGenerate
}
- override val enrolledFingerprints: Flow<List<FingerprintViewModel>> = flow {
+ override val enrolledFingerprints: Flow<List<FingerprintData>> = flow {
emit(enrolledFingerprintsInternal)
}
@@ -62,24 +63,22 @@
emit(enrolledFingerprintsInternal.size < enrollableFingerprints)
}
- override val sensorPropertiesInternal: Flow<FingerprintSensor?> = flow {
- emit(sensorProp)
- }
+ override val sensorPropertiesInternal: Flow<FingerprintSensor?> = flow { emit(sensorProp) }
override val maxEnrollableFingerprints: Flow<Int> = flow { emit(enrollableFingerprints) }
override suspend fun enroll(
hardwareAuthToken: ByteArray?,
enrollReason: EnrollReason
- ): Flow<FingerEnrollStateViewModel> = flowOf(enrollStateViewModel)
+ ): Flow<FingerEnrollState> = flowOf(*enrollStateViewModel.toTypedArray())
- override suspend fun removeFingerprint(fp: FingerprintViewModel): Boolean {
+ override suspend fun removeFingerprint(fp: FingerprintData): Boolean {
return enrolledFingerprintsInternal.remove(fp)
}
- override suspend fun renameFingerprint(fp: FingerprintViewModel, newName: String) {
+ override suspend fun renameFingerprint(fp: FingerprintData, newName: String) {
if (enrolledFingerprintsInternal.remove(fp)) {
- enrolledFingerprintsInternal.add(FingerprintViewModel(newName, fp.fingerId, fp.deviceId))
+ enrolledFingerprintsInternal.add(FingerprintData(newName, fp.fingerId, fp.deviceId))
}
}
diff --git a/tests/spa_unit/Android.bp b/tests/spa_unit/Android.bp
index 28a2667..c3e99f7 100644
--- a/tests/spa_unit/Android.bp
+++ b/tests/spa_unit/Android.bp
@@ -34,6 +34,7 @@
"androidx.compose.runtime_runtime",
"androidx.test.ext.junit",
"androidx.test.runner",
+ "flag-junit",
"mockito-target-extended-minus-junit4",
],
jni_libs: [
diff --git a/tests/spa_unit/src/com/android/settings/network/MobileNetworkListFragmentTest.kt b/tests/spa_unit/src/com/android/settings/network/MobileNetworkListFragmentTest.kt
new file mode 100644
index 0000000..3ba4bac
--- /dev/null
+++ b/tests/spa_unit/src/com/android/settings/network/MobileNetworkListFragmentTest.kt
@@ -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.network
+
+import android.content.Context
+import android.content.res.Resources
+import android.os.UserManager
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.settings.R
+import com.android.settingslib.spaprivileged.framework.common.userManager
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.spy
+import org.mockito.kotlin.stub
+
+@RunWith(AndroidJUnit4::class)
+class MobileNetworkListFragmentTest {
+ private val mockUserManager = mock<UserManager>()
+
+ private val mockResources = mock<Resources>()
+
+ private val context: Context = spy(ApplicationProvider.getApplicationContext()) {
+ on { userManager } doReturn mockUserManager
+ on { resources } doReturn mockResources
+ }
+
+ @Test
+ fun isPageSearchEnabled_adminUser_shouldReturnTrue() {
+ mockUserManager.stub {
+ on { isAdminUser } doReturn true
+ }
+ mockResources.stub {
+ on { getBoolean(R.bool.config_show_sim_info) } doReturn true
+ }
+
+ val isEnabled =
+ MobileNetworkListFragment.SEARCH_INDEX_DATA_PROVIDER.isPageSearchEnabled(context)
+
+ assertThat(isEnabled).isTrue()
+ }
+
+ @Test
+ fun isPageSearchEnabled_nonAdminUser_shouldReturnFalse() {
+ mockUserManager.stub {
+ on { isAdminUser } doReturn false
+ }
+ mockResources.stub {
+ on { getBoolean(R.bool.config_show_sim_info) } doReturn true
+ }
+
+ val isEnabled =
+ MobileNetworkListFragment.SEARCH_INDEX_DATA_PROVIDER.isPageSearchEnabled(context)
+
+ assertThat(isEnabled).isFalse()
+ }
+}
diff --git a/tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppButtonsTest.kt b/tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppButtonsTest.kt
index 8faf5c9..e2f55ef 100644
--- a/tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppButtonsTest.kt
+++ b/tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppButtonsTest.kt
@@ -17,16 +17,21 @@
package com.android.settings.spa.app.appinfo
import android.content.Context
+import android.content.Intent
import android.content.pm.ApplicationInfo
+import android.content.pm.FakeFeatureFlagsImpl
+import android.content.pm.Flags
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.assertIsNotDisplayed
import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.onRoot
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.android.dx.mockito.inline.extended.ExtendedMockito
+import com.android.settings.R
import com.android.settingslib.applications.AppUtils
import com.android.settingslib.spa.testutils.delay
import kotlinx.coroutines.flow.MutableStateFlow
@@ -57,6 +62,8 @@
@Mock
private lateinit var packageManager: PackageManager
+ private val featureFlags = FakeFeatureFlagsImpl()
+
@Before
fun setUp() {
mockSession = ExtendedMockito.mockitoSession()
@@ -69,6 +76,7 @@
whenever(packageInfoPresenter.userPackageManager).thenReturn(packageManager)
whenever(packageManager.getPackageInfo(PACKAGE_NAME, 0)).thenReturn(PACKAGE_INFO)
whenever(AppUtils.isMainlineModule(packageManager, PACKAGE_NAME)).thenReturn(false)
+ featureFlags.setFlag(Flags.FLAG_ARCHIVING, true)
}
@After
@@ -92,10 +100,28 @@
composeTestRule.onRoot().assertIsDisplayed()
}
+ @Test
+ fun launchButton_displayed_archivingDisabled() {
+ whenever(packageManager.getLaunchIntentForPackage(PACKAGE_NAME)).thenReturn(Intent())
+ featureFlags.setFlag(Flags.FLAG_ARCHIVING, false)
+ setContent()
+
+ composeTestRule.onNodeWithText(context.getString(R.string.launch_instant_app)).assertIsDisplayed()
+ }
+
+ @Test
+ fun launchButton_notDisplayed_archivingEnabled() {
+ whenever(packageManager.getLaunchIntentForPackage(PACKAGE_NAME)).thenReturn(Intent())
+ featureFlags.setFlag(Flags.FLAG_ARCHIVING, true)
+ setContent()
+
+ composeTestRule.onNodeWithText(context.getString(R.string.launch_instant_app)).assertIsNotDisplayed()
+ }
+
private fun setContent() {
whenever(packageInfoPresenter.flow).thenReturn(MutableStateFlow(PACKAGE_INFO))
composeTestRule.setContent {
- AppButtons(packageInfoPresenter)
+ AppButtons(packageInfoPresenter, featureFlags)
}
composeTestRule.delay()
diff --git a/tests/spa_unit/src/com/android/settings/spa/app/appinfo/TopBarAppLaunchButtonTest.kt b/tests/spa_unit/src/com/android/settings/spa/app/appinfo/TopBarAppLaunchButtonTest.kt
new file mode 100644
index 0000000..7b54247
--- /dev/null
+++ b/tests/spa_unit/src/com/android/settings/spa/app/appinfo/TopBarAppLaunchButtonTest.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.spa.app.appinfo
+
+import android.content.Context
+import android.content.Intent
+import android.content.pm.ApplicationInfo
+import android.content.pm.PackageManager
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.test.hasContentDescription
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithContentDescription
+import androidx.compose.ui.test.performClick
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.dx.mockito.inline.extended.ExtendedMockito
+import com.android.settings.R
+import com.android.settingslib.spa.testutils.waitUntilExists
+import com.android.settingslib.spaprivileged.model.app.userHandle
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.any
+import org.mockito.ArgumentMatchers.eq
+import org.mockito.Mock
+import org.mockito.Mockito.verify
+import org.mockito.MockitoSession
+import org.mockito.Spy
+import org.mockito.quality.Strictness
+import org.mockito.Mockito.`when` as whenever
+
+@RunWith(AndroidJUnit4::class)
+class TopBarAppLaunchButtonTest {
+ @get:Rule
+ val composeTestRule = createComposeRule()
+
+ private lateinit var mockSession: MockitoSession
+
+ @Spy
+ private val context: Context = ApplicationProvider.getApplicationContext()
+
+ @Mock
+ private lateinit var packageInfoPresenter: PackageInfoPresenter
+
+ @Mock
+ private lateinit var userPackageManager: PackageManager
+
+ @Before
+ fun setUp() {
+ mockSession = ExtendedMockito.mockitoSession()
+ .initMocks(this)
+ .strictness(Strictness.LENIENT)
+ .startMocking()
+ whenever(packageInfoPresenter.context).thenReturn(context)
+ whenever(packageInfoPresenter.userPackageManager).thenReturn(userPackageManager)
+ val intent = Intent()
+ whenever(userPackageManager.getLaunchIntentForPackage(PACKAGE_NAME)).thenReturn(intent)
+ }
+
+ @After
+ fun tearDown() {
+ mockSession.finishMocking()
+ }
+
+ @Test
+ fun topBarAppLaunchButton_isDisplayed() {
+ val app = ApplicationInfo().apply {
+ packageName = PACKAGE_NAME
+ }
+
+ setContent(app)
+
+ composeTestRule.waitUntilExists(
+ hasContentDescription(context.getString(R.string.launch_instant_app))
+ )
+ }
+
+ @Test
+ fun topBarAppLaunchButton_opensApp() {
+ val app = ApplicationInfo().apply {
+ packageName = PACKAGE_NAME
+ }
+
+ setContent(app)
+ composeTestRule.onNodeWithContentDescription(context.getString(R.string.launch_instant_app))
+ .performClick()
+
+ verify(context).startActivityAsUser(any(), eq(app.userHandle))
+ }
+
+ private fun setContent(app: ApplicationInfo) {
+ composeTestRule.setContent {
+ CompositionLocalProvider(LocalContext provides context) {
+ TopBarAppLaunchButton(packageInfoPresenter, app)
+ }
+ }
+ }
+
+ private companion object {
+ const val PACKAGE_NAME = "package.name"
+ }
+}
diff --git a/tests/spa_unit/src/com/android/settings/spa/app/specialaccess/TurnScreenOnAppsTest.kt b/tests/spa_unit/src/com/android/settings/spa/app/specialaccess/TurnScreenOnAppsTest.kt
new file mode 100644
index 0000000..54ae6c6
--- /dev/null
+++ b/tests/spa_unit/src/com/android/settings/spa/app/specialaccess/TurnScreenOnAppsTest.kt
@@ -0,0 +1,62 @@
+/*
+ * 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.Manifest
+import android.app.AppOpsManager
+import android.content.Context
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class TurnScreenOnAppsTest {
+ private val context: Context = ApplicationProvider.getApplicationContext()
+
+ private val listModel = TurnScreenOnAppsListModel(context)
+
+ @Test
+ fun pageTitleResId() {
+ assertThat(listModel.pageTitleResId).isEqualTo(com.android.settingslib.R.string.turn_screen_on_title)
+ }
+
+ @Test
+ fun switchTitleResId() {
+ assertThat(listModel.switchTitleResId).isEqualTo(com.android.settingslib.R.string.allow_turn_screen_on)
+ }
+
+ @Test
+ fun footerResId() {
+ assertThat(listModel.footerResId).isEqualTo(com.android.settingslib.R.string.allow_turn_screen_on_description)
+ }
+
+ @Test
+ fun appOp() {
+ assertThat(listModel.appOp).isEqualTo(AppOpsManager.OP_TURN_SCREEN_ON)
+ }
+
+ @Test
+ fun permission() {
+ assertThat(listModel.permission).isEqualTo(Manifest.permission.TURN_SCREEN_ON)
+ }
+
+ @Test
+ fun setModeByUid() {
+ assertThat(listModel.setModeByUid).isTrue()
+ }
+}
\ No newline at end of file
diff --git a/tests/spa_unit/src/com/android/settings/spa/app/specialaccess/VoiceActivationAppsPreferenceControllerTest.kt b/tests/spa_unit/src/com/android/settings/spa/app/specialaccess/VoiceActivationAppsPreferenceControllerTest.kt
new file mode 100644
index 0000000..2127497
--- /dev/null
+++ b/tests/spa_unit/src/com/android/settings/spa/app/specialaccess/VoiceActivationAppsPreferenceControllerTest.kt
@@ -0,0 +1,65 @@
+package com.android.settings.spa.app.specialaccess
+
+import android.content.Context
+import android.platform.test.annotations.RequiresFlagsDisabled
+import android.platform.test.annotations.RequiresFlagsEnabled
+import android.platform.test.flag.junit.CheckFlagsRule
+import android.platform.test.flag.junit.DeviceFlagsValueProvider
+import androidx.preference.Preference
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+
+import com.android.settings.flags.Flags
+import com.google.common.truth.Truth.assertThat
+
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.any
+import org.mockito.kotlin.doNothing
+import org.mockito.kotlin.spy
+import org.mockito.kotlin.whenever
+
+@RunWith(AndroidJUnit4::class)
+class VoiceActivationAppsPreferenceControllerTest {
+
+ @get:Rule
+ val checkFlagsRule: CheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule()
+
+ private val context: Context = spy(ApplicationProvider.getApplicationContext()) {
+ doNothing().whenever(mock).startActivity(any())
+ }
+
+ private val matchedPreference = Preference(context).apply { key = preferenceKey }
+
+ private val misMatchedPreference = Preference(context).apply { key = testPreferenceKey }
+
+ private val controller = VoiceActivationAppsPreferenceController(context, preferenceKey)
+
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_ENABLE_VOICE_ACTIVATION_APPS_IN_SETTINGS)
+ fun getAvailabilityStatus_enableVoiceActivationApps_returnAvailable() {
+ assertThat(controller.isAvailable).isTrue()
+ }
+
+ @Test
+ @RequiresFlagsDisabled(Flags.FLAG_ENABLE_VOICE_ACTIVATION_APPS_IN_SETTINGS)
+ fun getAvailableStatus_disableVoiceActivationApps_returnConditionallyUnavailable() {
+ assertThat(controller.isAvailable).isFalse()
+ }
+
+ @Test
+ fun handlePreferenceTreeClick_keyMatched_returnTrue() {
+ assertThat(controller.handlePreferenceTreeClick(matchedPreference)).isTrue()
+ }
+
+ @Test
+ fun handlePreferenceTreeClick_keyMisMatched_returnFalse() {
+ assertThat(controller.handlePreferenceTreeClick(misMatchedPreference)).isFalse()
+ }
+
+ companion object {
+ private const val preferenceKey: String = "voice_activation_apps"
+ private const val testPreferenceKey: String = "test_key"
+ }
+}
\ No newline at end of file
diff --git a/tests/spa_unit/src/com/android/settings/spa/app/specialaccess/VoiceActivationAppsTest.kt b/tests/spa_unit/src/com/android/settings/spa/app/specialaccess/VoiceActivationAppsTest.kt
new file mode 100644
index 0000000..7d636b3
--- /dev/null
+++ b/tests/spa_unit/src/com/android/settings/spa/app/specialaccess/VoiceActivationAppsTest.kt
@@ -0,0 +1,50 @@
+package com.android.settings.spa.app.specialaccess
+
+import android.Manifest
+import android.app.AppOpsManager
+import android.content.Context
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.settings.R
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class VoiceActivationAppsTest {
+ private val context: Context = ApplicationProvider.getApplicationContext()
+
+ private val listModel = VoiceActivationAppsListModel(context)
+
+ @Test
+ fun pageTitleResId() {
+ assertThat(listModel.pageTitleResId).isEqualTo(R.string.voice_activation_apps_title)
+ }
+
+ @Test
+ fun switchTitleResId() {
+ assertThat(listModel.switchTitleResId).isEqualTo(R.string.permit_voice_activation_apps)
+ }
+
+ @Test
+ fun footerResId() {
+ assertThat(listModel.footerResId)
+ .isEqualTo(R.string.allow_voice_activation_apps_description)
+ }
+
+ @Test
+ fun appOp() {
+ assertThat(listModel.appOp).isEqualTo(AppOpsManager.OP_RECEIVE_SANDBOX_TRIGGER_AUDIO)
+ }
+
+ @Test
+ fun permission() {
+ assertThat(listModel.permission).isEqualTo(
+ Manifest.permission.RECEIVE_SANDBOX_TRIGGER_AUDIO)
+ }
+
+ @Test
+ fun setModeByUid() {
+ assertThat(listModel.setModeByUid).isTrue()
+ }
+}
\ No newline at end of file
diff --git a/tests/spa_unit/src/com/android/settings/utils/SettingsGlobalBooleanDelegateTest.kt b/tests/spa_unit/src/com/android/settings/utils/SettingsGlobalBooleanDelegateTest.kt
new file mode 100644
index 0000000..75c3685
--- /dev/null
+++ b/tests/spa_unit/src/com/android/settings/utils/SettingsGlobalBooleanDelegateTest.kt
@@ -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.utils
+
+import android.content.Context
+import android.provider.Settings
+import androidx.lifecycle.testing.TestLifecycleOwner
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class SettingsGlobalBooleanDelegateTest {
+
+ private val context: Context = ApplicationProvider.getApplicationContext()
+
+ @Test
+ fun getValue_setTrue_returnTrue() {
+ Settings.Global.putInt(context.contentResolver, TEST_NAME, 1)
+
+ val value by context.settingsGlobalBoolean(TEST_NAME)
+
+ assertThat(value).isTrue()
+ }
+
+ @Test
+ fun getValue_setFalse_returnFalse() {
+ Settings.Global.putInt(context.contentResolver, TEST_NAME, 0)
+
+ val value by context.settingsGlobalBoolean(TEST_NAME)
+
+ assertThat(value).isFalse()
+ }
+
+ @Test
+ fun setValue_setTrue_returnTrue() {
+ var value by context.settingsGlobalBoolean(TEST_NAME)
+
+ value = true
+
+ assertThat(Settings.Global.getInt(context.contentResolver, TEST_NAME, 0)).isEqualTo(1)
+ }
+
+ @Test
+ fun setValue_setFalse_returnFalse() {
+ var value by context.settingsGlobalBoolean(TEST_NAME)
+
+ value = false
+
+ assertThat(Settings.Global.getInt(context.contentResolver, TEST_NAME, 1)).isEqualTo(0)
+ }
+
+ @Test
+ fun observeSettingsGlobalBoolean_valueNotChanged() {
+ var value by context.settingsGlobalBoolean(TEST_NAME)
+ value = false
+ var newValue: Boolean? = null
+
+ context.observeSettingsGlobalBoolean(TEST_NAME, TestLifecycleOwner().lifecycle) {
+ newValue = it
+ }
+
+ assertThat(newValue).isFalse()
+ }
+
+ @Test
+ fun observeSettingsGlobalBoolean_valueChanged() {
+ var value by context.settingsGlobalBoolean(TEST_NAME)
+ value = false
+ var newValue: Boolean? = null
+
+ context.observeSettingsGlobalBoolean(TEST_NAME, TestLifecycleOwner().lifecycle) {
+ newValue = it
+ }
+ value = true
+
+ assertThat(newValue).isFalse()
+ }
+
+ private companion object {
+ const val TEST_NAME = "test_boolean_delegate"
+ }
+}
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
index f0d0a0a..3440d2a 100644
--- a/tests/unit/src/com/android/settings/fingerprint2/domain/interactor/FingerprintManagerInteractorTest.kt
+++ b/tests/unit/src/com/android/settings/fingerprint2/domain/interactor/FingerprintManagerInteractorTest.kt
@@ -26,12 +26,14 @@
import android.os.Handler
import androidx.test.core.app.ApplicationProvider
import com.android.settings.biometrics.GatekeeperPasswordProvider
-import com.android.settings.biometrics.fingerprint2.shared.domain.interactor.FingerprintManagerInteractor
import com.android.settings.biometrics.fingerprint2.domain.interactor.FingerprintManagerInteractorImpl
+import com.android.settings.biometrics.fingerprint2.shared.data.repository.PressToAuthProvider
+import com.android.settings.biometrics.fingerprint2.shared.domain.interactor.FingerprintManagerInteractor
+import com.android.settings.biometrics.fingerprint2.shared.model.Default
import com.android.settings.biometrics.fingerprint2.shared.model.EnrollReason
-import com.android.settings.biometrics.fingerprint2.shared.model.FingerprintAuthAttemptViewModel
-import com.android.settings.biometrics.fingerprint2.shared.model.FingerprintViewModel
-import com.android.settings.biometrics.fingerprint2.shared.model.FingerEnrollStateViewModel
+import com.android.settings.biometrics.fingerprint2.shared.model.FingerEnrollState
+import com.android.settings.biometrics.fingerprint2.shared.model.FingerprintAuthAttemptModel
+import com.android.settings.biometrics.fingerprint2.shared.model.FingerprintData
import com.android.settings.password.ChooseLockSettingsHelper
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.cancelAndJoin
@@ -69,7 +71,11 @@
@Mock private lateinit var gateKeeperPasswordProvider: GatekeeperPasswordProvider
private var testScope = TestScope(backgroundDispatcher)
- private var pressToAuthProvider = { true }
+ private var pressToAuthProvider =
+ object : PressToAuthProvider {
+ override val isEnabled: Boolean
+ get() = false
+ }
@Before
fun setup() {
@@ -80,6 +86,7 @@
fingerprintManager,
gateKeeperPasswordProvider,
pressToAuthProvider,
+ Default,
)
}
@@ -164,7 +171,7 @@
@Test
fun testRemoveFingerprint_succeeds() =
testScope.runTest {
- val fingerprintViewModelToRemove = FingerprintViewModel("Finger 2", 1, 2L)
+ val fingerprintViewModelToRemove = FingerprintData("Finger 2", 1, 2L)
val fingerprintToRemove = Fingerprint("Finger 2", 1, 2L)
val removalCallback: ArgumentCaptor<FingerprintManager.RemovalCallback> = argumentCaptor()
@@ -187,7 +194,7 @@
@Test
fun testRemoveFingerprint_fails() =
testScope.runTest {
- val fingerprintViewModelToRemove = FingerprintViewModel("Finger 2", 1, 2L)
+ val fingerprintViewModelToRemove = FingerprintData("Finger 2", 1, 2L)
val fingerprintToRemove = Fingerprint("Finger 2", 1, 2L)
val removalCallback: ArgumentCaptor<FingerprintManager.RemovalCallback> = argumentCaptor()
@@ -214,7 +221,7 @@
@Test
fun testRenameFingerprint_succeeds() =
testScope.runTest {
- val fingerprintToRename = FingerprintViewModel("Finger 2", 1, 2L)
+ val fingerprintToRename = FingerprintData("Finger 2", 1, 2L)
underTest.renameFingerprint(fingerprintToRename, "Woo")
@@ -226,7 +233,7 @@
testScope.runTest {
val fingerprint = Fingerprint("Woooo", 100, 101L)
- var result: FingerprintAuthAttemptViewModel? = null
+ var result: FingerprintAuthAttemptModel? = null
val job = launch { result = underTest.authenticate() }
val authCallback: ArgumentCaptor<FingerprintManager.AuthenticationCallback> = argumentCaptor()
@@ -247,13 +254,13 @@
runCurrent()
job.cancelAndJoin()
- assertThat(result).isEqualTo(FingerprintAuthAttemptViewModel.Success(fingerprint.biometricId))
+ assertThat(result).isEqualTo(FingerprintAuthAttemptModel.Success(fingerprint.biometricId))
}
@Test
fun testAuth_lockout() =
testScope.runTest {
- var result: FingerprintAuthAttemptViewModel? = null
+ var result: FingerprintAuthAttemptModel? = null
val job = launch { result = underTest.authenticate() }
val authCallback: ArgumentCaptor<FingerprintManager.AuthenticationCallback> = argumentCaptor()
@@ -274,7 +281,7 @@
job.cancelAndJoin()
assertThat(result)
.isEqualTo(
- FingerprintAuthAttemptViewModel.Error(FINGERPRINT_ERROR_LOCKOUT_PERMANENT, "Lockout!!")
+ FingerprintAuthAttemptModel.Error(FINGERPRINT_ERROR_LOCKOUT_PERMANENT, "Lockout!!")
)
}
@@ -282,7 +289,7 @@
fun testEnroll_progress() =
testScope.runTest {
val token = byteArrayOf(5, 3, 2)
- var result: FingerEnrollStateViewModel? = null
+ var result: FingerEnrollState? = null
val job = launch { underTest.enroll(token, EnrollReason.FindSensor).collect { result = it } }
val enrollCallback: ArgumentCaptor<FingerprintManager.EnrollmentCallback> = argumentCaptor()
runCurrent()
@@ -299,14 +306,14 @@
runCurrent()
job.cancelAndJoin()
- assertThat(result).isEqualTo(FingerEnrollStateViewModel.EnrollProgress(1))
+ assertThat(result).isEqualTo(FingerEnrollState.EnrollProgress(1, 2))
}
@Test
fun testEnroll_help() =
testScope.runTest {
val token = byteArrayOf(5, 3, 2)
- var result: FingerEnrollStateViewModel? = null
+ var result: FingerEnrollState? = null
val job = launch { underTest.enroll(token, EnrollReason.FindSensor).collect { result = it } }
val enrollCallback: ArgumentCaptor<FingerprintManager.EnrollmentCallback> = argumentCaptor()
runCurrent()
@@ -323,14 +330,14 @@
runCurrent()
job.cancelAndJoin()
- assertThat(result).isEqualTo(FingerEnrollStateViewModel.EnrollHelp(-1, "help"))
+ assertThat(result).isEqualTo(FingerEnrollState.EnrollHelp(-1, "help"))
}
@Test
fun testEnroll_error() =
testScope.runTest {
val token = byteArrayOf(5, 3, 2)
- var result: FingerEnrollStateViewModel? = null
+ var result: FingerEnrollState? = null
val job = launch { underTest.enroll(token, EnrollReason.FindSensor).collect { result = it } }
val enrollCallback: ArgumentCaptor<FingerprintManager.EnrollmentCallback> = argumentCaptor()
runCurrent()
@@ -343,17 +350,20 @@
capture(enrollCallback),
eq(FingerprintManager.ENROLL_FIND_SENSOR)
)
- enrollCallback.value.onEnrollmentError(-2, "error")
+ enrollCallback.value.onEnrollmentError(-1, "error")
runCurrent()
job.cancelAndJoin()
-
- assertThat(result).isEqualTo(FingerEnrollStateViewModel.EnrollError(-2, "error"))
+ assertThat(result).isInstanceOf(FingerEnrollState.EnrollError::class.java)
}
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)
+
private fun <T> whenever(methodCall: T): OngoingStubbing<T> = `when`(methodCall)
+
inline fun <reified T : Any> argumentCaptor(): ArgumentCaptor<T> =
ArgumentCaptor.forClass(T::class.java)
}
diff --git a/tests/unit/src/com/android/settings/fingerprint2/enrollment/viewmodel/FingerprintEnrollFindSensorViewModelV2Test.kt b/tests/unit/src/com/android/settings/fingerprint2/enrollment/viewmodel/FingerprintEnrollFindSensorViewModelV2Test.kt
index 509b0ed..bd94cba 100644
--- a/tests/unit/src/com/android/settings/fingerprint2/enrollment/viewmodel/FingerprintEnrollFindSensorViewModelV2Test.kt
+++ b/tests/unit/src/com/android/settings/fingerprint2/enrollment/viewmodel/FingerprintEnrollFindSensorViewModelV2Test.kt
@@ -21,7 +21,9 @@
import android.view.accessibility.AccessibilityManager
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.test.core.app.ApplicationProvider
+import com.android.settings.biometrics.fingerprint2.shared.model.Default
import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.AccessibilityViewModel
+import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.BackgroundViewModel
import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.Education
import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintEnrollFindSensorViewModel
import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintEnrollNavigationViewModel
@@ -70,6 +72,7 @@
private lateinit var foldStateViewModel: FoldStateViewModel
private lateinit var orientationStateViewModel: OrientationStateViewModel
private lateinit var underTest: FingerprintEnrollFindSensorViewModel
+ private lateinit var backgroundViewModel: BackgroundViewModel
private val context: Context = ApplicationProvider.getApplicationContext()
private val accessibilityManager: AccessibilityManager =
context.getSystemService(AccessibilityManager::class.java)!!
@@ -93,12 +96,18 @@
fakeFingerprintManagerInteractor,
gatekeeperViewModel,
canSkipConfirm = true,
+ Default,
)
.create(FingerprintEnrollNavigationViewModel::class.java)
+
+ backgroundViewModel =
+ BackgroundViewModel.BackgroundViewModelFactory().create(BackgroundViewModel::class.java)
+ backgroundViewModel.inForeground()
enrollViewModel =
FingerprintEnrollViewModel.FingerprintEnrollViewModelFactory(
fakeFingerprintManagerInteractor,
- backgroundDispatcher
+ gatekeeperViewModel,
+ navigationViewModel,
)
.create(FingerprintEnrollViewModel::class.java)
accessibilityViewModel =
@@ -114,6 +123,7 @@
navigationViewModel,
enrollViewModel,
gatekeeperViewModel,
+ backgroundViewModel,
accessibilityViewModel,
foldStateViewModel,
orientationStateViewModel
@@ -123,6 +133,7 @@
// Navigate to Education page
navigationViewModel.nextStep()
}
+
@After
fun tearDown() {
Dispatchers.resetMain()
diff --git a/tests/unit/src/com/android/settings/fingerprint2/ui/enrollment/modules/enrolling/rfps/viewmodel/RFPSIconTouchViewModelTest.kt b/tests/unit/src/com/android/settings/fingerprint2/ui/enrollment/modules/enrolling/rfps/viewmodel/RFPSIconTouchViewModelTest.kt
new file mode 100644
index 0000000..46e883a
--- /dev/null
+++ b/tests/unit/src/com/android/settings/fingerprint2/ui/enrollment/modules/enrolling/rfps/viewmodel/RFPSIconTouchViewModelTest.kt
@@ -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.fingerprint2.ui.enrollment.modules.enrolling.rfps.viewmodel
+
+import androidx.arch.core.executor.testing.InstantTaskExecutorRule
+import com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.rfps.ui.viewmodel.RFPSIconTouchViewModel
+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 RFPSIconTouchViewModelTest {
+ @JvmField @Rule var rule = MockitoJUnit.rule()
+
+ @get:Rule val instantTaskRule = InstantTaskExecutorRule()
+
+ private var backgroundDispatcher = StandardTestDispatcher()
+ private var testScope = TestScope(backgroundDispatcher)
+ private lateinit var rfpsIconTouchViewModel: RFPSIconTouchViewModel
+
+ @Before
+ fun setup() {
+ Dispatchers.setMain(backgroundDispatcher)
+ testScope = TestScope(backgroundDispatcher)
+ rfpsIconTouchViewModel =
+ RFPSIconTouchViewModel()
+ }
+
+ @After
+ fun tearDown() {
+ Dispatchers.resetMain()
+ }
+
+ @Test
+ fun initShouldNotShowDialog() =
+ testScope.runTest {
+ var shouldShowDialog = false
+
+ val job = launch { rfpsIconTouchViewModel.shouldShowDialog.collect { shouldShowDialog = it } }
+
+ runCurrent()
+
+ assertThat(shouldShowDialog).isFalse()
+ job.cancel()
+ }
+
+ @Test
+ fun shouldShowDialogTest() =
+ testScope.runTest {
+ var shouldShowDialog = false
+
+ val job = launch { rfpsIconTouchViewModel.shouldShowDialog.collect { shouldShowDialog = it } }
+
+ rfpsIconTouchViewModel.userTouchedFingerprintIcon()
+ rfpsIconTouchViewModel.userTouchedFingerprintIcon()
+ rfpsIconTouchViewModel.userTouchedFingerprintIcon()
+
+ runCurrent()
+
+ assertThat(shouldShowDialog).isTrue()
+ job.cancel()
+ }
+
+ @Test
+ fun stateShouldBeFalseAfterReset() =
+ testScope.runTest {
+ var shouldShowDialog = false
+
+ val job = launch { rfpsIconTouchViewModel.shouldShowDialog.collect { shouldShowDialog = it } }
+
+ rfpsIconTouchViewModel.userTouchedFingerprintIcon()
+ rfpsIconTouchViewModel.userTouchedFingerprintIcon()
+ rfpsIconTouchViewModel.userTouchedFingerprintIcon()
+
+ runCurrent()
+
+ assertThat(shouldShowDialog).isTrue()
+
+ rfpsIconTouchViewModel.userTouchedFingerprintIcon()
+ runCurrent()
+
+ assertThat(shouldShowDialog).isFalse()
+
+ job.cancel()
+ }
+
+ @Test
+ fun toggleMultipleTimes() =
+ testScope.runTest {
+ var shouldShowDialog = false
+
+ val job = launch { rfpsIconTouchViewModel.shouldShowDialog.collect { shouldShowDialog = it } }
+
+ rfpsIconTouchViewModel.userTouchedFingerprintIcon()
+ rfpsIconTouchViewModel.userTouchedFingerprintIcon()
+ rfpsIconTouchViewModel.userTouchedFingerprintIcon()
+
+ runCurrent()
+
+ assertThat(shouldShowDialog).isTrue()
+
+ rfpsIconTouchViewModel.userTouchedFingerprintIcon()
+ runCurrent()
+
+ assertThat(shouldShowDialog).isFalse()
+
+ rfpsIconTouchViewModel.userTouchedFingerprintIcon()
+ rfpsIconTouchViewModel.userTouchedFingerprintIcon()
+
+ runCurrent()
+ assertThat(shouldShowDialog).isTrue()
+
+ job.cancel()
+ }
+}
diff --git a/tests/unit/src/com/android/settings/fingerprint2/ui/enrollment/viewmodel/FingerprintEnrollEnrollingViewModelTest.kt b/tests/unit/src/com/android/settings/fingerprint2/ui/enrollment/viewmodel/FingerprintEnrollEnrollingViewModelTest.kt
new file mode 100644
index 0000000..efb4a07
--- /dev/null
+++ b/tests/unit/src/com/android/settings/fingerprint2/ui/enrollment/viewmodel/FingerprintEnrollEnrollingViewModelTest.kt
@@ -0,0 +1,153 @@
+/*
+ * 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.ui.enrollment.viewmodel
+
+import androidx.arch.core.executor.testing.InstantTaskExecutorRule
+import com.android.settings.biometrics.fingerprint2.shared.model.Default
+import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.BackgroundViewModel
+import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.Enrollment
+import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintEnrollEnrollingViewModel
+import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintEnrollNavigationViewModel
+import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintEnrollViewModel
+import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintGatekeeperViewModel
+import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.GatekeeperInfo
+import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.NavState
+import com.android.settings.testutils2.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 FingerprintEnrollEnrollingViewModelTest {
+ @JvmField @Rule var rule = MockitoJUnit.rule()
+
+ @get:Rule val instantTaskRule = InstantTaskExecutorRule()
+
+ private var backgroundDispatcher = StandardTestDispatcher()
+ private lateinit var enrollEnrollingViewModel: FingerprintEnrollEnrollingViewModel
+ private lateinit var backgroundViewModel: BackgroundViewModel
+ private lateinit var gateKeeperViewModel: FingerprintGatekeeperViewModel
+ private lateinit var navigationViewModel: FingerprintEnrollNavigationViewModel
+ private val defaultGatekeeperInfo = GatekeeperInfo.GatekeeperPasswordInfo(byteArrayOf(1, 3), 3)
+ private var testScope = TestScope(backgroundDispatcher)
+
+ private lateinit var fakeFingerprintManagerInteractor: FakeFingerprintManagerInteractor
+
+ private fun initialize(gatekeeperInfo: GatekeeperInfo = defaultGatekeeperInfo) {
+ fakeFingerprintManagerInteractor = FakeFingerprintManagerInteractor()
+ gateKeeperViewModel =
+ FingerprintGatekeeperViewModel.FingerprintGatekeeperViewModelFactory(
+ gatekeeperInfo,
+ fakeFingerprintManagerInteractor
+ )
+ .create(FingerprintGatekeeperViewModel::class.java)
+
+ navigationViewModel =
+ FingerprintEnrollNavigationViewModel(
+ backgroundDispatcher,
+ fakeFingerprintManagerInteractor,
+ gateKeeperViewModel,
+ Enrollment,
+ NavState(true),
+ Default,
+ )
+
+ backgroundViewModel =
+ BackgroundViewModel.BackgroundViewModelFactory().create(BackgroundViewModel::class.java)
+ backgroundViewModel.inForeground()
+ val fingerprintEnrollViewModel =
+ FingerprintEnrollViewModel.FingerprintEnrollViewModelFactory(
+ fakeFingerprintManagerInteractor,
+ gateKeeperViewModel,
+ navigationViewModel,
+ )
+ .create(FingerprintEnrollViewModel::class.java)
+ enrollEnrollingViewModel =
+ FingerprintEnrollEnrollingViewModel.FingerprintEnrollEnrollingViewModelFactory(
+ fingerprintEnrollViewModel,
+ backgroundViewModel,
+ )
+ .create(FingerprintEnrollEnrollingViewModel::class.java)
+ }
+
+ @Before
+ fun setup() {
+ Dispatchers.setMain(backgroundDispatcher)
+ initialize()
+ }
+
+ @After
+ fun tearDown() {
+ Dispatchers.resetMain()
+ }
+
+ @Test
+ fun testEnrollShouldBeFalse() =
+ testScope.runTest {
+ var shouldEnroll = false
+
+ val job = launch {
+ enrollEnrollingViewModel.enrollFlowShouldBeRunning.collect { shouldEnroll = it }
+ }
+
+ assertThat(shouldEnroll).isFalse()
+ runCurrent()
+
+ enrollEnrollingViewModel.canEnroll()
+ runCurrent()
+
+ assertThat(shouldEnroll).isTrue()
+ job.cancel()
+ }
+
+ @Test
+ fun testEnrollShouldBeFalseWhenBackground() =
+ testScope.runTest {
+ var shouldEnroll = false
+
+ val job = launch {
+ enrollEnrollingViewModel.enrollFlowShouldBeRunning.collect { shouldEnroll = it }
+ }
+
+ assertThat(shouldEnroll).isFalse()
+ runCurrent()
+
+ enrollEnrollingViewModel.canEnroll()
+ runCurrent()
+
+ assertThat(shouldEnroll).isTrue()
+
+ backgroundViewModel.wentToBackground()
+ runCurrent()
+ assertThat(shouldEnroll).isFalse()
+
+ job.cancel()
+ }
+}
diff --git a/tests/unit/src/com/android/settings/fingerprint2/ui/settings/FingerprintSettingsNavigationViewModelTest.kt b/tests/unit/src/com/android/settings/fingerprint2/ui/settings/FingerprintSettingsNavigationViewModelTest.kt
index d4dbec5..064e087 100644
--- a/tests/unit/src/com/android/settings/fingerprint2/ui/settings/FingerprintSettingsNavigationViewModelTest.kt
+++ b/tests/unit/src/com/android/settings/fingerprint2/ui/settings/FingerprintSettingsNavigationViewModelTest.kt
@@ -18,7 +18,7 @@
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import com.android.settings.biometrics.BiometricEnrollBase
-import com.android.settings.biometrics.fingerprint2.shared.model.FingerprintViewModel
+import com.android.settings.biometrics.fingerprint2.shared.model.FingerprintData
import com.android.settings.biometrics.fingerprint2.ui.settings.viewmodel.EnrollFirstFingerprint
import com.android.settings.biometrics.fingerprint2.ui.settings.viewmodel.FingerprintSettingsNavigationViewModel
import com.android.settings.biometrics.fingerprint2.ui.settings.viewmodel.FinishSettings
@@ -208,7 +208,7 @@
fun enrollAdditionalFingerprints_fails() =
testScope.runTest {
fakeFingerprintManagerInteractor.enrolledFingerprintsInternal =
- mutableListOf(FingerprintViewModel("a", 1, 3L))
+ mutableListOf(FingerprintData("a", 1, 3L))
fakeFingerprintManagerInteractor.challengeToGenerate = Pair(4L, byteArrayOf(3, 3, 1))
var nextStep: NextStepViewModel? = null
@@ -227,7 +227,7 @@
fun enrollAdditional_success() =
testScope.runTest {
fakeFingerprintManagerInteractor.enrolledFingerprintsInternal =
- mutableListOf(FingerprintViewModel("a", 1, 3L))
+ mutableListOf(FingerprintData("a", 1, 3L))
var nextStep: NextStepViewModel? = null
val job = launch { underTest.nextStep.collect { nextStep = it } }
@@ -245,7 +245,7 @@
fun confirmDeviceCredential_withEnrolledFingerprint_showsSettings() =
testScope.runTest {
fakeFingerprintManagerInteractor.enrolledFingerprintsInternal =
- mutableListOf(FingerprintViewModel("a", 1, 3L))
+ mutableListOf(FingerprintData("a", 1, 3L))
fakeFingerprintManagerInteractor.challengeToGenerate = Pair(10L, byteArrayOf(1, 2, 3))
var nextStep: NextStepViewModel? = null
@@ -320,7 +320,7 @@
fun showSettings_shouldFinish() =
testScope.runTest {
fakeFingerprintManagerInteractor.enrolledFingerprintsInternal =
- mutableListOf(FingerprintViewModel("a", 1, 3L))
+ mutableListOf(FingerprintData("a", 1, 3L))
var nextStep: NextStepViewModel? = null
val job = launch { underTest.nextStep.collect { nextStep = it } }
diff --git a/tests/unit/src/com/android/settings/fingerprint2/ui/settings/FingerprintSettingsViewModelTest.kt b/tests/unit/src/com/android/settings/fingerprint2/ui/settings/FingerprintSettingsViewModelTest.kt
index d25ced0..4bd9121 100644
--- a/tests/unit/src/com/android/settings/fingerprint2/ui/settings/FingerprintSettingsViewModelTest.kt
+++ b/tests/unit/src/com/android/settings/fingerprint2/ui/settings/FingerprintSettingsViewModelTest.kt
@@ -17,8 +17,8 @@
package com.android.settings.fingerprint2.ui.settings
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
-import com.android.settings.biometrics.fingerprint2.shared.model.FingerprintAuthAttemptViewModel
-import com.android.settings.biometrics.fingerprint2.shared.model.FingerprintViewModel
+import com.android.settings.biometrics.fingerprint2.shared.model.FingerprintAuthAttemptModel
+import com.android.settings.biometrics.fingerprint2.shared.model.FingerprintData
import com.android.settings.biometrics.fingerprint2.ui.settings.viewmodel.FingerprintSettingsNavigationViewModel
import com.android.settings.biometrics.fingerprint2.ui.settings.viewmodel.FingerprintSettingsViewModel
import com.android.settings.biometrics.fingerprint2.ui.settings.viewmodel.PreferenceViewModel
@@ -103,7 +103,7 @@
FingerprintSensorType.UDFPS_OPTICAL,
)
fakeFingerprintManagerInteractor.enrolledFingerprintsInternal =
- mutableListOf(FingerprintViewModel("a", 1, 3L))
+ mutableListOf(FingerprintData("a", 1, 3L))
underTest =
FingerprintSettingsViewModel.FingerprintSettingsViewModelFactory(
@@ -114,7 +114,7 @@
)
.create(FingerprintSettingsViewModel::class.java)
- var authAttempt: FingerprintAuthAttemptViewModel? = null
+ var authAttempt: FingerprintAuthAttemptModel? = null
val job = launch { underTest.authFlow.take(5).collectLatest { authAttempt = it } }
underTest.shouldAuthenticate(true)
@@ -139,7 +139,7 @@
FingerprintSensorType.UDFPS_ULTRASONIC,
)
fakeFingerprintManagerInteractor.enrolledFingerprintsInternal =
- mutableListOf(FingerprintViewModel("a", 1, 3L))
+ mutableListOf(FingerprintData("a", 1, 3L))
underTest =
FingerprintSettingsViewModel.FingerprintSettingsViewModelFactory(
@@ -150,7 +150,7 @@
)
.create(FingerprintSettingsViewModel::class.java)
- var authAttempt: FingerprintAuthAttemptViewModel? = null
+ var authAttempt: FingerprintAuthAttemptModel? = null
val job = launch { underTest.authFlow.take(5).collectLatest { authAttempt = it } }
underTest.shouldAuthenticate(true)
@@ -173,8 +173,8 @@
FingerprintSensorType.POWER_BUTTON
)
fakeFingerprintManagerInteractor.enrolledFingerprintsInternal =
- mutableListOf(FingerprintViewModel("a", 1, 3L))
- val success = FingerprintAuthAttemptViewModel.Success(1)
+ mutableListOf(FingerprintData("a", 1, 3L))
+ val success = FingerprintAuthAttemptModel.Success(1)
fakeFingerprintManagerInteractor.authenticateAttempt = success
underTest =
@@ -186,7 +186,7 @@
)
.create(FingerprintSettingsViewModel::class.java)
- var authAttempt: FingerprintAuthAttemptViewModel? = null
+ var authAttempt: FingerprintAuthAttemptModel? = null
val job = launch { underTest.authFlow.take(5).collectLatest { authAttempt = it } }
underTest.shouldAuthenticate(true)
@@ -200,7 +200,7 @@
@Test
fun deleteDialog_showAndDismiss() = runTest {
- val fingerprintToDelete = FingerprintViewModel("A", 1, 10L)
+ val fingerprintToDelete = FingerprintData("A", 1, 10L)
fakeFingerprintManagerInteractor.enrolledFingerprintsInternal =
mutableListOf(fingerprintToDelete)
@@ -236,7 +236,7 @@
@Test
fun renameDialog_showAndDismiss() = runTest {
- val fingerprintToRename = FingerprintViewModel("World", 1, 10L)
+ val fingerprintToRename = FingerprintData("World", 1, 10L)
fakeFingerprintManagerInteractor.enrolledFingerprintsInternal =
mutableListOf(fingerprintToRename)
@@ -274,7 +274,7 @@
@Test
fun testTwoDialogsCannotShow_atSameTime() = runTest {
- val fingerprintToDelete = FingerprintViewModel("A", 1, 10L)
+ val fingerprintToDelete = FingerprintData("A", 1, 10L)
fakeFingerprintManagerInteractor.enrolledFingerprintsInternal =
mutableListOf(fingerprintToDelete)
@@ -311,9 +311,9 @@
fun authenticatePauses_whenPaused() =
testScope.runTest {
val fingerprints = setupAuth()
- val success = FingerprintAuthAttemptViewModel.Success(fingerprints.first().fingerId)
+ val success = FingerprintAuthAttemptModel.Success(fingerprints.first().fingerId)
- var authAttempt: FingerprintAuthAttemptViewModel? = null
+ var authAttempt: FingerprintAuthAttemptModel? = null
val job = launch { underTest.authFlow.take(5).collectLatest { authAttempt = it } }
@@ -325,7 +325,7 @@
assertThat(authAttempt).isEqualTo(success)
fakeFingerprintManagerInteractor.authenticateAttempt =
- FingerprintAuthAttemptViewModel.Success(10)
+ FingerprintAuthAttemptModel.Success(10)
underTest.shouldAuthenticate(false)
advanceTimeBy(400)
runCurrent()
@@ -340,7 +340,7 @@
testScope.runTest {
val fingerprints = setupAuth()
- var authAttempt: FingerprintAuthAttemptViewModel? = null
+ var authAttempt: FingerprintAuthAttemptModel? = null
val job = launch { underTest.authFlow.take(1).collectLatest { authAttempt = it } }
underTest.shouldAuthenticate(true)
navigationViewModel.onConfirmDevice(true, 10L)
@@ -357,7 +357,7 @@
testScope.runTest {
val fingerprints = setupAuth()
- var authAttempt: FingerprintAuthAttemptViewModel? = null
+ var authAttempt: FingerprintAuthAttemptModel? = null
val job = launch { underTest.authFlow.take(1).collectLatest { authAttempt = it } }
underTest.shouldAuthenticate(true)
navigationViewModel.onConfirmDevice(true, 10L)
@@ -370,7 +370,7 @@
assertThat(authAttempt).isEqualTo(null)
}
- private fun setupAuth(): MutableList<FingerprintViewModel> {
+ private fun setupAuth(): MutableList<FingerprintData> {
fakeFingerprintManagerInteractor.sensorProp =
FingerprintSensor(
0 /* sensorId */,
@@ -379,9 +379,9 @@
FingerprintSensorType.POWER_BUTTON
)
val fingerprints =
- mutableListOf(FingerprintViewModel("a", 1, 3L), FingerprintViewModel("b", 2, 5L))
+ mutableListOf(FingerprintData("a", 1, 3L), FingerprintData("b", 2, 5L))
fakeFingerprintManagerInteractor.enrolledFingerprintsInternal = fingerprints
- val success = FingerprintAuthAttemptViewModel.Success(1)
+ val success = FingerprintAuthAttemptModel.Success(1)
fakeFingerprintManagerInteractor.authenticateAttempt = success
underTest =
diff --git a/tests/unit/src/com/android/settings/localepicker/LocaleHelperPreferenceControllerTest.java b/tests/unit/src/com/android/settings/localepicker/LocaleHelperPreferenceControllerTest.java
index 31b8e79..5ac367e 100644
--- a/tests/unit/src/com/android/settings/localepicker/LocaleHelperPreferenceControllerTest.java
+++ b/tests/unit/src/com/android/settings/localepicker/LocaleHelperPreferenceControllerTest.java
@@ -19,12 +19,14 @@
import static org.mockito.Mockito.anyString;
import static org.mockito.Mockito.verify;
+import android.app.settings.SettingsEnums;
import android.content.Context;
import android.os.Looper;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
+import com.android.settings.testutils.FakeFeatureFactory;
import com.android.settingslib.widget.FooterPreference;
import org.junit.Before;
@@ -37,6 +39,7 @@
public class LocaleHelperPreferenceControllerTest {
private Context mContext;
private LocaleHelperPreferenceController mLocaleHelperPreferenceController;
+ private FakeFeatureFactory mFeatureFactory;
@Mock
private FooterPreference mMockFooterPreference;
@@ -49,11 +52,16 @@
}
mContext = ApplicationProvider.getApplicationContext();
mLocaleHelperPreferenceController = new LocaleHelperPreferenceController(mContext);
+ mFeatureFactory = FakeFeatureFactory.setupForTest();
}
@Test
public void updateFooterPreference_setFooterPreference_hasClickAction() {
mLocaleHelperPreferenceController.updateFooterPreference(mMockFooterPreference);
verify(mMockFooterPreference).setLearnMoreText(anyString());
+ mMockFooterPreference.setLearnMoreAction(v -> {
+ verify(mFeatureFactory.metricsFeatureProvider).action(
+ mContext, SettingsEnums.ACTION_LANGUAGES_LEARN_MORE);
+ });
}
}
diff --git a/tests/unit/src/com/android/settings/regionalpreferences/NumberingSystemItemControllerTest.java b/tests/unit/src/com/android/settings/regionalpreferences/NumberingSystemItemControllerTest.java
index 0a67824..5c42ad9 100644
--- a/tests/unit/src/com/android/settings/regionalpreferences/NumberingSystemItemControllerTest.java
+++ b/tests/unit/src/com/android/settings/regionalpreferences/NumberingSystemItemControllerTest.java
@@ -24,6 +24,7 @@
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
+import android.app.settings.SettingsEnums;
import android.content.Context;
import android.os.Bundle;
import android.os.LocaleList;
@@ -51,6 +52,7 @@
private NumberingPreferencesFragment mFragment;
private PreferenceScreen mPreferenceScreen;
private LocaleList mCacheLocale;
+ private FakeFeatureFactory mFeatureFactory;
@Before
@UiThreadTest
@@ -59,6 +61,7 @@
Looper.prepare();
}
mApplicationContext = ApplicationProvider.getApplicationContext();
+ mFeatureFactory = FakeFeatureFactory.setupForTest();
mFragment = spy(new NumberingPreferencesFragment());
PreferenceManager preferenceManager = new PreferenceManager(mApplicationContext);
mPreferenceScreen = preferenceManager.createPreferenceScreen(mApplicationContext);
@@ -94,6 +97,10 @@
}
assertTrue(isCallingStartActivity);
+ verify(mFeatureFactory.metricsFeatureProvider).action(
+ mApplicationContext,
+ SettingsEnums.ACTION_CHOOSE_LANGUAGE_FOR_NUMBERS_PREFERENCES,
+ "I_am_the_key");
}
@Test
@@ -114,6 +121,9 @@
mController.handlePreferenceTreeClick(preference);
verify(mFragment).setArguments(any());
+ verify(mFeatureFactory.metricsFeatureProvider).action(
+ mApplicationContext, SettingsEnums.ACTION_SET_NUMBERS_PREFERENCES,
+ "test_key");
}
@Test