Merge "Revert "[PK settings] Remove metrics."" into main
diff --git a/aconfig/Android.bp b/aconfig/Android.bp
index 78fba34..f485869 100644
--- a/aconfig/Android.bp
+++ b/aconfig/Android.bp
@@ -8,8 +8,10 @@
srcs: [
"settings_accessibility_flag_declarations.aconfig",
"settings_connecteddevice_flag_declarations.aconfig",
+ "settings_development_flag_declarations.aconfig",
"settings_globalintl_flag_declarations.aconfig",
"settings_experience_flag_declarations.aconfig",
+ "settings_notification_flag_declarations.aconfig",
"settings_onboarding_experience_flag_declarations.aconfig",
"settings_telephony_flag_declarations.aconfig",
"settings_biometrics_integration_declarations.aconfig",
diff --git a/aconfig/settings_development_flag_declarations.aconfig b/aconfig/settings_development_flag_declarations.aconfig
new file mode 100644
index 0000000..c23a38f
--- /dev/null
+++ b/aconfig/settings_development_flag_declarations.aconfig
@@ -0,0 +1,13 @@
+package: "com.android.settings.flags"
+
+# NOTE: Keep alphabetized to help limit merge conflicts from multiple simultaneous editors.
+
+# NOTE: All Settings flags share the same Flags class, so prefix our
+# flags with 'development' to prevent naming collision.
+
+flag {
+ name: "development_hdr_sdr_ratio"
+ namespace: "core_graphics"
+ description: "Shows hdr/sdr dev opton on the development options page from aconfig"
+ bug: "291863102"
+}
diff --git a/aconfig/settings_notification_flag_declarations.aconfig b/aconfig/settings_notification_flag_declarations.aconfig
new file mode 100644
index 0000000..f2bf1c8
--- /dev/null
+++ b/aconfig/settings_notification_flag_declarations.aconfig
@@ -0,0 +1,9 @@
+package: "com.android.settings.flags"
+
+flag {
+ name: "dedupe_dnd_settings_channels"
+ namespace: "systemui"
+ description: "Controls adding group names to channel names in the DND>Apps settings page"
+ bug: "294333850"
+}
+
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/protos/fuelgauge_log.proto b/protos/fuelgauge_log.proto
index e75ca48..36126a5 100644
--- a/protos/fuelgauge_log.proto
+++ b/protos/fuelgauge_log.proto
@@ -43,6 +43,7 @@
RECHECK_JOB = 3;
FETCH_USAGE_DATA = 4;
INSERT_USAGE_DATA = 5;
+ TIME_UPDATED = 6;
}
optional int64 timestamp = 1;
diff --git a/res/drawable/ic_error_red.xml b/res/drawable/ic_error_red.xml
new file mode 100644
index 0000000..d17c85b
--- /dev/null
+++ b/res/drawable/ic_error_red.xml
@@ -0,0 +1,26 @@
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="960"
+ android:viewportHeight="960"
+ android:tint="?android:attr/colorError">
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M480,680Q497,680 508.5,668.5Q520,657 520,640Q520,623 508.5,611.5Q497,600 480,600Q463,600 451.5,611.5Q440,623 440,640Q440,657 451.5,668.5Q463,680 480,680ZM440,520L520,520L520,280L440,280L440,520ZM480,880Q397,880 324,848.5Q251,817 197,763Q143,709 111.5,636Q80,563 80,480Q80,397 111.5,324Q143,251 197,197Q251,143 324,111.5Q397,80 480,80Q563,80 636,111.5Q709,143 763,197Q817,251 848.5,324Q880,397 880,480Q880,563 848.5,636Q817,709 763,763Q709,817 636,848.5Q563,880 480,880ZM480,800Q614,800 707,707Q800,614 800,480Q800,346 707,253Q614,160 480,160Q346,160 253,253Q160,346 160,480Q160,614 253,707Q346,800 480,800ZM480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Z"/>
+</vector>
diff --git a/res/layout/dialog_audio_sharing.xml b/res/layout/dialog_audio_sharing.xml
index 9624c90..5f26f74 100644
--- a/res/layout/dialog_audio_sharing.xml
+++ b/res/layout/dialog_audio_sharing.xml
@@ -23,7 +23,7 @@
android:orientation="vertical">
<TextView
- style="@style/device_info_dialog_value"
+ style="@style/DeviceAudioSharingText"
android:id="@+id/share_audio_subtitle1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
@@ -31,7 +31,7 @@
android:layout_gravity="center"/>
<TextView
- style="@style/device_info_dialog_value"
+ style="@style/DeviceAudioSharingText"
android:id="@+id/share_audio_subtitle2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
diff --git a/res/layout/dialog_eid_status.xml b/res/layout/dialog_eid_status.xml
index 77d6494..3297dac 100644
--- a/res/layout/dialog_eid_status.xml
+++ b/res/layout/dialog_eid_status.xml
@@ -27,8 +27,7 @@
style="@style/device_info_dialog_value"
android:id="@+id/esim_id_value"
android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:textIsSelectable="true" />
+ android:layout_height="wrap_content" />
<ImageView
android:id="@+id/esim_id_qrcode"
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/layout/privatespace_account_login_error.xml b/res/layout/privatespace_account_login_error.xml
new file mode 100644
index 0000000..a38dd50
--- /dev/null
+++ b/res/layout/privatespace_account_login_error.xml
@@ -0,0 +1,28 @@
+<?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"
+ android:id="@+id/ps_error_page_layout"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:filterTouchesWhenObscured="true"
+ app:sucHeaderText="@string/privatespace_retry_signin_title"
+ app:sudDescriptionText="@string/privatespace_retry_summary"
+ android:icon="@drawable/ic_error_red">
+</com.google.android.setupdesign.GlifLayout>
diff --git a/res/navigation/privatespace_main_context_nav.xml b/res/navigation/privatespace_main_context_nav.xml
index 5b2552a..ffc63ec 100644
--- a/res/navigation/privatespace_main_context_nav.xml
+++ b/res/navigation/privatespace_main_context_nav.xml
@@ -33,8 +33,8 @@
android:id="@+id/action_advance_profile_error"
app:destination="@id/ps_profile_error_fragment"/>
<action
- android:id="@+id/action_advance_to_success"
- app:destination="@id/ps_profile_success_fragment"/>
+ android:id="@+id/action_advance_login_error"
+ app:destination="@id/ps_account_error_fragment"/>
</fragment>
<fragment android:id="@+id/ps_profile_error_fragment"
android:name="com.android.settings.privatespace.PrivateProfileCreationError"
@@ -46,4 +46,18 @@
<fragment android:id="@+id/ps_profile_success_fragment"
android:name="com.android.settings.privatespace.SetupSuccessFragment"
android:label="fragment_ps_success"/>
-</navigation>
\ No newline at end of file
+ <fragment android:id="@+id/ps_account_error_fragment"
+ android:name="com.android.settings.privatespace.PrivateSpaceAccountLoginError"
+ android:label="fragment_account_error">
+ <action
+ android:id="@+id/action_advance_login_error"
+ app:destination="@id/ps_account_error_fragment"/>
+ </fragment>
+ <fragment android:id="@+id/ps_profile_lock_fragment"
+ android:name="com.android.settings.privatespace.PrivateSpaceSetLockFragment"
+ android:label="fragment_ps_lock"/>
+ <action android:id="@+id/action_success_fragment"
+ app:destination="@id/ps_profile_success_fragment"/>
+ <action android:id="@+id/action_set_lock_fragment"
+ app:destination="@id/ps_profile_lock_fragment"/>
+</navigation>
diff --git a/res/navigation/privatespace_private_context_nav.xml b/res/navigation/privatespace_private_context_nav.xml
deleted file mode 100644
index 3df8fa5..0000000
--- a/res/navigation/privatespace_private_context_nav.xml
+++ /dev/null
@@ -1,24 +0,0 @@
-<!--
- ~ Copyright (C) 2023 The Android Open Source Project
- ~
- ~ Licensed under the Apache License, Version 2.0 (the "License");
- ~ you may not use this file except in compliance with the License.
- ~ You may obtain a copy of the License at
- ~
- ~ http://www.apache.org/licenses/LICENSE-2.0
- ~
- ~ Unless required by applicable law or agreed to in writing, software
- ~ distributed under the License is distributed on an "AS IS" BASIS,
- ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- ~ See the License for the specific language governing permissions and
- ~ limitations under the License.
- -->
-
-<navigation xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:app="http://schemas.android.com/apk/res-auto"
- android:id="@+id/private_space_navigation"
- app:startDestination="@id/ps_profile_lock_fragment">
- <fragment android:id="@+id/ps_profile_lock_fragment"
- android:name="com.android.settings.privatespace.PrivateSpaceSetLockFragment"
- android:label="fragment_ps_lock"/>
-</navigation>
diff --git a/res/values/config.xml b/res/values/config.xml
index 99052ca..f50e918 100755
--- a/res/values/config.xml
+++ b/res/values/config.xml
@@ -753,6 +753,9 @@
<!-- Whether to display Cloned Apps page in Settings (Settings > Apps > Cloned Apps).-->
<bool name="config_cloned_apps_page_enabled">false</bool>
+ <!-- Whether to initiate Account login during Private Space setup.-->
+ <bool name="config_privatespace_account_login_enabled">false</bool>
+
<!-- Certificates of apps which are allowed to use activity embedding with Settings.-->
<string-array name="config_known_host_certs" translatable="false">
<item></item>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 1eb58ee..7ed9ade 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>
@@ -1298,6 +1298,10 @@
<string name="privatespace_done_label">Done</string>
<!-- Toast to show on private space setup completion informing user to scroll down All apps to access private space. [CHAR LIMIT=60] -->
<string name="scrolldown_to_access">Scroll down to access Private Space</string>
+ <!-- Title for Private Space account login error screen. [CHAR LIMIT=60] -->
+ <string name="privatespace_retry_signin_title">Sign in to set up Private Space</string>
+ <!-- Summary for the Private Space account login error screen. [CHAR LIMIT=NONE] -->
+ <string name="privatespace_retry_summary">You need to sign in to a Account to set up Private Space</string>
<!-- Text shown when "Add fingerprint" button is disabled -->
<string name="fingerprint_add_max">You can add up to <xliff:g id="count" example="5">%d</xliff:g> fingerprints</string>
@@ -1853,6 +1857,11 @@
<!-- Debugging developer settings: show refresh rate summary [CHAR LIMIT=58] -->
<string name="show_refresh_rate_summary">Show the current display refresh rate</string>
+ <!-- Debugging developer settings: show HDR/SDR ratio? [CHAR LIMIT=36] -->
+ <string name="show_hdr_sdr_ratio">Show HDR/SDR ratio</string>
+ <!-- Debugging developer settings: show HDR/SDR ratio summary [CHAR LIMIT=58] -->
+ <string name="show_hdr_sdr_ratio_summary">Show the current HDR/SDR ratio</string>
+
<!-- NFC settings -->
<!-- Used in the 1st-level settings screen to turn on NFC -->
<string name="nfc_quick_toggle_title">NFC</string>
@@ -3169,6 +3178,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]-->
@@ -9518,6 +9529,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. Built-in adaptive sensing ensures data stays private only to 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/values/styles.xml b/res/values/styles.xml
index 3a2f9ad..8736689 100644
--- a/res/values/styles.xml
+++ b/res/values/styles.xml
@@ -433,6 +433,15 @@
<item name="android:textSize">14sp</item>
<item name="android:textColor">?android:attr/textColorPrimary</item>
<item name="android:paddingBottom">24dp</item>
+ <item name="android:textIsSelectable">true</item>
+ </style>
+
+ <style name="DeviceAudioSharingText">
+ <item name="android:textAlignment">viewStart</item>
+ <item name="android:fontFamily">@*android:string/config_bodyFontFamily</item>
+ <item name="android:textSize">14sp</item>
+ <item name="android:textColor">?android:attr/textColorPrimary</item>
+ <item name="android:paddingBottom">24dp</item>
</style>
<style name="ContextualCardStyle">
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/development_settings.xml b/res/xml/development_settings.xml
index b053424..f890984 100644
--- a/res/xml/development_settings.xml
+++ b/res/xml/development_settings.xml
@@ -274,6 +274,11 @@
android:summary="@string/show_refresh_rate_summary" />
<SwitchPreferenceCompat
+ android:key="show_hdr_sdr_ratio"
+ android:title="@string/show_hdr_sdr_ratio"
+ android:summary="@string/show_hdr_sdr_ratio_summary" />
+
+ <SwitchPreferenceCompat
android:key="overlay_settings"
android:title="@string/overlay_settings_title"
android:summary="@string/overlay_settings_summary" />
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/appcompat/UserAspectRatioDetails.java b/src/com/android/settings/applications/appcompat/UserAspectRatioDetails.java
index 076d37c..02d5c27 100644
--- a/src/com/android/settings/applications/appcompat/UserAspectRatioDetails.java
+++ b/src/com/android/settings/applications/appcompat/UserAspectRatioDetails.java
@@ -28,6 +28,7 @@
import android.app.ActivityManager;
import android.app.IActivityManager;
+import android.app.settings.SettingsEnums;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.Build;
@@ -44,8 +45,10 @@
import com.android.settings.R;
import com.android.settings.Utils;
import com.android.settings.applications.AppInfoBase;
+import com.android.settings.overlay.FeatureFactory;
import com.android.settings.widget.EntityHeaderController;
import com.android.settingslib.applications.AppUtils;
+import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
import com.android.settingslib.widget.ActionButtonsPreference;
import java.util.ArrayList;
@@ -104,6 +107,7 @@
Log.e(TAG, "Unable to set user min aspect ratio");
return;
}
+ logActionMetrics(selectedKey, mSelectedKey);
// Only update to selected aspect ratio if nothing goes wrong
mSelectedKey = selectedKey;
updateAllPreferences(mSelectedKey);
@@ -118,8 +122,7 @@
@Override
public int getMetricsCategory() {
- // TODO(b/292566895): add metrics for logging
- return 0;
+ return SettingsEnums.USER_ASPECT_RATIO_APP_INFO_SETTINGS;
}
@Override
@@ -244,6 +247,68 @@
}
}
+ private void logActionMetrics(@NonNull String selectedKey, @NonNull String unselectedKey) {
+ final MetricsFeatureProvider metricsFeatureProvider =
+ FeatureFactory.getFeatureFactory().getMetricsFeatureProvider();
+ final int attribution = metricsFeatureProvider.getAttribution(getActivity());
+ metricsFeatureProvider.action(
+ attribution,
+ getUnselectedAspectRatioAction(unselectedKey),
+ getMetricsCategory(),
+ mPackageName,
+ mUserId
+ );
+ metricsFeatureProvider.action(
+ attribution,
+ getSelectedAspectRatioAction(selectedKey),
+ getMetricsCategory(),
+ mPackageName,
+ mUserId
+ );
+ }
+
+ private static int getSelectedAspectRatioAction(@NonNull String selectedKey) {
+ switch (selectedKey) {
+ case KEY_PREF_DEFAULT:
+ return SettingsEnums.ACTION_USER_ASPECT_RATIO_APP_DEFAULT_SELECTED;
+ case KEY_PREF_FULLSCREEN:
+ return SettingsEnums.ACTION_USER_ASPECT_RATIO_FULL_SCREEN_SELECTED;
+ case KEY_PREF_HALF_SCREEN:
+ return SettingsEnums.ACTION_USER_ASPECT_RATIO_HALF_SCREEN_SELECTED;
+ case KEY_PREF_4_3:
+ return SettingsEnums.ACTION_USER_ASPECT_RATIO_4_3_SELECTED;
+ case KEY_PREF_16_9:
+ return SettingsEnums.ACTION_USER_ASPECT_RATIO_16_9_SELECTED;
+ case KEY_PREF_3_2:
+ return SettingsEnums.ACTION_USER_ASPECT_RATIO_3_2_SELECTED;
+ case KEY_PREF_DISPLAY_SIZE:
+ return SettingsEnums.ACTION_USER_ASPECT_RATIO_DISPLAY_SIZE_SELECTED;
+ default:
+ return SettingsEnums.ACTION_UNKNOWN;
+ }
+ }
+
+ private static int getUnselectedAspectRatioAction(@NonNull String unselectedKey) {
+ switch (unselectedKey) {
+ case KEY_PREF_DEFAULT:
+ return SettingsEnums.ACTION_USER_ASPECT_RATIO_APP_DEFAULT_UNSELECTED;
+ case KEY_PREF_FULLSCREEN:
+ return SettingsEnums.ACTION_USER_ASPECT_RATIO_FULL_SCREEN_UNSELECTED;
+ case KEY_PREF_HALF_SCREEN:
+ return SettingsEnums.ACTION_USER_ASPECT_RATIO_HALF_SCREEN_UNSELECTED;
+ case KEY_PREF_4_3:
+ return SettingsEnums.ACTION_USER_ASPECT_RATIO_4_3_UNSELECTED;
+ case KEY_PREF_16_9:
+ return SettingsEnums.ACTION_USER_ASPECT_RATIO_16_9_UNSELECTED;
+ case KEY_PREF_3_2:
+ return SettingsEnums.ACTION_USER_ASPECT_RATIO_3_2_UNSELECTED;
+ case KEY_PREF_DISPLAY_SIZE:
+ return SettingsEnums.ACTION_USER_ASPECT_RATIO_DISPLAY_SIZE_UNSELECTED;
+ default:
+ return SettingsEnums.ACTION_UNKNOWN;
+ }
+ }
+
@VisibleForTesting
UserAspectRatioManager getAspectRatioManager() {
return mUserAspectRatioManager;
diff --git a/src/com/android/settings/applications/manageapplications/ApplicationViewHolder.java b/src/com/android/settings/applications/manageapplications/ApplicationViewHolder.java
index c94edc6..1d96688 100644
--- a/src/com/android/settings/applications/manageapplications/ApplicationViewHolder.java
+++ b/src/com/android/settings/applications/manageapplications/ApplicationViewHolder.java
@@ -46,14 +46,14 @@
import com.android.settingslib.applications.ApplicationsState;
import com.android.settingslib.applications.ApplicationsState.AppEntry;
import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
-import com.android.settingslib.spaprivileged.template.app.AppListItemKt;
+import com.android.settingslib.spaprivileged.template.app.AppListItemModelKt;
import com.android.settingslib.spaprivileged.template.app.AppListPageKt;
import com.android.settingslib.widget.LottieColorUtils;
import com.airbnb.lottie.LottieAnimationView;
/**
- * @deprecated Will be removed, use {@link AppListItemKt} {@link AppListPageKt} instead.
+ * @deprecated Will be removed, use {@link AppListItemModelKt} {@link AppListPageKt} instead.
*/
@Deprecated(forRemoval = true)
public class ApplicationViewHolder extends RecyclerView.ViewHolder {
diff --git a/src/com/android/settings/applications/manageapplications/ManageApplicationsUtil.kt b/src/com/android/settings/applications/manageapplications/ManageApplicationsUtil.kt
index 216ce47..82e987e 100644
--- a/src/com/android/settings/applications/manageapplications/ManageApplicationsUtil.kt
+++ b/src/com/android/settings/applications/manageapplications/ManageApplicationsUtil.kt
@@ -64,9 +64,11 @@
import com.android.settings.spa.app.specialaccess.AllFilesAccessAppListProvider
import com.android.settings.spa.app.specialaccess.DisplayOverOtherAppsAppListProvider
import com.android.settings.spa.app.specialaccess.InstallUnknownAppsListProvider
+import com.android.settings.spa.app.specialaccess.LongBackgroundTasksAppListProvider
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 +122,8 @@
LIST_TYPE_MAIN -> AllAppListPageProvider.name
LIST_TYPE_NFC_TAG_APPS -> NfcTagAppsSettingsProvider.getAppListRoute()
LIST_TYPE_USER_ASPECT_RATIO_APPS -> UserAspectRatioAppsPageProvider.name
+ LIST_TYPE_LONG_BACKGROUND_TASKS -> LongBackgroundTasksAppListProvider.getAppListRoute()
+ 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/AudioSharingDialogFragment.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDialogFragment.java
index 5b99907..1fd0b87 100644
--- a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDialogFragment.java
+++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDialogFragment.java
@@ -38,6 +38,26 @@
public class AudioSharingDialogFragment extends InstrumentedDialogFragment {
private static final String TAG = "AudioSharingDialog";
+ private static final String BUNDLE_KEY_DEVICE_NAMES = "bundle_key_device_names";
+
+ // The host creates an instance of this dialog fragment must implement this interface to receive
+ // event callbacks.
+ public interface DialogEventListener {
+ /**
+ * Called when users click the device item for sharing in the dialog.
+ *
+ * @param position The position of the item clicked.
+ */
+ void onItemClick(int position);
+
+ /**
+ * Called when users click the cancel button in the dialog.
+ */
+ void onCancelClick();
+ }
+
+ private static DialogEventListener sListener;
+
private View mRootView;
@Override
@@ -50,40 +70,59 @@
*
* @param host The Fragment this dialog will be hosted.
*/
- public static void show(Fragment host) {
+ public static void show(
+ Fragment host, ArrayList<String> deviceNames, DialogEventListener listener) {
if (!Flags.enableLeAudioSharing()) return;
final FragmentManager manager = host.getChildFragmentManager();
+ sListener = listener;
if (manager.findFragmentByTag(TAG) == null) {
- final AudioSharingDialogFragment dialog = new AudioSharingDialogFragment();
+ final Bundle bundle = new Bundle();
+ bundle.putStringArrayList(BUNDLE_KEY_DEVICE_NAMES, deviceNames);
+ AudioSharingDialogFragment dialog = new AudioSharingDialogFragment();
+ dialog.setArguments(bundle);
dialog.show(manager, TAG);
}
}
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
+ Bundle arguments = requireArguments();
+ ArrayList<String> deviceNames = arguments.getStringArrayList(BUNDLE_KEY_DEVICE_NAMES);
final AlertDialog.Builder builder =
- new AlertDialog.Builder(getActivity()).setTitle("Share audio");
+ new AlertDialog.Builder(getActivity()).setTitle("Share audio").setCancelable(false);
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");
+ if (deviceNames.isEmpty()) {
+ subTitle1.setVisibility(View.INVISIBLE);
+ subTitle2.setText("To start sharing audio, connect headphones that support LE audio");
+ builder.setNegativeButton(
+ "Close",
+ (dialog, which) -> {
+ sListener.onCancelClick();
+ });
+ } else if (deviceNames.size() == 1) {
+ // TODO: add real impl
+ subTitle1.setText("1 devices connected");
+ subTitle2.setText("placeholder");
+ } else {
+ // TODO: add real impl
+ 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,
+ deviceNames,
(int position) -> {
- // TODO: add on click callback.
+ sListener.onItemClick(position);
}));
recyclerView.setLayoutManager(
new LinearLayoutManager(getActivity(), LinearLayoutManager.VERTICAL, false));
- return builder.setView(mRootView).create();
+ AlertDialog dialog = builder.setView(mRootView).create();
+ dialog.setCanceledOnTouchOutside(false);
+ return dialog;
}
}
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 a375a3c..bd8027c 100644
--- a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingSwitchBarController.java
+++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingSwitchBarController.java
@@ -16,6 +16,8 @@
package com.android.settings.connecteddevice.audiosharing;
+import android.bluetooth.BluetoothLeBroadcast;
+import android.bluetooth.BluetoothLeBroadcastMetadata;
import android.content.Context;
import android.util.Log;
import android.widget.Switch;
@@ -24,36 +26,116 @@
import androidx.lifecycle.DefaultLifecycleObserver;
import androidx.lifecycle.LifecycleOwner;
+import com.android.settings.bluetooth.Utils;
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.bluetooth.LocalBluetoothLeBroadcast;
+import com.android.settingslib.bluetooth.LocalBluetoothManager;
+import com.android.settingslib.utils.ThreadUtils;
import com.android.settingslib.widget.OnMainSwitchChangeListener;
+import java.util.ArrayList;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+
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 final LocalBluetoothManager mBtManager;
+ private final LocalBluetoothLeBroadcast mBroadcast;
+ private final Executor mExecutor;
private DashboardFragment mFragment;
+ private final BluetoothLeBroadcast.Callback mBroadcastCallback =
+ new BluetoothLeBroadcast.Callback() {
+ @Override
+ public void onBroadcastStarted(int reason, int broadcastId) {
+ Log.d(
+ TAG,
+ "onBroadcastStarted(), reason = "
+ + reason
+ + ", broadcastId = "
+ + broadcastId);
+ updateSwitch();
+ }
+
+ @Override
+ public void onBroadcastStartFailed(int reason) {
+ Log.d(TAG, "onBroadcastStartFailed(), reason = " + reason);
+ // TODO: handle broadcast start fail
+ updateSwitch();
+ }
+
+ @Override
+ public void onBroadcastMetadataChanged(
+ int broadcastId, @NonNull BluetoothLeBroadcastMetadata metadata) {
+ Log.d(
+ TAG,
+ "onBroadcastMetadataChanged(), broadcastId = "
+ + broadcastId
+ + ", metadata = "
+ + metadata);
+ // TODO: handle add sink if there are connected lea devices.
+ }
+
+ @Override
+ public void onBroadcastStopped(int reason, int broadcastId) {
+ Log.d(
+ TAG,
+ "onBroadcastStopped(), reason = "
+ + reason
+ + ", broadcastId = "
+ + broadcastId);
+ updateSwitch();
+ }
+
+ @Override
+ public void onBroadcastStopFailed(int reason) {
+ Log.d(TAG, "onBroadcastStopFailed(), reason = " + reason);
+ // TODO: handle broadcast stop fail
+ updateSwitch();
+ }
+
+ @Override
+ public void onBroadcastUpdated(int reason, int broadcastId) {}
+
+ @Override
+ public void onBroadcastUpdateFailed(int reason, int broadcastId) {}
+
+ @Override
+ public void onPlaybackStarted(int reason, int broadcastId) {}
+
+ @Override
+ public void onPlaybackStopped(int reason, int broadcastId) {}
+ };
+
AudioSharingSwitchBarController(Context context, SettingsMainSwitchBar switchBar) {
super(context, PREF_KEY);
- mContext = context;
mSwitchBar = switchBar;
- mSwitchBar.setChecked(false);
+ mBtManager = Utils.getLocalBtManager(context);
+ mBroadcast = mBtManager.getProfileManager().getLeAudioBroadcastProfile();
+ mExecutor = Executors.newSingleThreadExecutor();
+ mSwitchBar.setChecked(isBroadcasting());
}
@Override
public void onStart(@NonNull LifecycleOwner owner) {
mSwitchBar.addOnSwitchChangeListener(this);
+ if (mBroadcast != null) {
+ mBroadcast.registerServiceCallBack(mExecutor, mBroadcastCallback);
+ }
}
@Override
public void onStop(@NonNull LifecycleOwner owner) {
mSwitchBar.removeOnSwitchChangeListener(this);
+ if (mBroadcast != null) {
+ mBroadcast.unregisterServiceCallBack(mBroadcastCallback);
+ }
}
@Override
@@ -63,7 +145,7 @@
if (isChecked) {
startAudioSharing();
} else {
- // TODO: stop sharing
+ stopAudioSharing();
}
}
@@ -82,10 +164,53 @@
}
private void startAudioSharing() {
- if (mFragment != null) {
- AudioSharingDialogFragment.show(mFragment);
- } else {
- Log.w(TAG, "Dialog fail to show due to null fragment.");
+ mSwitchBar.setEnabled(false);
+ if (mBroadcast == null || isBroadcasting()) {
+ Log.d(TAG, "Already in broadcasting or broadcast not support, ignore!");
+ mSwitchBar.setEnabled(true);
+ return;
}
+ if (mFragment == null) {
+ Log.w(TAG, "Dialog fail to show due to null fragment.");
+ mSwitchBar.setEnabled(true);
+ return;
+ }
+ ArrayList<String> deviceNames = new ArrayList<>();
+ AudioSharingDialogFragment.show(
+ mFragment,
+ deviceNames,
+ new AudioSharingDialogFragment.DialogEventListener() {
+ @Override
+ public void onItemClick(int position) {
+ // TODO: handle broadcast based on the dialog device item clicked
+ }
+
+ @Override
+ public void onCancelClick() {
+ mBroadcast.startBroadcast("test", /* language= */ null);
+ }
+ });
+ }
+
+ private void stopAudioSharing() {
+ mSwitchBar.setEnabled(false);
+ if (mBroadcast == null || !isBroadcasting()) {
+ Log.d(TAG, "Already not broadcasting or broadcast not support, ignore!");
+ mSwitchBar.setEnabled(true);
+ return;
+ }
+ mBroadcast.stopBroadcast(mBroadcast.getLatestBroadcastId());
+ }
+
+ private void updateSwitch() {
+ ThreadUtils.postOnMainThread(
+ () -> {
+ mSwitchBar.setChecked(isBroadcasting());
+ mSwitchBar.setEnabled(true);
+ });
+ }
+
+ private boolean isBroadcasting() {
+ return mBroadcast != null && mBroadcast.isEnabled(null);
}
}
diff --git a/src/com/android/settings/datausage/AppDataUsageCycleController.kt b/src/com/android/settings/datausage/AppDataUsageCycleController.kt
index b1a0e76..a3b7499 100644
--- a/src/com/android/settings/datausage/AppDataUsageCycleController.kt
+++ b/src/com/android/settings/datausage/AppDataUsageCycleController.kt
@@ -25,7 +25,6 @@
import androidx.lifecycle.repeatOnLifecycle
import androidx.preference.PreferenceScreen
import com.android.settings.core.BasePreferenceController
-import com.android.settings.datausage.lib.AppDataUsageDetailsRepository
import com.android.settings.datausage.lib.IAppDataUsageDetailsRepository
import com.android.settings.datausage.lib.NetworkUsageDetailsData
import kotlinx.coroutines.Dispatchers
@@ -40,11 +39,18 @@
private lateinit var preference: SpinnerPreference
private var cycleAdapter: CycleAdapter? = null
- private var initialCycles: List<Long> = emptyList()
- private var initialSelectedEndTime: Long = -1
-
private var usageDetailsDataList: List<NetworkUsageDetailsData> = emptyList()
+ override fun getAvailabilityStatus() = AVAILABLE
+
+ override fun displayPreference(screen: PreferenceScreen) {
+ super.displayPreference(screen)
+ preference = screen.findPreference(preferenceKey)!!
+ if (cycleAdapter == null) {
+ cycleAdapter = CycleAdapter(mContext, preference)
+ }
+ }
+
fun init(
repository: IAppDataUsageDetailsRepository,
onUsageDataUpdated: (NetworkUsageDetailsData) -> Unit,
@@ -60,22 +66,9 @@
* before loading to reduce flicker.
*/
fun setInitialCycles(initialCycles: List<Long>, initialSelectedEndTime: Long) {
- this.initialCycles = initialCycles
- this.initialSelectedEndTime = initialSelectedEndTime
- }
-
- override fun getAvailabilityStatus() = AVAILABLE
-
- override fun displayPreference(screen: PreferenceScreen) {
- super.displayPreference(screen)
- preference = screen.findPreference(preferenceKey)!!
- if (cycleAdapter == null) {
- cycleAdapter = CycleAdapter(mContext, preference).apply {
- if (initialCycles.isNotEmpty()) {
- setInitialCycleList(initialCycles, initialSelectedEndTime)
- preference.setHasCycles(true)
- }
- }
+ if (initialCycles.isNotEmpty()) {
+ cycleAdapter?.setInitialCycleList(initialCycles, initialSelectedEndTime)
+ preference.setHasCycles(true)
}
}
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/datausage/ChartDataUsagePreferenceController.kt b/src/com/android/settings/datausage/ChartDataUsagePreferenceController.kt
index 5149af0..780978f 100644
--- a/src/com/android/settings/datausage/ChartDataUsagePreferenceController.kt
+++ b/src/com/android/settings/datausage/ChartDataUsagePreferenceController.kt
@@ -39,6 +39,8 @@
private lateinit var repository: INetworkCycleDataRepository
private lateinit var preference: ChartDataUsagePreference
private lateinit var lifecycleScope: LifecycleCoroutineScope
+ private var lastStartTime: Long? = null
+ private var lastEndTime: Long? = null
open fun init(template: NetworkTemplate) {
this.repository = NetworkCycleDataRepository(mContext, template)
@@ -72,6 +74,10 @@
}
fun update(startTime: Long, endTime: Long) {
+ if (lastStartTime == startTime && lastEndTime == endTime) return
+ lastStartTime = startTime
+ lastEndTime = endTime
+
preference.setTime(startTime, endTime)
preference.setNetworkCycleData(NetworkCycleChartData.AllZero)
lifecycleScope.launch {
diff --git a/src/com/android/settings/datausage/DataUsageList.kt b/src/com/android/settings/datausage/DataUsageList.kt
index 7240150..30e8db3 100644
--- a/src/com/android/settings/datausage/DataUsageList.kt
+++ b/src/com/android/settings/datausage/DataUsageList.kt
@@ -31,9 +31,10 @@
import com.android.settings.R
import com.android.settings.datausage.lib.BillingCycleRepository
import com.android.settings.datausage.lib.NetworkUsageData
-import com.android.settings.network.MobileDataEnabledListener
import com.android.settings.network.MobileNetworkRepository
+import com.android.settings.network.mobileDataEnabledFlow
import com.android.settingslib.mobile.dataservice.SubscriptionInfoEntity
+import com.android.settingslib.spa.framework.util.collectLatestWithLifecycle
import com.android.settingslib.spaprivileged.framework.common.userManager
import com.android.settingslib.utils.ThreadUtils
import kotlin.jvm.optionals.getOrNull
@@ -43,10 +44,7 @@
* to inspect based on usage cycle and control through [NetworkPolicy].
*/
@OpenForTesting
-open class DataUsageList : DataUsageBaseFragment(), MobileDataEnabledListener.Client {
- @VisibleForTesting
- lateinit var dataStateListener: MobileDataEnabledListener
-
+open class DataUsageList : DataUsageBaseFragment() {
@JvmField
@VisibleForTesting
var template: NetworkTemplate? = null
@@ -89,7 +87,6 @@
return
}
updateSubscriptionInfoEntity()
- dataStateListener = MobileDataEnabledListener(activity, this)
dataUsageListAppsController = use(DataUsageListAppsController::class.java).apply {
init(template)
}
@@ -103,6 +100,9 @@
override fun onViewCreated(v: View, savedInstanceState: Bundle?) {
super.onViewCreated(v, savedInstanceState)
+ requireContext().mobileDataEnabledFlow(subId)
+ .collectLatestWithLifecycle(viewLifecycleOwner) { updatePolicy() }
+
val template = template ?: return
dataUsageListHeaderController = DataUsageListHeaderController(
setPinnedHeaderView(R.layout.apps_filter_spinner),
@@ -114,17 +114,6 @@
)
}
- override fun onResume() {
- super.onResume()
- dataStateListener.start(subId)
- updatePolicy()
- }
-
- override fun onPause() {
- super.onPause()
- dataStateListener.stop()
- }
-
override fun getPreferenceScreenResId() = R.xml.data_usage_list
override fun getLogTag() = TAG
@@ -154,13 +143,6 @@
}
}
- /**
- * Implementation of `MobileDataEnabledListener.Client`
- */
- override fun onMobileDataEnabledChange() {
- updatePolicy()
- }
-
/** Update chart sweeps and cycle list to reflect [NetworkPolicy] for current [template]. */
@VisibleForTesting
fun updatePolicy() {
diff --git a/src/com/android/settings/development/DevelopmentSettingsDashboardFragment.java b/src/com/android/settings/development/DevelopmentSettingsDashboardFragment.java
index b88c345..a995ac3 100644
--- a/src/com/android/settings/development/DevelopmentSettingsDashboardFragment.java
+++ b/src/com/android/settings/development/DevelopmentSettingsDashboardFragment.java
@@ -666,6 +666,7 @@
controllers.add(new ShowKeyPressesPreferenceController(context));
controllers.add(new ShowSurfaceUpdatesPreferenceController(context));
controllers.add(new ShowLayoutBoundsPreferenceController(context));
+ controllers.add(new ShowHdrSdrRatioPreferenceController(context));
controllers.add(new ShowRefreshRatePreferenceController(context));
controllers.add(new RtlLayoutPreferenceController(context));
controllers.add(new WindowAnimationScalePreferenceController(context));
diff --git a/src/com/android/settings/development/OWNERS b/src/com/android/settings/development/OWNERS
new file mode 100644
index 0000000..6443afe
--- /dev/null
+++ b/src/com/android/settings/development/OWNERS
@@ -0,0 +1,5 @@
+# ShowHdrSdrRatioPreferenceController
+per-file ShowHdrSdrRatioPreferenceController.java=file:platform/frameworks/native:/services/surfaceflinger/OWNERS
+
+# ShowRefreshRatePreferenceController
+per-file ShowRefreshRatePreferenceController.java=file:platform/frameworks/native:/services/surfaceflinger/OWNERS
diff --git a/src/com/android/settings/development/ShowHdrSdrRatioPreferenceController.java b/src/com/android/settings/development/ShowHdrSdrRatioPreferenceController.java
new file mode 100644
index 0000000..2e7807e
--- /dev/null
+++ b/src/com/android/settings/development/ShowHdrSdrRatioPreferenceController.java
@@ -0,0 +1,144 @@
+/**
+ * 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.development;
+
+import android.content.Context;
+import android.hardware.display.DisplayManager;
+import android.os.IBinder;
+import android.os.Parcel;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.view.Display;
+
+import androidx.annotation.VisibleForTesting;
+import androidx.preference.Preference;
+import androidx.preference.TwoStatePreference;
+
+import com.android.settings.core.PreferenceControllerMixin;
+import com.android.settings.flags.Flags;
+import com.android.settingslib.development.DeveloperOptionsPreferenceController;
+
+/**
+ * Controller class for controlling the hdr/sdr ratio on SurfaceFlinger
+ */
+public class ShowHdrSdrRatioPreferenceController extends DeveloperOptionsPreferenceController
+ implements Preference.OnPreferenceChangeListener, PreferenceControllerMixin {
+
+ private static final String SHOW_REFRESH_RATE_KEY = "show_hdr_sdr_ratio";
+
+ private static final int SETTING_VALUE_QUERY = 2;
+ private static final int SETTING_VALUE_ON = 1;
+ private static final int SETTING_VALUE_OFF = 0;
+
+ private static final String SURFACE_FLINGER_SERVICE_KEY = "SurfaceFlinger";
+
+ private static final int SURFACE_FLINGER_CODE = 1043;
+
+ private static final String SURFACE_COMPOSER_INTERFACE_KEY = "android.ui.ISurfaceComposer";
+
+ private final IBinder mSurfaceFlinger;
+
+ private final boolean mIsHdrSdrRatioAvailable;
+
+ public ShowHdrSdrRatioPreferenceController(Context context) {
+ super(context);
+ mSurfaceFlinger = ServiceManager.getService(SURFACE_FLINGER_SERVICE_KEY);
+ DisplayManager displayManager = context.getSystemService(DisplayManager.class);
+ Display display = displayManager.getDisplay(Display.DEFAULT_DISPLAY);
+ mIsHdrSdrRatioAvailable = display != null && display.isHdrSdrRatioAvailable();
+ }
+
+ @VisibleForTesting
+ ShowHdrSdrRatioPreferenceController(Context context, IBinder surfaceFlinger,
+ boolean isHdrSdrRatioAvailable) {
+ super(context);
+ mSurfaceFlinger = surfaceFlinger;
+ mIsHdrSdrRatioAvailable = isHdrSdrRatioAvailable;
+ }
+
+ @Override
+ public String getPreferenceKey() {
+ return SHOW_REFRESH_RATE_KEY;
+ }
+
+ @Override
+ public boolean onPreferenceChange(Preference preference, Object newValue) {
+ final boolean isEnabled = (Boolean) newValue;
+ writeShowHdrSdrRatioSetting(isEnabled);
+ return true;
+ }
+
+ @Override
+ public void updateState(Preference preference) {
+ super.updateState(preference);
+ updateShowHdrSdrRatioSetting();
+ }
+
+ @Override
+ public boolean isAvailable() {
+ return Flags.developmentHdrSdrRatio() && mIsHdrSdrRatioAvailable;
+ }
+
+ @Override
+ protected void onDeveloperOptionsSwitchDisabled() {
+ super.onDeveloperOptionsSwitchDisabled();
+ final TwoStatePreference preference = (TwoStatePreference) mPreference;
+ if (preference.isChecked()) {
+ // Writing false to the preference when the setting is already off will have a
+ // side effect of turning on the preference that we wish to avoid
+ writeShowHdrSdrRatioSetting(false);
+ preference.setChecked(false);
+ }
+ }
+
+ private void updateShowHdrSdrRatioSetting() {
+ // magic communication with surface flinger.
+ try {
+ if (mSurfaceFlinger != null) {
+ final Parcel data = Parcel.obtain();
+ final Parcel reply = Parcel.obtain();
+ data.writeInterfaceToken(SURFACE_COMPOSER_INTERFACE_KEY);
+ data.writeInt(SETTING_VALUE_QUERY);
+ mSurfaceFlinger.transact(SURFACE_FLINGER_CODE, data, reply, 0 /* flags */);
+ final boolean enabled = reply.readBoolean();
+ ((TwoStatePreference) mPreference).setChecked(enabled);
+ reply.recycle();
+ data.recycle();
+ }
+ } catch (RemoteException ex) {
+ // intentional no-op
+ }
+ }
+
+ @VisibleForTesting
+ void writeShowHdrSdrRatioSetting(boolean isEnabled) {
+ try {
+ if (mSurfaceFlinger != null) {
+ final Parcel data = Parcel.obtain();
+ data.writeInterfaceToken(SURFACE_COMPOSER_INTERFACE_KEY);
+ final int showHdrSdrRatio = isEnabled ? SETTING_VALUE_ON : SETTING_VALUE_OFF;
+ data.writeInt(showHdrSdrRatio);
+ mSurfaceFlinger.transact(SURFACE_FLINGER_CODE, data,
+ null /* reply */, 0 /* flags */);
+ data.recycle();
+ }
+ } catch (RemoteException ex) {
+ // intentional no-op
+ }
+ updateShowHdrSdrRatioSetting();
+ }
+}
diff --git a/src/com/android/settings/deviceinfo/simstatus/SimStatusDialogFragment.java b/src/com/android/settings/deviceinfo/simstatus/SimStatusDialogFragment.java
index e82d541..f212eea 100644
--- a/src/com/android/settings/deviceinfo/simstatus/SimStatusDialogFragment.java
+++ b/src/com/android/settings/deviceinfo/simstatus/SimStatusDialogFragment.java
@@ -111,10 +111,7 @@
Log.d(TAG, "Fragment not attached yet.");
return;
}
- setText(viewId, text, true);
- }
- public void setText(int viewId, CharSequence text, boolean enableCopy) {
final TextView textView = mRootView.findViewById(viewId);
if (textView == null) {
return;
@@ -125,6 +122,5 @@
text = PhoneNumberUtil.expandByTts(text);
}
textView.setText(text);
- textView.setTextIsSelectable(enableCopy);
}
}
diff --git a/src/com/android/settings/fuelgauge/batterytip/BatteryTipPreferenceController.java b/src/com/android/settings/fuelgauge/batterytip/BatteryTipPreferenceController.java
index 1a0ca4e..4b1f645 100644
--- a/src/com/android/settings/fuelgauge/batterytip/BatteryTipPreferenceController.java
+++ b/src/com/android/settings/fuelgauge/batterytip/BatteryTipPreferenceController.java
@@ -20,6 +20,7 @@
import android.content.Context;
import android.os.BadParcelableException;
import android.os.Bundle;
+import android.util.ArrayMap;
import android.util.Log;
import androidx.annotation.VisibleForTesting;
@@ -35,7 +36,6 @@
import com.android.settings.widget.CardPreference;
import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
-import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
@@ -65,7 +65,7 @@
public BatteryTipPreferenceController(Context context, String preferenceKey) {
super(context, preferenceKey);
- mBatteryTipMap = new HashMap<>();
+ mBatteryTipMap = new ArrayMap<>();
mMetricsFeatureProvider = FeatureFactory.getFeatureFactory().getMetricsFeatureProvider();
mNeedUpdate = true;
}
diff --git a/src/com/android/settings/fuelgauge/batterytip/tips/RestrictAppTip.java b/src/com/android/settings/fuelgauge/batterytip/tips/RestrictAppTip.java
index 55ac3fa..cba614b 100644
--- a/src/com/android/settings/fuelgauge/batterytip/tips/RestrictAppTip.java
+++ b/src/com/android/settings/fuelgauge/batterytip/tips/RestrictAppTip.java
@@ -18,9 +18,9 @@
import android.app.settings.SettingsEnums;
import android.content.Context;
-import android.content.res.Resources;
import android.icu.text.ListFormatter;
import android.os.Parcel;
+import android.util.ArrayMap;
import androidx.annotation.VisibleForTesting;
@@ -31,7 +31,6 @@
import com.android.settingslib.utils.StringUtil;
import java.util.ArrayList;
-import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -66,7 +65,7 @@
final CharSequence appLabel = num > 0 ? Utils.getApplicationLabel(context,
mRestrictAppList.get(0).packageName) : "";
- Map<String, Object> arguments = new HashMap<>();
+ Map<String, Object> arguments = new ArrayMap<>();
arguments.put("count", num);
arguments.put("label", appLabel);
return mState == StateType.HANDLED
@@ -84,7 +83,7 @@
final int resId = mState == StateType.HANDLED
? R.string.battery_tip_restrict_handled_summary
: R.string.battery_tip_restrict_summary;
- Map<String, Object> arguments = new HashMap<>();
+ Map<String, Object> arguments = new ArrayMap<>();
arguments.put("count", num);
arguments.put("label", appLabel);
return StringUtil.getIcuPluralsString(context, arguments, resId);
diff --git a/src/com/android/settings/fuelgauge/batteryusage/BatteryDiffEntry.java b/src/com/android/settings/fuelgauge/batteryusage/BatteryDiffEntry.java
index 9d7b629..b186c60 100644
--- a/src/com/android/settings/fuelgauge/batteryusage/BatteryDiffEntry.java
+++ b/src/com/android/settings/fuelgauge/batteryusage/BatteryDiffEntry.java
@@ -23,6 +23,7 @@
import android.os.UserHandle;
import android.os.UserManager;
import android.text.TextUtils;
+import android.util.ArrayMap;
import android.util.Log;
import android.util.Pair;
@@ -33,7 +34,6 @@
import com.android.settingslib.utils.StringUtil;
import java.util.Comparator;
-import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
@@ -43,11 +43,14 @@
static Locale sCurrentLocale = null;
// Caches app label and icon to improve loading performance.
- static final Map<String, BatteryEntry.NameAndIcon> sResourceCache = new HashMap<>();
+ static final Map<String, BatteryEntry.NameAndIcon> sResourceCache = new ArrayMap<>();
+
+ // Caches package name and uid to improve loading performance.
+ static final Map<String, Integer> sPackageNameAndUidCache = new ArrayMap<>();
// Whether a specific item is valid to launch restriction page?
@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
- static final Map<String, Boolean> sValidForRestriction = new HashMap<>();
+ static final Map<String, Boolean> sValidForRestriction = new ArrayMap<>();
/** A comparator for {@link BatteryDiffEntry} based on the sorting key. */
static final Comparator<BatteryDiffEntry> COMPARATOR =
@@ -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/BatteryEntry.java b/src/com/android/settings/fuelgauge/batteryusage/BatteryEntry.java
index 506607c..5d87302 100644
--- a/src/com/android/settings/fuelgauge/batteryusage/BatteryEntry.java
+++ b/src/com/android/settings/fuelgauge/batteryusage/BatteryEntry.java
@@ -33,6 +33,7 @@
import android.os.UserBatteryConsumer;
import android.os.UserHandle;
import android.os.UserManager;
+import android.util.ArrayMap;
import android.util.DebugUtils;
import android.util.Log;
@@ -41,7 +42,6 @@
import com.android.settingslib.Utils;
import java.util.Comparator;
-import java.util.HashMap;
import java.util.Locale;
/**
@@ -90,7 +90,7 @@
BatteryConsumer.POWER_COMPONENT_ANY, BatteryConsumer.PROCESS_STATE_CACHED),
};
- static final HashMap<String, UidToDetail> sUidCache = new HashMap<>();
+ static final ArrayMap<String, UidToDetail> sUidCache = new ArrayMap<>();
static Locale sCurrentLocale = null;
diff --git a/src/com/android/settings/fuelgauge/batteryusage/BatteryUsageBreakdownController.java b/src/com/android/settings/fuelgauge/batteryusage/BatteryUsageBreakdownController.java
index 7fedec7..983524f 100644
--- a/src/com/android/settings/fuelgauge/batteryusage/BatteryUsageBreakdownController.java
+++ b/src/com/android/settings/fuelgauge/batteryusage/BatteryUsageBreakdownController.java
@@ -23,6 +23,7 @@
import android.os.Handler;
import android.os.Looper;
import android.text.TextUtils;
+import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.Log;
import android.view.View;
@@ -50,7 +51,6 @@
import com.android.settingslib.widget.FooterPreference;
import java.util.ArrayList;
-import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
@@ -75,7 +75,7 @@
private final Handler mHandler = new Handler(Looper.getMainLooper());
@VisibleForTesting
- final Map<String, Preference> mPreferenceCache = new HashMap<>();
+ final Map<String, Preference> mPreferenceCache = new ArrayMap<>();
private int mSpinnerPosition;
private String mSlotTimestamp;
diff --git a/src/com/android/settings/fuelgauge/batteryusage/BootBroadcastReceiver.java b/src/com/android/settings/fuelgauge/batteryusage/BootBroadcastReceiver.java
index ebf1543..dc8dfb6 100644
--- a/src/com/android/settings/fuelgauge/batteryusage/BootBroadcastReceiver.java
+++ b/src/com/android/settings/fuelgauge/batteryusage/BootBroadcastReceiver.java
@@ -70,8 +70,7 @@
break;
case Intent.ACTION_TIME_CHANGED:
Log.d(TAG, "refresh job and clear all data from action=" + action);
- DatabaseUtils.clearAll(context);
- PeriodicJobManager.getInstance(context).refreshJob(/*fromBoot=*/ false);
+ DatabaseUtils.clearDataAfterTimeChangedIfNeeded(context);
break;
default:
Log.w(TAG, "receive unsupported action=" + action);
diff --git a/src/com/android/settings/fuelgauge/batteryusage/DataProcessManager.java b/src/com/android/settings/fuelgauge/batteryusage/DataProcessManager.java
index 1a226fd..99cf354 100644
--- a/src/com/android/settings/fuelgauge/batteryusage/DataProcessManager.java
+++ b/src/com/android/settings/fuelgauge/batteryusage/DataProcessManager.java
@@ -34,7 +34,6 @@
import java.util.ArrayList;
import java.util.Calendar;
-import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -72,8 +71,6 @@
private static final String TAG = "DataProcessManager";
private static final List<BatteryEventType> POWER_CONNECTION_EVENTS =
List.of(BatteryEventType.POWER_CONNECTED, BatteryEventType.POWER_DISCONNECTED);
- private static final List<BatteryEventType> BATTERY_LEVEL_RECORD_EVENTS =
- List.of(BatteryEventType.FULL_CHARGED, BatteryEventType.EVEN_HOUR);
// For testing only.
@VisibleForTesting
@@ -295,7 +292,7 @@
Log.d(TAG, "there is no work profile");
}
- final Map<Long, UsageEvents> usageEventsMap = new HashMap<>();
+ final Map<Long, UsageEvents> usageEventsMap = new ArrayMap<>();
usageEventsMap.put(Long.valueOf(currentUserId), usageEventsForCurrentUser);
if (usageEventsForWorkProfile != null) {
Log.d(TAG, "usageEventsForWorkProfile is null");
@@ -575,7 +572,7 @@
final List<BatteryEvent> batteryLevelRecordEvents =
DatabaseUtils.getBatteryEvents(
context, Calendar.getInstance(), lastFullChargeTime,
- BATTERY_LEVEL_RECORD_EVENTS);
+ DatabaseUtils.BATTERY_LEVEL_RECORD_EVENTS);
final long startTimestamp = batteryLevelRecordEvents.isEmpty()
? lastFullChargeTime : batteryLevelRecordEvents.get(0).getTimestamp();
final BatteryLevelData batteryLevelData = getPeriodBatteryLevelData(context, handler,
diff --git a/src/com/android/settings/fuelgauge/batteryusage/DatabaseUtils.java b/src/com/android/settings/fuelgauge/batteryusage/DatabaseUtils.java
index e78d25c..d099843 100644
--- a/src/com/android/settings/fuelgauge/batteryusage/DatabaseUtils.java
+++ b/src/com/android/settings/fuelgauge/batteryusage/DatabaseUtils.java
@@ -53,6 +53,7 @@
import java.util.ArrayList;
import java.util.Calendar;
import java.util.List;
+import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
@@ -133,6 +134,9 @@
.authority(AUTHORITY)
.appendPath(BATTERY_USAGE_SLOT_TABLE)
.build();
+ /** A list of level record event types to access battery usage data. */
+ public static final List<BatteryEventType> BATTERY_LEVEL_RECORD_EVENTS =
+ List.of(BatteryEventType.FULL_CHARGED, BatteryEventType.EVEN_HOUR);
// For testing only.
@VisibleForTesting
@@ -408,6 +412,35 @@
});
}
+ /** Clears all data and jobs if current timestamp is out of the range of last recorded job. */
+ public static void clearDataAfterTimeChangedIfNeeded(Context context) {
+ AsyncTask.execute(() -> {
+ try {
+ final List<BatteryEvent> batteryLevelRecordEvents =
+ DatabaseUtils.getBatteryEvents(context, Calendar.getInstance(),
+ getLastFullChargeTime(context), BATTERY_LEVEL_RECORD_EVENTS);
+ final long lastRecordTimestamp = batteryLevelRecordEvents.isEmpty()
+ ? INVALID_TIMESTAMP : batteryLevelRecordEvents.get(0).getTimestamp();
+ final long nextRecordTimestamp =
+ TimestampUtils.getNextEvenHourTimestamp(lastRecordTimestamp);
+ final long currentTime = System.currentTimeMillis();
+ final boolean isOutOfTimeRange = lastRecordTimestamp == INVALID_TIMESTAMP
+ || currentTime < lastRecordTimestamp || currentTime > nextRecordTimestamp;
+ final String logInfo = String.format(Locale.ENGLISH,
+ "clear database = %b, current time = %d, last record time = %d",
+ isOutOfTimeRange, currentTime, lastRecordTimestamp);
+ Log.d(TAG, logInfo);
+ BatteryUsageLogUtils.writeLog(context, Action.TIME_UPDATED, logInfo);
+ if (isOutOfTimeRange) {
+ DatabaseUtils.clearAll(context);
+ PeriodicJobManager.getInstance(context).refreshJob(/* fromBoot= */ false);
+ }
+ } catch (RuntimeException e) {
+ Log.e(TAG, "refreshDataAndJobIfNeededAfterTimeChanged() failed", e);
+ }
+ });
+ }
+
/** Returns the timestamp for 00:00 6 days before the calendar date. */
public static long getTimestampSixDaysAgo(Calendar calendar) {
Calendar startCalendar =
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/AppLocalePickerActivity.java b/src/com/android/settings/localepicker/AppLocalePickerActivity.java
index 194a08f..6706c6d 100644
--- a/src/com/android/settings/localepicker/AppLocalePickerActivity.java
+++ b/src/com/android/settings/localepicker/AppLocalePickerActivity.java
@@ -23,6 +23,7 @@
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
+import android.app.settings.SettingsEnums;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
@@ -45,12 +46,18 @@
import com.android.settings.applications.AppLocaleUtil;
import com.android.settings.applications.appinfo.AppLocaleDetails;
import com.android.settings.core.SettingsBaseActivity;
+import com.android.settings.overlay.FeatureFactory;
+import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
public class AppLocalePickerActivity extends SettingsBaseActivity
implements LocalePickerWithRegion.LocaleSelectedListener, MenuItem.OnActionExpandListener {
private static final String TAG = AppLocalePickerActivity.class.getSimpleName();
private static final String CHANNEL_ID_SUGGESTION = "suggestion";
private static final String CHANNEL_ID_SUGGESTION_TO_USER = "Locale suggestion";
+ private static final int SIM_LOCALE = 1 << 0;
+ private static final int SYSTEM_LOCALE = 1 << 1;
+ private static final int APP_LOCALE = 1 << 2;
+ private static final int IME_LOCALE = 1 << 3;
static final String EXTRA_APP_LOCALE = "app_locale";
static final String EXTRA_NOTIFICATION_ID = "notification_id";
@@ -59,6 +66,7 @@
private AppLocaleDetails mAppLocaleDetails;
private View mAppLocaleDetailContainer;
private NotificationController mNotificationController;
+ private MetricsFeatureProvider mMetricsFeatureProvider;
@Override
public void onCreate(Bundle savedInstanceState) {
@@ -84,6 +92,7 @@
setTitle(R.string.app_locale_picker_title);
getActionBar().setDisplayHomeAsUpEnabled(true);
+ mMetricsFeatureProvider = FeatureFactory.getFeatureFactory().getMetricsFeatureProvider();
mNotificationController = NotificationController.getInstance(this);
mLocalePickerWithRegion = LocalePickerWithRegion.createLanguagePicker(
@@ -113,6 +122,7 @@
if (localeInfo == null || localeInfo.getLocale() == null || localeInfo.isSystemLocale()) {
setAppDefaultLocale("");
} else {
+ logLocaleSource(localeInfo);
setAppDefaultLocale(localeInfo.getLocale().toLanguageTag());
broadcastAppLocaleChange(localeInfo);
}
@@ -268,4 +278,32 @@
return false;
}
+
+ private void logLocaleSource(LocaleStore.LocaleInfo localeInfo) {
+ if (!localeInfo.isSuggested() || localeInfo.isAppCurrentLocale()) {
+ return;
+ }
+ int localeSource = 0;
+ if (hasSuggestionType(localeInfo,
+ LocaleStore.LocaleInfo.SUGGESTION_TYPE_SYSTEM_AVAILABLE_LANGUAGE)) {
+ localeSource |= SYSTEM_LOCALE;
+ }
+ if (hasSuggestionType(localeInfo,
+ LocaleStore.LocaleInfo.SUGGESTION_TYPE_OTHER_APP_LANGUAGE)) {
+ localeSource |= APP_LOCALE;
+ }
+ if (hasSuggestionType(localeInfo, LocaleStore.LocaleInfo.SUGGESTION_TYPE_IME_LANGUAGE)) {
+ localeSource |= IME_LOCALE;
+ }
+ if (hasSuggestionType(localeInfo, LocaleStore.LocaleInfo.SUGGESTION_TYPE_SIM)) {
+ localeSource |= SIM_LOCALE;
+ }
+ mMetricsFeatureProvider.action(this,
+ SettingsEnums.ACTION_CHANGE_APP_LANGUAGE_FROM_SUGGESTED, localeSource);
+ }
+
+ private static boolean hasSuggestionType(LocaleStore.LocaleInfo localeInfo,
+ int suggestionType) {
+ return localeInfo.isSuggestionOfType(suggestionType);
+ }
}
diff --git a/src/com/android/settings/localepicker/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/MobileDataEnabledFlow.kt b/src/com/android/settings/network/MobileDataEnabledFlow.kt
new file mode 100644
index 0000000..2342377
--- /dev/null
+++ b/src/com/android/settings/network/MobileDataEnabledFlow.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.network
+
+import android.content.Context
+import android.provider.Settings
+import android.telephony.SubscriptionManager
+import com.android.settingslib.spaprivileged.settingsprovider.settingsGlobalChangeFlow
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.merge
+
+/**
+ * Flow for mobile data enabled changed event.
+ *
+ * Note: This flow can only notify enabled status changes, cannot provide the latest status.
+ */
+fun Context.mobileDataEnabledFlow(subId: Int): Flow<Unit> {
+ val flow = settingsGlobalChangeFlow(Settings.Global.MOBILE_DATA)
+ return when (subId) {
+ SubscriptionManager.INVALID_SUBSCRIPTION_ID -> flow
+ else -> {
+ val subIdFlow = settingsGlobalChangeFlow(
+ name = Settings.Global.MOBILE_DATA + subId,
+ sendInitialValue = false,
+ )
+ merge(flow, subIdFlow)
+ }
+ }
+}
diff --git a/src/com/android/settings/network/MobileDataEnabledListener.java b/src/com/android/settings/network/MobileDataEnabledListener.java
index b030823..f2d55ab 100644
--- a/src/com/android/settings/network/MobileDataEnabledListener.java
+++ b/src/com/android/settings/network/MobileDataEnabledListener.java
@@ -20,7 +20,12 @@
import android.provider.Settings;
import android.telephony.SubscriptionManager;
-/** Helper class to listen for changes in the enabled state of mobile data. */
+/**
+ * Helper class to listen for changes in the enabled state of mobile data.
+ *
+ * @deprecated use {@link MobileDataEnabledFlowKt} instead
+ */
+@Deprecated
public class MobileDataEnabledListener {
private Context mContext;
private Client mClient;
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..09b1150
--- /dev/null
+++ b/src/com/android/settings/network/MobileNetworkListFragment.kt
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.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.settingslib.search.SearchIndexable
+import com.android.settingslib.spa.framework.util.collectLatestWithLifecycle
+import com.android.settingslib.spaprivileged.framework.common.userManager
+import com.android.settingslib.spaprivileged.settingsprovider.settingsGlobalBooleanFlow
+
+@SearchIndexable(forTarget = SearchIndexable.ALL and SearchIndexable.ARC.inv())
+class MobileNetworkListFragment : DashboardFragment() {
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ collectAirplaneModeAndFinishIfOn()
+ }
+
+ 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.collectAirplaneModeAndFinishIfOn() {
+ requireContext().settingsGlobalBooleanFlow(Settings.Global.AIRPLANE_MODE_ON)
+ .collectLatestWithLifecycle(viewLifecycleOwner) { isAirplaneModeOn ->
+ 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/apn/ApnEditPageProvider.kt b/src/com/android/settings/network/apn/ApnEditPageProvider.kt
index 0e23a0e..0ed54e7 100644
--- a/src/com/android/settings/network/apn/ApnEditPageProvider.kt
+++ b/src/com/android/settings/network/apn/ApnEditPageProvider.kt
@@ -38,7 +38,6 @@
import com.android.settings.network.apn.ApnNetworkTypes.getNetworkTypeDisplayNames
import com.android.settings.network.apn.ApnNetworkTypes.getNetworkTypeSelectedOptionsState
import com.android.settingslib.spa.framework.common.SettingsPageProvider
-import com.android.settingslib.spa.framework.compose.stateOf
import com.android.settingslib.spa.widget.editor.SettingsExposedDropdownMenuBox
import com.android.settingslib.spa.widget.editor.SettingsExposedDropdownMenuCheckBox
import com.android.settingslib.spa.widget.editor.SettingsOutlinedTextField
@@ -96,7 +95,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(
@@ -186,10 +185,8 @@
SwitchPreference(
object : SwitchPreferenceModel {
override val title = context.resources.getString(R.string.carrier_enabled)
- override val changeable =
- stateOf(apnData.apnEnableEnabled)
- override val checked =
- stateOf(apnData.apnEnable)
+ override val changeable = { apnData.apnEnableEnabled }
+ override val checked = { apnData.apnEnable }
override val onCheckedChange = { newChecked: Boolean ->
apnData = apnData.copy(apnEnable = newChecked)
}
diff --git a/src/com/android/settings/network/telephony/MobileNetworkSettings.java b/src/com/android/settings/network/telephony/MobileNetworkSettings.java
index afc1b7e..a514414 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.collectAirplaneModeAndFinishIfOn;
+
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);
+ collectAirplaneModeAndFinishIfOn(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/notification/app/AppChannelsBypassingDndPreferenceController.java b/src/com/android/settings/notification/app/AppChannelsBypassingDndPreferenceController.java
index 2830024..200a47b 100644
--- a/src/com/android/settings/notification/app/AppChannelsBypassingDndPreferenceController.java
+++ b/src/com/android/settings/notification/app/AppChannelsBypassingDndPreferenceController.java
@@ -39,13 +39,18 @@
import com.android.settings.applications.AppInfoBase;
import com.android.settings.core.PreferenceControllerMixin;
import com.android.settings.core.SubSettingLauncher;
+import com.android.settings.flags.Flags;
import com.android.settings.notification.NotificationBackend;
import com.android.settingslib.PrimarySwitchPreference;
import com.android.settingslib.RestrictedSwitchPreference;
import java.util.ArrayList;
import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
import java.util.List;
+import java.util.Map;
+import java.util.Set;
/**
* Populates the PreferenceCategory with notification channels associated with the given app.
@@ -61,6 +66,9 @@
private RestrictedSwitchPreference mAllNotificationsToggle;
private PreferenceCategory mPreferenceCategory;
private List<NotificationChannel> mChannels = new ArrayList<>();
+ private Set<String> mDuplicateChannelNames = new HashSet<>();
+ private Map<NotificationChannel, String> mChannelGroupNames =
+ new HashMap<NotificationChannel, String>();
public AppChannelsBypassingDndPreferenceController(
Context context,
@@ -135,10 +143,20 @@
List<NotificationChannel> newChannelList = new ArrayList<>();
List<NotificationChannelGroup> mChannelGroupList = mBackend.getGroups(mAppRow.pkg,
mAppRow.uid).getList();
+ Set<String> allChannelNames = new HashSet<>();
for (NotificationChannelGroup channelGroup : mChannelGroupList) {
for (NotificationChannel channel : channelGroup.getChannels()) {
if (!isConversation(channel)) {
newChannelList.add(channel);
+ if (Flags.dedupeDndSettingsChannels()) {
+ mChannelGroupNames.put(channel, channelGroup.getName().toString());
+ // Check if channel name is unique on this page; if not, save it.
+ if (allChannelNames.contains(channel.getName())) {
+ mDuplicateChannelNames.add(channel.getName().toString());
+ } else {
+ allChannelNames.add(channel.getName().toString());
+ }
+ }
}
}
}
@@ -172,6 +190,17 @@
&& isChannelConfigurable(channel)
&& showNotification(channel));
channelPreference.setTitle(BidiFormatter.getInstance().unicodeWrap(channel.getName()));
+ if (Flags.dedupeDndSettingsChannels()) {
+ // If the channel shares its name with another channel, set group name as summary
+ // to disambiguate in the list.
+ if (mDuplicateChannelNames.contains(channel.getName().toString())
+ && mChannelGroupNames.containsKey(channel)
+ && mChannelGroupNames.get(channel) != null
+ && !mChannelGroupNames.get(channel).isEmpty()) {
+ channelPreference.setSummary(BidiFormatter.getInstance().unicodeWrap(
+ mChannelGroupNames.get(channel)));
+ }
+ }
channelPreference.setChecked(showNotificationInDnd(channel));
channelPreference.setOnPreferenceChangeListener(
new Preference.OnPreferenceChangeListener() {
diff --git a/src/com/android/settings/overlay/FeatureFactory.kt b/src/com/android/settings/overlay/FeatureFactory.kt
index ac689d9..bc0cf1f 100644
--- a/src/com/android/settings/overlay/FeatureFactory.kt
+++ b/src/com/android/settings/overlay/FeatureFactory.kt
@@ -39,6 +39,7 @@
import com.android.settings.onboarding.OnboardingFeatureProvider
import com.android.settings.overlay.FeatureFactory.Companion.setFactory
import com.android.settings.panel.PanelFeatureProvider
+import com.android.settings.privatespace.PrivateSpaceLoginFeatureProvider
import com.android.settings.search.SearchFeatureProvider
import com.android.settings.security.SecurityFeatureProvider
import com.android.settings.security.SecuritySettingsFeatureProvider
@@ -170,6 +171,11 @@
*/
abstract val fastPairFeatureProvider: FastPairFeatureProvider
+ /**
+ * Gets implementation for Private Space account login feature.
+ */
+ abstract val privateSpaceLoginFeatureProvider: PrivateSpaceLoginFeatureProvider
+
companion object {
private var _factory: FeatureFactory? = null
diff --git a/src/com/android/settings/overlay/FeatureFactoryImpl.kt b/src/com/android/settings/overlay/FeatureFactoryImpl.kt
index 7f991b7..28dbb23 100644
--- a/src/com/android/settings/overlay/FeatureFactoryImpl.kt
+++ b/src/com/android/settings/overlay/FeatureFactoryImpl.kt
@@ -59,6 +59,8 @@
import com.android.settings.security.SecurityFeatureProviderImpl
import com.android.settings.security.SecuritySettingsFeatureProvider
import com.android.settings.security.SecuritySettingsFeatureProviderImpl
+import com.android.settings.privatespace.PrivateSpaceLoginFeatureProvider
+import com.android.settings.privatespace.PrivateSpaceLoginFeatureProviderImpl
import com.android.settings.slices.SlicesFeatureProviderImpl
import com.android.settings.users.UserFeatureProviderImpl
import com.android.settings.vpn2.AdvancedVpnFeatureProviderImpl
@@ -184,4 +186,8 @@
override val fastPairFeatureProvider: FastPairFeatureProvider by lazy {
FastPairFeatureProviderImpl()
}
+
+ override val privateSpaceLoginFeatureProvider: PrivateSpaceLoginFeatureProvider by lazy {
+ PrivateSpaceLoginFeatureProviderImpl()
+ }
}
diff --git a/src/com/android/settings/privatespace/AutoAdvanceSetupFragment.java b/src/com/android/settings/privatespace/AutoAdvanceSetupFragment.java
index 5456c01..3b59166 100644
--- a/src/com/android/settings/privatespace/AutoAdvanceSetupFragment.java
+++ b/src/com/android/settings/privatespace/AutoAdvanceSetupFragment.java
@@ -16,7 +16,8 @@
package com.android.settings.privatespace;
-import static com.android.settings.privatespace.PrivateSpaceSetupActivity.SET_LOCK_ACTION;
+import static com.android.settings.privatespace.PrivateSpaceSetupActivity.ACCOUNT_LOGIN_ACTION;
+import static com.android.settings.privatespace.PrivateSpaceSetupActivity.EXTRA_ACTION_TYPE;
import android.annotation.SuppressLint;
import android.content.Intent;
@@ -134,7 +135,8 @@
private void startActivityInPrivateUser(UserHandle userHandle) {
/* Start new activity in private profile which is needed to set private profile lock */
Intent intent = new Intent(getContext(), PrivateProfileContextHelperActivity.class);
- getActivity().startActivityForResultAsUser(intent, SET_LOCK_ACTION, userHandle);
+ intent.putExtra(EXTRA_ACTION_TYPE, ACCOUNT_LOGIN_ACTION);
+ getActivity().startActivityForResultAsUser(intent, ACCOUNT_LOGIN_ACTION, userHandle);
}
private void showPrivateSpaceErrorScreen() {
diff --git a/src/com/android/settings/privatespace/PrivateProfileContextHelperActivity.java b/src/com/android/settings/privatespace/PrivateProfileContextHelperActivity.java
index c0d762a..0539f60 100644
--- a/src/com/android/settings/privatespace/PrivateProfileContextHelperActivity.java
+++ b/src/com/android/settings/privatespace/PrivateProfileContextHelperActivity.java
@@ -16,27 +16,84 @@
package com.android.settings.privatespace;
+import static android.app.admin.DevicePolicyManager.ACTION_SET_NEW_PASSWORD;
+import static android.app.admin.DevicePolicyManager.EXTRA_PASSWORD_COMPLEXITY;
+import static android.app.admin.DevicePolicyManager.PASSWORD_COMPLEXITY_LOW;
+
+import static com.android.settings.privatespace.PrivateSpaceSetupActivity.ACCOUNT_LOGIN_ACTION;
+import static com.android.settings.privatespace.PrivateSpaceSetupActivity.EXTRA_ACTION_TYPE;
+import static com.android.settings.privatespace.PrivateSpaceSetupActivity.SET_LOCK_ACTION;
+
+import android.app.KeyguardManager;
+import android.content.Intent;
import android.os.Bundle;
+import androidx.activity.result.ActivityResult;
+import androidx.activity.result.ActivityResultLauncher;
+import androidx.activity.result.contract.ActivityResultContracts;
+import androidx.annotation.Nullable;
import androidx.fragment.app.FragmentActivity;
-import androidx.navigation.fragment.NavHostFragment;
-import com.android.settings.R;
import com.android.settings.SetupWizardUtils;
+import com.android.settings.overlay.FeatureFactory;
import com.google.android.setupdesign.util.ThemeHelper;
-/** Activity that is started as private profile user that helps to set private profile lock. */
+/** Activity that is started as private profile user that helps to set private profile lock or
+ * add an account on the private profile. */
public class PrivateProfileContextHelperActivity extends FragmentActivity {
private static final String TAG = "PrivateProfileHelper";
+ private final ActivityResultLauncher<Intent> mAddAccountToPrivateProfile =
+ registerForActivityResult(new ActivityResultContracts.StartActivityForResult(),
+ this::onAccountAdded);
+ private final ActivityResultLauncher<Intent> mVerifyDeviceLock =
+ registerForActivityResult(new ActivityResultContracts.StartActivityForResult(),
+ this::onSetDeviceNewLock);
+
@Override
protected void onCreate(Bundle savedInstanceState) {
setTheme(SetupWizardUtils.getTheme(this, getIntent()));
ThemeHelper.trySetDynamicColor(this);
super.onCreate(savedInstanceState);
- setContentView(R.layout.privatespace_setup_root);
- NavHostFragment navHostFragment = (NavHostFragment) getSupportFragmentManager()
- .findFragmentById(R.id.ps_nav_host_fragment);
- navHostFragment.getNavController().setGraph(R.navigation.privatespace_private_context_nav);
+ if (savedInstanceState == null) {
+ int action = getIntent().getIntExtra(EXTRA_ACTION_TYPE, -1);
+ if (action == ACCOUNT_LOGIN_ACTION) {
+ PrivateSpaceLoginFeatureProvider privateSpaceLoginFeatureProvider =
+ FeatureFactory.getFeatureFactory().getPrivateSpaceLoginFeatureProvider();
+ if (!privateSpaceLoginFeatureProvider.initiateAccountLogin(this,
+ mAddAccountToPrivateProfile)) {
+ setResult(RESULT_OK);
+ finish();
+ }
+ } else if (action == SET_LOCK_ACTION) {
+ createPrivateSpaceLock();
+ }
+ }
+ }
+
+ private void createPrivateSpaceLock() {
+ final Intent intent = new Intent(ACTION_SET_NEW_PASSWORD);
+ intent.putExtra(EXTRA_PASSWORD_COMPLEXITY, PASSWORD_COMPLEXITY_LOW);
+ mVerifyDeviceLock.launch(intent);
+ }
+
+ private void onAccountAdded(@Nullable ActivityResult result) {
+ if (result != null && result.getResultCode() == RESULT_OK) {
+ setResult(RESULT_OK);
+ } else {
+ setResult(RESULT_CANCELED);
+ }
+ finish();
+ }
+
+ private void onSetDeviceNewLock(@Nullable ActivityResult result) {
+ // TODO(b/307281644) : Verify this for biometrics and check result code after new
+ // Authentication changes are merged.
+ if (result != null && getSystemService(KeyguardManager.class).isDeviceSecure()) {
+ setResult(RESULT_OK);
+ } else {
+ setResult(RESULT_CANCELED);
+ }
+ finish();
}
}
diff --git a/src/com/android/settings/privatespace/PrivateSpaceAccountLoginError.java b/src/com/android/settings/privatespace/PrivateSpaceAccountLoginError.java
new file mode 100644
index 0000000..2d263d7
--- /dev/null
+++ b/src/com/android/settings/privatespace/PrivateSpaceAccountLoginError.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.privatespace;
+
+import static com.android.settings.privatespace.PrivateSpaceSetupActivity.ACCOUNT_LOGIN_ACTION;
+import static com.android.settings.privatespace.PrivateSpaceSetupActivity.EXTRA_ACTION_TYPE;
+
+import android.annotation.SuppressLint;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.UserHandle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.activity.OnBackPressedCallback;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.Fragment;
+
+import com.android.settings.R;
+
+import com.google.android.setupcompat.template.FooterBarMixin;
+import com.google.android.setupcompat.template.FooterButton;
+import com.google.android.setupdesign.GlifLayout;
+
+/** Fragment to display error screen if the profile is not signed in with a Google account. */
+public class PrivateSpaceAccountLoginError extends Fragment {
+ @Override
+ public View onCreateView(
+ LayoutInflater inflater,
+ @Nullable ViewGroup container,
+ @Nullable Bundle savedInstanceState) {
+ GlifLayout rootView =
+ (GlifLayout) inflater
+ .inflate(R.layout.privatespace_account_login_error, container, false);
+ final FooterBarMixin mixin = rootView.getMixin(FooterBarMixin.class);
+ mixin.setPrimaryButton(
+ new FooterButton.Builder(getContext())
+ .setText(R.string.privatespace_tryagain_label)
+ .setListener(nextScreen())
+ .setButtonType(FooterButton.ButtonType.NEXT)
+ .setTheme(com.google.android.setupdesign.R.style.SudGlifButton_Primary)
+ .build());
+ OnBackPressedCallback callback =
+ new OnBackPressedCallback(true /* enabled by default */) {
+ @Override
+ public void handleOnBackPressed() {
+ // Handle the back button event
+ }
+ };
+ requireActivity().getOnBackPressedDispatcher().addCallback(this, callback);
+
+ return rootView;
+ }
+
+ @SuppressLint("MissingPermission")
+ private View.OnClickListener nextScreen() {
+ return v -> {
+ PrivateSpaceMaintainer privateSpaceMaintainer = PrivateSpaceMaintainer
+ .getInstance(getActivity());
+ UserHandle userHandle;
+ if (privateSpaceMaintainer.doesPrivateSpaceExist() && (userHandle =
+ privateSpaceMaintainer.getPrivateProfileHandle()) != null) {
+ Intent intent = new Intent(getContext(), PrivateProfileContextHelperActivity.class);
+ intent.putExtra(EXTRA_ACTION_TYPE, ACCOUNT_LOGIN_ACTION);
+ getActivity().startActivityForResultAsUser(intent, ACCOUNT_LOGIN_ACTION,
+ userHandle);
+ }
+ };
+ }
+}
diff --git a/src/com/android/settings/privatespace/PrivateSpaceLoginFeatureProvider.java b/src/com/android/settings/privatespace/PrivateSpaceLoginFeatureProvider.java
new file mode 100644
index 0000000..76ea9ac
--- /dev/null
+++ b/src/com/android/settings/privatespace/PrivateSpaceLoginFeatureProvider.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.privatespace;
+
+import android.content.Context;
+import android.content.Intent;
+
+import androidx.activity.result.ActivityResultLauncher;
+import androidx.annotation.NonNull;
+
+/** Feature provider for account login during Private Space setup. */
+public interface PrivateSpaceLoginFeatureProvider {
+ /** Returns true if login to an account is enabled during Private Space setup. */
+ boolean initiateAccountLogin(@NonNull Context context,
+ @NonNull ActivityResultLauncher<Intent> resultLauncher);
+}
diff --git a/src/com/android/settings/privatespace/PrivateSpaceLoginFeatureProviderImpl.java b/src/com/android/settings/privatespace/PrivateSpaceLoginFeatureProviderImpl.java
new file mode 100644
index 0000000..7fca2a4
--- /dev/null
+++ b/src/com/android/settings/privatespace/PrivateSpaceLoginFeatureProviderImpl.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.privatespace;
+
+import android.content.Context;
+import android.content.Intent;
+
+import androidx.activity.result.ActivityResultLauncher;
+import androidx.annotation.NonNull;
+
+/** Stub class for Private space to not initiate account login during setup */
+public class PrivateSpaceLoginFeatureProviderImpl implements PrivateSpaceLoginFeatureProvider {
+ @Override
+ public boolean initiateAccountLogin(@NonNull Context context,
+ @NonNull ActivityResultLauncher<Intent> resultLauncher) {
+ return false;
+ }
+}
diff --git a/src/com/android/settings/privatespace/PrivateSpaceSetLockFragment.java b/src/com/android/settings/privatespace/PrivateSpaceSetLockFragment.java
index 3d17638..93dc43b 100644
--- a/src/com/android/settings/privatespace/PrivateSpaceSetLockFragment.java
+++ b/src/com/android/settings/privatespace/PrivateSpaceSetLockFragment.java
@@ -16,25 +16,21 @@
package com.android.settings.privatespace;
-import static android.app.Activity.RESULT_OK;
-import static android.app.admin.DevicePolicyManager.ACTION_SET_NEW_PASSWORD;
-import static android.app.admin.DevicePolicyManager.EXTRA_PASSWORD_COMPLEXITY;
-import static android.app.admin.DevicePolicyManager.PASSWORD_COMPLEXITY_LOW;
+import static com.android.settings.privatespace.PrivateSpaceSetupActivity.EXTRA_ACTION_TYPE;
+import static com.android.settings.privatespace.PrivateSpaceSetupActivity.SET_LOCK_ACTION;
-import android.app.Activity;
-import android.app.KeyguardManager;
+import android.annotation.SuppressLint;
import android.content.Intent;
import android.os.Bundle;
+import android.os.UserHandle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.activity.OnBackPressedCallback;
-import androidx.activity.result.ActivityResult;
-import androidx.activity.result.ActivityResultLauncher;
-import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
+import androidx.navigation.fragment.NavHostFragment;
import com.android.settings.R;
@@ -45,9 +41,6 @@
/** Fragment that provides an option to user to choose between the existing screen lock or set a
* separate private profile lock. */
public class PrivateSpaceSetLockFragment extends Fragment {
- private final ActivityResultLauncher<Intent> mVerifyDeviceLock =
- registerForActivityResult(new ActivityResultContracts.StartActivityForResult(),
- this::onSetDeviceNewLock);
@Override
public View onCreateView(
@@ -90,11 +83,8 @@
private View.OnClickListener onClickUse() {
return v -> {
// Simply Use default screen lock. No need to handle
- Activity activity = getActivity();
- if (activity != null) {
- activity.setResult(RESULT_OK);
- activity.finish();
- }
+ NavHostFragment.findNavController(PrivateSpaceSetLockFragment.this)
+ .navigate(R.id.action_success_fragment);
};
}
@@ -104,22 +94,17 @@
};
}
+ @SuppressLint("MissingPermission")
private void createPrivateSpaceLock() {
- final Intent intent = new Intent(ACTION_SET_NEW_PASSWORD);
- intent.putExtra(EXTRA_PASSWORD_COMPLEXITY, PASSWORD_COMPLEXITY_LOW);
- mVerifyDeviceLock.launch(intent);
- }
-
- private void onSetDeviceNewLock(@Nullable ActivityResult result) {
- // TODO(b/307281644) : Verify this for biometrics and check result code after new
- // Authentication changes are merged.
- if (result != null) {
- Activity profileContextHelperActivity = getActivity();
- if (profileContextHelperActivity != null && profileContextHelperActivity
- .getSystemService(KeyguardManager.class).isDeviceSecure()) {
- profileContextHelperActivity.setResult(RESULT_OK);
- profileContextHelperActivity.finish();
- }
+ PrivateSpaceMaintainer privateSpaceMaintainer = PrivateSpaceMaintainer
+ .getInstance(getActivity());
+ UserHandle userHandle;
+ if (privateSpaceMaintainer.doesPrivateSpaceExist() && (userHandle =
+ privateSpaceMaintainer.getPrivateProfileHandle()) != null) {
+ Intent intent = new Intent(getContext(), PrivateProfileContextHelperActivity.class);
+ intent.putExtra(EXTRA_ACTION_TYPE, SET_LOCK_ACTION);
+ getActivity().startActivityForResultAsUser(intent, SET_LOCK_ACTION,
+ userHandle);
}
}
}
diff --git a/src/com/android/settings/privatespace/PrivateSpaceSetupActivity.java b/src/com/android/settings/privatespace/PrivateSpaceSetupActivity.java
index 3a58e9e..a5628c8 100644
--- a/src/com/android/settings/privatespace/PrivateSpaceSetupActivity.java
+++ b/src/com/android/settings/privatespace/PrivateSpaceSetupActivity.java
@@ -31,6 +31,8 @@
/** Activity class that helps in setting up of private space */
public class PrivateSpaceSetupActivity extends FragmentActivity {
public static final int SET_LOCK_ACTION = 1;
+ public static final int ACCOUNT_LOGIN_ACTION = 2;
+ public static final String EXTRA_ACTION_TYPE = "action_type";
private NavHostFragment mNavHostFragment;
@Override
protected void onCreate(Bundle savedInstanceState) {
@@ -46,7 +48,13 @@
@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
if (requestCode == SET_LOCK_ACTION && resultCode == RESULT_OK) {
- mNavHostFragment.getNavController().navigate(R.id.action_advance_to_success);
+ mNavHostFragment.getNavController().navigate(R.id.action_success_fragment);
+ } else if (requestCode == ACCOUNT_LOGIN_ACTION) {
+ if (resultCode == RESULT_OK) {
+ mNavHostFragment.getNavController().navigate(R.id.action_set_lock_fragment);
+ } else {
+ mNavHostFragment.getNavController().navigate(R.id.action_advance_login_error);
+ }
}
super.onActivityResult(requestCode, resultCode, data);
}
diff --git a/src/com/android/settings/privatespace/SetupSuccessFragment.java b/src/com/android/settings/privatespace/SetupSuccessFragment.java
index a8ca3f1..b761da7 100644
--- a/src/com/android/settings/privatespace/SetupSuccessFragment.java
+++ b/src/com/android/settings/privatespace/SetupSuccessFragment.java
@@ -70,7 +70,7 @@
private View.OnClickListener onClickNext() {
return v -> {
accessPrivateSpaceToast();
- // TODO: Replace with the intent to launch PS/PS Launch Settings
+ // TODO(b/306228087): Replace with the intent to launch All Apps once it is working.
Intent startMain = new Intent(Intent.ACTION_MAIN);
startMain.addCategory(Intent.CATEGORY_HOME);
startActivity(startMain);
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..6b96460 100644
--- a/src/com/android/settings/spa/SettingsSpaEnvironment.kt
+++ b/src/com/android/settings/spa/SettingsSpaEnvironment.kt
@@ -30,12 +30,15 @@
import com.android.settings.spa.app.specialaccess.AllFilesAccessAppListProvider
import com.android.settings.spa.app.specialaccess.DisplayOverOtherAppsAppListProvider
import com.android.settings.spa.app.specialaccess.InstallUnknownAppsListProvider
+import com.android.settings.spa.app.specialaccess.LongBackgroundTasksAppListProvider
import com.android.settings.spa.app.specialaccess.MediaManagementAppsAppListProvider
import com.android.settings.spa.app.specialaccess.ModifySystemSettingsAppListProvider
import com.android.settings.spa.app.specialaccess.NfcTagAppsSettingsProvider
import com.android.settings.spa.app.specialaccess.PictureInPictureListProvider
import com.android.settings.spa.app.specialaccess.SpecialAppAccessPageProvider
+import com.android.settings.spa.app.specialaccess.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 +69,11 @@
PictureInPictureListProvider,
InstallUnknownAppsListProvider,
AlarmsAndRemindersAppListProvider,
+ VoiceActivationAppsListProvider,
WifiControlAppListProvider,
NfcTagAppsSettingsProvider,
+ LongBackgroundTasksAppListProvider,
+ TurnScreenOnAppsAppListProvider,
)
}
diff --git a/src/com/android/settings/spa/about/AboutPhone.kt b/src/com/android/settings/spa/about/AboutPhone.kt
index 7343da0..5f9aa97 100644
--- a/src/com/android/settings/spa/about/AboutPhone.kt
+++ b/src/com/android/settings/spa/about/AboutPhone.kt
@@ -29,7 +29,6 @@
import com.android.settingslib.spa.framework.common.SpaEnvironmentFactory
import com.android.settingslib.spa.framework.common.createSettingsPage
import com.android.settingslib.spa.framework.compose.navigator
-import com.android.settingslib.spa.framework.compose.toState
import com.android.settingslib.spa.widget.preference.Preference
import com.android.settingslib.spa.widget.preference.PreferenceModel
import com.android.settingslib.spa.widget.scaffold.RegularScaffold
@@ -56,7 +55,7 @@
val deviceNamePresenter = remember { DeviceNamePresenter(context) }
Preference(object : PreferenceModel {
override val title = stringResource(R.string.about_settings)
- override val summary = deviceNamePresenter.deviceName.toState()
+ override val summary = { deviceNamePresenter.deviceName }
override val onClick = navigator(name)
override val icon = @Composable {
SettingsIcon(imageVector = Icons.Outlined.PermDeviceInformation)
diff --git a/src/com/android/settings/spa/about/DeviceName.kt b/src/com/android/settings/spa/about/DeviceName.kt
index c481e32..86a9512 100644
--- a/src/com/android/settings/spa/about/DeviceName.kt
+++ b/src/com/android/settings/spa/about/DeviceName.kt
@@ -24,7 +24,6 @@
import androidx.compose.ui.res.stringResource
import com.android.settings.R
import com.android.settings.deviceinfo.DeviceNamePreferenceController
-import com.android.settingslib.spa.framework.compose.toState
import com.android.settingslib.spa.widget.dialog.AlertDialogButton
import com.android.settingslib.spa.widget.dialog.rememberAlertDialogPresenter
import com.android.settingslib.spa.widget.preference.Preference
@@ -48,7 +47,7 @@
Preference(object : PreferenceModel {
override val title =
stringResource(R.string.my_device_info_device_name_preference_title)
- override val summary = deviceNamePresenter.deviceName.toState()
+ override val summary = { deviceNamePresenter.deviceName }
override val onClick = dialogPresenter::open
})
diff --git a/src/com/android/settings/spa/app/AllAppList.kt b/src/com/android/settings/spa/app/AllAppList.kt
index 383a0e8..5b13211 100644
--- a/src/com/android/settings/spa/app/AllAppList.kt
+++ b/src/com/android/settings/spa/app/AllAppList.kt
@@ -21,8 +21,6 @@
import android.os.Bundle
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
-import androidx.compose.runtime.derivedStateOf
-import androidx.compose.runtime.remember
import androidx.compose.ui.res.stringResource
import com.android.settings.R
import com.android.settings.spa.app.appinfo.AppInfoSettingsProvider
@@ -116,7 +114,7 @@
option: Int,
recordListFlow: Flow<List<AppRecordWithSize>>,
): Flow<List<AppRecordWithSize>> = recordListFlow.filterItem(
- when (SpinnerItem.values().getOrNull(option)) {
+ when (SpinnerItem.entries.getOrNull(option)) {
SpinnerItem.Enabled -> ({ it.app.enabled && !it.app.isInstantApp })
SpinnerItem.Disabled -> isDisabled
SpinnerItem.Instant -> isInstant
@@ -130,22 +128,20 @@
private val isInstant: (AppRecordWithSize) -> Boolean = { it.app.isInstantApp }
@Composable
- override fun getSummary(option: Int, record: AppRecordWithSize): State<String> {
+ override fun getSummary(option: Int, record: AppRecordWithSize): () -> String {
val storageSummary = record.app.getStorageSummary()
- return remember {
- derivedStateOf {
- storageSummary.value +
- when {
- !record.app.installed && !record.app.isArchived -> {
- System.lineSeparator() + context.getString(R.string.not_installed)
- }
- isDisabled(record) -> {
- System.lineSeparator() +
- context.getString(com.android.settingslib.R.string.disabled)
- }
- else -> ""
- }
+ return {
+ val summaryList = mutableListOf(storageSummary.value)
+ when {
+ !record.app.installed && !record.app.isArchived -> {
+ summaryList += context.getString(R.string.not_installed)
+ }
+
+ isDisabled(record) -> {
+ summaryList += context.getString(com.android.settingslib.R.string.disabled)
+ }
}
+ summaryList.joinToString(separator = System.lineSeparator())
}
}
diff --git a/src/com/android/settings/spa/app/AppsMain.kt b/src/com/android/settings/spa/app/AppsMain.kt
index 2dea9c5..83b3080 100644
--- a/src/com/android/settings/spa/app/AppsMain.kt
+++ b/src/com/android/settings/spa/app/AppsMain.kt
@@ -30,7 +30,6 @@
import com.android.settingslib.spa.framework.common.SpaEnvironmentFactory
import com.android.settingslib.spa.framework.common.createSettingsPage
import com.android.settingslib.spa.framework.compose.navigator
-import com.android.settingslib.spa.framework.compose.toState
import com.android.settingslib.spa.widget.preference.Preference
import com.android.settingslib.spa.widget.preference.PreferenceModel
import com.android.settingslib.spa.widget.scaffold.RegularScaffold
@@ -54,10 +53,10 @@
fun buildInjectEntry() =
SettingsEntryBuilder.createInject(owner = owner)
.setUiLayoutFn {
+ val summary = stringResource(R.string.app_and_notification_dashboard_summary)
Preference(object : PreferenceModel {
override val title = stringResource(R.string.apps_dashboard_title)
- override val summary =
- stringResource(R.string.app_and_notification_dashboard_summary).toState()
+ override val summary = { summary }
override val onClick = navigator(name)
override val icon = @Composable {
SettingsIcon(imageVector = Icons.Outlined.Apps)
diff --git a/src/com/android/settings/spa/app/appcompat/UserAspectRatioAppPreference.kt b/src/com/android/settings/spa/app/appcompat/UserAspectRatioAppPreference.kt
index 61098e8..96884be 100644
--- a/src/com/android/settings/spa/app/appcompat/UserAspectRatioAppPreference.kt
+++ b/src/com/android/settings/spa/app/appcompat/UserAspectRatioAppPreference.kt
@@ -19,6 +19,7 @@
import android.content.Context
import android.content.pm.ApplicationInfo
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
@@ -40,11 +41,12 @@
val presenter = remember { UserAspectRatioAppPresenter(context, app) }
if (!presenter.isAvailableFlow.collectAsStateWithLifecycle(initialValue = false).value) return
+ val summary by presenter.summaryFlow.collectAsStateWithLifecycle(
+ initialValue = stringResource(R.string.summary_placeholder),
+ )
Preference(object : PreferenceModel {
override val title = stringResource(R.string.aspect_ratio_experimental_title)
- override val summary = presenter.summaryFlow.collectAsStateWithLifecycle(
- initialValue = stringResource(R.string.summary_placeholder),
- )
+ override val summary = { summary }
override val onClick = presenter::startActivity
})
}
@@ -75,4 +77,4 @@
context,
AppInfoSettingsProvider.METRICS_CATEGORY,
)
-}
\ No newline at end of file
+}
diff --git a/src/com/android/settings/spa/app/appcompat/UserAspectRatioAppsPageProvider.kt b/src/com/android/settings/spa/app/appcompat/UserAspectRatioAppsPageProvider.kt
index deea745..5af29ef 100644
--- a/src/com/android/settings/spa/app/appcompat/UserAspectRatioAppsPageProvider.kt
+++ b/src/com/android/settings/spa/app/appcompat/UserAspectRatioAppsPageProvider.kt
@@ -28,7 +28,7 @@
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.State
+import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
@@ -41,15 +41,14 @@
import com.android.settingslib.spa.framework.common.createSettingsPage
import com.android.settingslib.spa.framework.compose.navigator
import com.android.settingslib.spa.framework.compose.rememberContext
-import com.android.settingslib.spa.framework.compose.toState
import com.android.settingslib.spa.framework.theme.SettingsDimension
import com.android.settingslib.spa.framework.util.asyncMap
import com.android.settingslib.spa.framework.util.filterItem
-import com.android.settingslib.spa.widget.preference.Preference
-import com.android.settingslib.spa.widget.preference.PreferenceModel
import com.android.settingslib.spa.widget.illustration.Illustration
import com.android.settingslib.spa.widget.illustration.IllustrationModel
import com.android.settingslib.spa.widget.illustration.ResourceType
+import com.android.settingslib.spa.widget.preference.Preference
+import com.android.settingslib.spa.widget.preference.PreferenceModel
import com.android.settingslib.spa.widget.ui.SettingsBody
import com.android.settingslib.spa.widget.ui.SpinnerOption
import com.android.settingslib.spaprivileged.model.app.AppListModel
@@ -80,12 +79,14 @@
@Composable
@VisibleForTesting
- fun EntryItem() =
+ fun EntryItem() {
+ val summary = getSummary()
Preference(object : PreferenceModel {
override val title = stringResource(R.string.aspect_ratio_experimental_title)
- override val summary = getSummary().toState()
+ override val summary = { summary }
override val onClick = navigator(name)
})
+ }
@VisibleForTesting
fun buildInjectEntry() = SettingsEntryBuilder
@@ -177,7 +178,7 @@
option: Int,
recordListFlow: Flow<List<UserAspectRatioAppListItemModel>>
): Flow<List<UserAspectRatioAppListItemModel>> = recordListFlow.filterItem(
- when (SpinnerItem.values().getOrNull(option)) {
+ when (SpinnerItem.entries.getOrNull(option)) {
SpinnerItem.Suggested -> ({ it.canDisplay && it.suggested })
SpinnerItem.Overridden -> ({ it.userOverride != USER_MIN_ASPECT_RATIO_UNSET })
else -> ({ it.canDisplay })
@@ -185,13 +186,15 @@
)
@Composable
- override fun getSummary(option: Int, record: UserAspectRatioAppListItemModel) : State<String> =
- remember(record.userOverride) {
+ override fun getSummary(option: Int, record: UserAspectRatioAppListItemModel): () -> String {
+ val summary by remember(record.userOverride) {
flow {
emit(userAspectRatioManager.getUserMinAspectRatioEntry(record.userOverride,
record.app.packageName))
}.flowOn(Dispatchers.IO)
}.collectAsStateWithLifecycle(initialValue = stringResource(R.string.summary_placeholder))
+ return { summary }
+ }
private fun getPackageAndActivityInfo(app: ApplicationInfo): PackageInfo? = try {
packageManager.getPackageInfoAsUser(app.packageName, GET_ACTIVITIES_FLAGS, app.userId)
diff --git a/src/com/android/settings/spa/app/appinfo/AppAllServicesPreference.kt b/src/com/android/settings/spa/app/appinfo/AppAllServicesPreference.kt
index 34272d4..31e068c 100644
--- a/src/com/android/settings/spa/app/appinfo/AppAllServicesPreference.kt
+++ b/src/com/android/settings/spa/app/appinfo/AppAllServicesPreference.kt
@@ -24,6 +24,7 @@
import android.os.Bundle
import android.util.Log
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.platform.LocalContext
@@ -52,11 +53,12 @@
val presenter = remember { AppAllServicesPresenter(context, app, coroutineScope) }
if (!presenter.isAvailableFlow.collectAsStateWithLifecycle(initialValue = false).value) return
+ val summary by presenter.summaryFlow.collectAsStateWithLifecycle(
+ initialValue = stringResource(R.string.summary_placeholder),
+ )
Preference(object : PreferenceModel {
override val title = stringResource(R.string.app_info_all_services_label)
- override val summary = presenter.summaryFlow.collectAsStateWithLifecycle(
- initialValue = stringResource(R.string.summary_placeholder),
- )
+ override val summary = { summary }
override val onClick = presenter::startActivity
})
}
diff --git a/src/com/android/settings/spa/app/appinfo/AppBatteryPreference.kt b/src/com/android/settings/spa/app/appinfo/AppBatteryPreference.kt
index 7dd78a9..c707b44b 100644
--- a/src/com/android/settings/spa/app/appinfo/AppBatteryPreference.kt
+++ b/src/com/android/settings/spa/app/appinfo/AppBatteryPreference.kt
@@ -21,7 +21,6 @@
import android.util.Log
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@@ -91,16 +90,17 @@
}
}
- val enabled = derivedStateOf { batteryDiffEntryState is LoadingState.Done }
+ val enabled = { batteryDiffEntryState is LoadingState.Done }
- val summary = derivedStateOf<String> {
- if (!app.installed) return@derivedStateOf ""
- batteryDiffEntryState.let { batteryDiffEntryState ->
- when (batteryDiffEntryState) {
- is LoadingState.Loading -> context.getString(R.string.summary_placeholder)
- is LoadingState.Done -> batteryDiffEntryState.result.getSummary()
+ val summary = {
+ if (app.installed) {
+ batteryDiffEntryState.let { batteryDiffEntryState ->
+ when (batteryDiffEntryState) {
+ is LoadingState.Loading -> context.getString(R.string.summary_placeholder)
+ is LoadingState.Done -> batteryDiffEntryState.result.getSummary()
+ }
}
- }
+ } else ""
}
private fun BatteryDiffEntry?.getSummary(): String =
@@ -155,7 +155,7 @@
}
private sealed class LoadingState<out T> {
- object Loading : LoadingState<Nothing>()
+ data object Loading : LoadingState<Nothing>()
data class Done<T>(val result: T) : LoadingState<T>()
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/AppDataUsagePreference.kt b/src/com/android/settings/spa/app/appinfo/AppDataUsagePreference.kt
index ceb3986..057f911 100644
--- a/src/com/android/settings/spa/app/appinfo/AppDataUsagePreference.kt
+++ b/src/com/android/settings/spa/app/appinfo/AppDataUsagePreference.kt
@@ -20,6 +20,7 @@
import android.content.pm.ApplicationInfo
import android.net.NetworkTemplate
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.platform.LocalContext
@@ -34,7 +35,6 @@
import com.android.settings.datausage.lib.INetworkTemplates
import com.android.settings.datausage.lib.NetworkTemplates
import com.android.settings.datausage.lib.NetworkTemplates.getTitleResId
-import com.android.settingslib.spa.framework.compose.toState
import com.android.settingslib.spa.widget.preference.Preference
import com.android.settingslib.spa.widget.preference.PreferenceModel
import com.android.settingslib.spaprivileged.model.app.hasFlag
@@ -64,16 +64,17 @@
}
if (!presenter.isAvailableFlow.collectAsStateWithLifecycle(initialValue = false).value) return
+ val summary by presenter.summaryFlow.collectAsStateWithLifecycle(
+ initialValue = stringResource(R.string.computing_size),
+ )
Preference(object : PreferenceModel {
override val title = stringResource(
presenter.titleResIdFlow.collectAsStateWithLifecycle(
initialValue = R.string.summary_placeholder,
).value
)
- override val summary = presenter.summaryFlow.collectAsStateWithLifecycle(
- initialValue = stringResource(R.string.computing_size),
- )
- override val enabled = presenter.isEnabled().toState()
+ override val summary = { summary }
+ override val enabled = { presenter.isEnabled() }
override val onClick = presenter::startActivity
})
}
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/AppInstallerInfoPreference.kt b/src/com/android/settings/spa/app/appinfo/AppInstallerInfoPreference.kt
index 5a348f7..62e714a 100644
--- a/src/com/android/settings/spa/app/appinfo/AppInstallerInfoPreference.kt
+++ b/src/com/android/settings/spa/app/appinfo/AppInstallerInfoPreference.kt
@@ -19,15 +19,16 @@
import android.content.Context
import android.content.pm.ApplicationInfo
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.android.settings.R
import com.android.settings.Utils
import com.android.settings.applications.AppStoreUtil
import com.android.settingslib.applications.AppUtils
-import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.android.settingslib.spa.widget.preference.Preference
import com.android.settingslib.spa.widget.preference.PreferenceModel
import com.android.settingslib.spaprivileged.framework.common.asUser
@@ -49,13 +50,14 @@
val presenter = remember { AppInstallerInfoPresenter(context, app, coroutineScope) }
if (!presenter.isAvailableFlow.collectAsStateWithLifecycle(initialValue = false).value) return
+ val summary by presenter.summaryFlow.collectAsStateWithLifecycle(
+ initialValue = stringResource(R.string.summary_placeholder),
+ )
+ val enabled by presenter.enabledFlow.collectAsStateWithLifecycle(initialValue = false)
Preference(object : PreferenceModel {
override val title = stringResource(R.string.app_install_details_title)
- override val summary = presenter.summaryFlow.collectAsStateWithLifecycle(
- initialValue = stringResource(R.string.summary_placeholder),
- )
- override val enabled =
- presenter.enabledFlow.collectAsStateWithLifecycle(initialValue = false)
+ override val summary = { summary }
+ override val enabled = { enabled }
override val onClick = presenter::startActivity
})
}
diff --git a/src/com/android/settings/spa/app/appinfo/AppLocalePreference.kt b/src/com/android/settings/spa/app/appinfo/AppLocalePreference.kt
index 2d6fbb6..3b2aace 100644
--- a/src/com/android/settings/spa/app/appinfo/AppLocalePreference.kt
+++ b/src/com/android/settings/spa/app/appinfo/AppLocalePreference.kt
@@ -23,15 +23,16 @@
import android.content.pm.PackageManager.ResolveInfoFlags
import android.net.Uri
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.android.settings.R
import com.android.settings.applications.AppInfoBase
import com.android.settings.applications.AppLocaleUtil
import com.android.settings.applications.appinfo.AppLocaleDetails
import com.android.settings.localepicker.AppLocalePickerActivity
-import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.android.settingslib.spa.widget.preference.Preference
import com.android.settingslib.spa.widget.preference.PreferenceModel
import com.android.settingslib.spaprivileged.model.app.userHandle
@@ -46,11 +47,12 @@
val presenter = remember { AppLocalePresenter(context, app) }
if (!presenter.isAvailableFlow.collectAsStateWithLifecycle(initialValue = false).value) return
+ val summary by presenter.summaryFlow.collectAsStateWithLifecycle(
+ initialValue = stringResource(R.string.summary_placeholder),
+ )
Preference(object : PreferenceModel {
override val title = stringResource(R.string.app_locale_preference_title)
- override val summary = presenter.summaryFlow.collectAsStateWithLifecycle(
- initialValue = stringResource(R.string.summary_placeholder),
- )
+ override val summary = { summary }
override val onClick = presenter::startActivity
})
}
diff --git a/src/com/android/settings/spa/app/appinfo/AppNotificationPreference.kt b/src/com/android/settings/spa/app/appinfo/AppNotificationPreference.kt
index 45033e7..28527c1 100644
--- a/src/com/android/settings/spa/app/appinfo/AppNotificationPreference.kt
+++ b/src/com/android/settings/spa/app/appinfo/AppNotificationPreference.kt
@@ -19,6 +19,7 @@
import android.content.Context
import android.content.pm.ApplicationInfo
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
@@ -29,7 +30,6 @@
import com.android.settings.spa.notification.AppNotificationRepository
import com.android.settings.spa.notification.IAppNotificationRepository
import com.android.settingslib.spa.framework.compose.rememberContext
-import com.android.settingslib.spa.framework.compose.stateOf
import com.android.settingslib.spa.widget.preference.Preference
import com.android.settingslib.spa.widget.preference.PreferenceModel
import com.android.settingslib.spaprivileged.model.app.installed
@@ -43,17 +43,17 @@
repository: IAppNotificationRepository = rememberContext(::AppNotificationRepository),
) {
val context = LocalContext.current
- val summaryFlow = remember(app) {
+ val summary by remember(app) {
flow {
emit(repository.getNotificationSummary(app))
- }.flowOn(Dispatchers.IO)
- }
+ }.flowOn(Dispatchers.Default)
+ }.collectAsStateWithLifecycle(
+ initialValue = stringResource(R.string.summary_placeholder)
+ )
Preference(object : PreferenceModel {
override val title = stringResource(R.string.notifications_label)
- override val summary = summaryFlow.collectAsStateWithLifecycle(
- initialValue = stringResource(R.string.summary_placeholder)
- )
- override val enabled = stateOf(app.installed)
+ override val summary = { summary }
+ override val enabled = { app.installed }
override val onClick = { navigateToAppNotificationSettings(context, app) }
})
}
@@ -65,4 +65,4 @@
context,
AppInfoSettingsProvider.METRICS_CATEGORY,
)
-}
\ No newline at end of file
+}
diff --git a/src/com/android/settings/spa/app/appinfo/AppOpenByDefaultPreference.kt b/src/com/android/settings/spa/app/appinfo/AppOpenByDefaultPreference.kt
index 757ddc2..aae9569 100644
--- a/src/com/android/settings/spa/app/appinfo/AppOpenByDefaultPreference.kt
+++ b/src/com/android/settings/spa/app/appinfo/AppOpenByDefaultPreference.kt
@@ -19,6 +19,7 @@
import android.content.Context
import android.content.pm.ApplicationInfo
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
@@ -28,7 +29,6 @@
import com.android.settings.applications.intentpicker.AppLaunchSettings
import com.android.settings.applications.intentpicker.IntentPickerUtils
import com.android.settingslib.applications.AppUtils
-import com.android.settingslib.spa.framework.compose.stateOf
import com.android.settingslib.spa.widget.preference.Preference
import com.android.settingslib.spa.widget.preference.PreferenceModel
import com.android.settingslib.spaprivileged.framework.common.asUser
@@ -46,12 +46,13 @@
val presenter = remember(app) { AppOpenByDefaultPresenter(context, app) }
if (remember(presenter) { !presenter.isAvailable() }) return
+ val summary by presenter.summaryFlow.collectAsStateWithLifecycle(
+ initialValue = stringResource(R.string.summary_placeholder),
+ )
Preference(object : PreferenceModel {
override val title = stringResource(R.string.launch_by_default)
- override val summary = presenter.summaryFlow.collectAsStateWithLifecycle(
- initialValue = stringResource(R.string.summary_placeholder),
- )
- override val enabled = stateOf(presenter.isEnabled())
+ override val summary = { summary }
+ override val enabled = { presenter.isEnabled() }
override val onClick = presenter::startActivity
})
}
diff --git a/src/com/android/settings/spa/app/appinfo/AppPermissionPreference.kt b/src/com/android/settings/spa/app/appinfo/AppPermissionPreference.kt
index ad666dc..ec1780f 100644
--- a/src/com/android/settings/spa/app/appinfo/AppPermissionPreference.kt
+++ b/src/com/android/settings/spa/app/appinfo/AppPermissionPreference.kt
@@ -22,7 +22,6 @@
import android.content.pm.ApplicationInfo
import android.util.Log
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext
@@ -52,8 +51,8 @@
model = remember {
object : PreferenceModel {
override val title = context.getString(R.string.permissions_label)
- override val summary = derivedStateOf { summaryState.value.summary }
- override val enabled = derivedStateOf { summaryState.value.enabled }
+ override val summary = { summaryState.value.summary }
+ override val enabled = { summaryState.value.enabled }
override val onClick = { startManagePermissionsActivity(context, app) }
}
},
diff --git a/src/com/android/settings/spa/app/appinfo/AppStoragePreference.kt b/src/com/android/settings/spa/app/appinfo/AppStoragePreference.kt
index e8b1018..2b96454 100644
--- a/src/com/android/settings/spa/app/appinfo/AppStoragePreference.kt
+++ b/src/com/android/settings/spa/app/appinfo/AppStoragePreference.kt
@@ -19,9 +19,6 @@
import android.content.Context
import android.content.pm.ApplicationInfo
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.State
-import androidx.compose.runtime.derivedStateOf
-import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import com.android.settings.R
@@ -47,12 +44,13 @@
}
@Composable
-private fun getSummary(context: Context, app: ApplicationInfo): State<String> {
+private fun getSummary(context: Context, app: ApplicationInfo): () -> String {
val sizeState = app.getStorageSize()
- return remember {
- derivedStateOf {
- val size = sizeState.value
- if (size.isBlank()) return@derivedStateOf context.getString(R.string.computing_size)
+ return {
+ val size = sizeState.value
+ if (size.isBlank()) {
+ context.getString(R.string.computing_size)
+ } else {
val storageType = context.getString(
when (app.hasFlag(ApplicationInfo.FLAG_EXTERNAL_STORAGE)) {
true -> R.string.storage_type_external
diff --git a/src/com/android/settings/spa/app/appinfo/AppTimeSpentPreference.kt b/src/com/android/settings/spa/app/appinfo/AppTimeSpentPreference.kt
index 21b3d73..7ba61dc 100644
--- a/src/com/android/settings/spa/app/appinfo/AppTimeSpentPreference.kt
+++ b/src/com/android/settings/spa/app/appinfo/AppTimeSpentPreference.kt
@@ -22,6 +22,7 @@
import android.content.pm.PackageManager.ResolveInfoFlags
import android.provider.Settings
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext
@@ -29,7 +30,6 @@
import androidx.lifecycle.liveData
import com.android.settings.R
import com.android.settings.overlay.FeatureFactory.Companion.featureFactory
-import com.android.settingslib.spa.framework.compose.stateOf
import com.android.settingslib.spa.widget.preference.Preference
import com.android.settingslib.spa.widget.preference.PreferenceModel
import com.android.settingslib.spaprivileged.model.app.hasFlag
@@ -43,12 +43,13 @@
val presenter = remember { AppTimeSpentPresenter(context, app) }
if (!presenter.isAvailable()) return
+ val summary by presenter.summaryLiveData.observeAsState(
+ initial = stringResource(R.string.summary_placeholder),
+ )
Preference(object : PreferenceModel {
override val title = stringResource(R.string.time_spent_in_app_pref_title)
- override val summary = presenter.summaryLiveData.observeAsState(
- initial = stringResource(R.string.summary_placeholder),
- )
- override val enabled = stateOf(presenter.isEnabled())
+ override val summary = { summary }
+ override val enabled = { presenter.isEnabled() }
override val onClick = presenter::startActivity
})
}
diff --git a/src/com/android/settings/spa/app/appinfo/DefaultAppShortcutPreference.kt b/src/com/android/settings/spa/app/appinfo/DefaultAppShortcutPreference.kt
index 74c0aa4..51f6845 100644
--- a/src/com/android/settings/spa/app/appinfo/DefaultAppShortcutPreference.kt
+++ b/src/com/android/settings/spa/app/appinfo/DefaultAppShortcutPreference.kt
@@ -22,6 +22,7 @@
import android.content.pm.ApplicationInfo
import androidx.annotation.StringRes
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext
@@ -57,11 +58,12 @@
if (remember(presenter) { !presenter.isAvailable() }) return
if (presenter.isVisible().observeAsState().value != true) return
+ val summary by presenter.summaryFlow.collectAsStateWithLifecycle(
+ initialValue = stringResource(R.string.summary_placeholder),
+ )
Preference(object : PreferenceModel {
override val title = stringResource(shortcut.titleResId)
- override val summary = presenter.summaryFlow.collectAsStateWithLifecycle(
- initialValue = stringResource(R.string.summary_placeholder),
- )
+ override val summary = { summary }
override val onClick = presenter::startActivity
})
}
diff --git a/src/com/android/settings/spa/app/appinfo/HibernationSwitchPreference.kt b/src/com/android/settings/spa/app/appinfo/HibernationSwitchPreference.kt
index f62a3be..78ca15b 100644
--- a/src/com/android/settings/spa/app/appinfo/HibernationSwitchPreference.kt
+++ b/src/com/android/settings/spa/app/appinfo/HibernationSwitchPreference.kt
@@ -28,15 +28,14 @@
import android.provider.DeviceConfig
import android.provider.DeviceConfig.NAMESPACE_APP_HIBERNATION
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.android.settings.R
import com.android.settings.Utils.PROPERTY_APP_HIBERNATION_ENABLED
import com.android.settings.Utils.PROPERTY_HIBERNATION_TARGETS_PRE_S_APPS
import com.android.settingslib.spa.framework.compose.OverridableFlow
-import androidx.lifecycle.compose.collectAsStateWithLifecycle
-import com.android.settingslib.spa.framework.compose.stateOf
import com.android.settingslib.spa.widget.preference.SwitchPreference
import com.android.settingslib.spa.widget.preference.SwitchPreferenceModel
import com.android.settingslib.spaprivileged.framework.common.appHibernationManager
@@ -44,12 +43,12 @@
import com.android.settingslib.spaprivileged.framework.common.asUser
import com.android.settingslib.spaprivileged.framework.common.permissionControllerManager
import com.android.settingslib.spaprivileged.model.app.userHandle
+import kotlin.coroutines.resume
+import kotlin.coroutines.suspendCoroutine
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.asExecutor
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.withContext
-import kotlin.coroutines.resume
-import kotlin.coroutines.suspendCoroutine
@Composable
fun HibernationSwitchPreference(app: ApplicationInfo) {
@@ -57,18 +56,14 @@
val presenter = remember { HibernationSwitchPresenter(context, app) }
if (!presenter.isAvailable()) return
- val isEligibleState = presenter.isEligibleFlow.collectAsStateWithLifecycle(initialValue = false)
+ val isEligibleState by presenter.isEligibleFlow.collectAsStateWithLifecycle(initialValue = false)
val isCheckedState = presenter.isCheckedFlow.collectAsStateWithLifecycle(initialValue = null)
SwitchPreference(remember {
object : SwitchPreferenceModel {
override val title = context.getString(R.string.unused_apps_switch)
- override val summary = stateOf(context.getString(R.string.unused_apps_switch_summary))
- override val changeable = isEligibleState
-
- override val checked = derivedStateOf {
- if (!changeable.value) false else isCheckedState.value
- }
-
+ override val summary = { context.getString(R.string.unused_apps_switch_summary) }
+ override val changeable = { isEligibleState }
+ override val checked = { if (changeable()) isCheckedState.value else false }
override val onCheckedChange = presenter::onCheckedChange
}
})
diff --git a/src/com/android/settings/spa/app/appinfo/InstantAppDomainsPreference.kt b/src/com/android/settings/spa/app/appinfo/InstantAppDomainsPreference.kt
index 7b9480d..9c3ec97 100644
--- a/src/com/android/settings/spa/app/appinfo/InstantAppDomainsPreference.kt
+++ b/src/com/android/settings/spa/app/appinfo/InstantAppDomainsPreference.kt
@@ -32,9 +32,9 @@
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.android.settings.R
import com.android.settings.Utils
-import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.android.settingslib.spa.framework.theme.SettingsDimension
import com.android.settingslib.spa.widget.preference.Preference
import com.android.settingslib.spa.widget.preference.PreferenceModel
@@ -53,11 +53,12 @@
val presenter = remember { InstantAppDomainsPresenter(context, app) }
var openDialog by rememberSaveable { mutableStateOf(false) }
+ val summary by presenter.summaryFlow.collectAsStateWithLifecycle(
+ initialValue = stringResource(R.string.summary_placeholder),
+ )
Preference(object : PreferenceModel {
override val title = stringResource(R.string.app_launch_supported_domain_urls_title)
- override val summary = presenter.summaryFlow.collectAsStateWithLifecycle(
- initialValue = stringResource(R.string.summary_placeholder),
- )
+ override val summary = { summary }
override val onClick = { openDialog = true }
})
diff --git a/src/com/android/settings/spa/app/appinfo/InteractAcrossProfilesDetailsPreference.kt b/src/com/android/settings/spa/app/appinfo/InteractAcrossProfilesDetailsPreference.kt
index 12f6907..905e057 100644
--- a/src/com/android/settings/spa/app/appinfo/InteractAcrossProfilesDetailsPreference.kt
+++ b/src/com/android/settings/spa/app/appinfo/InteractAcrossProfilesDetailsPreference.kt
@@ -19,13 +19,14 @@
import android.content.Context
import android.content.pm.ApplicationInfo
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.android.settings.R
import com.android.settings.applications.appinfo.AppInfoDashboardFragment
import com.android.settings.applications.specialaccess.interactacrossprofiles.InteractAcrossProfilesDetails
-import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.android.settingslib.spa.widget.preference.Preference
import com.android.settingslib.spa.widget.preference.PreferenceModel
import com.android.settingslib.spaprivileged.framework.common.crossProfileApps
@@ -39,11 +40,12 @@
val presenter = remember { InteractAcrossProfilesDetailsPresenter(context, app) }
if (!presenter.isAvailableFlow.collectAsStateWithLifecycle(initialValue = false).value) return
+ val summary by presenter.summaryFlow.collectAsStateWithLifecycle(
+ initialValue = stringResource(R.string.summary_placeholder),
+ )
Preference(object : PreferenceModel {
override val title = stringResource(R.string.interact_across_profiles_title)
- override val summary = presenter.summaryFlow.collectAsStateWithLifecycle(
- initialValue = stringResource(R.string.summary_placeholder),
- )
+ override val summary = { summary }
override val onClick = presenter::startActivity
})
}
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/backgroundinstall/BackgroundInstalledAppsPageProvider.kt b/src/com/android/settings/spa/app/backgroundinstall/BackgroundInstalledAppsPageProvider.kt
index 6e0643b..89f473b 100644
--- a/src/com/android/settings/spa/app/backgroundinstall/BackgroundInstalledAppsPageProvider.kt
+++ b/src/com/android/settings/spa/app/backgroundinstall/BackgroundInstalledAppsPageProvider.kt
@@ -32,6 +32,7 @@
import androidx.compose.material.icons.outlined.Delete
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
+import androidx.compose.runtime.getValue
import androidx.compose.runtime.produceState
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
@@ -88,9 +89,10 @@
@Composable
fun EntryItem() {
if(featureIsDisabled) return
+ val summary by generatePreferenceSummary()
Preference(object : PreferenceModel {
override val title = stringResource(R.string.background_install_title)
- override val summary = generatePreferenceSummary()
+ override val summary = { summary }
override val onClick = navigator(name)
})
}
diff --git a/src/com/android/settings/spa/app/specialaccess/AlarmsAndRemindersAppList.kt b/src/com/android/settings/spa/app/specialaccess/AlarmsAndRemindersAppList.kt
index c31eb7a..c990927 100644
--- a/src/com/android/settings/spa/app/specialaccess/AlarmsAndRemindersAppList.kt
+++ b/src/com/android/settings/spa/app/specialaccess/AlarmsAndRemindersAppList.kt
@@ -24,10 +24,9 @@
import android.content.pm.ApplicationInfo
import android.os.PowerExemptionManager
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.livedata.observeAsState
import com.android.settings.overlay.FeatureFactory.Companion.featureFactory
import com.android.settingslib.R
-import com.android.settingslib.spa.framework.compose.stateOf
+import com.android.settingslib.spa.livedata.observeAsCallback
import com.android.settingslib.spaprivileged.model.app.AppRecord
import com.android.settingslib.spaprivileged.model.app.IPackageManagers
import com.android.settingslib.spaprivileged.model.app.PackageManagers
@@ -79,9 +78,10 @@
}
@Composable
- override fun isAllowed(record: AlarmsAndRemindersAppRecord) =
- if (record.isTrumped) stateOf(true)
- else record.controller.isAllowed.observeAsState()
+ override fun isAllowed(record: AlarmsAndRemindersAppRecord): () -> Boolean? = when {
+ record.isTrumped -> ({ true })
+ else -> record.controller.isAllowed.observeAsCallback()
+ }
override fun isChangeable(record: AlarmsAndRemindersAppRecord) = record.isChangeable
diff --git a/src/com/android/settings/spa/app/specialaccess/InstallUnknownApps.kt b/src/com/android/settings/spa/app/specialaccess/InstallUnknownApps.kt
index c98b2ee..7f63e38 100644
--- a/src/com/android/settings/spa/app/specialaccess/InstallUnknownApps.kt
+++ b/src/com/android/settings/spa/app/specialaccess/InstallUnknownApps.kt
@@ -24,8 +24,8 @@
import android.content.pm.ApplicationInfo
import android.os.UserManager
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.livedata.observeAsState
import com.android.settings.R
+import com.android.settingslib.spa.livedata.observeAsCallback
import com.android.settingslib.spaprivileged.model.app.AppOpsController
import com.android.settingslib.spaprivileged.model.app.AppRecord
import com.android.settingslib.spaprivileged.model.app.userId
@@ -79,7 +79,7 @@
@Composable
override fun isAllowed(record: InstallUnknownAppsRecord) =
- record.appOpsController.isAllowed.observeAsState()
+ record.appOpsController.isAllowed.observeAsCallback()
override fun isChangeable(record: InstallUnknownAppsRecord) =
isChangeable(record, getPotentialPackageNames(record.app.userId))
diff --git a/src/com/android/settings/spa/app/specialaccess/LongBackgroundTasksApps.kt b/src/com/android/settings/spa/app/specialaccess/LongBackgroundTasksApps.kt
new file mode 100644
index 0000000..3ba9b08
--- /dev/null
+++ b/src/com/android/settings/spa/app/specialaccess/LongBackgroundTasksApps.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 LongBackgroundTasksAppListProvider : TogglePermissionAppListProvider {
+ override val permissionType = "LongBackgroundTasksApps"
+ override fun createModel(context: Context) = LongBackgroundTasksAppsListModel(context)
+}
+
+class LongBackgroundTasksAppsListModel(context: Context) : AppOpPermissionListModel(context) {
+ override val pageTitleResId = R.string.long_background_tasks_title
+ override val switchTitleResId = R.string.long_background_tasks_switch_title
+ override val footerResId = R.string.long_background_tasks_footer_title
+ override val appOp = AppOpsManager.OP_RUN_USER_INITIATED_JOBS
+ override val permission = Manifest.permission.RUN_USER_INITIATED_JOBS
+ 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.ACTION_LONG_BACKGROUND_TASKS_TOGGLE,
+ if (newAllowed) 1 else 0
+ )
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/settings/spa/app/specialaccess/NfcTagAppsSettings.kt b/src/com/android/settings/spa/app/specialaccess/NfcTagAppsSettings.kt
index 3dede42..f02a6a1 100644
--- a/src/com/android/settings/spa/app/specialaccess/NfcTagAppsSettings.kt
+++ b/src/com/android/settings/spa/app/specialaccess/NfcTagAppsSettings.kt
@@ -23,8 +23,8 @@
import android.nfc.NfcAdapter
import android.util.Log
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.livedata.observeAsState
import com.android.settings.R
+import com.android.settingslib.spa.livedata.observeAsCallback
import com.android.settingslib.spaprivileged.model.app.AppRecord
import com.android.settingslib.spaprivileged.model.app.userId
import com.android.settingslib.spaprivileged.template.app.TogglePermissionAppListModel
@@ -100,7 +100,7 @@
@Composable
override fun isAllowed(record: NfcTagAppsSettingsRecord) =
- record.controller.isAllowed.observeAsState()
+ record.controller.isAllowed.observeAsCallback()
override fun isChangeable(record: NfcTagAppsSettingsRecord) = true
diff --git a/src/com/android/settings/spa/app/specialaccess/PictureInPicture.kt b/src/com/android/settings/spa/app/specialaccess/PictureInPicture.kt
index 5ed3615..cd615919 100644
--- a/src/com/android/settings/spa/app/specialaccess/PictureInPicture.kt
+++ b/src/com/android/settings/spa/app/specialaccess/PictureInPicture.kt
@@ -25,8 +25,8 @@
import android.content.pm.PackageManager.PackageInfoFlags
import android.util.Log
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.livedata.observeAsState
import com.android.settings.R
+import com.android.settingslib.spa.livedata.observeAsCallback
import com.android.settingslib.spaprivileged.model.app.AppOpsController
import com.android.settingslib.spaprivileged.model.app.AppRecord
import com.android.settingslib.spaprivileged.model.app.installed
@@ -90,7 +90,7 @@
@Composable
override fun isAllowed(record: PictureInPictureRecord) =
- record.appOpsController.isAllowed.observeAsState()
+ record.appOpsController.isAllowed.observeAsCallback()
override fun isChangeable(record: PictureInPictureRecord) = record.isSupport
diff --git a/src/com/android/settings/spa/app/specialaccess/SpecialAppAccess.kt b/src/com/android/settings/spa/app/specialaccess/SpecialAppAccess.kt
index b40e32b..fb05a38 100644
--- a/src/com/android/settings/spa/app/specialaccess/SpecialAppAccess.kt
+++ b/src/com/android/settings/spa/app/specialaccess/SpecialAppAccess.kt
@@ -66,7 +66,10 @@
PictureInPictureListProvider,
InstallUnknownAppsListProvider,
AlarmsAndRemindersAppListProvider,
+ VoiceActivationAppsListProvider,
WifiControlAppListProvider,
+ LongBackgroundTasksAppListProvider,
+ 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/spa/app/storage/StorageAppList.kt b/src/com/android/settings/spa/app/storage/StorageAppList.kt
index 8fc3eb5..c33de33 100644
--- a/src/com/android/settings/spa/app/storage/StorageAppList.kt
+++ b/src/com/android/settings/spa/app/storage/StorageAppList.kt
@@ -22,7 +22,7 @@
import androidx.annotation.StringRes
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
-import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
@@ -121,13 +121,9 @@
): Flow<List<AppRecordWithSize>> = recordListFlow.filterItem { type.filter(it) }
@Composable
- override fun getSummary(option: Int, record: AppRecordWithSize): State<String> {
- val storageSummary = record.app.getStorageSummary()
- return remember {
- derivedStateOf {
- storageSummary.value
- }
- }
+ override fun getSummary(option: Int, record: AppRecordWithSize): () -> String {
+ val storageSummary by record.app.getStorageSummary()
+ return { storageSummary }
}
@Composable
diff --git a/src/com/android/settings/spa/development/UsageStatsListModel.kt b/src/com/android/settings/spa/development/UsageStatsListModel.kt
index 61c24ac..d27796d 100644
--- a/src/com/android/settings/spa/development/UsageStatsListModel.kt
+++ b/src/com/android/settings/spa/development/UsageStatsListModel.kt
@@ -22,10 +22,8 @@
import android.content.pm.ApplicationInfo
import android.text.format.DateUtils
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.State
import com.android.settings.R
import com.android.settings.spa.development.UsageStatsListModel.SpinnerItem.Companion.toSpinnerItem
-import com.android.settingslib.spa.framework.compose.stateOf
import com.android.settingslib.spa.widget.ui.SpinnerOption
import com.android.settingslib.spaprivileged.model.app.AppEntry
import com.android.settingslib.spaprivileged.model.app.AppListModel
@@ -55,7 +53,7 @@
}
override fun getSpinnerOptions(recordList: List<UsageStatsAppRecord>): List<SpinnerOption> =
- SpinnerItem.values().map {
+ SpinnerItem.entries.map {
SpinnerOption(
id = it.ordinal,
text = context.getString(it.stringResId),
@@ -77,7 +75,7 @@
}.then(super.getComparator(option))
@Composable
- override fun getSummary(option: Int, record: UsageStatsAppRecord): State<String>? {
+ override fun getSummary(option: Int, record: UsageStatsAppRecord): (() -> String)? {
val usageStats = record.usageStats ?: return null
val lastTimeUsed = DateUtils.formatSameDayTime(
usageStats.lastTimeUsed, now, DateFormat.MEDIUM, DateFormat.MEDIUM
@@ -85,7 +83,7 @@
val lastTimeUsedLine = "${context.getString(R.string.last_time_used_label)}: $lastTimeUsed"
val usageTime = DateUtils.formatElapsedTime(usageStats.totalTimeInForeground / 1000)
val usageTimeLine = "${context.getString(R.string.usage_time_label)}: $usageTime"
- return stateOf("$lastTimeUsedLine\n$usageTimeLine")
+ return { "$lastTimeUsedLine\n$usageTimeLine" }
}
private fun getUsageStats(): Map<String, UsageStats> {
@@ -101,7 +99,7 @@
AppName(R.string.usage_stats_sort_by_app_name);
companion object {
- fun Int.toSpinnerItem(): SpinnerItem = values()[this]
+ fun Int.toSpinnerItem(): SpinnerItem = entries[this]
}
}
}
diff --git a/src/com/android/settings/spa/development/compat/PlatformCompatAppListModel.kt b/src/com/android/settings/spa/development/compat/PlatformCompatAppListModel.kt
index c6752b9..8f53698 100644
--- a/src/com/android/settings/spa/development/compat/PlatformCompatAppListModel.kt
+++ b/src/com/android/settings/spa/development/compat/PlatformCompatAppListModel.kt
@@ -24,7 +24,6 @@
import androidx.core.os.bundleOf
import com.android.settings.core.SubSettingLauncher
import com.android.settings.development.compat.PlatformCompatDashboard
-import com.android.settingslib.spa.framework.compose.stateOf
import com.android.settingslib.spa.framework.util.filterItem
import com.android.settingslib.spa.framework.util.mapItem
import com.android.settingslib.spaprivileged.model.app.AppListModel
@@ -53,8 +52,9 @@
}
@Composable
- override fun getSummary(option: Int, record: PlatformCompatAppRecord) =
- stateOf(record.app.packageName)
+ override fun getSummary(option: Int, record: PlatformCompatAppRecord): () -> String = {
+ record.app.packageName
+ }
@Composable
override fun AppListItemModel<PlatformCompatAppRecord>.AppItem() {
diff --git a/src/com/android/settings/spa/network/AirplaneModePreference.kt b/src/com/android/settings/spa/network/AirplaneModePreference.kt
index 462c121..27261b6 100644
--- a/src/com/android/settings/spa/network/AirplaneModePreference.kt
+++ b/src/com/android/settings/spa/network/AirplaneModePreference.kt
@@ -20,8 +20,9 @@
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.AirplanemodeActive
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.remember
+import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
+import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
@@ -37,11 +38,12 @@
val context = LocalContext.current
val controller = remember { AirplaneModeController(context) }
if (!controller.isAvailable()) return
+ val checked by controller.airplaneModeState.observeAsState(
+ initial = controller.isAirplaneModeOn()
+ )
SwitchPreference(object : SwitchPreferenceModel {
override val title = context.getString(R.string.airplane_mode)
- override val checked = controller.airplaneModeState.observeAsState(
- initial = controller.isAirplaneModeOn()
- )
+ override val checked = { checked }
override val onCheckedChange = { newChecked: Boolean ->
controller.setChecked(newChecked)
}
diff --git a/src/com/android/settings/spa/network/NetworkAndInternet.kt b/src/com/android/settings/spa/network/NetworkAndInternet.kt
index 777133e..f985237 100644
--- a/src/com/android/settings/spa/network/NetworkAndInternet.kt
+++ b/src/com/android/settings/spa/network/NetworkAndInternet.kt
@@ -34,7 +34,6 @@
import com.android.settingslib.spa.framework.common.SpaEnvironmentFactory
import com.android.settingslib.spa.framework.common.createSettingsPage
import com.android.settingslib.spa.framework.compose.navigator
-import com.android.settingslib.spa.framework.compose.toState
import com.android.settingslib.spa.widget.preference.Preference
import com.android.settingslib.spa.widget.preference.PreferenceModel
import com.android.settingslib.spa.widget.scaffold.RegularScaffold
@@ -60,9 +59,10 @@
fun buildInjectEntry(): SettingsEntryBuilder {
return SettingsEntryBuilder.createInject(owner = owner)
.setUiLayoutFn {
+ val summary = stringResource(getSummaryResId())
Preference(object : PreferenceModel {
override val title = stringResource(R.string.network_dashboard_title)
- override val summary = stringResource(getSummaryResId()).toState()
+ override val summary = { summary }
override val onClick = navigator(name)
override val icon = @Composable {
SettingsIcon(imageVector = Icons.Outlined.Wifi)
diff --git a/src/com/android/settings/spa/notification/AppListNotifications.kt b/src/com/android/settings/spa/notification/AppListNotifications.kt
index c1e5d64..00e4394 100644
--- a/src/com/android/settings/spa/notification/AppListNotifications.kt
+++ b/src/com/android/settings/spa/notification/AppListNotifications.kt
@@ -23,7 +23,6 @@
import com.android.settingslib.spa.framework.common.SettingsPageProvider
import com.android.settingslib.spa.framework.compose.navigator
import com.android.settingslib.spa.framework.compose.rememberContext
-import com.android.settingslib.spa.framework.compose.toState
import com.android.settingslib.spa.widget.preference.Preference
import com.android.settingslib.spa.widget.preference.PreferenceModel
import com.android.settingslib.spaprivileged.template.app.AppListPage
@@ -41,9 +40,10 @@
@Composable
fun EntryItem() {
+ val summary = stringResource(R.string.app_notification_field_summary)
Preference(object : PreferenceModel {
override val title = stringResource(R.string.app_notifications_title)
- override val summary = stringResource(R.string.app_notification_field_summary).toState()
+ override val summary = { summary }
override val onClick = navigator(name)
})
}
diff --git a/src/com/android/settings/spa/notification/AppNotificationsListModel.kt b/src/com/android/settings/spa/notification/AppNotificationsListModel.kt
index 0b9b676..2f3de3a 100644
--- a/src/com/android/settings/spa/notification/AppNotificationsListModel.kt
+++ b/src/com/android/settings/spa/notification/AppNotificationsListModel.kt
@@ -21,15 +21,15 @@
import android.content.pm.ApplicationInfo
import android.icu.text.RelativeDateTimeFormatter
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.livedata.observeAsState
+import androidx.compose.runtime.getValue
import androidx.compose.runtime.produceState
import com.android.settings.R
import com.android.settings.applications.AppInfoBase
import com.android.settings.notification.app.AppNotificationSettings
import com.android.settings.spa.notification.SpinnerItem.Companion.toSpinnerItem
-import com.android.settingslib.spa.framework.compose.stateOf
import com.android.settingslib.spa.framework.util.asyncFilter
import com.android.settingslib.spa.framework.util.asyncForEach
+import com.android.settingslib.spa.livedata.observeAsCallback
import com.android.settingslib.spa.widget.ui.SpinnerOption
import com.android.settingslib.spaprivileged.model.app.AppEntry
import com.android.settingslib.spaprivileged.model.app.AppListModel
@@ -37,9 +37,11 @@
import com.android.settingslib.spaprivileged.template.app.AppListItemModel
import com.android.settingslib.spaprivileged.template.app.AppListTwoTargetSwitchItem
import com.android.settingslib.utils.StringUtil
+import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.withContext
data class AppNotificationsRecord(
override val app: ApplicationInfo,
@@ -91,16 +93,17 @@
}.then(super.getComparator(option))
@Composable
- override fun getSummary(option: Int, record: AppNotificationsRecord) = record.sentState?.let {
- when (option.toSpinnerItem()) {
- SpinnerItem.MostRecent -> stateOf(formatLastSent(it.lastSent))
- SpinnerItem.MostFrequent -> stateOf(repository.calculateFrequencySummary(it.sentCount))
- else -> null
+ override fun getSummary(option: Int, record: AppNotificationsRecord): (() -> String)? =
+ record.sentState?.let {
+ when (option.toSpinnerItem()) {
+ SpinnerItem.MostRecent -> ({ formatLastSent(it.lastSent) })
+ SpinnerItem.MostFrequent -> ({ repository.calculateFrequencySummary(it.sentCount) })
+ else -> null
+ }
}
- }
override fun getSpinnerOptions(recordList: List<AppNotificationsRecord>): List<SpinnerOption> =
- SpinnerItem.values().map {
+ SpinnerItem.entries.map {
SpinnerOption(
id = it.ordinal,
text = context.getString(it.stringResId),
@@ -117,12 +120,15 @@
@Composable
override fun AppListItemModel<AppNotificationsRecord>.AppItem() {
+ val changeable by produceState(initialValue = false) {
+ withContext(Dispatchers.Default) {
+ value = repository.isChangeable(record.app)
+ }
+ }
AppListTwoTargetSwitchItem(
onClick = { navigateToAppNotificationSettings(app = record.app) },
- checked = record.controller.isEnabled.observeAsState(),
- changeable = produceState(initialValue = false) {
- value = repository.isChangeable(record.app)
- },
+ checked = record.controller.isEnabled.observeAsCallback(),
+ changeable = { changeable },
onCheckedChange = record.controller::setEnabled,
)
}
@@ -145,6 +151,6 @@
TurnedOff(R.string.filter_notif_blocked_apps);
companion object {
- fun Int.toSpinnerItem(): SpinnerItem = values()[this]
+ fun Int.toSpinnerItem(): SpinnerItem = entries[this]
}
}
diff --git a/src/com/android/settings/spa/notification/NotificationMain.kt b/src/com/android/settings/spa/notification/NotificationMain.kt
index 305f201..b3c7a55 100644
--- a/src/com/android/settings/spa/notification/NotificationMain.kt
+++ b/src/com/android/settings/spa/notification/NotificationMain.kt
@@ -25,13 +25,12 @@
import com.android.settingslib.spa.framework.common.SettingsEntryBuilder
import com.android.settingslib.spa.framework.common.SettingsPageProvider
import com.android.settingslib.spa.framework.common.SpaEnvironmentFactory
+import com.android.settingslib.spa.framework.common.createSettingsPage
import com.android.settingslib.spa.framework.compose.navigator
-import com.android.settingslib.spa.framework.compose.toState
import com.android.settingslib.spa.widget.preference.Preference
import com.android.settingslib.spa.widget.preference.PreferenceModel
import com.android.settingslib.spa.widget.scaffold.RegularScaffold
import com.android.settingslib.spa.widget.ui.SettingsIcon
-import com.android.settingslib.spa.framework.common.createSettingsPage
object NotificationMainPageProvider : SettingsPageProvider {
override val name = "NotificationMain"
@@ -53,9 +52,10 @@
fun buildInjectEntry(): SettingsEntryBuilder {
return SettingsEntryBuilder.createInject(owner = owner)
.setUiLayoutFn {
+ val summary = stringResource(R.string.notification_dashboard_summary)
Preference(object : PreferenceModel {
override val title = stringResource(R.string.configure_notification_settings)
- override val summary = stringResource(R.string.notification_dashboard_summary).toState()
+ override val summary = { summary }
override val onClick = navigator(name)
override val icon = @Composable {
SettingsIcon(imageVector = Icons.Outlined.Notifications)
diff --git a/src/com/android/settings/spa/system/AppLanguages.kt b/src/com/android/settings/spa/system/AppLanguages.kt
index b878aa7..d836a32 100644
--- a/src/com/android/settings/spa/system/AppLanguages.kt
+++ b/src/com/android/settings/spa/system/AppLanguages.kt
@@ -26,7 +26,6 @@
import com.android.settingslib.spa.framework.common.SettingsPageProvider
import com.android.settingslib.spa.framework.compose.navigator
import com.android.settingslib.spa.framework.compose.rememberContext
-import com.android.settingslib.spa.framework.compose.toState
import com.android.settingslib.spa.framework.theme.SettingsDimension
import com.android.settingslib.spa.widget.preference.Preference
import com.android.settingslib.spa.widget.preference.PreferenceModel
@@ -52,9 +51,10 @@
@Composable
fun EntryItem() {
+ val summary = stringResource(R.string.app_locale_picker_summary)
Preference(object : PreferenceModel {
override val title = stringResource(R.string.app_locales_picker_menu_title)
- override val summary = stringResource(R.string.app_locale_picker_summary).toState()
+ override val summary = { summary }
override val onClick = navigator(name)
})
}
diff --git a/src/com/android/settings/spa/system/AppLanguagesListModel.kt b/src/com/android/settings/spa/system/AppLanguagesListModel.kt
index 3413ff0..3573e25 100644
--- a/src/com/android/settings/spa/system/AppLanguagesListModel.kt
+++ b/src/com/android/settings/spa/system/AppLanguagesListModel.kt
@@ -23,7 +23,7 @@
import android.net.Uri
import android.os.UserHandle
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.State
+import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.res.stringResource
import androidx.lifecycle.compose.collectAsStateWithLifecycle
@@ -79,12 +79,14 @@
) = recordListFlow.filterItem { it.isAppLocaleSupported }
@Composable
- override fun getSummary(option: Int, record: AppLanguagesRecord): State<String> =
- remember(record.app) {
+ override fun getSummary(option: Int, record: AppLanguagesRecord): () -> String {
+ val summary by remember(record.app) {
flow {
emit(getSummary(record.app))
}.flowOn(Dispatchers.IO)
}.collectAsStateWithLifecycle(initialValue = stringResource(R.string.summary_placeholder))
+ return { summary }
+ }
private fun getSummary(app: ApplicationInfo): String =
AppLocaleDetails.getSummary(context, app).toString()
diff --git a/src/com/android/settings/spa/system/LanguageAndInputPageProvider.kt b/src/com/android/settings/spa/system/LanguageAndInputPageProvider.kt
index b5cd299..5c1038d 100644
--- a/src/com/android/settings/spa/system/LanguageAndInputPageProvider.kt
+++ b/src/com/android/settings/spa/system/LanguageAndInputPageProvider.kt
@@ -24,7 +24,6 @@
import com.android.settings.R
import com.android.settingslib.spa.framework.common.SettingsPageProvider
import com.android.settingslib.spa.framework.compose.navigator
-import com.android.settingslib.spa.framework.compose.toState
import com.android.settingslib.spa.widget.preference.Preference
import com.android.settingslib.spa.widget.preference.PreferenceModel
import com.android.settingslib.spa.widget.scaffold.RegularScaffold
@@ -42,9 +41,10 @@
@Composable
fun EntryItem() {
+ val summary = stringResource(R.string.language_settings)
Preference(object : PreferenceModel {
override val title = stringResource(R.string.language_settings)
- override val summary = stringResource(R.string.language_settings).toState()
+ override val summary = { summary }
override val onClick = navigator(name)
override val icon = @Composable {
SettingsIcon(imageVector = Icons.Outlined.Language)
diff --git a/src/com/android/settings/spa/system/SystemMain.kt b/src/com/android/settings/spa/system/SystemMain.kt
index 04ae512..c9aa8cc 100644
--- a/src/com/android/settings/spa/system/SystemMain.kt
+++ b/src/com/android/settings/spa/system/SystemMain.kt
@@ -27,7 +27,6 @@
import com.android.settingslib.spa.framework.common.SpaEnvironmentFactory
import com.android.settingslib.spa.framework.common.createSettingsPage
import com.android.settingslib.spa.framework.compose.navigator
-import com.android.settingslib.spa.framework.compose.toState
import com.android.settingslib.spa.widget.preference.Preference
import com.android.settingslib.spa.widget.preference.PreferenceModel
import com.android.settingslib.spa.widget.scaffold.RegularScaffold
@@ -53,9 +52,10 @@
fun buildInjectEntry(): SettingsEntryBuilder {
return SettingsEntryBuilder.createInject(owner = owner)
.setUiLayoutFn {
+ val summary = stringResource(R.string.system_dashboard_summary)
Preference(object : PreferenceModel {
override val title = stringResource(R.string.header_category_system)
- override val summary = stringResource(R.string.system_dashboard_summary).toState()
+ override val summary = { summary }
override val onClick = navigator(name)
override val icon = @Composable {
SettingsIcon(imageVector = Icons.Outlined.Info)
diff --git a/tests/robotests/src/com/android/settings/applications/appcompat/UserAspectRatioDetailsTest.java b/tests/robotests/src/com/android/settings/applications/appcompat/UserAspectRatioDetailsTest.java
index d98b0e7..b615163 100644
--- a/tests/robotests/src/com/android/settings/applications/appcompat/UserAspectRatioDetailsTest.java
+++ b/tests/robotests/src/com/android/settings/applications/appcompat/UserAspectRatioDetailsTest.java
@@ -21,21 +21,27 @@
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.app.IActivityManager;
+import android.app.settings.SettingsEnums;
import android.content.Context;
import android.os.RemoteException;
import androidx.test.core.app.ApplicationProvider;
+import com.android.settings.testutils.FakeFeatureFactory;
import com.android.settings.testutils.shadow.ShadowActivityManager;
+import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.mockito.InOrder;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.robolectric.RobolectricTestRunner;
@@ -56,6 +62,7 @@
private RadioWithImagePreference mRadioButtonPref;
private Context mContext;
private UserAspectRatioDetails mFragment;
+ private MetricsFeatureProvider mMetricsFeatureProvider;
@Before
public void setUp() {
@@ -67,6 +74,8 @@
when(mFragment.getAspectRatioManager()).thenReturn(mUserAspectRatioManager);
ShadowActivityManager.setService(mAm);
mRadioButtonPref = new RadioWithImagePreference(mContext);
+ final FakeFeatureFactory featureFactory = FakeFeatureFactory.setupForTest();
+ mMetricsFeatureProvider = featureFactory.metricsFeatureProvider;
}
@Test
@@ -93,4 +102,31 @@
verify(mUserAspectRatioManager).setUserMinAspectRatio(
any(), anyInt(), anyInt());
}
+
+ @Test
+ public void onRadioButtonClicked_prefChange_logMetrics() throws NullPointerException {
+ // Default was already selected
+ mRadioButtonPref.setKey(KEY_PREF_DEFAULT);
+ mFragment.onRadioButtonClicked(mRadioButtonPref);
+ // Preference changed
+ mRadioButtonPref.setKey(KEY_PREF_3_2);
+ mFragment.onRadioButtonClicked(mRadioButtonPref);
+ InOrder inOrder = inOrder(mMetricsFeatureProvider);
+ // Check the old aspect ratio value is logged as having been unselected
+ inOrder.verify(mMetricsFeatureProvider)
+ .action(
+ eq(SettingsEnums.PAGE_UNKNOWN),
+ eq(SettingsEnums.ACTION_USER_ASPECT_RATIO_APP_DEFAULT_UNSELECTED),
+ eq(SettingsEnums.USER_ASPECT_RATIO_APP_INFO_SETTINGS),
+ any(),
+ anyInt());
+ // Check the new aspect ratio value is logged as having been selected
+ inOrder.verify(mMetricsFeatureProvider)
+ .action(
+ eq(SettingsEnums.PAGE_UNKNOWN),
+ eq(SettingsEnums.ACTION_USER_ASPECT_RATIO_3_2_SELECTED),
+ eq(SettingsEnums.USER_ASPECT_RATIO_APP_INFO_SETTINGS),
+ any(),
+ anyInt());
+ }
}
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/datausage/DataUsageListTest.kt b/tests/robotests/src/com/android/settings/datausage/DataUsageListTest.kt
index 90bb048..39b8446 100644
--- a/tests/robotests/src/com/android/settings/datausage/DataUsageListTest.kt
+++ b/tests/robotests/src/com/android/settings/datausage/DataUsageListTest.kt
@@ -23,22 +23,18 @@
import android.os.UserManager
import android.provider.Settings
import androidx.preference.Preference
-import androidx.preference.PreferenceManager
import androidx.test.core.app.ApplicationProvider
import com.android.settings.datausage.DataUsageListTest.ShadowDataUsageBaseFragment
import com.android.settings.datausage.TemplatePreference.NetworkServices
import com.android.settings.datausage.lib.BillingCycleRepository
-import com.android.settings.network.MobileDataEnabledListener
import com.android.settings.testutils.FakeFeatureFactory
import com.android.settingslib.NetworkPolicyEditor
import com.android.settingslib.core.AbstractPreferenceController
-import com.android.settingslib.core.instrumentation.VisibilityLoggerMixin
import com.google.common.truth.Truth.assertThat
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
-import org.mockito.ArgumentMatchers
import org.mockito.Mock
import org.mockito.Mockito.doNothing
import org.mockito.Mockito.doReturn
@@ -62,9 +58,6 @@
val mockito: MockitoRule = MockitoJUnit.rule()
@Mock
- private lateinit var mobileDataEnabledListener: MobileDataEnabledListener
-
- @Mock
private lateinit var networkServices: NetworkServices
@Mock
@@ -86,7 +79,6 @@
fun setUp() {
FakeFeatureFactory.setupForTest()
networkServices.mPolicyEditor = mock(NetworkPolicyEditor::class.java)
- dataUsageList.dataStateListener = mobileDataEnabledListener
doReturn(context).`when`(dataUsageList).context
doReturn(userManager).`when`(context).getSystemService(UserManager::class.java)
doReturn(false).`when`(userManager).isGuestUser
@@ -113,46 +105,6 @@
}
@Test
- fun resume_shouldListenDataStateChange() {
- dataUsageList.template = mock(NetworkTemplate::class.java)
- dataUsageList.onCreate(null)
- dataUsageList.dataStateListener = mobileDataEnabledListener
- ReflectionHelpers.setField(
- dataUsageList,
- "mVisibilityLoggerMixin",
- mock(VisibilityLoggerMixin::class.java),
- )
- ReflectionHelpers.setField(
- dataUsageList,
- "mPreferenceManager",
- mock(PreferenceManager::class.java),
- )
- dataUsageList.onResume()
- verify(mobileDataEnabledListener).start(ArgumentMatchers.anyInt())
- dataUsageList.onPause()
- }
-
- @Test
- fun pause_shouldUnlistenDataStateChange() {
- dataUsageList.template = mock(NetworkTemplate::class.java)
- dataUsageList.onCreate(null)
- dataUsageList.dataStateListener = mobileDataEnabledListener
- ReflectionHelpers.setField(
- dataUsageList, "mVisibilityLoggerMixin", mock(
- VisibilityLoggerMixin::class.java
- )
- )
- ReflectionHelpers.setField(
- dataUsageList, "mPreferenceManager", mock(
- PreferenceManager::class.java
- )
- )
- dataUsageList.onResume()
- dataUsageList.onPause()
- verify(mobileDataEnabledListener).stop()
- }
-
- @Test
fun processArgument_shouldGetTemplateFromArgument() {
val args = Bundle()
args.putParcelable(
diff --git a/tests/robotests/src/com/android/settings/development/ShowHdrSdrRatioPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/development/ShowHdrSdrRatioPreferenceControllerTest.java
new file mode 100644
index 0000000..2234754
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/development/ShowHdrSdrRatioPreferenceControllerTest.java
@@ -0,0 +1,171 @@
+/**
+ * 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.development;
+
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.assertFalse;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.platform.test.flag.junit.SetFlagsRule;
+
+import androidx.preference.PreferenceScreen;
+import androidx.preference.TwoStatePreference;
+
+import com.android.settings.flags.Flags;
+import com.android.settings.testutils.shadow.ShadowParcel;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Config;
+
+@RunWith(RobolectricTestRunner.class)
+public class ShowHdrSdrRatioPreferenceControllerTest {
+
+ @Mock
+ private Context mContext;
+ @Mock
+ private PreferenceScreen mScreen;
+ @Mock
+ private TwoStatePreference mPreference;
+ @Mock
+ private IBinder mSurfaceFlinger;
+
+ private ShowHdrSdrRatioPreferenceController mController;
+
+ @Rule
+ public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ mContext = RuntimeEnvironment.application;
+ mController = new ShowHdrSdrRatioPreferenceController(mContext, mSurfaceFlinger, true);
+ when(mScreen.findPreference(mController.getPreferenceKey())).thenReturn(mPreference);
+ mController.displayPreference(mScreen);
+ }
+
+ @Test
+ @Config(shadows = ShadowParcel.class)
+ public void onPreferenceChange_settingEnabled_shouldChecked() throws RemoteException {
+ mSetFlagsRule.enableFlags(Flags.FLAG_DEVELOPMENT_HDR_SDR_RATIO);
+ assertTrue(mController.isAvailable());
+ ShadowParcel.sReadBoolResult = true;
+ doReturn(true).when(mSurfaceFlinger)
+ .transact(anyInt(), any(), any(), eq(0 /* flags */));
+ mController.onPreferenceChange(mPreference, true /* new value */);
+ verify(mPreference).setChecked(true);
+ }
+
+ @Test
+ @Config(shadows = ShadowParcel.class)
+ public void onPreferenceChange_settingDisabled_shouldUnchecked() throws RemoteException {
+ mSetFlagsRule.enableFlags(Flags.FLAG_DEVELOPMENT_HDR_SDR_RATIO);
+ assertTrue(mController.isAvailable());
+ ShadowParcel.sReadBoolResult = false;
+ doReturn(true).when(mSurfaceFlinger)
+ .transact(anyInt(), any(), any(), eq(0 /* flags */));
+ mController.onPreferenceChange(mPreference, false /* new value */);
+ verify(mPreference).setChecked(false);
+ }
+
+ @Test
+ @Config(shadows = ShadowParcel.class)
+ public void updateState_settingEnabled_shouldChecked() throws RemoteException {
+ mSetFlagsRule.enableFlags(Flags.FLAG_DEVELOPMENT_HDR_SDR_RATIO);
+ assertTrue(mController.isAvailable());
+ ShadowParcel.sReadBoolResult = true;
+ doReturn(true).when(mSurfaceFlinger)
+ .transact(anyInt(), any(), any(), eq(0 /* flags */));
+ mController.updateState(mPreference);
+ verify(mPreference).setChecked(true);
+ }
+
+ @Test
+ @Config(shadows = ShadowParcel.class)
+ public void updateState_settingDisabled_shouldUnchecked() throws RemoteException {
+ mSetFlagsRule.enableFlags(Flags.FLAG_DEVELOPMENT_HDR_SDR_RATIO);
+ assertTrue(mController.isAvailable());
+ ShadowParcel.sReadBoolResult = false;
+ doReturn(true).when(mSurfaceFlinger)
+ .transact(anyInt(), any(), any(), eq(0 /* flags */));
+ mController.updateState(mPreference);
+ verify(mPreference).setChecked(false);
+ }
+
+ @Test
+ public void settingNotAvailable_isHdrSdrRatioAvailableFalse_flagsOff() {
+ mSetFlagsRule.disableFlags(Flags.FLAG_DEVELOPMENT_HDR_SDR_RATIO);
+ mController = new ShowHdrSdrRatioPreferenceController(mContext, mSurfaceFlinger, true);
+ assertFalse(mController.isAvailable());
+ }
+
+ @Test
+ public void settingNotAvailable_isHdrSdrRatioAvailableTrue_flagsOn() {
+ mSetFlagsRule.enableFlags(Flags.FLAG_DEVELOPMENT_HDR_SDR_RATIO);
+ mController = new ShowHdrSdrRatioPreferenceController(mContext, mSurfaceFlinger, false);
+ assertFalse(mController.isAvailable());
+ }
+
+ @Test
+ @Config(shadows = ShadowParcel.class)
+ public void onDeveloperOptionsSwitchDisabled_preferenceUnchecked_shouldNotTurnOffPreference()
+ throws RemoteException {
+ mSetFlagsRule.enableFlags(Flags.FLAG_DEVELOPMENT_HDR_SDR_RATIO);
+ assertTrue(mController.isAvailable());
+ ShadowParcel.sReadBoolResult = false;
+ doReturn(true).when(mSurfaceFlinger)
+ .transact(anyInt(), any(), any(), eq(0 /* flags */));
+ when(mPreference.isChecked()).thenReturn(false);
+ mController.onDeveloperOptionsSwitchDisabled();
+
+ mController.writeShowHdrSdrRatioSetting(true);
+ verify(mPreference).setChecked(false);
+ verify(mPreference).setEnabled(false);
+ }
+
+ @Test
+ @Config(shadows = ShadowParcel.class)
+ public void onDeveloperOptionsSwitchDisabled_preferenceChecked_shouldTurnOffPreference()
+ throws RemoteException {
+ mSetFlagsRule.enableFlags(Flags.FLAG_DEVELOPMENT_HDR_SDR_RATIO);
+ assertTrue(mController.isAvailable());
+ ShadowParcel.sReadBoolResult = true;
+ doReturn(true).when(mSurfaceFlinger)
+ .transact(anyInt(), any(), any(), eq(0 /* flags */));
+ when(mPreference.isChecked()).thenReturn(true);
+ mController.onDeveloperOptionsSwitchDisabled();
+
+ mController.writeShowHdrSdrRatioSetting(false);
+ verify(mPreference).setChecked(false);
+ verify(mPreference).setEnabled(false);
+ }
+}
+
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 f43feec..bbba294 100644
--- a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryDiffEntryTest.java
+++ b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryDiffEntryTest.java
@@ -352,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
@@ -445,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
@@ -457,7 +463,9 @@
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
@@ -469,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
@@ -590,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/localepicker/AppLocalePickerActivityTest.java b/tests/robotests/src/com/android/settings/localepicker/AppLocalePickerActivityTest.java
index 72b01f8..817df4c 100644
--- a/tests/robotests/src/com/android/settings/localepicker/AppLocalePickerActivityTest.java
+++ b/tests/robotests/src/com/android/settings/localepicker/AppLocalePickerActivityTest.java
@@ -18,6 +18,8 @@
import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
@@ -27,6 +29,7 @@
import android.app.Activity;
import android.app.ApplicationPackageManager;
import android.app.LocaleConfig;
+import android.app.settings.SettingsEnums;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
@@ -51,6 +54,7 @@
import com.android.settings.applications.AppInfoBase;
import com.android.settings.applications.AppLocaleUtil;
import com.android.settings.flags.Flags;
+import com.android.settings.testutils.FakeFeatureFactory;
import org.junit.After;
import org.junit.Before;
@@ -91,6 +95,7 @@
private static final String EN_US = "en-US";
private static int sUid;
+ private FakeFeatureFactory mFeatureFactory;
private LocaleNotificationDataManager mDataManager;
private AppLocalePickerActivity mActivity;
@@ -117,6 +122,7 @@
when(mLocaleConfig.getSupportedLocales()).thenReturn(LocaleList.forLanguageTags("en-US"));
ReflectionHelpers.setStaticField(AppLocaleUtil.class, "sLocaleConfig", mLocaleConfig);
sUid = Process.myUid();
+ mFeatureFactory = FakeFeatureFactory.setupForTest();
}
@After
@@ -229,6 +235,37 @@
}
@Test
+ public void onLocaleSelected_logLocaleSource() {
+ ActivityController<TestAppLocalePickerActivity> controller =
+ initActivityController(true);
+ LocaleList.setDefault(LocaleList.forLanguageTags("ja-JP,en-CA,en-US"));
+ Locale locale = new Locale("en", "US");
+ when(mLocaleInfo.getLocale()).thenReturn(locale);
+ when(mLocaleInfo.isSystemLocale()).thenReturn(false);
+ when(mLocaleInfo.isSuggested()).thenReturn(true);
+ when(mLocaleInfo.isSuggestionOfType(LocaleStore.LocaleInfo.SUGGESTION_TYPE_SIM)).thenReturn(
+ true);
+ when(mLocaleInfo.isSuggestionOfType(
+ LocaleStore.LocaleInfo.SUGGESTION_TYPE_SYSTEM_AVAILABLE_LANGUAGE)).thenReturn(
+ true);
+ when(mLocaleInfo.isSuggestionOfType(
+ LocaleStore.LocaleInfo.SUGGESTION_TYPE_OTHER_APP_LANGUAGE)).thenReturn(
+ true);
+ when(mLocaleInfo.isSuggestionOfType(
+ LocaleStore.LocaleInfo.SUGGESTION_TYPE_IME_LANGUAGE)).thenReturn(
+ true);
+
+ controller.create();
+ AppLocalePickerActivity mActivity = controller.get();
+ mActivity.onLocaleSelected(mLocaleInfo);
+
+ int localeSource = 15; // SIM_LOCALE | SYSTEM_LOCALE |IME_LOCALE|APP_LOCALE
+ verify(mFeatureFactory.metricsFeatureProvider).action(
+ any(), eq(SettingsEnums.ACTION_CHANGE_APP_LANGUAGE_FROM_SUGGESTED),
+ eq(localeSource));
+ }
+
+ @Test
@RequiresFlagsEnabled(Flags.FLAG_LOCALE_NOTIFICATION_ENABLED)
public void onLocaleSelected_evaluateNotification_simpleLocaleUpdate_localeCreatedWithUid()
throws Exception {
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/robotests/src/com/android/settings/notification/app/AppChannelsBypassingDndPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/notification/app/AppChannelsBypassingDndPreferenceControllerTest.java
index 13528b4..053b352 100644
--- a/tests/robotests/src/com/android/settings/notification/app/AppChannelsBypassingDndPreferenceControllerTest.java
+++ b/tests/robotests/src/com/android/settings/notification/app/AppChannelsBypassingDndPreferenceControllerTest.java
@@ -26,16 +26,19 @@
import android.app.NotificationManager;
import android.content.Context;
import android.content.pm.ParceledListSlice;
+import android.platform.test.flag.junit.SetFlagsRule;
import androidx.preference.PreferenceCategory;
import androidx.preference.PreferenceManager;
import androidx.preference.PreferenceScreen;
import androidx.test.core.app.ApplicationProvider;
+import com.android.settings.flags.Flags;
import com.android.settings.notification.NotificationBackend;
import com.android.settingslib.PrimarySwitchPreference;
import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
@@ -51,6 +54,9 @@
@LooperMode(LooperMode.Mode.LEGACY)
public class AppChannelsBypassingDndPreferenceControllerTest {
+ @Rule
+ public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
@Mock
private NotificationBackend mBackend;
@@ -150,4 +156,44 @@
}
return new ParceledListSlice<>(Collections.singletonList(group));
}
+
+ @Test
+ public void displayPreference_duplicateChannelName_AddsGroupNameAsSummary() {
+ mSetFlagsRule.enableFlags(Flags.FLAG_DEDUPE_DND_SETTINGS_CHANNELS);
+ NotificationChannelGroup group1 = new NotificationChannelGroup("group1_id", "Group1");
+ NotificationChannelGroup group2 = new NotificationChannelGroup("group2_id", "Group2");
+
+ group1.addChannel(new NotificationChannel("mail_group1_id", "Mail",
+ NotificationManager.IMPORTANCE_DEFAULT));
+ group1.addChannel(new NotificationChannel("other_group1_id", "Other",
+ NotificationManager.IMPORTANCE_DEFAULT));
+
+ group2.addChannel(new NotificationChannel("music_group2_id", "Music",
+ NotificationManager.IMPORTANCE_DEFAULT));
+ // This channel has the same name as a channel in group1.
+ group2.addChannel(new NotificationChannel("mail_group2_id", "Mail",
+ NotificationManager.IMPORTANCE_DEFAULT));
+
+ ParceledListSlice<NotificationChannelGroup> groups = new ParceledListSlice<>(
+ new ArrayList<NotificationChannelGroup>() {
+ {
+ add(group1);
+ add(group2);
+ }
+ }
+ );
+
+ when(mBackend.getGroups(eq(mAppRow.pkg), eq(mAppRow.uid))).thenReturn(groups);
+ mController.displayPreference(mPreferenceScreen);
+ ShadowApplication.runBackgroundTasks();
+ // Check that we've added the group name as a summary to channels that have identical names.
+ // Channels are also alphabetized.
+ assertThat(mCategory.getPreference(1).getTitle().toString()).isEqualTo("Mail");
+ assertThat(mCategory.getPreference(1).getSummary().toString()).isEqualTo("Group1");
+ assertThat(mCategory.getPreference(2).getTitle().toString()).isEqualTo("Mail");
+ assertThat(mCategory.getPreference(2).getSummary().toString()).isEqualTo("Group2");
+ assertThat(mCategory.getPreference(3).getTitle().toString()).isEqualTo("Music");
+ assertThat(mCategory.getPreference(4).getTitle().toString()).isEqualTo("Other");
+
+ }
}
diff --git a/tests/robotests/testutils/com/android/settings/testutils/FakeFeatureFactory.java b/tests/robotests/testutils/com/android/settings/testutils/FakeFeatureFactory.java
index 9156cae..5a5008c 100644
--- a/tests/robotests/testutils/com/android/settings/testutils/FakeFeatureFactory.java
+++ b/tests/robotests/testutils/com/android/settings/testutils/FakeFeatureFactory.java
@@ -46,6 +46,7 @@
import com.android.settings.overlay.SupportFeatureProvider;
import com.android.settings.overlay.SurveyFeatureProvider;
import com.android.settings.panel.PanelFeatureProvider;
+import com.android.settings.privatespace.PrivateSpaceLoginFeatureProvider;
import com.android.settings.search.SearchFeatureProvider;
import com.android.settings.security.SecurityFeatureProvider;
import com.android.settings.security.SecuritySettingsFeatureProvider;
@@ -99,6 +100,7 @@
public StylusFeatureProvider mStylusFeatureProvider;
public OnboardingFeatureProvider mOnboardingFeatureProvider;
public FastPairFeatureProvider mFastPairFeatureProvider;
+ public PrivateSpaceLoginFeatureProvider mPrivateSpaceLoginFeatureProvider;
/**
* Call this in {@code @Before} method of the test class to use fake factory.
@@ -146,6 +148,7 @@
mStylusFeatureProvider = mock(StylusFeatureProvider.class);
mOnboardingFeatureProvider = mock(OnboardingFeatureProvider.class);
mFastPairFeatureProvider = mock(FastPairFeatureProvider.class);
+ mPrivateSpaceLoginFeatureProvider = mock(PrivateSpaceLoginFeatureProvider.class);
}
@Override
@@ -323,5 +326,10 @@
public FastPairFeatureProvider getFastPairFeatureProvider() {
return mFastPairFeatureProvider;
}
+
+ @Override
+ public PrivateSpaceLoginFeatureProvider getPrivateSpaceLoginFeatureProvider() {
+ return mPrivateSpaceLoginFeatureProvider;
+ }
}
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/datausage/AppDataUsageCycleControllerTest.kt b/tests/spa_unit/src/com/android/settings/datausage/AppDataUsageCycleControllerTest.kt
index ea51f01..fe35259 100644
--- a/tests/spa_unit/src/com/android/settings/datausage/AppDataUsageCycleControllerTest.kt
+++ b/tests/spa_unit/src/com/android/settings/datausage/AppDataUsageCycleControllerTest.kt
@@ -22,7 +22,6 @@
import androidx.preference.PreferenceManager
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
-import com.android.settings.datausage.lib.AppDataUsageDetailsRepository
import com.android.settings.datausage.lib.IAppDataUsageDetailsRepository
import com.android.settings.datausage.lib.NetworkUsageDetailsData
import com.google.common.truth.Truth.assertThat
@@ -31,23 +30,18 @@
import org.junit.Before
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
import org.mockito.kotlin.verify
@RunWith(AndroidJUnit4::class)
class AppDataUsageCycleControllerTest {
private val context: Context = ApplicationProvider.getApplicationContext()
- private val controller = AppDataUsageCycleController(context, KEY)
-
private val preference = spy(SpinnerPreference(context, null).apply { key = KEY })
private val preferenceScreen = PreferenceManager(context).createPreferenceScreen(context)
- private val onUsageDataUpdated: (NetworkUsageDetailsData) -> Unit = {}
+ private val controller = AppDataUsageCycleController(context, KEY)
@Before
fun setUp() {
@@ -59,8 +53,8 @@
val repository = object : IAppDataUsageDetailsRepository {
override suspend fun queryDetailsForCycles() = emptyList<NetworkUsageDetailsData>()
}
- controller.init(repository, onUsageDataUpdated)
controller.displayPreference(preferenceScreen)
+ controller.init(repository) {}
controller.onViewCreated(TestLifecycleOwner())
delay(100)
@@ -79,8 +73,8 @@
val repository = object : IAppDataUsageDetailsRepository {
override suspend fun queryDetailsForCycles() = listOf(detailsData)
}
- controller.init(repository, onUsageDataUpdated)
controller.displayPreference(preferenceScreen)
+ controller.init(repository) {}
controller.onViewCreated(TestLifecycleOwner())
delay(100)
@@ -93,14 +87,14 @@
val repository = object : IAppDataUsageDetailsRepository {
override suspend fun queryDetailsForCycles() = emptyList<NetworkUsageDetailsData>()
}
- controller.init(repository, onUsageDataUpdated)
+ controller.displayPreference(preferenceScreen)
+ controller.init(repository) {}
+
controller.setInitialCycles(
initialCycles = listOf(CYCLE2_END_TIME, CYCLE1_END_TIME, CYCLE1_START_TIME),
initialSelectedEndTime = CYCLE1_END_TIME,
)
- controller.displayPreference(preferenceScreen)
-
verify(preference).setSelection(1)
}
diff --git a/tests/spa_unit/src/com/android/settings/network/MobileDataEnabledFlowTest.kt b/tests/spa_unit/src/com/android/settings/network/MobileDataEnabledFlowTest.kt
new file mode 100644
index 0000000..4862309
--- /dev/null
+++ b/tests/spa_unit/src/com/android/settings/network/MobileDataEnabledFlowTest.kt
@@ -0,0 +1,91 @@
+/*
+ * 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.provider.Settings
+import android.telephony.SubscriptionManager
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.settingslib.spa.testutils.firstWithTimeoutOrNull
+import com.android.settingslib.spa.testutils.toListWithTimeout
+import com.android.settingslib.spaprivileged.settingsprovider.settingsGlobalBoolean
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.async
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.runBlocking
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class MobileDataEnabledFlowTest {
+ private val context: Context = ApplicationProvider.getApplicationContext()
+
+ @Test
+ fun mobileDataEnabledFlow_notified(): Unit = runBlocking {
+ val flow = context.mobileDataEnabledFlow(SubscriptionManager.INVALID_SUBSCRIPTION_ID)
+
+ assertThat(flow.firstWithTimeoutOrNull()).isNotNull()
+ }
+
+ @Test
+ fun mobileDataEnabledFlow_changed_notified(): Unit = runBlocking {
+ var mobileDataEnabled by context.settingsGlobalBoolean(Settings.Global.MOBILE_DATA)
+ mobileDataEnabled = false
+
+ val flow = context.mobileDataEnabledFlow(SubscriptionManager.INVALID_SUBSCRIPTION_ID)
+ mobileDataEnabled = true
+
+ assertThat(flow.firstWithTimeoutOrNull()).isNotNull()
+ }
+
+ @Test
+ fun mobileDataEnabledFlow_forSubIdNotChanged(): Unit = runBlocking {
+ var mobileDataEnabled by context.settingsGlobalBoolean(Settings.Global.MOBILE_DATA)
+ mobileDataEnabled = false
+ var mobileDataEnabledForSubId
+ by context.settingsGlobalBoolean(Settings.Global.MOBILE_DATA + SUB_ID)
+ mobileDataEnabledForSubId = false
+
+ val listDeferred = async {
+ context.mobileDataEnabledFlow(SUB_ID).toListWithTimeout()
+ }
+
+ assertThat(listDeferred.await()).hasSize(1)
+ }
+
+ @Test
+ fun mobileDataEnabledFlow_forSubIdChanged(): Unit = runBlocking {
+ var mobileDataEnabled by context.settingsGlobalBoolean(Settings.Global.MOBILE_DATA)
+ mobileDataEnabled = false
+ var mobileDataEnabledForSubId
+ by context.settingsGlobalBoolean(Settings.Global.MOBILE_DATA + SUB_ID)
+ mobileDataEnabledForSubId = false
+
+ val listDeferred = async {
+ context.mobileDataEnabledFlow(SUB_ID).toListWithTimeout()
+ }
+ delay(100)
+ mobileDataEnabledForSubId = true
+
+ assertThat(listDeferred.await()).hasSize(2)
+ }
+
+ private companion object {
+ const val SUB_ID = 123
+ }
+}
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/AllAppListTest.kt b/tests/spa_unit/src/com/android/settings/spa/app/AllAppListTest.kt
index 53ed4f0..1a05479 100644
--- a/tests/spa_unit/src/com/android/settings/spa/app/AllAppListTest.kt
+++ b/tests/spa_unit/src/com/android/settings/spa/app/AllAppListTest.kt
@@ -21,7 +21,6 @@
import android.content.pm.PackageManager
import android.graphics.drawable.Drawable
import androidx.compose.runtime.SideEffect
-import androidx.compose.runtime.State
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithText
@@ -146,12 +145,12 @@
fun allAppListModel_getSummary() {
val listModel = AllAppListModel(context) { stateOf(SUMMARY) }
- lateinit var summaryState: State<String>
+ lateinit var summary: () -> String
composeTestRule.setContent {
- summaryState = listModel.getSummary(option = 0, record = AppRecordWithSize(app = APP))
+ summary = listModel.getSummary(option = 0, record = AppRecordWithSize(app = APP))
}
- assertThat(summaryState.value).isEqualTo(SUMMARY)
+ assertThat(summary()).isEqualTo(SUMMARY)
}
@Test
@@ -163,13 +162,13 @@
enabled = false
}
- lateinit var summaryState: State<String>
+ lateinit var summary: () -> String
composeTestRule.setContent {
- summaryState =
+ summary =
listModel.getSummary(option = 0, record = AppRecordWithSize(app = disabledApp))
}
- assertThat(summaryState.value).isEqualTo("$SUMMARY${System.lineSeparator()}Disabled")
+ assertThat(summary()).isEqualTo("$SUMMARY${System.lineSeparator()}Disabled")
}
@Test
@@ -179,13 +178,13 @@
packageName = PACKAGE_NAME
}
- lateinit var summaryState: State<String>
+ lateinit var summary: () -> String
composeTestRule.setContent {
- summaryState =
+ summary =
listModel.getSummary(option = 0, record = AppRecordWithSize(app = notInstalledApp))
}
- assertThat(summaryState.value)
+ assertThat(summary())
.isEqualTo("$SUMMARY${System.lineSeparator()}Not installed for this user")
}
@@ -207,7 +206,7 @@
AppListItemModel(
record = AppRecordWithSize(app = app),
label = LABEL,
- summary = stateOf(SUMMARY),
+ summary = { SUMMARY },
).AppItem()
}
}
@@ -224,13 +223,13 @@
isArchived = true
}
- lateinit var summaryState: State<String>
+ lateinit var summary: () -> String
composeTestRule.setContent {
- summaryState =
+ summary =
listModel.getSummary(option = 0, record = AppRecordWithSize(app = archivedApp))
}
- assertThat(summaryState.value).isEqualTo(SUMMARY)
+ assertThat(summary()).isEqualTo(SUMMARY)
}
private fun getAppListInput(): AppListInput<AppRecordWithSize> {
@@ -252,7 +251,7 @@
AppListItemModel(
record = AppRecordWithSize(app = APP),
label = LABEL,
- summary = stateOf(SUMMARY),
+ summary = { SUMMARY },
).AppItem()
}
}
diff --git a/tests/spa_unit/src/com/android/settings/spa/app/WifiControlAppListModelTest.kt b/tests/spa_unit/src/com/android/settings/spa/app/WifiControlAppListModelTest.kt
index c5c48f5..74aa861 100644
--- a/tests/spa_unit/src/com/android/settings/spa/app/WifiControlAppListModelTest.kt
+++ b/tests/spa_unit/src/com/android/settings/spa/app/WifiControlAppListModelTest.kt
@@ -18,11 +18,8 @@
import android.Manifest
import android.app.AppOpsManager
-import android.app.AppOpsManager.MODE_ALLOWED
-import android.app.AppOpsManager.MODE_DEFAULT
import android.content.Context
import android.content.pm.ApplicationInfo
-import androidx.compose.runtime.State
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.lifecycle.MutableLiveData
import androidx.test.core.app.ApplicationProvider
@@ -40,9 +37,9 @@
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
-import org.mockito.Mockito.`when` as whenever
import org.mockito.junit.MockitoJUnit
import org.mockito.junit.MockitoRule
+import org.mockito.Mockito.`when` as whenever
@ExperimentalCoroutinesApi
@RunWith(AndroidJUnit4::class)
@@ -248,9 +245,9 @@
}
private fun getIsAllowed(record: AppOpPermissionRecord): Boolean? {
- lateinit var isAllowedState: State<Boolean?>
+ lateinit var isAllowedState: () -> Boolean?
composeTestRule.setContent { isAllowedState = listModel.isAllowed(record) }
- return isAllowedState.value
+ return isAllowedState()
}
private companion object {
diff --git a/tests/spa_unit/src/com/android/settings/spa/app/appcompat/UserAspectRatioAppsPageProviderTest.kt b/tests/spa_unit/src/com/android/settings/spa/app/appcompat/UserAspectRatioAppsPageProviderTest.kt
index 4f372e2..dfacca8 100644
--- a/tests/spa_unit/src/com/android/settings/spa/app/appcompat/UserAspectRatioAppsPageProviderTest.kt
+++ b/tests/spa_unit/src/com/android/settings/spa/app/appcompat/UserAspectRatioAppsPageProviderTest.kt
@@ -21,7 +21,6 @@
import android.content.pm.PackageManager.USER_MIN_ASPECT_RATIO_SPLIT_SCREEN
import android.content.pm.PackageManager.USER_MIN_ASPECT_RATIO_UNSET
import android.os.Build
-import androidx.compose.runtime.State
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithText
@@ -29,7 +28,6 @@
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.android.settings.R
-import com.android.settingslib.spa.framework.compose.stateOf
import com.android.settingslib.spa.testutils.FakeNavControllerWrapper
import com.android.settingslib.spa.testutils.firstWithTimeoutOrNull
import com.android.settingslib.spaprivileged.template.app.AppListItemModel
@@ -132,7 +130,7 @@
AppListItemModel(
record = APP_RECORD_SUGGESTED,
label = LABEL,
- summary = stateOf(SUMMARY)
+ summary = { SUMMARY }
).AppItem()
}
}
@@ -141,23 +139,23 @@
@Test
fun aspectRatioAppListModel_getSummaryDefault() {
- val summaryState = setSummaryState(USER_MIN_ASPECT_RATIO_UNSET)
- assertThat(summaryState.value)
- .isEqualTo(context.getString(R.string.user_aspect_ratio_app_default))
+ val summary = getSummary(USER_MIN_ASPECT_RATIO_UNSET)
+
+ assertThat(summary).isEqualTo(context.getString(R.string.user_aspect_ratio_app_default))
}
@Test
fun aspectRatioAppListModel_getSummaryWhenSplitScreen() {
- val summaryState = setSummaryState(USER_MIN_ASPECT_RATIO_SPLIT_SCREEN)
- assertThat(summaryState.value)
- .isEqualTo(context.getString(R.string.user_aspect_ratio_half_screen))
+ val summary = getSummary(USER_MIN_ASPECT_RATIO_SPLIT_SCREEN)
+
+ assertThat(summary).isEqualTo(context.getString(R.string.user_aspect_ratio_half_screen))
}
- private fun setSummaryState(userOverride: Int): State<String> {
+ private fun getSummary(userOverride: Int): String {
val listModel = UserAspectRatioAppListModel(context)
- lateinit var summaryState: State<String>
+ lateinit var summary: () -> String
composeTestRule.setContent {
- summaryState = listModel.getSummary(option = 0,
+ summary = listModel.getSummary(option = 0,
record = UserAspectRatioAppListItemModel(
app = APP,
userOverride = userOverride,
@@ -165,7 +163,7 @@
canDisplay = true,
))
}
- return summaryState
+ return summary()
}
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/backgroundinstall/BackgroundInstalledAppsPageProviderTest.kt b/tests/spa_unit/src/com/android/settings/spa/app/backgroundinstall/BackgroundInstalledAppsPageProviderTest.kt
index ccd385f..5c65da1 100644
--- a/tests/spa_unit/src/com/android/settings/spa/app/backgroundinstall/BackgroundInstalledAppsPageProviderTest.kt
+++ b/tests/spa_unit/src/com/android/settings/spa/app/backgroundinstall/BackgroundInstalledAppsPageProviderTest.kt
@@ -29,7 +29,6 @@
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.android.settings.R
-import com.android.settingslib.spa.framework.compose.stateOf
import com.android.settingslib.spa.testutils.FakeNavControllerWrapper
import com.android.settingslib.spaprivileged.template.app.AppListItemModel
import com.google.common.truth.Truth.assertThat
@@ -244,7 +243,7 @@
app = APP,
dateOfInstall = TEST_FIRST_INSTALL_TIME),
label = TEST_LABEL,
- summary = stateOf(TEST_SUMMARY),
+ summary = { TEST_SUMMARY },
).AppItem()
}
}
diff --git a/tests/spa_unit/src/com/android/settings/spa/app/specialaccess/AllFilesAccessTest.kt b/tests/spa_unit/src/com/android/settings/spa/app/specialaccess/AllFilesAccessTest.kt
index f5d422d..4c65d90 100644
--- a/tests/spa_unit/src/com/android/settings/spa/app/specialaccess/AllFilesAccessTest.kt
+++ b/tests/spa_unit/src/com/android/settings/spa/app/specialaccess/AllFilesAccessTest.kt
@@ -33,33 +33,12 @@
private val listModel = AllFilesAccessListModel(context)
@Test
- fun pageTitleResId() {
+ fun modelResourceIdAndProperties() {
assertThat(listModel.pageTitleResId).isEqualTo(R.string.manage_external_storage_title)
- }
-
- @Test
- fun switchTitleResId() {
assertThat(listModel.switchTitleResId).isEqualTo(R.string.permit_manage_external_storage)
- }
-
- @Test
- fun footerResId() {
- assertThat(listModel.footerResId)
- .isEqualTo(R.string.allow_manage_external_storage_description)
- }
-
- @Test
- fun appOp() {
+ assertThat(listModel.footerResId).isEqualTo(R.string.allow_manage_external_storage_description)
assertThat(listModel.appOp).isEqualTo(AppOpsManager.OP_MANAGE_EXTERNAL_STORAGE)
- }
-
- @Test
- fun permission() {
assertThat(listModel.permission).isEqualTo(Manifest.permission.MANAGE_EXTERNAL_STORAGE)
- }
-
- @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/LongBackgroundTasksAppsTest.kt b/tests/spa_unit/src/com/android/settings/spa/app/specialaccess/LongBackgroundTasksAppsTest.kt
new file mode 100644
index 0000000..579c6c9
--- /dev/null
+++ b/tests/spa_unit/src/com/android/settings/spa/app/specialaccess/LongBackgroundTasksAppsTest.kt
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.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 com.android.settings.R
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class LongBackgroundTasksAppsTest {
+ private val context: Context = ApplicationProvider.getApplicationContext()
+
+ private val listModel = LongBackgroundTasksAppsListModel(context)
+
+ @Test
+ fun modelResourceIdAndProperties() {
+ assertThat(listModel.pageTitleResId).isEqualTo(R.string.long_background_tasks_title)
+ assertThat(listModel.switchTitleResId).isEqualTo(R.string.long_background_tasks_switch_title)
+ assertThat(listModel.footerResId).isEqualTo(R.string.long_background_tasks_footer_title)
+ assertThat(listModel.appOp).isEqualTo(AppOpsManager.OP_RUN_USER_INITIATED_JOBS)
+ assertThat(listModel.permission).isEqualTo(Manifest.permission.RUN_USER_INITIATED_JOBS)
+ assertThat(listModel.setModeByUid).isTrue()
+ }
+}
\ No newline at end of file
diff --git a/tests/spa_unit/src/com/android/settings/spa/app/specialaccess/MediaManagementAppsTest.kt b/tests/spa_unit/src/com/android/settings/spa/app/specialaccess/MediaManagementAppsTest.kt
index b56d997..b901043 100644
--- a/tests/spa_unit/src/com/android/settings/spa/app/specialaccess/MediaManagementAppsTest.kt
+++ b/tests/spa_unit/src/com/android/settings/spa/app/specialaccess/MediaManagementAppsTest.kt
@@ -33,33 +33,12 @@
private val listModel = MediaManagementAppsListModel(context)
@Test
- fun pageTitleResId() {
+ fun modelResourceIdAndProperties() {
assertThat(listModel.pageTitleResId).isEqualTo(R.string.media_management_apps_title)
- }
-
- @Test
- fun switchTitleResId() {
- assertThat(listModel.switchTitleResId)
- .isEqualTo(R.string.media_management_apps_toggle_label)
- }
-
- @Test
- fun footerResId() {
+ assertThat(listModel.switchTitleResId).isEqualTo(R.string.media_management_apps_toggle_label)
assertThat(listModel.footerResId).isEqualTo(R.string.media_management_apps_description)
- }
-
- @Test
- fun appOp() {
assertThat(listModel.appOp).isEqualTo(AppOpsManager.OP_MANAGE_MEDIA)
- }
-
- @Test
- fun permission() {
assertThat(listModel.permission).isEqualTo(Manifest.permission.MANAGE_MEDIA)
- }
-
- @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/PictureInPictureTest.kt b/tests/spa_unit/src/com/android/settings/spa/app/specialaccess/PictureInPictureTest.kt
index 6054bb5..4229247 100644
--- a/tests/spa_unit/src/com/android/settings/spa/app/specialaccess/PictureInPictureTest.kt
+++ b/tests/spa_unit/src/com/android/settings/spa/app/specialaccess/PictureInPictureTest.kt
@@ -67,18 +67,9 @@
}
@Test
- fun pageTitleResId() {
+ fun modelResourceId() {
assertThat(listModel.pageTitleResId).isEqualTo(R.string.picture_in_picture_title)
- }
-
- @Test
- fun switchTitleResId() {
- assertThat(listModel.switchTitleResId)
- .isEqualTo(R.string.picture_in_picture_app_detail_switch)
- }
-
- @Test
- fun footerResId() {
+ assertThat(listModel.switchTitleResId).isEqualTo(R.string.picture_in_picture_app_detail_switch)
assertThat(listModel.footerResId).isEqualTo(R.string.picture_in_picture_app_detail_summary)
}
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..9c6079d
--- /dev/null
+++ b/tests/spa_unit/src/com/android/settings/spa/app/specialaccess/TurnScreenOnAppsTest.kt
@@ -0,0 +1,42 @@
+/*
+ * 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 modelResourceIdAndProperties() {
+ assertThat(listModel.pageTitleResId).isEqualTo(com.android.settingslib.R.string.turn_screen_on_title)
+ assertThat(listModel.switchTitleResId).isEqualTo(com.android.settingslib.R.string.allow_turn_screen_on)
+ assertThat(listModel.footerResId).isEqualTo(com.android.settingslib.R.string.allow_turn_screen_on_description)
+ assertThat(listModel.appOp).isEqualTo(AppOpsManager.OP_TURN_SCREEN_ON)
+ assertThat(listModel.permission).isEqualTo(Manifest.permission.TURN_SCREEN_ON)
+ 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..a2aa293
--- /dev/null
+++ b/tests/spa_unit/src/com/android/settings/spa/app/specialaccess/VoiceActivationAppsTest.kt
@@ -0,0 +1,30 @@
+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 modelResourceIdAndProperties() {
+ assertThat(listModel.pageTitleResId).isEqualTo(R.string.voice_activation_apps_title)
+ assertThat(listModel.switchTitleResId).isEqualTo(R.string.permit_voice_activation_apps)
+ assertThat(listModel.footerResId).isEqualTo(R.string.allow_voice_activation_apps_description)
+ assertThat(listModel.appOp).isEqualTo(AppOpsManager.OP_RECEIVE_SANDBOX_TRIGGER_AUDIO)
+ assertThat(listModel.permission).isEqualTo(
+ Manifest.permission.RECEIVE_SANDBOX_TRIGGER_AUDIO
+ )
+ assertThat(listModel.setModeByUid).isTrue()
+ }
+}
\ No newline at end of file
diff --git a/tests/spa_unit/src/com/android/settings/spa/development/compat/PlatformCompatAppListModelTest.kt b/tests/spa_unit/src/com/android/settings/spa/development/compat/PlatformCompatAppListModelTest.kt
index 0cfdc7d..6aee4ce 100644
--- a/tests/spa_unit/src/com/android/settings/spa/development/compat/PlatformCompatAppListModelTest.kt
+++ b/tests/spa_unit/src/com/android/settings/spa/development/compat/PlatformCompatAppListModelTest.kt
@@ -20,7 +20,6 @@
import android.content.pm.ApplicationInfo
import android.content.pm.PackageManager
import android.content.pm.PackageManager.PackageInfoFlags
-import androidx.compose.runtime.State
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -79,20 +78,20 @@
@Test
fun getSummary() = runTest {
- val summaryState = getSummaryState(APP)
+ val summary = getSummary(APP)
- assertThat(summaryState.value).isEqualTo(PACKAGE_NAME)
+ assertThat(summary).isEqualTo(PACKAGE_NAME)
}
- private fun getSummaryState(app: ApplicationInfo): State<String> {
- lateinit var summary: State<String>
+ private fun getSummary(app: ApplicationInfo): String {
+ lateinit var summary: () -> String
composeTestRule.setContent {
summary = listModel.getSummary(
option = 0,
record = PlatformCompatAppRecord(app),
)
}
- return summary
+ return summary()
}
private companion object {
diff --git a/tests/spa_unit/src/com/android/settings/testutils/FakeFeatureFactory.kt b/tests/spa_unit/src/com/android/settings/testutils/FakeFeatureFactory.kt
index 54299eb..9b098a7 100644
--- a/tests/spa_unit/src/com/android/settings/testutils/FakeFeatureFactory.kt
+++ b/tests/spa_unit/src/com/android/settings/testutils/FakeFeatureFactory.kt
@@ -41,6 +41,7 @@
import com.android.settings.overlay.FeatureFactory
import com.android.settings.overlay.SurveyFeatureProvider
import com.android.settings.panel.PanelFeatureProvider
+import com.android.settings.privatespace.PrivateSpaceLoginFeatureProvider
import com.android.settings.search.SearchFeatureProvider
import com.android.settings.security.SecurityFeatureProvider
import com.android.settings.security.SecuritySettingsFeatureProvider
@@ -143,4 +144,6 @@
get() = TODO("Not yet implemented")
override val fastPairFeatureProvider: FastPairFeatureProvider
get() = TODO("Not yet implemented")
+ override val privateSpaceLoginFeatureProvider: PrivateSpaceLoginFeatureProvider
+ get() = TODO("Not yet implemented")
}
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
diff --git a/tests/unit/src/com/android/settings/testutils/FakeFeatureFactory.java b/tests/unit/src/com/android/settings/testutils/FakeFeatureFactory.java
index b5062a0..bf2c84a 100644
--- a/tests/unit/src/com/android/settings/testutils/FakeFeatureFactory.java
+++ b/tests/unit/src/com/android/settings/testutils/FakeFeatureFactory.java
@@ -46,6 +46,7 @@
import com.android.settings.overlay.SupportFeatureProvider;
import com.android.settings.overlay.SurveyFeatureProvider;
import com.android.settings.panel.PanelFeatureProvider;
+import com.android.settings.privatespace.PrivateSpaceLoginFeatureProvider;
import com.android.settings.search.SearchFeatureProvider;
import com.android.settings.security.SecurityFeatureProvider;
import com.android.settings.security.SecuritySettingsFeatureProvider;
@@ -98,6 +99,7 @@
public StylusFeatureProvider mStylusFeatureProvider;
public OnboardingFeatureProvider mOnboardingFeatureProvider;
public FastPairFeatureProvider mFastPairFeatureProvider;
+ public PrivateSpaceLoginFeatureProvider mPrivateSpaceLoginFeatureProvider;
/**
* Call this in {@code @Before} method of the test class to use fake factory.
@@ -145,6 +147,7 @@
mStylusFeatureProvider = mock(StylusFeatureProvider.class);
mOnboardingFeatureProvider = mock(OnboardingFeatureProvider.class);
mFastPairFeatureProvider = mock(FastPairFeatureProvider.class);
+ mPrivateSpaceLoginFeatureProvider = mock(PrivateSpaceLoginFeatureProvider.class);
}
@Override
@@ -322,4 +325,9 @@
public FastPairFeatureProvider getFastPairFeatureProvider() {
return mFastPairFeatureProvider;
}
+
+ @Override
+ public PrivateSpaceLoginFeatureProvider getPrivateSpaceLoginFeatureProvider() {
+ return mPrivateSpaceLoginFeatureProvider;
+ }
}