Merge "Fix NPE of mDismissListener in Rename Dialog"
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 74cf865..8abdb09 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -2403,6 +2403,11 @@
</intent-filter>
</activity>
+ <activity android:name=".biometrics2.ui.view.FingerprintEnrollmentActivity"
+ android:exported="true"
+ android:permission="android.permission.MANAGE_FINGERPRINT"
+ android:theme="@style/GlifTheme.Light"/>
+
<activity android:name=".biometrics.fingerprint.FingerprintEnrollIntroductionInternal"
android:exported="false"
android:theme="@style/GlifTheme.Light"
diff --git a/OWNERS b/OWNERS
index cc33111..c8f206f 100644
--- a/OWNERS
+++ b/OWNERS
@@ -10,7 +10,6 @@
millchen@google.com
stanleytfwang@google.com
sunnyshao@google.com
-tmfang@google.com
yantingyang@google.com
ykhung@google.com
diff --git a/res-product/values/strings.xml b/res-product/values/strings.xml
index 18f81ac..93c2a1f 100644
--- a/res-product/values/strings.xml
+++ b/res-product/values/strings.xml
@@ -240,11 +240,11 @@
<!-- Message shown in SFPS enrollment dialog to locate the sensor (default) [CHAR LIMIT=NONE]-->
<string name="security_settings_sfps_enroll_find_sensor_message" product="default">The fingerprint sensor is on the power button. It’s the flat button next to the raised volume button on the edge of the phone.\n\nPressing the power button turns off the screen.</string>
<!-- Message shown in fingerprint enrollment dialog once enrollment is completed (tablet) [CHAR LIMIT=NONE] -->
- <string name="security_settings_fingerprint_enroll_finish_v2_message" product="tablet">Now you can use your fingerprint to unlock your tablet or verify it\u2019s you, like when you sign in to apps</string>
+ <string name="security_settings_fingerprint_enroll_finish_v2_message" product="tablet">Now you can use your fingerprint to unlock your tablet or verify it\u2019s you, like when you sign in to apps or approve a purchase</string>
<!-- Message shown in fingerprint enrollment dialog once enrollment is completed (device) [CHAR LIMIT=NONE] -->
- <string name="security_settings_fingerprint_enroll_finish_v2_message" product="device">Now you can use your fingerprint to unlock your device or verify it\u2019s you, like when you sign in to apps</string>
+ <string name="security_settings_fingerprint_enroll_finish_v2_message" product="device">Now you can use your fingerprint to unlock your device or verify it\u2019s you, like when you sign in to apps or approve a purchase</string>
<!-- Message shown in fingerprint enrollment dialog once enrollment is completed (default) [CHAR LIMIT=NONE] -->
- <string name="security_settings_fingerprint_enroll_finish_v2_message" product="default">Now you can use your fingerprint to unlock your phone or verify it\u2019s you, like when you sign in to apps</string>
+ <string name="security_settings_fingerprint_enroll_finish_v2_message" product="default">Now you can use your fingerprint to unlock your phone or verify it\u2019s you, like when you sign in to apps or approve a purchase</string>
<!-- Dialog text shown when the user tries to skip setting up a screen lock, warning that they can't continue to set up fingerprint. (tablet) [CHAR LIMIT=NONE] -->
<string name="lock_screen_pin_skip_message" product="tablet">A PIN protects the tablet if it\u2019s lost or stolen</string>
<!-- Dialog text shown when the user tries to skip setting up a screen lock, warning that they can't continue to set up fingerprint. (tablet) [CHAR LIMIT=NONE] -->
diff --git a/res/layout/biometric_enrollment_container.xml b/res/layout/biometric_enrollment_container.xml
new file mode 100644
index 0000000..b6ed0ae
--- /dev/null
+++ b/res/layout/biometric_enrollment_container.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.fragment.app.FragmentContainerView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/fragment_container_view"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" />
\ No newline at end of file
diff --git a/res/layout/sfps_enroll_finish_base.xml b/res/layout/sfps_enroll_finish_base.xml
index e7dbaba..f442e90 100644
--- a/res/layout/sfps_enroll_finish_base.xml
+++ b/res/layout/sfps_enroll_finish_base.xml
@@ -36,7 +36,7 @@
android:layout_marginTop="24dp"
android:paddingTop="0dp"
android:paddingBottom="0dp"
- android:gravity="center">
+ android:layout_gravity="center">
<ImageView
android:id="@+id/fingerprint_in_app_indicator"
diff --git a/res/values/config.xml b/res/values/config.xml
index d7b2afa..e7efa6f 100755
--- a/res/values/config.xml
+++ b/res/values/config.xml
@@ -615,6 +615,9 @@
<item>android.uid.system:1000</item>
</string-array>
+ <!-- The default value for RedactionInterstitial in SUW -->
+ <bool name="default_allow_sensitive_lockscreen_content">true</bool>
+
<!-- Whether to enable the app battery usage list page feature. -->
<bool name="config_app_battery_usage_list_enabled">false</bool>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 8319571..1f525e6 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -797,8 +797,6 @@
<string name="security_settings_udfps_enroll_progress_a11y_message">Enrolling fingerprint <xliff:g id="percentage" example="10">%d</xliff:g> percent</string>
<!-- Title shown in fingerprint enrollment dialog once enrollment is completed [CHAR LIMIT=29] -->
<string name="security_settings_fingerprint_enroll_finish_title">Fingerprint added</string>
- <!-- Message shown in SFPS enrollment dialog once enrollment is completed [CHAR LIMIT=NONE] -->
- <string name="security_settings_sfps_enroll_finish">Now you can use your fingerprint to unlock your tablet or verify it\u2019s you, like when you sign in to apps or approve a purchase</string>
<!-- Title for require screen on to auth toggle shown in fingerprint enrollment dialog once enrollment is completed. [CHAR LIMIT=NONE] -->
<string name="security_settings_require_screen_on_to_auth_title">Unlock only when screen is on</string>
<!-- Description for require screen on to auth toggle shown in fingerprint enrollment dialog once enrollment is completed. [CHAR LIMIT=NONE] -->
@@ -2269,19 +2267,19 @@
<string name="short_summary_font_size">Make text bigger or smaller</string>
<!-- SIM lock settings title [CHAR LIMIT=40] -->
- <string name="sim_lock_settings">SIM card lock settings</string>
- <!-- Security & screen lock settings screen, SIM card lock setting option name [CHAR LIMIT=40] -->
- <string name="sim_lock_settings_category">SIM card lock</string>
- <!-- SIM card lock settings screen, setting check box label [CHAR LIMIT=40] -->
- <string name="sim_pin_toggle">Lock SIM card</string>
+ <string name="sim_lock_settings">SIM lock settings</string>
+ <!-- Security & screen lock settings screen, SIM lock setting option name [CHAR LIMIT=40] -->
+ <string name="sim_lock_settings_category">SIM lock</string>
+ <!-- SIM lock settings screen, setting check box label [CHAR LIMIT=40] -->
+ <string name="sim_pin_toggle">Lock SIM</string>
<!-- SIM card lock settings screen, setting option name to change the SIM PIN [CHAR LIMIT=40] -->
<string name="sim_pin_change">Change SIM PIN</string>
<!-- SIM card lock settings screen, SIM PIN dialog message instruction [CHAR LIMIT=40] -->
<string name="sim_enter_pin">SIM PIN</string>
- <!-- SIM card lock settings screen, SIM PIN dialog message instruction [CHAR LIMIT=40] -->
- <string name="sim_enable_sim_lock">Lock SIM card</string>
- <!-- SIM card lock settings screen, SIM PIN dialog message instruction [CHAR LIMIT=40] -->
- <string name="sim_disable_sim_lock">Unlock SIM card</string>
+ <!-- SIM lock settings screen, SIM PIN dialog message instruction [CHAR LIMIT=40] -->
+ <string name="sim_enable_sim_lock">Lock SIM</string>
+ <!-- SIM lock settings screen, SIM PIN dialog message instruction [CHAR LIMIT=40] -->
+ <string name="sim_disable_sim_lock">Unlock SIM</string>
<!-- SIM card lock settings screen, SIM PIN dialog message instruction [CHAR LIMIT=40] -->
<string name="sim_enter_old">Old SIM PIN</string>
<!-- SIM card lock settings screen, SIM PIN dialog message instruction [CHAR LIMIT=40] -->
@@ -2321,7 +2319,7 @@
Use <xliff:g id="carrier" example="Verizon">%1$s</xliff:g>
</string>
<!-- Title for the dialog asking to user to change the preferred SIM [CHAR LIMIT=30] -->
- <string name="sim_preferred_title">Update preferred SIM card?</string>
+ <string name="sim_preferred_title">Update preferred SIM?</string>
<!-- Message for the dialog asking to user to change the preferred SIM [CHAR LIMIT=NONE] -->
<string name="sim_preferred_message"><xliff:g id="new_sim">%1$s</xliff:g> is the only SIM in your device. Do you want to use this SIM for mobile data, calls, and SMS messages?</string>
@@ -2690,7 +2688,7 @@
<!-- Confirmation button of dialog to confirm resetting user's app preferences [CHAR LIMIT=NONE] -->
<string name="erase_euicc_data_button">Erase</string>
<!-- Erase Euicc dialog and SD card & phone storage settings screen, title for the menu option and checkbox to let user decide whether erase eSIM data together [CHAR LIMIT=50] -->
- <string name="reset_esim_title">Erase downloaded SIMs</string>
+ <string name="reset_esim_title">Erase eSIMs</string>
<!-- Erase Euicc dialog and SD card & phone storage settings screen, message for the checkbox to let user decide whether erase eSIM data together [CHAR LIMIT=NONE] -->
<string name="reset_esim_desc">This won’t cancel any mobile service plans. To download replacement SIMs, contact your carrier.</string>
@@ -2699,7 +2697,7 @@
<!-- SD card & phone storage settings screen, message on screen after user selects Reset settings button -->
<string name="reset_network_final_desc">Reset all network settings? You can\u2019t undo this action.</string>
<!-- SD card & phone storage settings screen, message on screen after user selects Reset settings button [CHAR LIMIT=NONE] -->
- <string name="reset_network_final_desc_esim">Reset all network settings and erase downloaded SIMs? You can\u2019t undo this action.</string>
+ <string name="reset_network_final_desc_esim">Reset all network settings and erase eSIMs? You can\u2019t undo this action.</string>
<!-- SD card & phone storage settings screen, button on screen after user selects Reset settings button -->
<string name="reset_network_final_button_text">Reset settings</string>
<!-- Reset settings confirmation screen title [CHAR LIMIT=30] -->
@@ -2711,7 +2709,7 @@
<!-- Title of the error message shown when error happens during erase eSIM data [CHAR LIMIT=NONE] -->
<string name="reset_esim_error_title">Can\u2019t erase SIMs</string>
<!-- Message of the error message shown when error happens during erase eSIM data [CHAR LIMIT=NONE] -->
- <string name="reset_esim_error_msg">Downloaded SIMs can\u2019t be erased due to an error.\n\nRestart your device and try again.</string>
+ <string name="reset_esim_error_msg">eSIMs can\u2019t be erased due to an error.\n\nRestart your device and try again.</string>
<!-- Main Clear -->
<!-- Button title to factory data reset the entire device [CHAR LIMIT=NONE] -->
@@ -6279,7 +6277,7 @@
<string name="regulatory_info_text"></string>
<!-- Title for SIM settings title settings during Setup Wizard. [CHAR LIMIT=40] -->
- <string name="sim_settings_title">SIM cards</string>
+ <string name="sim_settings_title">SIMs</string>
<!-- Message that Cellular data is unavailable. [CHAR LIMIT=40] -->
<string name="sim_cellular_data_unavailable">Mobile data is unavailable</string>
<!-- Message summary that Cellular data is unavailable. [CHAR LIMIT=60] -->
@@ -6314,8 +6312,8 @@
<string name="sim_status_title_sim_slot">SIM status (sim slot %1$d)</string>
<!-- Summary text describing signal strength to the user. [CHAR LIMIT=60] -->
<string name="sim_signal_strength"><xliff:g id="dbm">%1$d</xliff:g> dBm <xliff:g id="asu">%2$d</xliff:g> asu</string>
- <!-- Title for SIM card notification. [CHAR LIMIT=40] -->
- <string name="sim_notification_title">SIM cards changed.</string>
+ <!-- Title for SIM notification. [CHAR LIMIT=40] -->
+ <string name="sim_notification_title">SIMs changed.</string>
<!-- Message under title informing the user to touch to go to SIM Cards in Settings. [CHAR LIMIT=40] -->
<string name="sim_notification_summary">Tap to set up</string>
@@ -8014,7 +8012,7 @@
<!-- [CHAR LIMIT=60] turn eSim deletion confirmation on/off -->
<string name="confirm_sim_deletion_title">Confirm SIM deletion</string>
<!-- [CHAR LIMIT=NONE] eSim deletion confirmation description -->
- <string name="confirm_sim_deletion_description">Verify it\u0027s you before erasing a downloaded SIM</string>
+ <string name="confirm_sim_deletion_description">Verify it\u0027s you before erasing a eSIM</string>
<!-- TODO(b/258550150): Finalize all strings in this section and remove translatable="false" -->
<!-- [CHAR LIMIT=32] Name of Advanced memory protection page in "More Security Settings" and heading of page. -->
@@ -10086,7 +10084,7 @@
</string>
<!-- Mobile network setting screen, summary of Mobile data switch preference when the network
is unavailable, the preference selection will be disabled. [CHAR LIMIT=NONE] -->
- <string name="mobile_data_settings_summary_unavailable">No SIM card available</string>
+ <string name="mobile_data_settings_summary_unavailable">No SIM available</string>
<!-- Mobile network settings screen, title of item showing the name of the default subscription
that will be used for calls. This only appears in multi-SIM mode. [CHAR LIMIT=NONE] -->
@@ -10140,10 +10138,10 @@
<string name="mobile_network_inactive_sim">Inactive / SIM</string>
<!-- Summary for an item in the page listing multiple mobile service subscriptions, indicating
that service is active and is tied to an eSIM profile [CHAR LIMIT=40] -->
- <string name="mobile_network_active_esim">Active / Downloaded SIM</string>
+ <string name="mobile_network_active_esim">Active / eSIM</string>
<!-- Summary for an item in the page listing multiple mobile service subscriptions, indicating
that service is inactive and is tied to an eSIM profile [CHAR LIMIT=40] -->
- <string name="mobile_network_inactive_esim">Inactive / Downloaded SIM</string>
+ <string name="mobile_network_inactive_esim">Inactive / eSIM</string>
<!-- Title of a dialog that lets a user modify the display name used for a mobile network
subscription in various places in the Settings app. The default name is typically just the
carrier name, but especially in multi-SIM configurations users may want to use a different
@@ -10243,8 +10241,8 @@
<string name="sim_action_switch_sub_dialog_mep_title">Use <xliff:g id="carrier_name" example="Google Fi">%1$s</xliff:g>?</string>
<!-- Body text of confirmation dialog for switching subscription that involves switching SIM slots. Indicates that only one SIM can be active at a time. Also that switching will not cancel the user's mobile service plan. [CHAR_LIMIT=NONE] -->
<string name="sim_action_switch_sub_dialog_text">Only one SIM can be active at a time.\n\nSwitching to <xliff:g id="to_carrier_name" example="Google Fi">%1$s</xliff:g> won\u2019t cancel your <xliff:g id="from_carrier_name" example="Sprint">%2$s</xliff:g> service.</string>
- <!-- Body text of confirmation dialog for switching subscription between two eSIM profiles. Indicates that only one downloaded SIM can be active at a time. Also that switching will not cancel the user's mobile service plan. [CHAR_LIMIT=NONE] -->
- <string name="sim_action_switch_sub_dialog_text_downloaded">Only one downloaded SIM can be active at a time.\n\nSwitching to <xliff:g id="to_carrier_name" example="Google Fi">%1$s</xliff:g> won\u2019t cancel your <xliff:g id="from_carrier_name" example="Sprint">%2$s</xliff:g> service.</string>
+ <!-- Body text of confirmation dialog for switching subscription between two eSIM profiles. Indicates that only 1 eSIM can be active at a time. Also that switching will not cancel the user's mobile service plan. [CHAR_LIMIT=NONE] -->
+ <string name="sim_action_switch_sub_dialog_text_downloaded">Only 1 eSIM can be active at a time.\n\nSwitching to <xliff:g id="to_carrier_name" example="Google Fi">%1$s</xliff:g> won\u2019t cancel your <xliff:g id="from_carrier_name" example="Sprint">%2$s</xliff:g> service.</string>
<!-- Body text of confirmation dialog for switching subscription between two eSIM profiles. Indicates that only one SIM can be active at a time. Also that switching will not cancel the user's mobile service plan. [CHAR_LIMIT=NONE] -->
<string name="sim_action_switch_sub_dialog_text_single_sim">Only one SIM can be active at a time.\n\nSwitching won\u2019t cancel your <xliff:g id="to_carrier_name" example="Google Fi">%1$s</xliff:g> service.</string>
<!-- Body text of confirmation dialog for switching subscription between two eSIM profiles. Indicates that only one downloaded SIM can be active at a time. Also that switching will not cancel the user's mobile service plan. [CHAR_LIMIT=NONE] -->
@@ -10338,12 +10336,12 @@
<!-- The body text of skip sim switch dialog. [CHAR LIMIT=NONE] -->
<string name="switch_sim_dialog_no_switch_text">To use mobile data, call features, and SMS at a later time, go to your network settings</string>
- <!-- Button label of the removable sim card. [CHAR LIMIT=NONE] -->
- <string name="sim_card_label">SIM card</string>
+ <!-- Button label of the removable sim. [CHAR LIMIT=NONE] -->
+ <string name="sim_card_label">SIM</string>
<!-- Strings for deleting eUICC subscriptions dialog activity -->
- <!-- Title on confirmation dialog asking the user if they want to erase the downloaded SIM from the device. [CHAR_LIMIT=NONE] -->
- <string name="erase_sim_dialog_title">Erase this downloaded SIM?</string>
+ <!-- Title on confirmation dialog asking the user if they want to erase the eSIM from the device. [CHAR_LIMIT=NONE] -->
+ <string name="erase_sim_dialog_title">Erase this eSIM?</string>
<!-- Body text in confirmation dialog indicating what erasing a SIM entails. [CHAR_LIMIT=NONE] -->
<string name="erase_sim_dialog_text">Erasing this SIM removes <xliff:g id="carrier_name_a" example="Google Fi">%1$s</xliff:g> service from this device.\n\nService for <xliff:g id="carrier_name_b" example="Google Fi">%1$s</xliff:g> won\'t be canceled.</string>
<!-- Button label to erase the eSIM [CHAR_LIMIT=20] -->
@@ -10717,10 +10715,10 @@
<string name="carrier_wifi_network_title">W+ network</string>
<!-- Provider Model: title of SIM category -->
<string name="sim_category_title">SIM</string>
- <!-- Provider Model: title of Downloaded SIM category. [CHAR LIMIT=50] -->
- <string name="downloaded_sim_category_title">DOWNLOADED SIM</string>
- <!-- Provider Model: title of Downloaded SIMs category. [CHAR LIMIT=50] -->
- <string name="downloaded_sims_category_title">DOWNLOADED SIMS</string>
+ <!-- Provider Model: title of eSIM category. [CHAR LIMIT=50] -->
+ <string name="downloaded_sim_category_title">eSIM</string>
+ <!-- Provider Model: title of eSIMs category. [CHAR LIMIT=50] -->
+ <string name="downloaded_sims_category_title">eSIMs</string>
<!-- Provider Model: summary of Active in SIM category. [CHAR LIMIT=50] -->
<string name="sim_category_active_sim">Active</string>
<!-- Provider Model: summary of Inactive in SIM category. [CHAR LIMIT=50] -->
@@ -10850,7 +10848,7 @@
<!-- Dialog title for smart forwarding failed [CHAR LIMIT=50]-->
<string name="smart_forwarding_failed_title">Call Settings error</string>
<!-- Subtext for smart forwarding failed [CHAR LIMIT=50]-->
- <string name="smart_forwarding_failed_text">Network or SIM card error.</string>
+ <string name="smart_forwarding_failed_text">Network or SIM error.</string>
<!-- Subtext for sim is not activated [CHAR LIMIT=50]-->
<string name="smart_forwarding_failed_not_activated_text">Sim is not activated.</string>
<!-- Title when smart forwarding can't get the phone number [CHAR LIMIT=50]-->
@@ -10911,14 +10909,14 @@
<!-- Content description of preview pager in colors preview -->
<string name="colors_viewpager_content_description">Color preview</string>
- <!-- Bluetooth sim card permission alert for notification title [CHAR LIMIT=none] -->
- <string name="bluetooth_sim_card_access_notification_title">SIM card access request</string>
- <!-- Bluetooth sim card permission alert for notification content [CHAR LIMIT=none] -->
- <string name="bluetooth_sim_card_access_notification_content">A device wants to access your SIM card. Tap for details.</string>
- <!-- Bluetooth sim card permission alert for dialog title [CHAR LIMIT=none] -->
- <string name="bluetooth_sim_card_access_dialog_title">Allow access to SIM card?</string>
- <!-- Bluetooth sim card permission alert for dialog content [CHAR LIMIT=none] -->
- <string name="bluetooth_sim_card_access_dialog_content">A Bluetooth device, <xliff:g id="device_name" example="My device">%1$s</xliff:g>, wants to access data on your SIM card. This includes your contacts.\n\nWhile connected, <xliff:g id="device_name" example="My device">%2$s</xliff:g> will receive all calls made to <xliff:g id="phone_number" example="0912345678">%3$s</xliff:g>.</string>
+ <!-- Bluetooth sim permission alert for notification title [CHAR LIMIT=none] -->
+ <string name="bluetooth_sim_card_access_notification_title">SIM access request</string>
+ <!-- Bluetooth sim permission alert for notification content [CHAR LIMIT=none] -->
+ <string name="bluetooth_sim_card_access_notification_content">A device wants to access your SIM. Tap for details.</string>
+ <!-- Bluetooth sim permission alert for dialog title [CHAR LIMIT=none] -->
+ <string name="bluetooth_sim_card_access_dialog_title">Allow access to SIM?</string>
+ <!-- Bluetooth sim permission alert for dialog content [CHAR LIMIT=none] -->
+ <string name="bluetooth_sim_card_access_dialog_content">A Bluetooth device, <xliff:g id="device_name" example="My device">%1$s</xliff:g>, wants to access data from your SIM. This includes your contacts.\n\nWhile connected, <xliff:g id="device_name" example="My device">%2$s</xliff:g> will receive all calls made to <xliff:g id="phone_number" example="0912345678">%3$s</xliff:g>.</string>
<!-- Bluetooth connect permission alert for notification title [CHAR LIMIT=none] -->
<string name="bluetooth_connect_access_notification_title">Bluetooth device available</string>
<!-- Bluetooth connect permission alert for notification content [CHAR LIMIT=none] -->
diff --git a/src/com/android/settings/SettingsActivity.java b/src/com/android/settings/SettingsActivity.java
index 1553a54..076d0e1 100644
--- a/src/com/android/settings/SettingsActivity.java
+++ b/src/com/android/settings/SettingsActivity.java
@@ -24,6 +24,7 @@
import android.app.ActionBar;
import android.app.ActivityManager;
+import android.app.settings.SettingsEnums;
import android.content.ActivityNotFoundException;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
@@ -230,11 +231,32 @@
@Override
public SharedPreferences getSharedPreferences(String name, int mode) {
- if (name.equals(getPackageName() + "_preferences")) {
- return new SharedPreferencesLogger(this, getMetricsTag(),
- FeatureFactory.getFactory(this).getMetricsFeatureProvider());
+ if (!TextUtils.equals(name, getPackageName() + "_preferences")) {
+ return super.getSharedPreferences(name, mode);
}
- return super.getSharedPreferences(name, mode);
+
+ String tag = getMetricsTag();
+
+ return new SharedPreferencesLogger(this, tag,
+ FeatureFactory.getFactory(this).getMetricsFeatureProvider(),
+ lookupMetricsCategory());
+ }
+
+ private int lookupMetricsCategory() {
+ int category = SettingsEnums.PAGE_UNKNOWN;
+ Bundle args = null;
+ if (getIntent() != null) {
+ args = getIntent().getBundleExtra(EXTRA_SHOW_FRAGMENT_ARGUMENTS);
+ }
+
+ Fragment fragment = Utils.getTargetFragment(this, getMetricsTag(), args);
+
+ if (fragment instanceof Instrumentable) {
+ category = ((Instrumentable) fragment).getMetricsCategory();
+ }
+ Log.d(LOG_TAG, "MetricsCategory is " + category);
+
+ return category;
}
private String getMetricsTag() {
@@ -242,13 +264,11 @@
if (getIntent() != null && getIntent().hasExtra(EXTRA_SHOW_FRAGMENT)) {
tag = getInitialFragmentName(getIntent());
}
+
if (TextUtils.isEmpty(tag)) {
Log.w(LOG_TAG, "MetricsTag is invalid " + tag);
tag = getClass().getName();
}
- if (tag.startsWith("com.android.settings.")) {
- tag = tag.replace("com.android.settings.", "");
- }
return tag;
}
@@ -320,7 +340,7 @@
}
mMainSwitch = findViewById(R.id.switch_bar);
if (mMainSwitch != null) {
- mMainSwitch.setMetricsTag(getMetricsTag());
+ mMainSwitch.setMetricsCategory(lookupMetricsCategory());
mMainSwitch.setTranslationZ(findViewById(R.id.main_content).getTranslationZ() + 1);
}
diff --git a/src/com/android/settings/activityembedding/ActivityEmbeddingRulesController.java b/src/com/android/settings/activityembedding/ActivityEmbeddingRulesController.java
index ad0d4ea..0bd9996 100644
--- a/src/com/android/settings/activityembedding/ActivityEmbeddingRulesController.java
+++ b/src/com/android/settings/activityembedding/ActivityEmbeddingRulesController.java
@@ -39,6 +39,7 @@
import com.android.settings.biometrics.fingerprint.FingerprintEnrollEnrolling;
import com.android.settings.biometrics.fingerprint.FingerprintEnrollIntroduction;
import com.android.settings.biometrics.fingerprint.FingerprintEnrollIntroductionInternal;
+import com.android.settings.biometrics2.ui.view.FingerprintEnrollmentActivity;
import com.android.settings.core.FeatureFlags;
import com.android.settings.homepage.DeepLinkHomepageActivity;
import com.android.settings.homepage.DeepLinkHomepageActivityInternal;
@@ -225,6 +226,7 @@
.buildSearchIntent(mContext, SettingsEnums.SETTINGS_HOMEPAGE);
addActivityFilter(activityFilters, searchIntent);
}
+ addActivityFilter(activityFilters, FingerprintEnrollmentActivity.class);
addActivityFilter(activityFilters, FingerprintEnrollIntroduction.class);
addActivityFilter(activityFilters, FingerprintEnrollIntroductionInternal.class);
addActivityFilter(activityFilters, FingerprintEnrollEnrolling.class);
diff --git a/src/com/android/settings/biometrics/BiometricUtils.java b/src/com/android/settings/biometrics/BiometricUtils.java
index f395aca..5c5ff99 100644
--- a/src/com/android/settings/biometrics/BiometricUtils.java
+++ b/src/com/android/settings/biometrics/BiometricUtils.java
@@ -25,7 +25,9 @@
import android.hardware.biometrics.SensorProperties;
import android.hardware.face.FaceManager;
import android.hardware.face.FaceSensorPropertiesInternal;
+import android.os.Bundle;
import android.os.storage.StorageManager;
+import android.util.FeatureFlagUtils;
import android.util.Log;
import android.view.Surface;
@@ -40,6 +42,7 @@
import com.android.settings.biometrics.fingerprint.FingerprintEnrollFindSensor;
import com.android.settings.biometrics.fingerprint.FingerprintEnrollIntroduction;
import com.android.settings.biometrics.fingerprint.SetupFingerprintEnrollIntroduction;
+import com.android.settings.biometrics2.ui.view.FingerprintEnrollmentActivity;
import com.android.settings.password.ChooseLockGeneric;
import com.android.settings.password.ChooseLockSettingsHelper;
import com.android.settings.password.SetupChooseLockGeneric;
@@ -147,6 +150,31 @@
/**
* @param context caller's context
+ * @param isSuw if it is running in setup wizard flows
+ * @param suwExtras setup wizard extras for new intent
+ * @return Intent for starting ChooseLock*
+ */
+ public static Intent getChooseLockIntent(@NonNull Context context,
+ boolean isSuw, @NonNull Bundle suwExtras) {
+ if (isSuw) {
+ // Default to PIN lock in setup wizard
+ Intent intent = new Intent(context, SetupChooseLockGeneric.class);
+ if (StorageManager.isFileEncrypted()) {
+ intent.putExtra(
+ LockPatternUtils.PASSWORD_TYPE_KEY,
+ DevicePolicyManager.PASSWORD_QUALITY_NUMERIC);
+ intent.putExtra(ChooseLockGeneric.ChooseLockGenericFragment
+ .EXTRA_SHOW_OPTIONS_BUTTON, true);
+ }
+ intent.putExtras(suwExtras);
+ return intent;
+ } else {
+ return new Intent(context, ChooseLockGeneric.class);
+ }
+ }
+
+ /**
+ * @param context caller's context
* @param activityIntent The intent that started the caller's activity
* @return Intent for starting FingerprintEnrollFindSensor
*/
@@ -164,7 +192,13 @@
*/
public static Intent getFingerprintIntroIntent(@NonNull Context context,
@NonNull Intent activityIntent) {
- if (WizardManagerHelper.isAnySetupWizard(activityIntent)) {
+ if (FeatureFlagUtils.isEnabled(context, FeatureFlagUtils.SETTINGS_BIOMETRICS2_ENROLLMENT)) {
+ final Intent intent = new Intent(context, FingerprintEnrollmentActivity.class);
+ if (WizardManagerHelper.isAnySetupWizard(activityIntent)) {
+ WizardManagerHelper.copyWizardManagerExtras(activityIntent, intent);
+ }
+ return intent;
+ } else if (WizardManagerHelper.isAnySetupWizard(activityIntent)) {
Intent intent = new Intent(context, SetupFingerprintEnrollIntroduction.class);
WizardManagerHelper.copyWizardManagerExtras(activityIntent, intent);
return intent;
diff --git a/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollFinish.java b/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollFinish.java
index d4b1eb0..b8ec5e6 100644
--- a/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollFinish.java
+++ b/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollFinish.java
@@ -69,11 +69,7 @@
setContentView(R.layout.fingerprint_enroll_finish);
}
setHeaderText(R.string.security_settings_fingerprint_enroll_finish_title);
- if (mCanAssumeSfps) {
- setDescriptionText(R.string.security_settings_sfps_enroll_finish);
- } else {
- setDescriptionText(R.string.security_settings_fingerprint_enroll_finish_v2_message);
- }
+ setDescriptionText(R.string.security_settings_fingerprint_enroll_finish_v2_message);
mFooterBarMixin = getLayout().getMixin(FooterBarMixin.class);
mFooterBarMixin.setSecondaryButton(
diff --git a/src/com/android/settings/biometrics/fingerprint/FingerprintSettings.java b/src/com/android/settings/biometrics/fingerprint/FingerprintSettings.java
index ada7ba4..dddef4e 100644
--- a/src/com/android/settings/biometrics/fingerprint/FingerprintSettings.java
+++ b/src/com/android/settings/biometrics/fingerprint/FingerprintSettings.java
@@ -42,6 +42,7 @@
import android.text.InputFilter;
import android.text.Spanned;
import android.text.TextUtils;
+import android.util.FeatureFlagUtils;
import android.util.Log;
import android.view.View;
import android.widget.ImeAwareEditText;
@@ -63,6 +64,7 @@
import com.android.settings.Utils;
import com.android.settings.biometrics.BiometricEnrollBase;
import com.android.settings.biometrics.BiometricUtils;
+import com.android.settings.biometrics2.ui.view.FingerprintEnrollmentActivity;
import com.android.settings.core.SettingsBaseActivity;
import com.android.settings.core.instrumentation.InstrumentedDialogFragment;
import com.android.settings.dashboard.DashboardFragment;
@@ -887,7 +889,11 @@
private void addFirstFingerprint() {
Intent intent = new Intent();
intent.setClassName(SETTINGS_PACKAGE_NAME,
- FingerprintEnrollIntroductionInternal.class.getName());
+ FeatureFlagUtils.isEnabled(getActivity(),
+ FeatureFlagUtils.SETTINGS_BIOMETRICS2_ENROLLMENT)
+ ? FingerprintEnrollmentActivity.class.getName()
+ : FingerprintEnrollIntroductionInternal.class.getName()
+ );
intent.putExtra(EXTRA_FROM_SETTINGS_SUMMARY, true);
intent.putExtra(SettingsBaseActivity.EXTRA_PAGE_TRANSITION_TYPE,
diff --git a/src/com/android/settings/biometrics/fingerprint/SetupFingerprintEnrollIntroduction.java b/src/com/android/settings/biometrics/fingerprint/SetupFingerprintEnrollIntroduction.java
index b313961..eb68687 100644
--- a/src/com/android/settings/biometrics/fingerprint/SetupFingerprintEnrollIntroduction.java
+++ b/src/com/android/settings/biometrics/fingerprint/SetupFingerprintEnrollIntroduction.java
@@ -32,7 +32,7 @@
/**
* Returns the number of fingerprint enrolled.
*/
- private static final String EXTRA_FINGERPRINT_ENROLLED_COUNT = "fingerprint_enrolled_count";
+ public static final String EXTRA_FINGERPRINT_ENROLLED_COUNT = "fingerprint_enrolled_count";
private static final String KEY_LOCK_SCREEN_PRESENT = "wasLockScreenPresent";
diff --git a/src/com/android/settings/biometrics2/data/repository/FingerprintRepository.java b/src/com/android/settings/biometrics2/data/repository/FingerprintRepository.java
new file mode 100644
index 0000000..f58175a
--- /dev/null
+++ b/src/com/android/settings/biometrics2/data/repository/FingerprintRepository.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.biometrics2.data.repository;
+
+import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FINGERPRINT;
+
+import android.app.admin.DevicePolicyManager;
+import android.content.Context;
+import android.content.res.Resources;
+import android.hardware.fingerprint.Fingerprint;
+import android.hardware.fingerprint.FingerprintManager;
+import android.hardware.fingerprint.FingerprintSensorPropertiesInternal;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.settings.R;
+import com.android.settings.biometrics.ParentalControlsUtils;
+import com.android.settingslib.RestrictedLockUtilsInternal;
+
+import java.util.List;
+
+/**
+ * This repository is used to call all APIs in {@link FingerprintManager}
+ */
+public class FingerprintRepository {
+
+ @NonNull private final FingerprintManager mFingerprintManager;
+
+ public FingerprintRepository(@NonNull FingerprintManager fingerprintManager) {
+ mFingerprintManager = fingerprintManager;
+ }
+
+ /**
+ * The first sensor type is UDFPS sensor or not
+ */
+ public boolean canAssumeUdfps() {
+ FingerprintSensorPropertiesInternal prop = getFirstFingerprintSensorPropertiesInternal();
+ return prop != null && prop.isAnyUdfpsType();
+ }
+
+ /**
+ * Get max possible number of fingerprints for a user
+ */
+ public int getMaxFingerprints() {
+ FingerprintSensorPropertiesInternal prop = getFirstFingerprintSensorPropertiesInternal();
+ return prop != null ? prop.maxEnrollmentsPerUser : 0;
+ }
+
+ /**
+ * Get number of fingerprints that this user enrolled.
+ */
+ public int getNumOfEnrolledFingerprintsSize(int userId) {
+ final List<Fingerprint> list = mFingerprintManager.getEnrolledFingerprints(userId);
+ return list != null ? list.size() : 0;
+ }
+
+ /**
+ * Get maximum possible fingerprints in setup wizard flow
+ */
+ public int getMaxFingerprintsInSuw(@NonNull Resources resources) {
+ return resources.getInteger(R.integer.suw_max_fingerprints_enrollable);
+ }
+
+ @Nullable
+ private FingerprintSensorPropertiesInternal getFirstFingerprintSensorPropertiesInternal() {
+ final List<FingerprintSensorPropertiesInternal> props =
+ mFingerprintManager.getSensorPropertiesInternal();
+ return props.size() > 0 ? props.get(0) : null;
+ }
+
+ /**
+ * Call FingerprintManager to generate challenge for first sensor
+ */
+ public void generateChallenge(int userId,
+ @NonNull FingerprintManager.GenerateChallengeCallback callback) {
+ mFingerprintManager.generateChallenge(userId, callback);
+ }
+
+ /**
+ * Get parental consent required or not during enrollment process
+ */
+ public boolean isParentalConsentRequired(@NonNull Context context) {
+ return ParentalControlsUtils.parentConsentRequired(context, TYPE_FINGERPRINT) != null;
+ }
+
+ /**
+ * Get fingerprint is disable by admin or not
+ */
+ public boolean isDisabledByAdmin(@NonNull Context context, int userId) {
+ return RestrictedLockUtilsInternal.checkIfKeyguardFeaturesDisabled(
+ context, DevicePolicyManager.KEYGUARD_DISABLE_FINGERPRINT, userId) != null;
+ }
+}
diff --git a/src/com/android/settings/biometrics2/factory/BiometricsFragmentFactory.java b/src/com/android/settings/biometrics2/factory/BiometricsFragmentFactory.java
new file mode 100644
index 0000000..9a0cab2
--- /dev/null
+++ b/src/com/android/settings/biometrics2/factory/BiometricsFragmentFactory.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.biometrics2.factory;
+
+import android.app.Application;
+import android.app.admin.DevicePolicyManager;
+
+import androidx.annotation.NonNull;
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentFactory;
+import androidx.lifecycle.ViewModelProvider;
+
+import com.android.settings.biometrics2.ui.view.FingerprintEnrollIntroFragment;
+
+/**
+ * Fragment factory for biometrics
+ */
+public class BiometricsFragmentFactory extends FragmentFactory {
+
+ private final Application mApplication;
+ private final ViewModelProvider mViewModelProvider;
+
+ public BiometricsFragmentFactory(Application application,
+ ViewModelProvider viewModelProvider) {
+ mApplication = application;
+ mViewModelProvider = viewModelProvider;
+ }
+
+ @NonNull
+ @Override
+ public Fragment instantiate(@NonNull ClassLoader classLoader, @NonNull String className) {
+ final Class<? extends Fragment> clazz = loadFragmentClass(classLoader, className);
+ if (FingerprintEnrollIntroFragment.class.equals(clazz)) {
+ final DevicePolicyManager devicePolicyManager =
+ mApplication.getSystemService(DevicePolicyManager.class);
+ if (devicePolicyManager != null) {
+ return new FingerprintEnrollIntroFragment(mViewModelProvider,
+ devicePolicyManager.getResources());
+ }
+ }
+ return super.instantiate(classLoader, className);
+ }
+}
diff --git a/src/com/android/settings/biometrics2/factory/BiometricsRepositoryProvider.java b/src/com/android/settings/biometrics2/factory/BiometricsRepositoryProvider.java
new file mode 100644
index 0000000..fdc5745
--- /dev/null
+++ b/src/com/android/settings/biometrics2/factory/BiometricsRepositoryProvider.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.biometrics2.factory;
+
+import android.app.Application;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.settings.biometrics2.data.repository.FingerprintRepository;
+
+/**
+ * Interface for BiometricsRepositoryProvider
+ */
+public interface BiometricsRepositoryProvider {
+
+ /**
+ * Get FingerprintRepository
+ */
+ @Nullable
+ FingerprintRepository getFingerprintRepository(@NonNull Application application);
+}
diff --git a/src/com/android/settings/biometrics2/factory/BiometricsRepositoryProviderImpl.java b/src/com/android/settings/biometrics2/factory/BiometricsRepositoryProviderImpl.java
new file mode 100644
index 0000000..87b41e9
--- /dev/null
+++ b/src/com/android/settings/biometrics2/factory/BiometricsRepositoryProviderImpl.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.biometrics2.factory;
+
+import android.app.Application;
+import android.hardware.fingerprint.FingerprintManager;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.settings.Utils;
+import com.android.settings.biometrics2.data.repository.FingerprintRepository;
+
+/**
+ * Implementation for BiometricsRepositoryProvider
+ */
+public class BiometricsRepositoryProviderImpl implements BiometricsRepositoryProvider {
+
+ /**
+ * Get FingerprintRepository
+ */
+ @Nullable
+ @Override
+ public FingerprintRepository getFingerprintRepository(@NonNull Application application) {
+ final FingerprintManager fingerprintManager =
+ Utils.getFingerprintManagerOrNull(application);
+ if (fingerprintManager == null) {
+ return null;
+ }
+ return new FingerprintRepository(fingerprintManager);
+ }
+}
diff --git a/src/com/android/settings/biometrics2/factory/BiometricsViewModelFactory.java b/src/com/android/settings/biometrics2/factory/BiometricsViewModelFactory.java
new file mode 100644
index 0000000..477fdb6
--- /dev/null
+++ b/src/com/android/settings/biometrics2/factory/BiometricsViewModelFactory.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.biometrics2.factory;
+
+import android.app.Application;
+import android.app.KeyguardManager;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.lifecycle.ViewModel;
+import androidx.lifecycle.ViewModelProvider;
+import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory;
+import androidx.lifecycle.viewmodel.CreationExtras;
+
+import com.android.internal.widget.LockPatternUtils;
+import com.android.settings.biometrics2.data.repository.FingerprintRepository;
+import com.android.settings.biometrics2.ui.viewmodel.AutoCredentialViewModel;
+import com.android.settings.biometrics2.ui.viewmodel.AutoCredentialViewModel.ChallengeGenerator;
+import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollIntroViewModel;
+import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollmentViewModel;
+import com.android.settings.overlay.FeatureFactory;
+
+/**
+ * View model factory for biometric enrollment fragment
+ */
+public class BiometricsViewModelFactory implements ViewModelProvider.Factory {
+
+ private static final String TAG = "BiometricsViewModelFact";
+
+ public static final CreationExtras.Key<ChallengeGenerator> CHALLENGE_GENERATOR =
+ new CreationExtras.Key<>() {};
+
+ @NonNull
+ @Override
+ @SuppressWarnings("unchecked")
+ public <T extends ViewModel> T create(@NonNull Class<T> modelClass,
+ @NonNull CreationExtras extras) {
+ final Application application = extras.get(AndroidViewModelFactory.APPLICATION_KEY);
+
+ if (application == null) {
+ Log.w(TAG, "create, null application");
+ return create(modelClass);
+ }
+ final FeatureFactory featureFactory = FeatureFactory.getFactory(application);
+ final BiometricsRepositoryProvider provider = FeatureFactory.getFactory(application)
+ .getBiometricsRepositoryProvider();
+
+ if (modelClass.isAssignableFrom(FingerprintEnrollIntroViewModel.class)) {
+ final FingerprintRepository repository = provider.getFingerprintRepository(application);
+ if (repository != null) {
+ return (T) new FingerprintEnrollIntroViewModel(application, repository);
+ }
+ } else if (modelClass.isAssignableFrom(FingerprintEnrollmentViewModel.class)) {
+ final FingerprintRepository repository = provider.getFingerprintRepository(application);
+ if (repository != null) {
+ return (T) new FingerprintEnrollmentViewModel(application, repository,
+ application.getSystemService(KeyguardManager.class));
+ }
+ } else if (modelClass.isAssignableFrom(AutoCredentialViewModel.class)) {
+ final LockPatternUtils lockPatternUtils =
+ featureFactory.getSecurityFeatureProvider().getLockPatternUtils(application);
+ final ChallengeGenerator challengeGenerator = extras.get(CHALLENGE_GENERATOR);
+ if (challengeGenerator != null) {
+ return (T) new AutoCredentialViewModel(application, lockPatternUtils,
+ challengeGenerator);
+ }
+ }
+ return create(modelClass);
+ }
+}
diff --git a/src/com/android/settings/biometrics2/ui/model/CredentialModel.java b/src/com/android/settings/biometrics2/ui/model/CredentialModel.java
new file mode 100644
index 0000000..06caf5e
--- /dev/null
+++ b/src/com/android/settings/biometrics2/ui/model/CredentialModel.java
@@ -0,0 +1,202 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.biometrics2.ui.model;
+
+import static com.android.settings.biometrics.BiometricEnrollBase.EXTRA_KEY_CHALLENGE;
+import static com.android.settings.biometrics.BiometricEnrollBase.EXTRA_KEY_SENSOR_ID;
+import static com.android.settings.password.ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN;
+import static com.android.settings.password.ChooseLockSettingsHelper.EXTRA_KEY_GK_PW_HANDLE;
+
+import android.content.Intent;
+import android.os.UserHandle;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+
+import java.time.Clock;
+
+/**
+ * Secret credential data including
+ * 1. userId
+ * 2. sensorId
+ * 3. challenge
+ * 4. token
+ * 5. gkPwHandle
+ */
+public final class CredentialModel {
+
+ /**
+ * Default value for an invalid challenge
+ */
+ @VisibleForTesting
+ public static final long INVALID_CHALLENGE = -1L;
+
+ /**
+ * Default value if GkPwHandle is invalid.
+ */
+ public static final long INVALID_GK_PW_HANDLE = 0L;
+
+ /**
+ * Default value for a invalid sensor id
+ */
+ @VisibleForTesting
+ public static final int INVALID_SENSOR_ID = -1;
+
+ private final Clock mClock;
+
+ private final long mInitMillis;
+
+ private final int mUserId;
+
+ private int mSensorId;
+ @Nullable
+ private Long mUpdateSensorIdMillis = null;
+
+ private long mChallenge;
+ @Nullable
+ private Long mUpdateChallengeMillis = null;
+
+ @Nullable
+ private byte[] mToken;
+ @Nullable
+ private Long mUpdateTokenMillis = null;
+
+ private long mGkPwHandle;
+ @Nullable
+ private Long mClearGkPwHandleMillis = null;
+
+ public CredentialModel(@NonNull Intent intent, @NonNull Clock clock) {
+ mUserId = intent.getIntExtra(Intent.EXTRA_USER_ID, UserHandle.myUserId());
+ mSensorId = intent.getIntExtra(EXTRA_KEY_SENSOR_ID, INVALID_SENSOR_ID);
+ mChallenge = intent.getLongExtra(EXTRA_KEY_CHALLENGE, INVALID_CHALLENGE);
+ mToken = intent.getByteArrayExtra(EXTRA_KEY_CHALLENGE_TOKEN);
+ mGkPwHandle = intent.getLongExtra(EXTRA_KEY_GK_PW_HANDLE,
+ INVALID_GK_PW_HANDLE);
+ mClock = clock;
+ mInitMillis = mClock.millis();
+ }
+
+ /**
+ * Get userId for this credential
+ */
+ public int getUserId() {
+ return mUserId;
+ }
+
+ /**
+ * Check user id is valid or not
+ */
+ public static boolean isValidUserId(int userId) {
+ return userId != UserHandle.USER_NULL;
+ }
+
+ /**
+ * Get challenge
+ */
+ public long getChallenge() {
+ return mChallenge;
+ }
+
+ /**
+ * Set challenge
+ */
+ public void setChallenge(long value) {
+ mUpdateChallengeMillis = mClock.millis();
+ mChallenge = value;
+ }
+
+ /**
+ * Get challenge token
+ */
+ @Nullable
+ public byte[] getToken() {
+ return mToken;
+ }
+
+ /**
+ * Set challenge token
+ */
+ public void setToken(@Nullable byte[] value) {
+ mUpdateTokenMillis = mClock.millis();
+ mToken = value;
+ }
+
+ /**
+ * Check challengeToken is valid or not
+ */
+ public static boolean isValidToken(@Nullable byte[] token) {
+ return token != null;
+ }
+
+ /**
+ * Get gatekeeper password handle
+ */
+ public long getGkPwHandle() {
+ return mGkPwHandle;
+ }
+
+ /**
+ * Clear gatekeeper password handle data
+ */
+ public void clearGkPwHandle() {
+ mClearGkPwHandleMillis = mClock.millis();
+ mGkPwHandle = INVALID_GK_PW_HANDLE;
+ }
+
+ /**
+ * Check gkPwHandle is valid or not
+ */
+ public static boolean isValidGkPwHandle(long gkPwHandle) {
+ return gkPwHandle != INVALID_GK_PW_HANDLE;
+ }
+
+ /**
+ * Get sensor id
+ */
+ public int getSensorId() {
+ return mSensorId;
+ }
+
+ /**
+ * Set sensor id
+ */
+ public void setSensorId(int value) {
+ mUpdateSensorIdMillis = mClock.millis();
+ mSensorId = value;
+ }
+
+ /**
+ * Returns a string representation of the object
+ */
+ @Override
+ public String toString() {
+ final int gkPwHandleLen = ("" + mGkPwHandle).length();
+ final int tokenLen = mToken == null ? 0 : mToken.length;
+ final int challengeLen = ("" + mChallenge).length();
+ return getClass().getSimpleName() + ":{initMillis:" + mInitMillis
+ + ", userId:" + mUserId
+ + ", challenge:{len:" + challengeLen
+ + ", updateMillis:" + mUpdateChallengeMillis + "}"
+ + ", token:{len:" + tokenLen + ", isValid:" + isValidToken(mToken)
+ + ", updateMillis:" + mUpdateTokenMillis + "}"
+ + ", gkPwHandle:{len:" + gkPwHandleLen + ", isValid:"
+ + isValidGkPwHandle(mGkPwHandle) + ", clearMillis:" + mClearGkPwHandleMillis + "}"
+ + ", mSensorId:{id:" + mSensorId + ", updateMillis:" + mUpdateSensorIdMillis + "}"
+ + " }";
+ }
+}
diff --git a/src/com/android/settings/biometrics2/ui/model/EnrollmentRequest.java b/src/com/android/settings/biometrics2/ui/model/EnrollmentRequest.java
new file mode 100644
index 0000000..de8526a
--- /dev/null
+++ b/src/com/android/settings/biometrics2/ui/model/EnrollmentRequest.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.biometrics2.ui.model;
+
+import static com.android.settings.biometrics.BiometricEnrollBase.EXTRA_FROM_SETTINGS_SUMMARY;
+
+import static com.google.android.setupcompat.util.WizardManagerHelper.EXTRA_IS_SUW_SUGGESTED_ACTION_FLOW;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+
+import androidx.annotation.NonNull;
+
+import com.android.settings.SetupWizardUtils;
+
+import com.google.android.setupcompat.util.WizardManagerHelper;
+
+/**
+ * Biometric enrollment generic intent data, which includes
+ * 1. isSuw
+ * 2. isAfterSuwOrSuwSuggestedAction
+ * 3. theme
+ * 4. isFromSettingsSummery
+ * 5. a helper method, getSetupWizardExtras
+ */
+public final class EnrollmentRequest {
+
+ private final boolean mIsSuw;
+ private final boolean mIsAfterSuwOrSuwSuggestedAction;
+ private final boolean mIsFromSettingsSummery;
+ private final int mTheme;
+ private final Bundle mSuwExtras;
+
+ public EnrollmentRequest(@NonNull Intent intent, @NonNull Context context) {
+ mIsSuw = WizardManagerHelper.isAnySetupWizard(intent);
+ mIsAfterSuwOrSuwSuggestedAction = WizardManagerHelper.isDeferredSetupWizard(intent)
+ || WizardManagerHelper.isPortalSetupWizard(intent)
+ || intent.getBooleanExtra(EXTRA_IS_SUW_SUGGESTED_ACTION_FLOW, false);
+ mSuwExtras = getSuwExtras(mIsSuw, intent);
+ mIsFromSettingsSummery = intent.getBooleanExtra(EXTRA_FROM_SETTINGS_SUMMARY, false);
+ mTheme = SetupWizardUtils.getTheme(context, intent);
+ }
+
+ public boolean isSuw() {
+ return mIsSuw;
+ }
+
+ public boolean isAfterSuwOrSuwSuggestedAction() {
+ return mIsAfterSuwOrSuwSuggestedAction;
+ }
+
+ public boolean isFromSettingsSummery() {
+ return mIsFromSettingsSummery;
+ }
+
+ public int getTheme() {
+ return mTheme;
+ }
+
+ @NonNull
+ public Bundle getSuwExtras() {
+ return new Bundle(mSuwExtras);
+ }
+
+ /**
+ * Returns a string representation of the object
+ */
+ @Override
+ public String toString() {
+ return getClass().getSimpleName() + ":{isSuw:" + mIsSuw
+ + ", isAfterSuwOrSuwSuggestedAction:" + mIsAfterSuwOrSuwSuggestedAction
+ + ", isFromSettingsSummery:" + mIsFromSettingsSummery
+ + "}";
+ }
+
+ @NonNull
+ private static Bundle getSuwExtras(boolean isSuw, @NonNull Intent intent) {
+ final Intent toIntent = new Intent();
+ if (isSuw) {
+ SetupWizardUtils.copySetupExtras(intent, toIntent);
+ }
+ return toIntent.getExtras() != null ? toIntent.getExtras() : new Bundle();
+ }
+}
diff --git a/src/com/android/settings/biometrics2/ui/model/FingerprintEnrollIntroStatus.java b/src/com/android/settings/biometrics2/ui/model/FingerprintEnrollIntroStatus.java
new file mode 100644
index 0000000..b5e462e
--- /dev/null
+++ b/src/com/android/settings/biometrics2/ui/model/FingerprintEnrollIntroStatus.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.biometrics2.ui.model;
+
+import android.annotation.IntDef;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Fingerprint onboarding introduction page data, it contains following information which needs
+ * to be passed from view model to view.
+ * 1. mEnrollableStatus: User is allowed to enroll a new fingerprint or not.
+ * 2. mHasScrollToBottom: User has scrolled to the bottom of this page or not.
+ */
+public final class FingerprintEnrollIntroStatus {
+
+ /**
+ * Unconfirmed case, it means that this value is invalid, and view shall bypass this value.
+ */
+ public static final int FINGERPRINT_ENROLLABLE_UNKNOWN = -1;
+
+ /**
+ * User is allowed to enrolled a new fingerprint.
+ */
+ public static final int FINGERPRINT_ENROLLABLE_OK = 0;
+
+ /**
+ * User is not allowed to enrolled a new fingerprint because the number of enrolled fingerprint
+ * has reached maximum.
+ */
+ public static final int FINGERPRINT_ENROLLABLE_ERROR_REACH_MAX = 1;
+
+ @IntDef(prefix = {"FINGERPRINT_ENROLLABLE_"}, value = {
+ FINGERPRINT_ENROLLABLE_UNKNOWN,
+ FINGERPRINT_ENROLLABLE_OK,
+ FINGERPRINT_ENROLLABLE_ERROR_REACH_MAX
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface FingerprintEnrollableStatus {
+ }
+
+ private final boolean mHasScrollToBottom;
+
+ @FingerprintEnrollableStatus
+ private final int mEnrollableStatus;
+
+ public FingerprintEnrollIntroStatus(boolean hasScrollToBottom, int enrollableStatus) {
+ mEnrollableStatus = enrollableStatus;
+ mHasScrollToBottom = hasScrollToBottom;
+ }
+
+ /**
+ * Get enrollable status. It means that user is allowed to enroll a new fingerprint or not.
+ */
+ @FingerprintEnrollableStatus
+ public int getEnrollableStatus() {
+ return mEnrollableStatus;
+ }
+
+ /**
+ * Get info for this onboarding introduction page has scrolled to bottom or not
+ */
+ public boolean hasScrollToBottom() {
+ return mHasScrollToBottom;
+ }
+}
diff --git a/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollIntroFragment.java b/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollIntroFragment.java
new file mode 100644
index 0000000..e788da5
--- /dev/null
+++ b/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollIntroFragment.java
@@ -0,0 +1,288 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.biometrics2.ui.view;
+
+import static android.app.admin.DevicePolicyResources.Strings.Settings.FINGERPRINT_UNLOCK_DISABLED;
+
+import static com.android.settings.biometrics2.ui.model.FingerprintEnrollIntroStatus.FINGERPRINT_ENROLLABLE_ERROR_REACH_MAX;
+import static com.android.settings.biometrics2.ui.model.FingerprintEnrollIntroStatus.FINGERPRINT_ENROLLABLE_OK;
+import static com.android.settings.biometrics2.ui.model.FingerprintEnrollIntroStatus.FINGERPRINT_ENROLLABLE_UNKNOWN;
+
+import static com.google.android.setupdesign.util.DynamicColorPalette.ColorType.ACCENT;
+
+import android.app.Activity;
+import android.app.admin.DevicePolicyResourcesManager;
+import android.content.Context;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffColorFilter;
+import android.os.Bundle;
+import android.text.Html;
+import android.text.TextUtils;
+import android.text.method.LinkMovementMethod;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.StringRes;
+import androidx.fragment.app.Fragment;
+import androidx.lifecycle.ViewModelProvider;
+
+import com.android.settings.R;
+import com.android.settings.biometrics2.ui.model.FingerprintEnrollIntroStatus;
+import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollIntroViewModel;
+
+import com.google.android.setupcompat.template.FooterBarMixin;
+import com.google.android.setupcompat.template.FooterButton;
+import com.google.android.setupdesign.GlifLayout;
+import com.google.android.setupdesign.template.RequireScrollMixin;
+import com.google.android.setupdesign.util.DynamicColorPalette;
+
+/**
+ * Fingerprint intro onboarding page fragment implementation
+ */
+public class FingerprintEnrollIntroFragment extends Fragment {
+
+ private static final String TAG = "FingerprintEnrollIntroFragment";
+
+ @NonNull private final ViewModelProvider mViewModelProvider;
+ @Nullable private final DevicePolicyResourcesManager mDevicePolicyMgrRes;
+
+ private FingerprintEnrollIntroViewModel mViewModel = null;
+
+ private View mView = null;
+ private FooterButton mPrimaryFooterButton = null;
+ private FooterButton mSecondaryFooterButton = null;
+ private ImageView mIconShield = null;
+ private TextView mFooterMessage6 = null;
+ @Nullable private PorterDuffColorFilter mIconColorFilter;
+
+ public FingerprintEnrollIntroFragment(
+ @NonNull ViewModelProvider viewModelProvider,
+ @Nullable DevicePolicyResourcesManager devicePolicyMgrRes) {
+ super();
+ mViewModelProvider = viewModelProvider;
+ mDevicePolicyMgrRes = devicePolicyMgrRes;
+ }
+
+ @Nullable
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
+ @Nullable Bundle savedInstanceState) {
+
+ final Context context = inflater.getContext();
+ mView = inflater.inflate(R.layout.fingerprint_enroll_introduction, container);
+
+ final ImageView iconFingerprint = mView.findViewById(R.id.icon_fingerprint);
+ final ImageView iconDeviceLocked = mView.findViewById(R.id.icon_device_locked);
+ final ImageView iconTrashCan = mView.findViewById(R.id.icon_trash_can);
+ final ImageView iconInfo = mView.findViewById(R.id.icon_info);
+ mIconShield = mView.findViewById(R.id.icon_shield);
+ final ImageView iconLink = mView.findViewById(R.id.icon_link);
+ iconFingerprint.getDrawable().setColorFilter(getIconColorFilter(context));
+ iconDeviceLocked.getDrawable().setColorFilter(getIconColorFilter(context));
+ iconTrashCan.getDrawable().setColorFilter(getIconColorFilter(context));
+ iconInfo.getDrawable().setColorFilter(getIconColorFilter(context));
+ mIconShield.getDrawable().setColorFilter(getIconColorFilter(context));
+ iconLink.getDrawable().setColorFilter(getIconColorFilter(context));
+
+ final TextView footerMessage2 = mView.findViewById(R.id.footer_message_2);
+ final TextView footerMessage3 = mView.findViewById(R.id.footer_message_3);
+ final TextView footerMessage4 = mView.findViewById(R.id.footer_message_4);
+ final TextView footerMessage5 = mView.findViewById(R.id.footer_message_5);
+ mFooterMessage6 = mView.findViewById(R.id.footer_message_6);
+ footerMessage2.setText(
+ R.string.security_settings_fingerprint_v2_enroll_introduction_footer_message_2);
+ footerMessage3.setText(
+ R.string.security_settings_fingerprint_v2_enroll_introduction_footer_message_3);
+ footerMessage4.setText(
+ R.string.security_settings_fingerprint_v2_enroll_introduction_footer_message_4);
+ footerMessage5.setText(
+ R.string.security_settings_fingerprint_v2_enroll_introduction_footer_message_5);
+ mFooterMessage6.setText(
+ R.string.security_settings_fingerprint_v2_enroll_introduction_footer_message_6);
+
+ final TextView footerTitle1 = mView.findViewById(R.id.footer_title_1);
+ final TextView footerTitle2 = mView.findViewById(R.id.footer_title_2);
+ footerTitle1.setText(
+ R.string.security_settings_fingerprint_enroll_introduction_footer_title_1);
+ footerTitle2.setText(
+ R.string.security_settings_fingerprint_enroll_introduction_footer_title_2);
+
+ return mView;
+ }
+
+ @Override
+ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+
+ final Context context = view.getContext();
+
+ final TextView footerLink = mView.findViewById(R.id.footer_learn_more);
+ footerLink.setMovementMethod(LinkMovementMethod.getInstance());
+ final String footerLinkStr = getContext().getString(
+ R.string.security_settings_fingerprint_v2_enroll_introduction_message_learn_more,
+ Html.FROM_HTML_MODE_LEGACY);
+ footerLink.setText(Html.fromHtml(footerLinkStr));
+
+ // footer buttons
+ mPrimaryFooterButton = new FooterButton.Builder(context)
+ .setText(R.string.security_settings_fingerprint_enroll_introduction_agree)
+ .setListener(mViewModel::onNextButtonClick)
+ .setButtonType(FooterButton.ButtonType.OPT_IN)
+ .setTheme(R.style.SudGlifButton_Primary)
+ .build();
+ mSecondaryFooterButton = new FooterButton.Builder(context)
+ .setListener(mViewModel::onSkipOrCancelButtonClick)
+ .setButtonType(FooterButton.ButtonType.NEXT)
+ .setTheme(R.style.SudGlifButton_Primary)
+ .build();
+ getFooterBarMixin().setPrimaryButton(mPrimaryFooterButton);
+ getFooterBarMixin().setSecondaryButton(mSecondaryFooterButton, true /* usePrimaryStyle */);
+
+ if (mViewModel.canAssumeUdfps()) {
+ mFooterMessage6.setVisibility(View.VISIBLE);
+ mIconShield.setVisibility(View.VISIBLE);
+ } else {
+ mFooterMessage6.setVisibility(View.GONE);
+ mIconShield.setVisibility(View.GONE);
+ }
+ mSecondaryFooterButton.setText(getContext(),
+ mViewModel.getEnrollmentRequest().isAfterSuwOrSuwSuggestedAction()
+ ? R.string.security_settings_fingerprint_enroll_introduction_cancel
+ : R.string.security_settings_fingerprint_enroll_introduction_no_thanks);
+
+ if (mViewModel.isBiometricUnlockDisabledByAdmin()
+ && !mViewModel.isParentalConsentRequired()) {
+ setHeaderText(
+ getActivity(),
+ R.string.security_settings_fingerprint_enroll_introduction_title_unlock_disabled
+ );
+ getLayout().setDescriptionText(getDescriptionDisabledByAdmin(context));
+ } else {
+ setHeaderText(getActivity(),
+ R.string.security_settings_fingerprint_enroll_introduction_title);
+ }
+
+ mViewModel.getPageStatusLiveData().observe(this, this::updateFooterButtons);
+
+ final RequireScrollMixin requireScrollMixin = getLayout()
+ .getMixin(RequireScrollMixin.class);
+ requireScrollMixin.requireScrollWithButton(getActivity(), mPrimaryFooterButton,
+ getMoreButtonTextRes(), mViewModel::onNextButtonClick);
+ requireScrollMixin.setOnRequireScrollStateChangedListener(scrollNeeded -> {
+ if (!scrollNeeded) {
+ mViewModel.setHasScrolledToBottom();
+ }
+ });
+ }
+
+ @Override
+ public void onAttach(@NonNull Context context) {
+ mViewModel = mViewModelProvider.get(FingerprintEnrollIntroViewModel.class);
+ getLifecycle().addObserver(mViewModel);
+ super.onAttach(context);
+ }
+
+ @Override
+ public void onDetach() {
+ getLifecycle().removeObserver(mViewModel);
+ super.onDetach();
+ }
+
+ @NonNull
+ private PorterDuffColorFilter getIconColorFilter(@NonNull Context context) {
+ if (mIconColorFilter == null) {
+ mIconColorFilter = new PorterDuffColorFilter(
+ DynamicColorPalette.getColor(context, ACCENT),
+ PorterDuff.Mode.SRC_IN);
+ }
+ return mIconColorFilter;
+ }
+
+ private GlifLayout getLayout() {
+ return mView.findViewById(R.id.setup_wizard_layout);
+ }
+
+ @NonNull
+ private FooterBarMixin getFooterBarMixin() {
+ final GlifLayout layout = getLayout();
+ return layout.getMixin(FooterBarMixin.class);
+ }
+
+ @NonNull
+ private String getDescriptionDisabledByAdmin(@NonNull Context context) {
+ final int defaultStrId =
+ R.string.security_settings_fingerprint_enroll_introduction_message_unlock_disabled;
+ if (mDevicePolicyMgrRes == null) {
+ Log.w(TAG, "getDescriptionDisabledByAdmin, null device policy manager res");
+ return "";
+ }
+ return mDevicePolicyMgrRes.getString(FINGERPRINT_UNLOCK_DISABLED,
+ () -> context.getString(defaultStrId));
+ }
+
+ private void setHeaderText(@NonNull Activity activity, int resId) {
+ TextView layoutTitle = getLayout().getHeaderTextView();
+ CharSequence previousTitle = layoutTitle.getText();
+ CharSequence title = activity.getText(resId);
+ if (previousTitle != title) {
+ if (!TextUtils.isEmpty(previousTitle)) {
+ layoutTitle.setAccessibilityLiveRegion(View.ACCESSIBILITY_LIVE_REGION_POLITE);
+ }
+ getLayout().setHeaderText(title);
+ getLayout().getHeaderTextView().setContentDescription(title);
+ activity.setTitle(title);
+ }
+ getLayout().getHeaderTextView().setContentDescription(activity.getText(resId));
+ }
+
+ void updateFooterButtons(@NonNull FingerprintEnrollIntroStatus status) {
+ @StringRes final int scrollToBottomPrimaryResId =
+ status.getEnrollableStatus() == FINGERPRINT_ENROLLABLE_OK
+ ? R.string.security_settings_fingerprint_enroll_introduction_agree
+ : R.string.done;
+
+ mPrimaryFooterButton.setText(getContext(),
+ status.hasScrollToBottom() ? scrollToBottomPrimaryResId : getMoreButtonTextRes());
+ mSecondaryFooterButton.setVisibility(
+ status.hasScrollToBottom() ? View.VISIBLE : View.INVISIBLE);
+
+ final TextView errorTextView = mView.findViewById(R.id.error_text);
+ switch (status.getEnrollableStatus()) {
+ case FINGERPRINT_ENROLLABLE_OK:
+ errorTextView.setText(null);
+ errorTextView.setVisibility(View.GONE);
+ break;
+ case FINGERPRINT_ENROLLABLE_ERROR_REACH_MAX:
+ errorTextView.setText(R.string.fingerprint_intro_error_max);
+ errorTextView.setVisibility(View.VISIBLE);
+ break;
+ case FINGERPRINT_ENROLLABLE_UNKNOWN:
+ // default case, do nothing.
+ }
+ }
+
+ @StringRes
+ private int getMoreButtonTextRes() {
+ return R.string.security_settings_face_enroll_introduction_more;
+ }
+}
diff --git a/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollmentActivity.java b/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollmentActivity.java
new file mode 100644
index 0000000..e9cf6fd
--- /dev/null
+++ b/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollmentActivity.java
@@ -0,0 +1,276 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.biometrics2.ui.view;
+
+import static androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY;
+
+import static com.android.settings.biometrics2.factory.BiometricsViewModelFactory.CHALLENGE_GENERATOR;
+import static com.android.settings.biometrics2.ui.viewmodel.AutoCredentialViewModel.CREDENTIAL_FAIL_DURING_GENERATE_CHALLENGE;
+import static com.android.settings.biometrics2.ui.viewmodel.AutoCredentialViewModel.CREDENTIAL_FAIL_NEED_TO_CHOOSE_LOCK;
+import static com.android.settings.biometrics2.ui.viewmodel.AutoCredentialViewModel.CREDENTIAL_FAIL_NEED_TO_CONFIRM_LOCK;
+import static com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollIntroViewModel.FINGERPRINT_ENROLL_INTRO_ACTION_CONTINUE_ENROLL;
+import static com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollIntroViewModel.FINGERPRINT_ENROLL_INTRO_ACTION_DONE_AND_FINISH;
+import static com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollIntroViewModel.FINGERPRINT_ENROLL_INTRO_ACTION_SKIP_OR_CANCEL;
+
+import android.app.Activity;
+import android.app.Application;
+import android.content.Intent;
+import android.content.res.ColorStateList;
+import android.graphics.Color;
+import android.os.Bundle;
+import android.os.SystemClock;
+import android.util.Log;
+
+import androidx.activity.result.ActivityResult;
+import androidx.activity.result.ActivityResultLauncher;
+import androidx.activity.result.contract.ActivityResultContracts;
+import androidx.annotation.ColorInt;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.FragmentActivity;
+import androidx.fragment.app.FragmentManager;
+import androidx.lifecycle.ViewModelProvider;
+import androidx.lifecycle.viewmodel.CreationExtras;
+import androidx.lifecycle.viewmodel.MutableCreationExtras;
+
+import com.android.settings.R;
+import com.android.settings.Utils;
+import com.android.settings.biometrics.BiometricEnrollBase;
+import com.android.settings.biometrics.fingerprint.FingerprintEnrollFindSensor;
+import com.android.settings.biometrics.fingerprint.SetupFingerprintEnrollEnrolling;
+import com.android.settings.biometrics2.data.repository.FingerprintRepository;
+import com.android.settings.biometrics2.factory.BiometricsFragmentFactory;
+import com.android.settings.biometrics2.factory.BiometricsViewModelFactory;
+import com.android.settings.biometrics2.ui.model.CredentialModel;
+import com.android.settings.biometrics2.ui.model.EnrollmentRequest;
+import com.android.settings.biometrics2.ui.viewmodel.AutoCredentialViewModel;
+import com.android.settings.biometrics2.ui.viewmodel.AutoCredentialViewModel.FingerprintChallengeGenerator;
+import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollIntroViewModel;
+import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollmentViewModel;
+import com.android.settings.overlay.FeatureFactory;
+
+import com.google.android.setupdesign.util.ThemeHelper;
+
+/**
+ * Fingerprint enrollment activity implementation
+ */
+public class FingerprintEnrollmentActivity extends FragmentActivity {
+
+ private static final String TAG = "FingerprintEnrollmentActivity";
+
+ protected static final int LAUNCH_CONFIRM_LOCK_ACTIVITY = 1;
+
+ private FingerprintEnrollmentViewModel mViewModel;
+ private AutoCredentialViewModel mAutoCredentialViewModel;
+ private ActivityResultLauncher<Intent> mNextActivityLauncher;
+ private ActivityResultLauncher<Intent> mChooseLockLauncher;
+
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ mNextActivityLauncher = registerForActivityResult(
+ new ActivityResultContracts.StartActivityForResult(),
+ (it) -> mViewModel.onContinueEnrollActivityResult(
+ it,
+ mAutoCredentialViewModel.getUserId())
+ );
+ mChooseLockLauncher = registerForActivityResult(
+ new ActivityResultContracts.StartActivityForResult(),
+ (it) -> onChooseOrConfirmLockResult(true, it)
+ );
+
+ ViewModelProvider viewModelProvider = new ViewModelProvider(this);
+
+ mViewModel = viewModelProvider.get(FingerprintEnrollmentViewModel.class);
+ mViewModel.setRequest(new EnrollmentRequest(getIntent(), getApplicationContext()));
+ mViewModel.setSavedInstanceState(savedInstanceState);
+ getLifecycle().addObserver(mViewModel);
+
+ mAutoCredentialViewModel = viewModelProvider.get(AutoCredentialViewModel.class);
+ mAutoCredentialViewModel.setCredentialModel(new CredentialModel(getIntent(),
+ SystemClock.elapsedRealtimeClock()));
+ getLifecycle().addObserver(mAutoCredentialViewModel);
+
+ mViewModel.getSetResultLiveData().observe(this, this::onSetActivityResult);
+ mAutoCredentialViewModel.getActionLiveData().observe(this, this::onCredentialAction);
+
+ // Theme
+ setTheme(mViewModel.getRequest().getTheme());
+ ThemeHelper.trySetDynamicColor(this);
+ getWindow().setStatusBarColor(android.graphics.Color.TRANSPARENT);
+
+ // fragment
+ setContentView(R.layout.biometric_enrollment_container);
+ final FragmentManager fragmentManager = getSupportFragmentManager();
+ fragmentManager.setFragmentFactory(
+ new BiometricsFragmentFactory(getApplication(), viewModelProvider));
+
+ final FingerprintEnrollIntroViewModel fingerprintEnrollIntroViewModel =
+ viewModelProvider.get(FingerprintEnrollIntroViewModel.class);
+ fingerprintEnrollIntroViewModel.setEnrollmentRequest(mViewModel.getRequest());
+ fingerprintEnrollIntroViewModel.setUserId(mAutoCredentialViewModel.getUserId());
+ fingerprintEnrollIntroViewModel.getActionLiveData().observe(
+ this, this::observeIntroAction);
+ final String tag = "FingerprintEnrollIntroFragment";
+ fragmentManager.beginTransaction()
+ .setReorderingAllowed(true)
+ .add(R.id.fragment_container_view, FingerprintEnrollIntroFragment.class, null, tag)
+ .commit();
+ }
+
+ private void onSetActivityResult(@NonNull ActivityResult result) {
+ setResult(mViewModel.getRequest().isAfterSuwOrSuwSuggestedAction()
+ ? RESULT_CANCELED
+ : result.getResultCode(),
+ result.getData());
+ finish();
+ }
+
+ private void onCredentialAction(@NonNull Integer action) {
+ switch (action) {
+ case CREDENTIAL_FAIL_NEED_TO_CHOOSE_LOCK: {
+ final Intent intent = mAutoCredentialViewModel.getChooseLockIntent(this,
+ mViewModel.getRequest().isSuw(), mViewModel.getRequest().getSuwExtras());
+ if (!mViewModel.isWaitingActivityResult().compareAndSet(false, true)) {
+ Log.w(TAG, "chooseLock, fail to set isWaiting flag to true");
+ }
+ mChooseLockLauncher.launch(intent);
+ return;
+ }
+ case CREDENTIAL_FAIL_NEED_TO_CONFIRM_LOCK: {
+ final boolean launched = mAutoCredentialViewModel.getConfirmLockLauncher(
+ this,
+ LAUNCH_CONFIRM_LOCK_ACTIVITY,
+ getString(R.string.security_settings_fingerprint_preference_title)
+ ).launch();
+ if (!launched) {
+ // This shouldn't happen, as we should only end up at this step if a lock thingy
+ // is already set.
+ Log.e(TAG, "confirmLock, launched is true");
+ finish();
+ } else if (!mViewModel.isWaitingActivityResult().compareAndSet(false, true)) {
+ Log.w(TAG, "confirmLock, fail to set isWaiting flag to true");
+ }
+ return;
+ }
+ case CREDENTIAL_FAIL_DURING_GENERATE_CHALLENGE: {
+ Log.w(TAG, "observeCredentialLiveData, finish with action:" + action);
+ if (mViewModel.getRequest().isAfterSuwOrSuwSuggestedAction()) {
+ setResult(Activity.RESULT_CANCELED);
+ }
+ finish();
+ }
+ }
+ }
+
+ private void onChooseOrConfirmLockResult(boolean isChooseLock,
+ @NonNull ActivityResult activityResult) {
+ if (!mViewModel.isWaitingActivityResult().compareAndSet(true, false)) {
+ Log.w(TAG, "isChooseLock:" + isChooseLock + ", fail to unset waiting flag");
+ }
+ if (mAutoCredentialViewModel.checkNewCredentialFromActivityResult(
+ isChooseLock, activityResult)) {
+ overridePendingTransition(R.anim.sud_slide_next_in, R.anim.sud_slide_next_out);
+ }
+ }
+
+ private void observeIntroAction(@NonNull Integer action) {
+ switch (action) {
+ case FINGERPRINT_ENROLL_INTRO_ACTION_DONE_AND_FINISH: {
+ onSetActivityResult(
+ new ActivityResult(BiometricEnrollBase.RESULT_FINISHED, null));
+ return;
+ }
+ case FINGERPRINT_ENROLL_INTRO_ACTION_SKIP_OR_CANCEL: {
+ onSetActivityResult(
+ new ActivityResult(BiometricEnrollBase.RESULT_SKIP, null));
+ return;
+ }
+ case FINGERPRINT_ENROLL_INTRO_ACTION_CONTINUE_ENROLL: {
+ final boolean isSuw = mViewModel.getRequest().isSuw();
+ if (!mViewModel.isWaitingActivityResult().compareAndSet(false, true)) {
+ Log.w(TAG, "startNext, isSuw:" + isSuw + ", fail to set isWaiting flag");
+ }
+ final Intent intent = new Intent(this, isSuw
+ ? SetupFingerprintEnrollEnrolling.class
+ : FingerprintEnrollFindSensor.class);
+ intent.putExtras(mAutoCredentialViewModel.getCredentialBundle());
+ intent.putExtras(mViewModel.getNextActivityBaseIntentExtras());
+ mNextActivityLauncher.launch(intent);
+ }
+ }
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ mViewModel.checkFinishActivityDuringOnPause(isFinishing(), isChangingConfigurations());
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
+ if (requestCode == LAUNCH_CONFIRM_LOCK_ACTIVITY) {
+ onChooseOrConfirmLockResult(false, new ActivityResult(resultCode, data));
+ return;
+ }
+ super.onActivityResult(requestCode, resultCode, data);
+ }
+
+ @NonNull
+ @Override
+ public CreationExtras getDefaultViewModelCreationExtras() {
+ final Application application =
+ super.getDefaultViewModelCreationExtras().get(APPLICATION_KEY);
+ final MutableCreationExtras ret = new MutableCreationExtras();
+ ret.set(APPLICATION_KEY, application);
+ final FingerprintRepository repository = FeatureFactory.getFactory(application)
+ .getBiometricsRepositoryProvider().getFingerprintRepository(application);
+ ret.set(CHALLENGE_GENERATOR, new FingerprintChallengeGenerator(repository));
+ return ret;
+ }
+
+ @NonNull
+ @Override
+ public ViewModelProvider.Factory getDefaultViewModelProviderFactory() {
+ return new BiometricsViewModelFactory();
+ }
+
+ @Override
+ public void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ getWindow().setStatusBarColor(getBackgroundColor());
+ }
+
+ @ColorInt
+ private int getBackgroundColor() {
+ final ColorStateList stateList = Utils.getColorAttr(this, android.R.attr.windowBackground);
+ return stateList != null ? stateList.getDefaultColor() : Color.TRANSPARENT;
+ }
+
+ @Override
+ protected void onDestroy() {
+ getLifecycle().removeObserver(mViewModel);
+ super.onDestroy();
+ }
+
+ @Override
+ protected void onSaveInstanceState(@NonNull Bundle outState) {
+ super.onSaveInstanceState(outState);
+ mViewModel.onSaveInstanceState(outState);
+ }
+}
diff --git a/src/com/android/settings/biometrics2/ui/viewmodel/AutoCredentialViewModel.java b/src/com/android/settings/biometrics2/ui/viewmodel/AutoCredentialViewModel.java
new file mode 100644
index 0000000..b1a7f90
--- /dev/null
+++ b/src/com/android/settings/biometrics2/ui/viewmodel/AutoCredentialViewModel.java
@@ -0,0 +1,354 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.biometrics2.ui.viewmodel;
+
+import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED;
+
+import static com.android.settings.biometrics.BiometricEnrollBase.EXTRA_KEY_CHALLENGE;
+import static com.android.settings.biometrics.BiometricEnrollBase.EXTRA_KEY_SENSOR_ID;
+import static com.android.settings.biometrics2.ui.model.CredentialModel.INVALID_GK_PW_HANDLE;
+import static com.android.settings.password.ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN;
+import static com.android.settings.password.ChooseLockSettingsHelper.EXTRA_KEY_GK_PW_HANDLE;
+
+import android.annotation.IntDef;
+import android.app.Activity;
+import android.app.Application;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.util.Log;
+
+import androidx.activity.result.ActivityResult;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.lifecycle.AndroidViewModel;
+import androidx.lifecycle.DefaultLifecycleObserver;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.MutableLiveData;
+
+import com.android.internal.widget.LockPatternUtils;
+import com.android.internal.widget.VerifyCredentialResponse;
+import com.android.settings.biometrics.BiometricUtils;
+import com.android.settings.biometrics2.data.repository.FingerprintRepository;
+import com.android.settings.biometrics2.ui.model.CredentialModel;
+import com.android.settings.password.ChooseLockGeneric;
+import com.android.settings.password.ChooseLockPattern;
+import com.android.settings.password.ChooseLockSettingsHelper;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * AutoCredentialViewModel which uses CredentialModel to determine next actions for activity, like
+ * start ChooseLockActivity, start ConfirmLockActivity, GenerateCredential, or do nothing.
+ */
+public class AutoCredentialViewModel extends AndroidViewModel implements DefaultLifecycleObserver {
+
+ private static final String TAG = "AutoCredentialViewModel";
+ private static final boolean DEBUG = true;
+
+ /**
+ * Need activity to run choose lock
+ */
+ public static final int CREDENTIAL_FAIL_NEED_TO_CHOOSE_LOCK = 1;
+
+ /**
+ * Need activity to run confirm lock
+ */
+ public static final int CREDENTIAL_FAIL_NEED_TO_CONFIRM_LOCK = 2;
+
+ /**
+ * Fail to use challenge from hardware generateChallenge(), shall finish activity with proper
+ * error code
+ */
+ public static final int CREDENTIAL_FAIL_DURING_GENERATE_CHALLENGE = 3;
+
+ @IntDef(prefix = { "CREDENTIAL_" }, value = {
+ CREDENTIAL_FAIL_NEED_TO_CHOOSE_LOCK,
+ CREDENTIAL_FAIL_NEED_TO_CONFIRM_LOCK,
+ CREDENTIAL_FAIL_DURING_GENERATE_CHALLENGE
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface CredentialAction {}
+
+ /**
+ * Generic callback for FingerprintManager#generateChallenge or FaceManager#generateChallenge
+ */
+ public interface GenerateChallengeCallback {
+ /**
+ * Generic generateChallenge method for FingerprintManager or FaceManager
+ */
+ void onChallengeGenerated(int sensorId, int userId, long challenge);
+ }
+
+ /**
+ * A generic interface class for calling different generateChallenge from FingerprintManager or
+ * FaceManager
+ */
+ public interface ChallengeGenerator {
+ /**
+ * Get callback that will be called later after challenge generated
+ */
+ @Nullable
+ GenerateChallengeCallback getCallback();
+
+ /**
+ * Set callback that will be called later after challenge generated
+ */
+ void setCallback(@Nullable GenerateChallengeCallback callback);
+
+ /**
+ * Method for generating challenge from FingerprintManager or FaceManager
+ */
+ void generateChallenge(int userId);
+ }
+
+ /**
+ * Used to generate challenge through FingerprintRepository
+ */
+ public static class FingerprintChallengeGenerator implements ChallengeGenerator {
+
+ private static final String TAG = "FingerprintChallengeGenerator";
+
+ @NonNull
+ private final FingerprintRepository mFingerprintRepository;
+
+ @Nullable
+ private GenerateChallengeCallback mCallback = null;
+
+ public FingerprintChallengeGenerator(@NonNull FingerprintRepository fingerprintRepository) {
+ mFingerprintRepository = fingerprintRepository;
+ }
+
+ @Nullable
+ @Override
+ public GenerateChallengeCallback getCallback() {
+ return mCallback;
+ }
+
+ @Override
+ public void setCallback(@Nullable GenerateChallengeCallback callback) {
+ mCallback = callback;
+ }
+
+ @Override
+ public void generateChallenge(int userId) {
+ final GenerateChallengeCallback callback = mCallback;
+ if (callback == null) {
+ Log.e(TAG, "generateChallenge, null callback");
+ return;
+ }
+ mFingerprintRepository.generateChallenge(userId, callback::onChallengeGenerated);
+ }
+ }
+
+
+ @NonNull private final LockPatternUtils mLockPatternUtils;
+ @NonNull private final ChallengeGenerator mChallengeGenerator;
+ private CredentialModel mCredentialModel = null;
+ @NonNull private final MutableLiveData<Integer> mActionLiveData =
+ new MutableLiveData<>();
+
+ public AutoCredentialViewModel(
+ @NonNull Application application,
+ @NonNull LockPatternUtils lockPatternUtils,
+ @NonNull ChallengeGenerator challengeGenerator) {
+ super(application);
+ mLockPatternUtils = lockPatternUtils;
+ mChallengeGenerator = challengeGenerator;
+ }
+
+ public void setCredentialModel(@NonNull CredentialModel credentialModel) {
+ mCredentialModel = credentialModel;
+ }
+
+ /**
+ * Observe ActionLiveData for actions about choosing lock, confirming lock, or finishing
+ * activity
+ */
+ @NonNull
+ public LiveData<Integer> getActionLiveData() {
+ return mActionLiveData;
+ }
+
+ @Override
+ public void onCreate(@NonNull LifecycleOwner owner) {
+ checkCredential();
+ }
+
+ /**
+ * Check credential status for biometric enrollment.
+ */
+ private void checkCredential() {
+ if (isValidCredential()) {
+ return;
+ }
+ final long gkPwHandle = mCredentialModel.getGkPwHandle();
+ if (isUnspecifiedPassword()) {
+ mActionLiveData.postValue(CREDENTIAL_FAIL_NEED_TO_CHOOSE_LOCK);
+ } else if (CredentialModel.isValidGkPwHandle(gkPwHandle)) {
+ generateChallenge(gkPwHandle);
+ } else {
+ mActionLiveData.postValue(CREDENTIAL_FAIL_NEED_TO_CONFIRM_LOCK);
+ }
+ }
+
+ private void generateChallenge(long gkPwHandle) {
+ mChallengeGenerator.setCallback((sensorId, userId, challenge) -> {
+ mCredentialModel.setSensorId(sensorId);
+ mCredentialModel.setChallenge(challenge);
+ try {
+ final byte[] newToken = requestGatekeeperHat(gkPwHandle, challenge, userId);
+ mCredentialModel.setToken(newToken);
+ } catch (IllegalStateException e) {
+ Log.e(TAG, "generateChallenge, IllegalStateException", e);
+ mActionLiveData.postValue(CREDENTIAL_FAIL_DURING_GENERATE_CHALLENGE);
+ return;
+ }
+
+ mLockPatternUtils.removeGatekeeperPasswordHandle(gkPwHandle);
+ mCredentialModel.clearGkPwHandle();
+
+ if (DEBUG) {
+ Log.d(TAG, "generateChallenge " + mCredentialModel);
+ }
+
+ // Check credential again
+ if (!isValidCredential()) {
+ Log.w(TAG, "generateChallenge, invalid Credential");
+ mActionLiveData.postValue(CREDENTIAL_FAIL_DURING_GENERATE_CHALLENGE);
+ }
+ });
+ mChallengeGenerator.generateChallenge(getUserId());
+ }
+
+ private boolean isValidCredential() {
+ return !isUnspecifiedPassword()
+ && CredentialModel.isValidToken(mCredentialModel.getToken());
+ }
+
+ private boolean isUnspecifiedPassword() {
+ return mLockPatternUtils.getActivePasswordQuality(getUserId())
+ == PASSWORD_QUALITY_UNSPECIFIED;
+ }
+
+ /**
+ * Handle activity result from ChooseLockGeneric, ConfirmLockPassword, or ConfirmLockPattern
+ * @param isChooseLock true if result is coming from ChooseLockGeneric. False if result is
+ * coming from ConfirmLockPassword or ConfirmLockPattern
+ * @param result activity result
+ * @return if it is a valid result
+ */
+ public boolean checkNewCredentialFromActivityResult(boolean isChooseLock,
+ @NonNull ActivityResult result) {
+ if ((isChooseLock && result.getResultCode() == ChooseLockPattern.RESULT_FINISHED)
+ || (!isChooseLock && result.getResultCode() == Activity.RESULT_OK)) {
+ final Intent data = result.getData();
+ if (data != null) {
+ final long gkPwHandle = result.getData().getLongExtra(
+ EXTRA_KEY_GK_PW_HANDLE, INVALID_GK_PW_HANDLE);
+ generateChallenge(gkPwHandle);
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Get userId for this credential
+ */
+ public int getUserId() {
+ return mCredentialModel.getUserId();
+ }
+
+ @Nullable
+ private byte[] requestGatekeeperHat(long gkPwHandle, long challenge, int userId)
+ throws IllegalStateException {
+ final VerifyCredentialResponse response = mLockPatternUtils
+ .verifyGatekeeperPasswordHandle(gkPwHandle, challenge, userId);
+ if (!response.isMatched()) {
+ throw new IllegalStateException("Unable to request Gatekeeper HAT");
+ }
+ return response.getGatekeeperHAT();
+ }
+
+ /**
+ * Get Credential bundle which will be used to launch next activity.
+ */
+ @NonNull
+ public Bundle getCredentialBundle() {
+ final Bundle retBundle = new Bundle();
+ final long gkPwHandle = mCredentialModel.getGkPwHandle();
+ if (CredentialModel.isValidGkPwHandle(gkPwHandle)) {
+ retBundle.putLong(EXTRA_KEY_GK_PW_HANDLE, gkPwHandle);
+ }
+ final byte[] token = mCredentialModel.getToken();
+ if (CredentialModel.isValidToken(token)) {
+ retBundle.putByteArray(EXTRA_KEY_CHALLENGE_TOKEN, token);
+ }
+ final int userId = getUserId();
+ if (CredentialModel.isValidUserId(userId)) {
+ retBundle.putInt(Intent.EXTRA_USER_ID, userId);
+ }
+ retBundle.putLong(EXTRA_KEY_CHALLENGE, mCredentialModel.getChallenge());
+ retBundle.putInt(EXTRA_KEY_SENSOR_ID, mCredentialModel.getSensorId());
+ return retBundle;
+ }
+
+ /**
+ * Get Intent for choosing lock
+ */
+ @NonNull
+ public Intent getChooseLockIntent(@NonNull Context context, boolean isSuw,
+ @NonNull Bundle suwExtras) {
+ final Intent intent = BiometricUtils.getChooseLockIntent(context, isSuw,
+ suwExtras);
+ intent.putExtra(ChooseLockGeneric.ChooseLockGenericFragment.HIDE_INSECURE_OPTIONS,
+ true);
+ intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_REQUEST_GK_PW_HANDLE, true);
+ intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_FOR_FINGERPRINT, true);
+
+ final int userId = getUserId();
+ if (CredentialModel.isValidUserId(userId)) {
+ intent.putExtra(Intent.EXTRA_USER_ID, userId);
+ }
+ return intent;
+ }
+
+ /**
+ * Get ConfirmLockLauncher
+ */
+ @NonNull
+ public ChooseLockSettingsHelper getConfirmLockLauncher(@NonNull Activity activity,
+ int requestCode, @NonNull String title) {
+ final ChooseLockSettingsHelper.Builder builder =
+ new ChooseLockSettingsHelper.Builder(activity);
+ builder.setRequestCode(requestCode)
+ .setTitle(title)
+ .setRequestGatekeeperPasswordHandle(true)
+ .setForegroundOnly(true)
+ .setReturnCredentials(true);
+
+ final int userId = mCredentialModel.getUserId();
+ if (CredentialModel.isValidUserId(userId)) {
+ builder.setUserId(userId);
+ }
+ return builder.build();
+ }
+
+}
diff --git a/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollIntroViewModel.java b/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollIntroViewModel.java
new file mode 100644
index 0000000..252a508
--- /dev/null
+++ b/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollIntroViewModel.java
@@ -0,0 +1,211 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.biometrics2.ui.viewmodel;
+
+import static com.android.settings.biometrics2.ui.model.FingerprintEnrollIntroStatus.FINGERPRINT_ENROLLABLE_ERROR_REACH_MAX;
+import static com.android.settings.biometrics2.ui.model.FingerprintEnrollIntroStatus.FINGERPRINT_ENROLLABLE_OK;
+import static com.android.settings.biometrics2.ui.model.FingerprintEnrollIntroStatus.FINGERPRINT_ENROLLABLE_UNKNOWN;
+
+import android.annotation.IntDef;
+import android.app.Application;
+import android.os.UserHandle;
+import android.util.Log;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.lifecycle.AndroidViewModel;
+import androidx.lifecycle.DefaultLifecycleObserver;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.MediatorLiveData;
+import androidx.lifecycle.MutableLiveData;
+
+import com.android.settings.biometrics2.data.repository.FingerprintRepository;
+import com.android.settings.biometrics2.ui.model.EnrollmentRequest;
+import com.android.settings.biometrics2.ui.model.FingerprintEnrollIntroStatus;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Fingerprint intro onboarding page view model implementation
+ */
+public class FingerprintEnrollIntroViewModel extends AndroidViewModel
+ implements DefaultLifecycleObserver {
+
+ private static final String TAG = "FingerprintEnrollIntroViewModel";
+ private static final boolean HAS_SCROLLED_TO_BOTTOM_DEFAULT = false;
+ private static final int ENROLLABLE_STATUS_DEFAULT = FINGERPRINT_ENROLLABLE_UNKNOWN;
+
+ /**
+ * User clicks 'Done' button on this page
+ */
+ public static final int FINGERPRINT_ENROLL_INTRO_ACTION_DONE_AND_FINISH = 0;
+
+ /**
+ * User clicks 'Agree' button on this page
+ */
+ public static final int FINGERPRINT_ENROLL_INTRO_ACTION_CONTINUE_ENROLL = 1;
+
+ /**
+ * User clicks 'Skip' button on this page
+ */
+ public static final int FINGERPRINT_ENROLL_INTRO_ACTION_SKIP_OR_CANCEL = 2;
+
+ @IntDef(prefix = { "FINGERPRINT_ENROLL_INTRO_ACTION_" }, value = {
+ FINGERPRINT_ENROLL_INTRO_ACTION_DONE_AND_FINISH,
+ FINGERPRINT_ENROLL_INTRO_ACTION_CONTINUE_ENROLL,
+ FINGERPRINT_ENROLL_INTRO_ACTION_SKIP_OR_CANCEL
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface FingerprintEnrollIntroAction {}
+
+ @NonNull private final FingerprintRepository mFingerprintRepository;
+
+ private final MutableLiveData<Boolean> mHasScrolledToBottomLiveData =
+ new MutableLiveData<>(HAS_SCROLLED_TO_BOTTOM_DEFAULT);
+ private final MutableLiveData<Integer> mEnrollableStatusLiveData =
+ new MutableLiveData<>(ENROLLABLE_STATUS_DEFAULT);
+ private final MediatorLiveData<FingerprintEnrollIntroStatus> mPageStatusLiveData =
+ new MediatorLiveData<>();
+ private final MutableLiveData<Integer> mActionLiveData = new MutableLiveData<>();
+ private int mUserId = UserHandle.myUserId();
+ private EnrollmentRequest mEnrollmentRequest = null;
+
+ public FingerprintEnrollIntroViewModel(@NonNull Application application,
+ @NonNull FingerprintRepository fingerprintRepository) {
+ super(application);
+ mFingerprintRepository = fingerprintRepository;
+
+ mPageStatusLiveData.addSource(
+ mEnrollableStatusLiveData,
+ enrollable -> {
+ final Boolean toBottomValue = mHasScrolledToBottomLiveData.getValue();
+ final FingerprintEnrollIntroStatus status = new FingerprintEnrollIntroStatus(
+ toBottomValue != null ? toBottomValue : HAS_SCROLLED_TO_BOTTOM_DEFAULT,
+ enrollable);
+ mPageStatusLiveData.setValue(status);
+ });
+ mPageStatusLiveData.addSource(
+ mHasScrolledToBottomLiveData,
+ hasScrolledToBottom -> {
+ final Integer enrollableValue = mEnrollableStatusLiveData.getValue();
+ final FingerprintEnrollIntroStatus status = new FingerprintEnrollIntroStatus(
+ hasScrolledToBottom,
+ enrollableValue != null ? enrollableValue : ENROLLABLE_STATUS_DEFAULT);
+ mPageStatusLiveData.setValue(status);
+ });
+ }
+
+ public void setUserId(int userId) {
+ mUserId = userId;
+ }
+
+ public void setEnrollmentRequest(@NonNull EnrollmentRequest enrollmentRequest) {
+ mEnrollmentRequest = enrollmentRequest;
+ }
+
+ /**
+ * Get enrollment request
+ */
+ public EnrollmentRequest getEnrollmentRequest() {
+ return mEnrollmentRequest;
+ }
+
+ private void updateEnrollableStatus() {
+ final int num = mFingerprintRepository.getNumOfEnrolledFingerprintsSize(mUserId);
+ final int max =
+ mEnrollmentRequest.isSuw() && !mEnrollmentRequest.isAfterSuwOrSuwSuggestedAction()
+ ? mFingerprintRepository.getMaxFingerprintsInSuw(getApplication().getResources())
+ : mFingerprintRepository.getMaxFingerprints();
+ mEnrollableStatusLiveData.postValue(num >= max
+ ? FINGERPRINT_ENROLLABLE_ERROR_REACH_MAX
+ : FINGERPRINT_ENROLLABLE_OK);
+ }
+
+ /**
+ * Get enrollable status and hasScrollToBottom live data
+ */
+ public LiveData<FingerprintEnrollIntroStatus> getPageStatusLiveData() {
+ return mPageStatusLiveData;
+ }
+
+ /**
+ * Get user's action live data (like clicking Agree, Skip, or Done)
+ */
+ public LiveData<Integer> getActionLiveData() {
+ return mActionLiveData;
+ }
+
+ /**
+ * The first sensor type is UDFPS sensor or not
+ */
+ public boolean canAssumeUdfps() {
+ return mFingerprintRepository.canAssumeUdfps();
+ }
+
+ /**
+ * Update onboarding intro page has scrolled to bottom
+ */
+ public void setHasScrolledToBottom() {
+ mHasScrolledToBottomLiveData.postValue(true);
+ }
+
+ /**
+ * Get parental consent required or not during enrollment process
+ */
+ public boolean isParentalConsentRequired() {
+ return mFingerprintRepository.isParentalConsentRequired(getApplication());
+ }
+
+ /**
+ * Get fingerprint is disable by admin or not
+ */
+ public boolean isBiometricUnlockDisabledByAdmin() {
+ return mFingerprintRepository.isDisabledByAdmin(getApplication(), mUserId);
+ }
+
+ /**
+ * User clicks next button
+ */
+ public void onNextButtonClick(View ignoredView) {
+ final Integer status = mEnrollableStatusLiveData.getValue();
+ switch (status != null ? status : ENROLLABLE_STATUS_DEFAULT) {
+ case FINGERPRINT_ENROLLABLE_ERROR_REACH_MAX:
+ mActionLiveData.postValue(FINGERPRINT_ENROLL_INTRO_ACTION_DONE_AND_FINISH);
+ break;
+ case FINGERPRINT_ENROLLABLE_OK:
+ mActionLiveData.postValue(FINGERPRINT_ENROLL_INTRO_ACTION_CONTINUE_ENROLL);
+ break;
+ default:
+ Log.w(TAG, "fail to click next, enrolled:" + status);
+ }
+ }
+
+ /**
+ * User clicks skip/cancel button
+ */
+ public void onSkipOrCancelButtonClick(View ignoredView) {
+ mActionLiveData.postValue(FINGERPRINT_ENROLL_INTRO_ACTION_SKIP_OR_CANCEL);
+ }
+
+ @Override
+ public void onStart(@NonNull LifecycleOwner owner) {
+ updateEnrollableStatus();
+ }
+
+}
diff --git a/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollmentViewModel.java b/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollmentViewModel.java
new file mode 100644
index 0000000..468e132
--- /dev/null
+++ b/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollmentViewModel.java
@@ -0,0 +1,185 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.biometrics2.ui.viewmodel;
+
+import static com.android.settings.biometrics.BiometricEnrollBase.EXTRA_FROM_SETTINGS_SUMMARY;
+import static com.android.settings.biometrics.BiometricEnrollBase.RESULT_FINISHED;
+import static com.android.settings.biometrics.BiometricEnrollBase.RESULT_SKIP;
+import static com.android.settings.biometrics.BiometricEnrollBase.RESULT_TIMEOUT;
+import static com.android.settings.biometrics.fingerprint.SetupFingerprintEnrollIntroduction.EXTRA_FINGERPRINT_ENROLLED_COUNT;
+
+import android.app.Application;
+import android.app.KeyguardManager;
+import android.content.Intent;
+import android.os.Bundle;
+import android.util.Log;
+
+import androidx.activity.result.ActivityResult;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+import androidx.lifecycle.AndroidViewModel;
+import androidx.lifecycle.DefaultLifecycleObserver;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.MutableLiveData;
+
+import com.android.settings.biometrics.BiometricEnrollBase;
+import com.android.settings.biometrics2.data.repository.FingerprintRepository;
+import com.android.settings.biometrics2.ui.model.EnrollmentRequest;
+import com.android.settings.password.SetupSkipDialog;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * Fingerprint enrollment view model implementation
+ */
+public class FingerprintEnrollmentViewModel extends AndroidViewModel implements
+ DefaultLifecycleObserver {
+
+ private static final String TAG = "FingerprintEnrollmentViewModel";
+
+ @VisibleForTesting
+ static final String SAVED_STATE_IS_WAITING_ACTIVITY_RESULT = "is_waiting_activity_result";
+
+ @NonNull private final FingerprintRepository mFingerprintRepository;
+ @Nullable private final KeyguardManager mKeyguardManager;
+
+ private final AtomicBoolean mIsWaitingActivityResult = new AtomicBoolean(false);
+ private final MutableLiveData<ActivityResult> mSetResultLiveData = new MutableLiveData<>();
+
+ /**
+ * Even this variable may be nullable, but activity will call setIntent() immediately during
+ * its onCreate(), we do not assign @Nullable for this variable here.
+ */
+ private EnrollmentRequest mRequest = null;
+
+ public FingerprintEnrollmentViewModel(
+ @NonNull Application application,
+ @NonNull FingerprintRepository fingerprintRepository,
+ @Nullable KeyguardManager keyguardManager) {
+ super(application);
+ mFingerprintRepository = fingerprintRepository;
+ mKeyguardManager = keyguardManager;
+ }
+
+ /**
+ * Set EnrollmentRequest
+ */
+ public void setRequest(@NonNull EnrollmentRequest request) {
+ mRequest = request;
+ }
+
+ /**
+ * Get EnrollmentRequest
+ */
+ public EnrollmentRequest getRequest() {
+ return mRequest;
+ }
+
+ /**
+ * Copy necessary extra data from activity intent
+ */
+ @NonNull
+ public Bundle getNextActivityBaseIntentExtras() {
+ final Bundle bundle = mRequest.getSuwExtras();
+ bundle.putBoolean(EXTRA_FROM_SETTINGS_SUMMARY, mRequest.isFromSettingsSummery());
+ return bundle;
+ }
+
+ /**
+ * Handle activity result from FingerprintFindSensor
+ */
+ public void onContinueEnrollActivityResult(@NonNull ActivityResult result, int userId) {
+ if (mIsWaitingActivityResult.compareAndSet(true, false)) {
+ Log.w(TAG, "fail to reset isWaiting flag for enrollment");
+ }
+ if (result.getResultCode() == RESULT_FINISHED
+ || result.getResultCode() == RESULT_TIMEOUT) {
+ Intent data = result.getData();
+ if (mRequest.isSuw() && isKeyguardSecure()
+ && result.getResultCode() == RESULT_FINISHED) {
+ if (data == null) {
+ data = new Intent();
+ }
+ data.putExtras(getSuwFingerprintCountExtra(userId));
+ }
+ mSetResultLiveData.postValue(new ActivityResult(result.getResultCode(), data));
+ } else if (result.getResultCode() == RESULT_SKIP
+ || result.getResultCode() == SetupSkipDialog.RESULT_SKIP) {
+ mSetResultLiveData.postValue(result);
+ }
+ }
+
+
+
+ private boolean isKeyguardSecure() {
+ return mKeyguardManager != null && mKeyguardManager.isKeyguardSecure();
+ }
+
+ /**
+ * Activity calls this method during onPause() to finish itself when back to background.
+ *
+ * @param isActivityFinishing Activity has called finish() or not
+ * @param isChangingConfigurations Activity is finished because of configuration changed or not.
+ */
+ public void checkFinishActivityDuringOnPause(boolean isActivityFinishing,
+ boolean isChangingConfigurations) {
+ if (isChangingConfigurations || isActivityFinishing || mRequest.isSuw()
+ || isWaitingActivityResult().get()) {
+ return;
+ }
+
+ mSetResultLiveData.postValue(new ActivityResult(BiometricEnrollBase.RESULT_TIMEOUT, null));
+ }
+
+ @NonNull
+ private Bundle getSuwFingerprintCountExtra(int userId) {
+ final Bundle bundle = new Bundle();
+ bundle.putInt(EXTRA_FINGERPRINT_ENROLLED_COUNT,
+ mFingerprintRepository.getNumOfEnrolledFingerprintsSize(userId));
+ return bundle;
+ }
+
+ @NonNull
+ public LiveData<ActivityResult> getSetResultLiveData() {
+ return mSetResultLiveData;
+ }
+
+ @NonNull
+ public AtomicBoolean isWaitingActivityResult() {
+ return mIsWaitingActivityResult;
+ }
+
+ /**
+ * Handle savedInstanceState from activity onCreated()
+ */
+ public void setSavedInstanceState(@Nullable Bundle savedInstanceState) {
+ if (savedInstanceState == null) {
+ return;
+ }
+ mIsWaitingActivityResult.set(
+ savedInstanceState.getBoolean(SAVED_STATE_IS_WAITING_ACTIVITY_RESULT, false)
+ );
+ }
+
+ /**
+ * Handle onSaveInstanceState from activity
+ */
+ public void onSaveInstanceState(@NonNull Bundle outState) {
+ outState.putBoolean(SAVED_STATE_IS_WAITING_ACTIVITY_RESULT, mIsWaitingActivityResult.get());
+ }
+}
diff --git a/src/com/android/settings/core/instrumentation/SettingsEventLogWriter.java b/src/com/android/settings/core/instrumentation/SettingsEventLogWriter.java
index f165897..e85576b 100644
--- a/src/com/android/settings/core/instrumentation/SettingsEventLogWriter.java
+++ b/src/com/android/settings/core/instrumentation/SettingsEventLogWriter.java
@@ -41,6 +41,22 @@
}
@Override
+ public void clicked(int sourceCategory, String key) {
+ if (shouldDisableGenericEventLogging()) {
+ return;
+ }
+ super.clicked(sourceCategory, key);
+ }
+
+ @Override
+ public void changed(int category, String key, int value) {
+ if (shouldDisableGenericEventLogging()) {
+ return;
+ }
+ super.changed(category, key, value);
+ }
+
+ @Override
public void action(Context context, int category, String pkg) {
if (shouldDisableGenericEventLogging()) {
return;
diff --git a/src/com/android/settings/core/instrumentation/SettingsIntelligenceLogWriter.java b/src/com/android/settings/core/instrumentation/SettingsIntelligenceLogWriter.java
index 8dfa095..86ee3d6 100644
--- a/src/com/android/settings/core/instrumentation/SettingsIntelligenceLogWriter.java
+++ b/src/com/android/settings/core/instrumentation/SettingsIntelligenceLogWriter.java
@@ -85,6 +85,10 @@
}
@Override
+ public void changed(int category, String key, int value) {
+ }
+
+ @Override
public void action(Context context, int action, Pair<Integer, Object>... taggedData) {
action(SettingsEnums.PAGE_UNKNOWN /* attribution */,
action,
diff --git a/src/com/android/settings/core/instrumentation/StatsLogWriter.java b/src/com/android/settings/core/instrumentation/StatsLogWriter.java
index 15b589f..7b5915a 100644
--- a/src/com/android/settings/core/instrumentation/StatsLogWriter.java
+++ b/src/com/android/settings/core/instrumentation/StatsLogWriter.java
@@ -55,6 +55,16 @@
}
@Override
+ public void changed(int sourceCategory, String key, int value) {
+ SettingsStatsLog.write(SettingsStatsLog.SETTINGS_UI_CHANGED /* Atom name */,
+ sourceCategory /* attribution */,
+ SettingsEnums.ACTION_SETTINGS_PREFERENCE_CHANGE /* action */,
+ SettingsEnums.PAGE_UNKNOWN /* pageId */,
+ key /* changedPreferenceKey */,
+ value /* changedPreferenceIntValue */);
+ }
+
+ @Override
public void action(Context context, int action, Pair<Integer, Object>... taggedData) {
action(SettingsEnums.PAGE_UNKNOWN /* attribution */,
action,
diff --git a/src/com/android/settings/dashboard/profileselector/ProfileSelectDialog.java b/src/com/android/settings/dashboard/profileselector/ProfileSelectDialog.java
index f82694c..ef6ad83 100644
--- a/src/com/android/settings/dashboard/profileselector/ProfileSelectDialog.java
+++ b/src/com/android/settings/dashboard/profileselector/ProfileSelectDialog.java
@@ -23,6 +23,7 @@
import android.content.DialogInterface.OnDismissListener;
import android.content.DialogInterface.OnShowListener;
import android.content.Intent;
+import android.content.pm.UserInfo;
import android.os.Bundle;
import android.os.UserHandle;
import android.os.UserManager;
@@ -168,7 +169,8 @@
}
final UserManager userManager = UserManager.get(context);
for (int i = userHandles.size() - 1; i >= 0; i--) {
- if (userManager.getUserInfo(userHandles.get(i).getIdentifier()) == null) {
+ UserInfo userInfo = userManager.getUserInfo(userHandles.get(i).getIdentifier());
+ if (userInfo == null || userInfo.isCloneProfile()) {
if (DEBUG) {
Log.d(TAG, "Delete the user: " + userHandles.get(i).getIdentifier());
}
diff --git a/src/com/android/settings/fuelgauge/batterysaver/BatterySaverScheduleRadioButtonsController.java b/src/com/android/settings/fuelgauge/batterysaver/BatterySaverScheduleRadioButtonsController.java
index 7b17dcb..c584b9b 100644
--- a/src/com/android/settings/fuelgauge/batterysaver/BatterySaverScheduleRadioButtonsController.java
+++ b/src/com/android/settings/fuelgauge/batterysaver/BatterySaverScheduleRadioButtonsController.java
@@ -22,6 +22,7 @@
import android.provider.Settings;
import android.provider.Settings.Global;
import android.text.TextUtils;
+import android.util.Log;
import com.android.settingslib.fuelgauge.BatterySaverUtils;
@@ -36,9 +37,9 @@
* See {@link Settings.Global#AUTOMATIC_POWER_SAVE_MODE} for more details.
*/
public class BatterySaverScheduleRadioButtonsController {
+ private static final String TAG = "BatterySaverScheduleRadioButtonsController";
public static final String KEY_NO_SCHEDULE = "key_battery_saver_no_schedule";
- public static final String KEY_ROUTINE = "key_battery_saver_routine";
public static final String KEY_PERCENTAGE = "key_battery_saver_percentage";
public static final int TRIGGER_LEVEL_MIN = 10;
@@ -53,20 +54,17 @@
public String getDefaultKey() {
final ContentResolver resolver = mContext.getContentResolver();
- // Note: this can also be obtained via PowerManager.getPowerSaveModeTrigger()
final int mode = Settings.Global.getInt(resolver, Global.AUTOMATIC_POWER_SAVE_MODE,
PowerManager.POWER_SAVE_MODE_TRIGGER_PERCENTAGE);
- // if mode is "dynamic" we are in routine mode, percentage with non-zero threshold is
- // percentage mode, otherwise it is no schedule mode
if (mode == PowerManager.POWER_SAVE_MODE_TRIGGER_PERCENTAGE) {
final int threshold =
Settings.Global.getInt(resolver, Global.LOW_POWER_MODE_TRIGGER_LEVEL, 0);
- if (threshold <= 0) {
- return KEY_NO_SCHEDULE;
- }
- return KEY_PERCENTAGE;
+ return threshold <= 0 ? KEY_NO_SCHEDULE : KEY_PERCENTAGE;
}
- return KEY_ROUTINE;
+ // Convert the legacy routine mode into none.
+ BatterySaverUtils.revertScheduleToNoneIfNeeded(mContext);
+ Log.w(TAG, "Found the legacy routine mode and set into none");
+ return KEY_NO_SCHEDULE;
}
public boolean setDefaultKey(String key) {
@@ -89,12 +87,6 @@
confirmationExtras.putInt(BatterySaverUtils.EXTRA_POWER_SAVE_MODE_TRIGGER_LEVEL,
triggerLevel);
break;
- case KEY_ROUTINE:
- mode = PowerManager.POWER_SAVE_MODE_TRIGGER_DYNAMIC;
- confirmationExtras.putBoolean(BatterySaverUtils.EXTRA_CONFIRM_TEXT_ONLY, true);
- confirmationExtras.putInt(BatterySaverUtils.EXTRA_POWER_SAVE_MODE_TRIGGER,
- PowerManager.POWER_SAVE_MODE_TRIGGER_DYNAMIC);
- break;
default:
throw new IllegalStateException(
"Not a valid key for " + this.getClass().getSimpleName());
diff --git a/src/com/android/settings/inputmethod/PhysicalKeyboardFragment.java b/src/com/android/settings/inputmethod/PhysicalKeyboardFragment.java
index 808129f..d7a208c 100644
--- a/src/com/android/settings/inputmethod/PhysicalKeyboardFragment.java
+++ b/src/com/android/settings/inputmethod/PhysicalKeyboardFragment.java
@@ -20,6 +20,7 @@
import android.annotation.Nullable;
import android.app.Activity;
import android.app.settings.SettingsEnums;
+import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.database.ContentObserver;
@@ -211,8 +212,10 @@
private final OnPreferenceChangeListener mShowVirtualKeyboardSwitchPreferenceChangeListener =
(preference, newValue) -> {
- Secure.putInt(getContentResolver(), Secure.SHOW_IME_WITH_HARD_KEYBOARD,
- ((Boolean) newValue) ? 1 : 0);
+ final ContentResolver cr = getContentResolver();
+ Secure.putInt(cr, Secure.SHOW_IME_WITH_HARD_KEYBOARD, ((Boolean) newValue) ? 1 : 0);
+ cr.notifyChange(Secure.getUriFor(Secure.SHOW_IME_WITH_HARD_KEYBOARD),
+ null /* observer */, ContentResolver.NOTIFY_NO_DELAY);
return true;
};
diff --git a/src/com/android/settings/notification/RedactionInterstitial.java b/src/com/android/settings/notification/RedactionInterstitial.java
index f243250..d6fdaf8 100644
--- a/src/com/android/settings/notification/RedactionInterstitial.java
+++ b/src/com/android/settings/notification/RedactionInterstitial.java
@@ -189,13 +189,16 @@
}
private void loadFromSettings() {
+ final boolean showUnRedactedDefault = getContext().getResources().getBoolean(
+ R.bool.default_allow_sensitive_lockscreen_content);
final boolean managedProfile = UserManager.get(getContext()).isManagedProfile(mUserId);
// Hiding all notifications is device-wide setting, managed profiles can only set
// whether their notifications are show in full or redacted.
final boolean showNotifications = managedProfile || Settings.Secure.getIntForUser(
getContentResolver(), LOCK_SCREEN_SHOW_NOTIFICATIONS, 0, mUserId) != 0;
final boolean showUnredacted = Settings.Secure.getIntForUser(
- getContentResolver(), LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS, 1, mUserId) != 0;
+ getContentResolver(), LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS,
+ showUnRedactedDefault ? 1 : 0, mUserId) != 0;
int checkedButtonId = R.id.hide_all;
if (showNotifications) {
diff --git a/src/com/android/settings/overlay/FeatureFactory.java b/src/com/android/settings/overlay/FeatureFactory.java
index cf8698c..5da9310 100644
--- a/src/com/android/settings/overlay/FeatureFactory.java
+++ b/src/com/android/settings/overlay/FeatureFactory.java
@@ -29,6 +29,7 @@
import com.android.settings.applications.ApplicationFeatureProvider;
import com.android.settings.aware.AwareFeatureProvider;
import com.android.settings.biometrics.face.FaceFeatureProvider;
+import com.android.settings.biometrics2.factory.BiometricsRepositoryProvider;
import com.android.settings.bluetooth.BluetoothFeatureProvider;
import com.android.settings.dashboard.DashboardFeatureProvider;
import com.android.settings.dashboard.suggestions.SuggestionFeatureProvider;
@@ -162,6 +163,11 @@
public abstract FaceFeatureProvider getFaceFeatureProvider();
/**
+ * Gets implementation for Biometrics repository provider.
+ */
+ public abstract BiometricsRepositoryProvider getBiometricsRepositoryProvider();
+
+ /**
* Gets implementation for the WifiTrackerLib.
*/
public abstract WifiTrackerLibProvider getWifiTrackerLibProvider();
diff --git a/src/com/android/settings/overlay/FeatureFactoryImpl.java b/src/com/android/settings/overlay/FeatureFactoryImpl.java
index b779716..bc78f2e 100644
--- a/src/com/android/settings/overlay/FeatureFactoryImpl.java
+++ b/src/com/android/settings/overlay/FeatureFactoryImpl.java
@@ -37,6 +37,8 @@
import com.android.settings.aware.AwareFeatureProviderImpl;
import com.android.settings.biometrics.face.FaceFeatureProvider;
import com.android.settings.biometrics.face.FaceFeatureProviderImpl;
+import com.android.settings.biometrics2.factory.BiometricsRepositoryProvider;
+import com.android.settings.biometrics2.factory.BiometricsRepositoryProviderImpl;
import com.android.settings.bluetooth.BluetoothFeatureProvider;
import com.android.settings.bluetooth.BluetoothFeatureProviderImpl;
import com.android.settings.connecteddevice.dock.DockUpdaterFeatureProviderImpl;
@@ -104,6 +106,7 @@
private BluetoothFeatureProvider mBluetoothFeatureProvider;
private AwareFeatureProvider mAwareFeatureProvider;
private FaceFeatureProvider mFaceFeatureProvider;
+ private BiometricsRepositoryProvider mBiometricsRepositoryProvider;
private WifiTrackerLibProvider mWifiTrackerLibProvider;
private SecuritySettingsFeatureProvider mSecuritySettingsFeatureProvider;
private AccessibilitySearchFeatureProvider mAccessibilitySearchFeatureProvider;
@@ -306,6 +309,14 @@
}
@Override
+ public BiometricsRepositoryProvider getBiometricsRepositoryProvider() {
+ if (mBiometricsRepositoryProvider == null) {
+ mBiometricsRepositoryProvider = new BiometricsRepositoryProviderImpl();
+ }
+ return mBiometricsRepositoryProvider;
+ }
+
+ @Override
public WifiTrackerLibProvider getWifiTrackerLibProvider() {
if (mWifiTrackerLibProvider == null) {
mWifiTrackerLibProvider = new WifiTrackerLibProviderImpl();
diff --git a/src/com/android/settings/tts/OWNERS b/src/com/android/settings/tts/OWNERS
index 8d9c2c6..7ba7dc1 100644
--- a/src/com/android/settings/tts/OWNERS
+++ b/src/com/android/settings/tts/OWNERS
@@ -1,5 +1,2 @@
# Default reviewers for this and subdirectories.
rni@google.com
-
-# Emergency approvers in case the above are not available
-tmfang@google.com
\ No newline at end of file
diff --git a/src/com/android/settings/widget/SettingsMainSwitchBar.java b/src/com/android/settings/widget/SettingsMainSwitchBar.java
index 5f752f9..5ad16d7 100644
--- a/src/com/android/settings/widget/SettingsMainSwitchBar.java
+++ b/src/com/android/settings/widget/SettingsMainSwitchBar.java
@@ -18,7 +18,6 @@
import static com.android.settingslib.RestrictedLockUtils.EnforcedAdmin;
-import android.app.settings.SettingsEnums;
import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
@@ -55,7 +54,7 @@
private final MetricsFeatureProvider mMetricsFeatureProvider;
private OnBeforeCheckedChangeListener mOnBeforeListener;
- private String mMetricsTag;
+ private int mMetricsCategory;
public SettingsMainSwitchBar(Context context) {
this(context, null);
@@ -125,12 +124,7 @@
}
protected void onRestrictedIconClick() {
- mMetricsFeatureProvider.action(
- SettingsEnums.PAGE_UNKNOWN,
- SettingsEnums.ACTION_SETTINGS_PREFERENCE_CHANGE,
- SettingsEnums.PAGE_UNKNOWN,
- mMetricsTag + "/switch_bar|restricted",
- 1);
+ mMetricsFeatureProvider.clicked(mMetricsCategory, "switch_bar|restricted");
}
@Override
@@ -159,8 +153,8 @@
/**
* Set the metrics tag.
*/
- public void setMetricsTag(String tag) {
- mMetricsTag = tag;
+ public void setMetricsCategory(int category) {
+ mMetricsCategory = category;
}
private View getDelegatingView() {
@@ -168,11 +162,6 @@
}
private void logMetrics(boolean isChecked) {
- mMetricsFeatureProvider.action(
- SettingsEnums.PAGE_UNKNOWN,
- SettingsEnums.ACTION_SETTINGS_PREFERENCE_CHANGE,
- SettingsEnums.PAGE_UNKNOWN,
- mMetricsTag + "/switch_bar",
- isChecked ? 1 : 0);
+ mMetricsFeatureProvider.changed(mMetricsCategory, "switch_bar", isChecked ? 1 : 0);
}
}
diff --git a/tests/robotests/src/com/android/settings/dashboard/profileselector/ProfileSelectDialogTest.java b/tests/robotests/src/com/android/settings/dashboard/profileselector/ProfileSelectDialogTest.java
index e1cf52b..4e81cee 100644
--- a/tests/robotests/src/com/android/settings/dashboard/profileselector/ProfileSelectDialogTest.java
+++ b/tests/robotests/src/com/android/settings/dashboard/profileselector/ProfileSelectDialogTest.java
@@ -53,6 +53,7 @@
private static final UserHandle NORMAL_USER = new UserHandle(1111);
private static final UserHandle REMOVED_USER = new UserHandle(2222);
+ private static final UserHandle CLONE_USER = new UserHandle(3333);
@Spy
private Context mContext = ApplicationProvider.getApplicationContext();
@@ -102,6 +103,22 @@
}
@Test
+ public void updateUserHandlesIfNeeded_removesCloneProfile() {
+ final UserInfo userInfo = new UserInfo(CLONE_USER.getIdentifier(), "clone_user", null,
+ UserInfo.FLAG_PROFILE, UserManager.USER_TYPE_PROFILE_CLONE);
+ when(mUserManager.getUserInfo(CLONE_USER.getIdentifier())).thenReturn(userInfo);
+ final Tile tile = new ActivityTile(mActivityInfo, CategoryKey.CATEGORY_HOMEPAGE);
+ tile.userHandle.add(CLONE_USER);
+ tile.userHandle.add(NORMAL_USER);
+
+ ProfileSelectDialog.updateUserHandlesIfNeeded(mContext, tile);
+
+ assertThat(tile.userHandle).hasSize(1);
+ assertThat(tile.userHandle.get(0).getIdentifier()).isEqualTo(NORMAL_USER.getIdentifier());
+ verify(mUserManager, times(1)).getUserInfo(CLONE_USER.getIdentifier());
+ }
+
+ @Test
public void createDialog_showsCorrectTitle() {
mContext.setTheme(R.style.Theme_AppCompat);
diff --git a/tests/robotests/src/com/android/settings/notification/RedactionInterstitialTest.java b/tests/robotests/src/com/android/settings/notification/RedactionInterstitialTest.java
index 5c6da49..9d475b8 100644
--- a/tests/robotests/src/com/android/settings/notification/RedactionInterstitialTest.java
+++ b/tests/robotests/src/com/android/settings/notification/RedactionInterstitialTest.java
@@ -21,6 +21,7 @@
import com.android.settings.R;
import com.android.settings.RestrictedRadioButton;
import com.android.settings.notification.RedactionInterstitial.RedactionInterstitialFragment;
+import com.android.settings.testutils.shadow.SettingsShadowResources;
import com.android.settings.testutils.shadow.ShadowRestrictedLockUtilsInternal;
import com.android.settings.testutils.shadow.ShadowUtils;
@@ -38,6 +39,7 @@
@Config(shadows = {
ShadowUtils.class,
ShadowRestrictedLockUtilsInternal.class,
+ SettingsShadowResources.class,
})
public class RedactionInterstitialTest {
private RedactionInterstitial mActivity;
@@ -134,6 +136,28 @@
assertSelectedButton(R.id.redact_sensitive);
}
+ @Test
+ public void defaultShowSensitiveContent_configDeny() {
+ final ContentResolver resolver = RuntimeEnvironment.application.getContentResolver();
+ Settings.Secure.putIntForUser(resolver,
+ LOCK_SCREEN_SHOW_NOTIFICATIONS, 1, UserHandle.myUserId());
+ setupConfig(false);
+ setupActivity();
+
+ assertSelectedButton(R.id.redact_sensitive);
+ }
+
+ @Test
+ public void defaultShowSensitiveContent_configAllow() {
+ final ContentResolver resolver = RuntimeEnvironment.application.getContentResolver();
+ Settings.Secure.putIntForUser(resolver,
+ LOCK_SCREEN_SHOW_NOTIFICATIONS, 1, UserHandle.myUserId());
+ setupConfig(true);
+ setupActivity();
+
+ assertSelectedButton(R.id.show_all);
+ }
+
private void setupActivity() {
mActivity = buildActivity(RedactionInterstitial.class, new Intent()).setup().get();
mFragment = (RedactionInterstitialFragment)
@@ -142,6 +166,11 @@
assertThat(mFragment).isNotNull();
}
+ private void setupConfig(boolean allowSensitiveContent) {
+ SettingsShadowResources.overrideResource(
+ R.bool.default_allow_sensitive_lockscreen_content, allowSensitiveContent);
+ }
+
private void setupSettings(int show, int showUnredacted) {
final ContentResolver resolver = RuntimeEnvironment.application.getContentResolver();
Settings.Secure.putIntForUser(resolver,
diff --git a/tests/robotests/src/com/android/settings/testutils/FakeFeatureFactory.java b/tests/robotests/src/com/android/settings/testutils/FakeFeatureFactory.java
index b87d983..c1e7bfb 100644
--- a/tests/robotests/src/com/android/settings/testutils/FakeFeatureFactory.java
+++ b/tests/robotests/src/com/android/settings/testutils/FakeFeatureFactory.java
@@ -27,6 +27,7 @@
import com.android.settings.applications.ApplicationFeatureProvider;
import com.android.settings.aware.AwareFeatureProvider;
import com.android.settings.biometrics.face.FaceFeatureProvider;
+import com.android.settings.biometrics2.factory.BiometricsRepositoryProvider;
import com.android.settings.bluetooth.BluetoothFeatureProvider;
import com.android.settings.dashboard.DashboardFeatureProvider;
import com.android.settings.dashboard.suggestions.SuggestionFeatureProvider;
@@ -78,6 +79,7 @@
public final BluetoothFeatureProvider mBluetoothFeatureProvider;
public final AwareFeatureProvider mAwareFeatureProvider;
public final FaceFeatureProvider mFaceFeatureProvider;
+ public final BiometricsRepositoryProvider mBiometricsRepositoryProvider;
public PanelFeatureProvider panelFeatureProvider;
public SlicesFeatureProvider slicesFeatureProvider;
@@ -134,6 +136,7 @@
mBluetoothFeatureProvider = mock(BluetoothFeatureProvider.class);
mAwareFeatureProvider = mock(AwareFeatureProvider.class);
mFaceFeatureProvider = mock(FaceFeatureProvider.class);
+ mBiometricsRepositoryProvider = mock(BiometricsRepositoryProvider.class);
wifiTrackerLibProvider = mock(WifiTrackerLibProvider.class);
securitySettingsFeatureProvider = mock(SecuritySettingsFeatureProvider.class);
mAccessibilitySearchFeatureProvider = mock(AccessibilitySearchFeatureProvider.class);
@@ -257,6 +260,11 @@
}
@Override
+ public BiometricsRepositoryProvider getBiometricsRepositoryProvider() {
+ return mBiometricsRepositoryProvider;
+ }
+
+ @Override
public WifiTrackerLibProvider getWifiTrackerLibProvider() {
return wifiTrackerLibProvider;
}
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 7a93f11..054b415 100644
--- a/tests/spa_unit/src/com/android/settings/testutils/FakeFeatureFactory.kt
+++ b/tests/spa_unit/src/com/android/settings/testutils/FakeFeatureFactory.kt
@@ -23,6 +23,7 @@
import com.android.settings.applications.ApplicationFeatureProvider
import com.android.settings.aware.AwareFeatureProvider
import com.android.settings.biometrics.face.FaceFeatureProvider
+import com.android.settings.biometrics2.factory.BiometricsRepositoryProvider
import com.android.settings.bluetooth.BluetoothFeatureProvider
import com.android.settings.dashboard.DashboardFeatureProvider
import com.android.settings.dashboard.suggestions.SuggestionFeatureProvider
@@ -153,6 +154,10 @@
TODO("Not yet implemented")
}
+ override fun getBiometricsRepositoryProvider(): BiometricsRepositoryProvider {
+ TODO("Not yet implemented")
+ }
+
override fun getWifiTrackerLibProvider(): WifiTrackerLibProvider {
TODO("Not yet implemented")
}
diff --git a/tests/unit/src/com/android/settings/biometrics2/data/repository/FingerprintRepositoryTest.java b/tests/unit/src/com/android/settings/biometrics2/data/repository/FingerprintRepositoryTest.java
new file mode 100644
index 0000000..e5920f3
--- /dev/null
+++ b/tests/unit/src/com/android/settings/biometrics2/data/repository/FingerprintRepositoryTest.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.biometrics2.data.repository;
+
+import static android.hardware.fingerprint.FingerprintSensorProperties.TYPE_HOME_BUTTON;
+import static android.hardware.fingerprint.FingerprintSensorProperties.TYPE_POWER_BUTTON;
+import static android.hardware.fingerprint.FingerprintSensorProperties.TYPE_REAR;
+import static android.hardware.fingerprint.FingerprintSensorProperties.TYPE_UDFPS_OPTICAL;
+import static android.hardware.fingerprint.FingerprintSensorProperties.TYPE_UDFPS_ULTRASONIC;
+import static android.hardware.fingerprint.FingerprintSensorProperties.TYPE_UNKNOWN;
+
+import static com.android.settings.biometrics2.util.FingerprintManagerUtil.setupFingerprintEnrolledFingerprints;
+import static com.android.settings.biometrics2.util.FingerprintManagerUtil.setupFingerprintFirstSensor;
+
+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.hardware.fingerprint.FingerprintManager;
+
+import androidx.annotation.NonNull;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import com.android.settings.testutils.ResourcesUtils;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+@RunWith(AndroidJUnit4.class)
+public class FingerprintRepositoryTest {
+
+ @Rule public final MockitoRule mockito = MockitoJUnit.rule();
+
+ @Mock private Resources mResources;
+ @Mock private FingerprintManager mFingerprintManager;
+
+ private Context mContext;
+ private FingerprintRepository mFingerprintRepository;
+
+ @Before
+ public void setUp() {
+ mContext = ApplicationProvider.getApplicationContext();
+ mFingerprintRepository = new FingerprintRepository(mFingerprintManager);
+ }
+
+ @Test
+ public void testCanAssumeSensorType() {
+ setupFingerprintFirstSensor(mFingerprintManager, TYPE_UNKNOWN, 1);
+ assertThat(mFingerprintRepository.canAssumeUdfps()).isFalse();
+
+ setupFingerprintFirstSensor(mFingerprintManager, TYPE_REAR, 1);
+ assertThat(mFingerprintRepository.canAssumeUdfps()).isFalse();
+
+ setupFingerprintFirstSensor(mFingerprintManager, TYPE_UDFPS_ULTRASONIC, 1);
+ assertThat(mFingerprintRepository.canAssumeUdfps()).isTrue();
+
+ setupFingerprintFirstSensor(mFingerprintManager, TYPE_UDFPS_OPTICAL, 1);
+ assertThat(mFingerprintRepository.canAssumeUdfps()).isTrue();
+
+ setupFingerprintFirstSensor(mFingerprintManager, TYPE_POWER_BUTTON, 1);
+ assertThat(mFingerprintRepository.canAssumeUdfps()).isFalse();
+
+ setupFingerprintFirstSensor(mFingerprintManager, TYPE_HOME_BUTTON, 1);
+ assertThat(mFingerprintRepository.canAssumeUdfps()).isFalse();
+ }
+
+ @Test
+ public void testGetMaxFingerprints() {
+ setupFingerprintFirstSensor(mFingerprintManager, TYPE_UNKNOWN, 44);
+ assertThat(mFingerprintRepository.getMaxFingerprints()).isEqualTo(44);
+
+ setupFingerprintFirstSensor(mFingerprintManager, TYPE_UNKNOWN, 999);
+ assertThat(mFingerprintRepository.getMaxFingerprints()).isEqualTo(999);
+ }
+
+ @Test
+ public void testGetNumOfEnrolledFingerprintsSize() {
+ setupFingerprintEnrolledFingerprints(mFingerprintManager, 10, 3);
+ setupFingerprintEnrolledFingerprints(mFingerprintManager, 22, 99);
+
+ assertThat(mFingerprintRepository.getNumOfEnrolledFingerprintsSize(10)).isEqualTo(3);
+ assertThat(mFingerprintRepository.getNumOfEnrolledFingerprintsSize(22)).isEqualTo(99);
+ }
+
+ @Test
+ public void testGetMaxFingerprintsInSuw() {
+ setupSuwMaxFingerprintsEnrollable(mContext, mResources, 333);
+ assertThat(mFingerprintRepository.getMaxFingerprintsInSuw(mResources))
+ .isEqualTo(333);
+
+ setupSuwMaxFingerprintsEnrollable(mContext, mResources, 20);
+ assertThat(mFingerprintRepository.getMaxFingerprintsInSuw(mResources)).isEqualTo(20);
+ }
+
+ public static void setupSuwMaxFingerprintsEnrollable(
+ @NonNull Context context,
+ @NonNull Resources mockedResources,
+ int numOfFp) {
+ final int resId = ResourcesUtils.getResourcesId(context, "integer",
+ "suw_max_fingerprints_enrollable");
+ when(mockedResources.getInteger(resId)).thenReturn(numOfFp);
+ }
+}
diff --git a/tests/unit/src/com/android/settings/biometrics2/ui/viewmodel/AutoCredentialViewModelTest.java b/tests/unit/src/com/android/settings/biometrics2/ui/viewmodel/AutoCredentialViewModelTest.java
new file mode 100644
index 0000000..7a13875
--- /dev/null
+++ b/tests/unit/src/com/android/settings/biometrics2/ui/viewmodel/AutoCredentialViewModelTest.java
@@ -0,0 +1,380 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.biometrics2.ui.viewmodel;
+
+import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_ALPHABETIC;
+import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_NUMERIC;
+import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_SOMETHING;
+import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED;
+
+import static com.android.settings.biometrics.BiometricEnrollBase.EXTRA_KEY_CHALLENGE;
+import static com.android.settings.biometrics.BiometricEnrollBase.EXTRA_KEY_SENSOR_ID;
+import static com.android.settings.biometrics2.ui.model.CredentialModel.INVALID_CHALLENGE;
+import static com.android.settings.biometrics2.ui.model.CredentialModel.INVALID_SENSOR_ID;
+import static com.android.settings.biometrics2.ui.viewmodel.AutoCredentialViewModel.CREDENTIAL_FAIL_NEED_TO_CHOOSE_LOCK;
+import static com.android.settings.biometrics2.ui.viewmodel.AutoCredentialViewModel.CREDENTIAL_FAIL_NEED_TO_CONFIRM_LOCK;
+import static com.android.settings.biometrics2.ui.viewmodel.AutoCredentialViewModel.ChallengeGenerator;
+import static com.android.settings.biometrics2.ui.viewmodel.AutoCredentialViewModel.CredentialAction;
+import static com.android.settings.biometrics2.ui.viewmodel.AutoCredentialViewModel.GenerateChallengeCallback;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.when;
+
+import android.annotation.NonNull;
+import android.app.Activity;
+import android.content.Intent;
+import android.os.SystemClock;
+import android.os.UserHandle;
+
+import androidx.activity.result.ActivityResult;
+import androidx.annotation.Nullable;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import com.android.internal.widget.LockPatternUtils;
+import com.android.internal.widget.VerifyCredentialResponse;
+import com.android.settings.biometrics2.ui.model.CredentialModel;
+import com.android.settings.password.ChooseLockPattern;
+import com.android.settings.password.ChooseLockSettingsHelper;
+import com.android.settings.testutils.InstantTaskExecutorRule;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+@RunWith(AndroidJUnit4.class)
+public class AutoCredentialViewModelTest {
+
+ @Rule public final MockitoRule mockito = MockitoJUnit.rule();
+ @Rule public final InstantTaskExecutorRule mTaskExecutorRule = new InstantTaskExecutorRule();
+
+ @Mock private LifecycleOwner mLifecycleOwner;
+ @Mock private LockPatternUtils mLockPatternUtils;
+ private TestChallengeGenerator mChallengeGenerator = null;
+ private AutoCredentialViewModel mAutoCredentialViewModel;
+
+ @Before
+ public void setUp() {
+ mChallengeGenerator = new TestChallengeGenerator();
+ mAutoCredentialViewModel = new AutoCredentialViewModel(
+ ApplicationProvider.getApplicationContext(),
+ mLockPatternUtils,
+ mChallengeGenerator);
+ }
+
+ private CredentialModel newCredentialModel(int userId, long challenge,
+ @Nullable byte[] token, long gkPwHandle) {
+ final Intent intent = new Intent();
+ intent.putExtra(Intent.EXTRA_USER_ID, userId);
+ intent.putExtra(EXTRA_KEY_SENSOR_ID, 1);
+ intent.putExtra(EXTRA_KEY_CHALLENGE, challenge);
+ intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN, token);
+ intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_GK_PW_HANDLE, gkPwHandle);
+ return new CredentialModel(intent, SystemClock.elapsedRealtimeClock());
+ }
+
+ private CredentialModel newValidTokenCredentialModel(int userId) {
+ return newCredentialModel(userId, 1L, new byte[] { 0 }, 0L);
+ }
+
+ private CredentialModel newInvalidChallengeCredentialModel(int userId) {
+ return newCredentialModel(userId, INVALID_CHALLENGE, null, 0L);
+ }
+
+ private CredentialModel newGkPwHandleCredentialModel(int userId, long gkPwHandle) {
+ return newCredentialModel(userId, INVALID_CHALLENGE, null, gkPwHandle);
+ }
+
+ private void verifyNothingHappen() {
+ assertThat(mAutoCredentialViewModel.getActionLiveData().getValue()).isNull();
+ }
+
+ private void verifyOnlyActionLiveData(@CredentialAction int action) {
+ final Integer value = mAutoCredentialViewModel.getActionLiveData().getValue();
+ assertThat(value).isEqualTo(action);
+ }
+
+ private void setupGenerateTokenFlow(long gkPwHandle, int userId, int newSensorId,
+ long newChallenge) {
+ when(mLockPatternUtils.getActivePasswordQuality(userId)).thenReturn(
+ PASSWORD_QUALITY_SOMETHING);
+ mChallengeGenerator.mUserId = userId;
+ mChallengeGenerator.mSensorId = newSensorId;
+ mChallengeGenerator.mChallenge = newChallenge;
+ when(mLockPatternUtils.verifyGatekeeperPasswordHandle(gkPwHandle, newChallenge, userId))
+ .thenReturn(newGoodCredential(gkPwHandle, new byte[] { 1 }));
+ }
+
+ @Test
+ public void checkCredential_validCredentialCase() {
+ final int userId = 99;
+ mAutoCredentialViewModel.setCredentialModel(newValidTokenCredentialModel(userId));
+ when(mLockPatternUtils.getActivePasswordQuality(userId)).thenReturn(
+ PASSWORD_QUALITY_SOMETHING);
+
+ // Run credential check
+ mAutoCredentialViewModel.onCreate(mLifecycleOwner);
+
+ verifyNothingHappen();
+ }
+
+ @Test
+ public void checkCredential_needToChooseLock() {
+ final int userId = 100;
+ mAutoCredentialViewModel.setCredentialModel(newInvalidChallengeCredentialModel(userId));
+ when(mLockPatternUtils.getActivePasswordQuality(userId)).thenReturn(
+ PASSWORD_QUALITY_UNSPECIFIED);
+
+ // Run credential check
+ mAutoCredentialViewModel.onCreate(mLifecycleOwner);
+
+ verifyOnlyActionLiveData(CREDENTIAL_FAIL_NEED_TO_CHOOSE_LOCK);
+ }
+
+ @Test
+ public void checkCredential_needToConfirmLockFoSomething() {
+ final int userId = 101;
+ mAutoCredentialViewModel.setCredentialModel(newInvalidChallengeCredentialModel(userId));
+ when(mLockPatternUtils.getActivePasswordQuality(userId)).thenReturn(
+ PASSWORD_QUALITY_SOMETHING);
+
+ // Run credential check
+ mAutoCredentialViewModel.onCreate(mLifecycleOwner);
+
+ verifyOnlyActionLiveData(CREDENTIAL_FAIL_NEED_TO_CONFIRM_LOCK);
+ }
+
+ @Test
+ public void checkCredential_needToConfirmLockForNumeric() {
+ final int userId = 102;
+ mAutoCredentialViewModel.setCredentialModel(newInvalidChallengeCredentialModel(userId));
+ when(mLockPatternUtils.getActivePasswordQuality(userId)).thenReturn(
+ PASSWORD_QUALITY_NUMERIC);
+
+ // Run credential check
+ mAutoCredentialViewModel.onCreate(mLifecycleOwner);
+
+ verifyOnlyActionLiveData(CREDENTIAL_FAIL_NEED_TO_CONFIRM_LOCK);
+ }
+
+ @Test
+ public void checkCredential_needToConfirmLockForAlphabetic() {
+ final int userId = 103;
+ mAutoCredentialViewModel.setCredentialModel(newInvalidChallengeCredentialModel(userId));
+ when(mLockPatternUtils.getActivePasswordQuality(userId)).thenReturn(
+ PASSWORD_QUALITY_ALPHABETIC);
+
+ // Run credential check
+ mAutoCredentialViewModel.onCreate(mLifecycleOwner);
+
+ verifyOnlyActionLiveData(CREDENTIAL_FAIL_NEED_TO_CONFIRM_LOCK);
+ }
+
+ @Test
+ public void checkCredential_generateChallenge() {
+ final int userId = 104;
+ final long gkPwHandle = 1111L;
+ final CredentialModel credentialModel = newGkPwHandleCredentialModel(userId, gkPwHandle);
+ mAutoCredentialViewModel.setCredentialModel(credentialModel);
+ when(mLockPatternUtils.getActivePasswordQuality(userId)).thenReturn(
+ PASSWORD_QUALITY_SOMETHING);
+
+ final int newSensorId = 10;
+ final long newChallenge = 20L;
+ setupGenerateTokenFlow(gkPwHandle, userId, newSensorId, newChallenge);
+
+ // Run credential check
+ mAutoCredentialViewModel.onCreate(mLifecycleOwner);
+
+ assertThat(mAutoCredentialViewModel.getActionLiveData().getValue()).isNull();
+ assertThat(credentialModel.getSensorId()).isEqualTo(newSensorId);
+ assertThat(credentialModel.getChallenge()).isEqualTo(newChallenge);
+ assertThat(CredentialModel.isValidToken(credentialModel.getToken())).isTrue();
+ assertThat(CredentialModel.isValidGkPwHandle(credentialModel.getGkPwHandle())).isFalse();
+ assertThat(mChallengeGenerator.mCallbackRunCount).isEqualTo(1);
+ }
+
+ @Test
+ public void testGetUserId() {
+ final int userId = 106;
+ mAutoCredentialViewModel.setCredentialModel(newInvalidChallengeCredentialModel(userId));
+
+ // Get userId
+ assertThat(mAutoCredentialViewModel.getUserId()).isEqualTo(userId);
+ }
+
+ @Test
+ public void testCheckNewCredentialFromActivityResult_invalidChooseLock() {
+ final int userId = 107;
+ final long gkPwHandle = 3333L;
+ mAutoCredentialViewModel.setCredentialModel(
+ newGkPwHandleCredentialModel(userId, gkPwHandle));
+ final Intent intent = new Intent();
+ intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_GK_PW_HANDLE, gkPwHandle);
+
+ // run checkNewCredentialFromActivityResult()
+ final boolean ret = mAutoCredentialViewModel.checkNewCredentialFromActivityResult(true,
+ new ActivityResult(ChooseLockPattern.RESULT_FINISHED + 1, intent));
+
+ assertThat(ret).isFalse();
+ verifyNothingHappen();
+ }
+
+ @Test
+ public void testCheckNewCredentialFromActivityResult_invalidConfirmLock() {
+ final int userId = 107;
+ final long gkPwHandle = 3333L;
+ mAutoCredentialViewModel.setCredentialModel(
+ newGkPwHandleCredentialModel(userId, gkPwHandle));
+ final Intent intent = new Intent();
+ intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_GK_PW_HANDLE, gkPwHandle);
+
+ // run checkNewCredentialFromActivityResult()
+ final boolean ret = mAutoCredentialViewModel.checkNewCredentialFromActivityResult(false,
+ new ActivityResult(Activity.RESULT_OK + 1, intent));
+
+ assertThat(ret).isFalse();
+ verifyNothingHappen();
+ }
+
+ @Test
+ public void testCheckNewCredentialFromActivityResult_nullDataChooseLock() {
+ final int userId = 108;
+ final long gkPwHandle = 4444L;
+ mAutoCredentialViewModel.setCredentialModel(
+ newGkPwHandleCredentialModel(userId, gkPwHandle));
+
+ // run checkNewCredentialFromActivityResult()
+ final boolean ret = mAutoCredentialViewModel.checkNewCredentialFromActivityResult(true,
+ new ActivityResult(ChooseLockPattern.RESULT_FINISHED, null));
+
+ assertThat(ret).isFalse();
+ verifyNothingHappen();
+ }
+
+ @Test
+ public void testCheckNewCredentialFromActivityResult_nullDataConfirmLock() {
+ final int userId = 109;
+ mAutoCredentialViewModel.setCredentialModel(newInvalidChallengeCredentialModel(userId));
+
+ // run checkNewCredentialFromActivityResult()
+ final boolean ret = mAutoCredentialViewModel.checkNewCredentialFromActivityResult(false,
+ new ActivityResult(Activity.RESULT_OK, null));
+
+ assertThat(ret).isFalse();
+ verifyNothingHappen();
+ }
+
+ @Test
+ public void testCheckNewCredentialFromActivityResult_validChooseLock() {
+ final int userId = 108;
+ final CredentialModel credentialModel = newInvalidChallengeCredentialModel(userId);
+ mAutoCredentialViewModel.setCredentialModel(credentialModel);
+ when(mLockPatternUtils.getActivePasswordQuality(userId)).thenReturn(
+ PASSWORD_QUALITY_SOMETHING);
+
+ final long gkPwHandle = 6666L;
+ final int newSensorId = 50;
+ final long newChallenge = 60L;
+ setupGenerateTokenFlow(gkPwHandle, userId, newSensorId, newChallenge);
+ final Intent intent = new Intent();
+ intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_GK_PW_HANDLE, gkPwHandle);
+
+ // Run checkNewCredentialFromActivityResult()
+ final boolean ret = mAutoCredentialViewModel.checkNewCredentialFromActivityResult(true,
+ new ActivityResult(ChooseLockPattern.RESULT_FINISHED, intent));
+
+ assertThat(ret).isTrue();
+ assertThat(mAutoCredentialViewModel.getActionLiveData().getValue()).isNull();
+ assertThat(credentialModel.getSensorId()).isEqualTo(newSensorId);
+ assertThat(credentialModel.getChallenge()).isEqualTo(newChallenge);
+ assertThat(CredentialModel.isValidToken(credentialModel.getToken())).isTrue();
+ assertThat(CredentialModel.isValidGkPwHandle(credentialModel.getGkPwHandle())).isFalse();
+ assertThat(mChallengeGenerator.mCallbackRunCount).isEqualTo(1);
+ }
+
+
+ @Test
+ public void testCheckNewCredentialFromActivityResult_validConfirmLock() {
+ final int userId = 109;
+ final CredentialModel credentialModel = newInvalidChallengeCredentialModel(userId);
+ mAutoCredentialViewModel.setCredentialModel(credentialModel);
+ when(mLockPatternUtils.getActivePasswordQuality(userId)).thenReturn(
+ PASSWORD_QUALITY_SOMETHING);
+
+ final long gkPwHandle = 5555L;
+ final int newSensorId = 80;
+ final long newChallenge = 90L;
+ setupGenerateTokenFlow(gkPwHandle, userId, newSensorId, newChallenge);
+ final Intent intent = new Intent();
+ intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_GK_PW_HANDLE, gkPwHandle);
+
+ // Run checkNewCredentialFromActivityResult()
+ final boolean ret = mAutoCredentialViewModel.checkNewCredentialFromActivityResult(false,
+ new ActivityResult(Activity.RESULT_OK, intent));
+
+ assertThat(ret).isTrue();
+ assertThat(mAutoCredentialViewModel.getActionLiveData().getValue()).isNull();
+ assertThat(credentialModel.getSensorId()).isEqualTo(newSensorId);
+ assertThat(credentialModel.getChallenge()).isEqualTo(newChallenge);
+ assertThat(CredentialModel.isValidToken(credentialModel.getToken())).isTrue();
+ assertThat(CredentialModel.isValidGkPwHandle(credentialModel.getGkPwHandle())).isFalse();
+ assertThat(mChallengeGenerator.mCallbackRunCount).isEqualTo(1);
+ }
+
+ public static class TestChallengeGenerator implements ChallengeGenerator {
+ public int mSensorId = INVALID_SENSOR_ID;
+ public int mUserId = UserHandle.myUserId();
+ public long mChallenge = INVALID_CHALLENGE;
+ public int mCallbackRunCount = 0;
+ private GenerateChallengeCallback mCallback;
+
+ @Nullable
+ @Override
+ public GenerateChallengeCallback getCallback() {
+ return mCallback;
+ }
+
+ @Override
+ public void setCallback(@Nullable GenerateChallengeCallback callback) {
+ mCallback = callback;
+ }
+
+ @Override
+ public void generateChallenge(int userId) {
+ final GenerateChallengeCallback callback = mCallback;
+ if (callback == null) {
+ return;
+ }
+ callback.onChallengeGenerated(mSensorId, mUserId, mChallenge);
+ ++mCallbackRunCount;
+ }
+ }
+
+ private VerifyCredentialResponse newGoodCredential(long gkPwHandle, @NonNull byte[] hat) {
+ return new VerifyCredentialResponse.Builder()
+ .setGatekeeperPasswordHandle(gkPwHandle)
+ .setGatekeeperHAT(hat)
+ .build();
+ }
+}
diff --git a/tests/unit/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollIntroViewModelTest.java b/tests/unit/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollIntroViewModelTest.java
new file mode 100644
index 0000000..5069ea1
--- /dev/null
+++ b/tests/unit/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollIntroViewModelTest.java
@@ -0,0 +1,259 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.biometrics2.ui.viewmodel;
+
+import static android.hardware.fingerprint.FingerprintSensorProperties.TYPE_REAR;
+import static android.hardware.fingerprint.FingerprintSensorProperties.TYPE_UDFPS_OPTICAL;
+import static android.hardware.fingerprint.FingerprintSensorProperties.TYPE_UDFPS_ULTRASONIC;
+
+import static com.android.settings.biometrics2.data.repository.FingerprintRepositoryTest.setupSuwMaxFingerprintsEnrollable;
+import static com.android.settings.biometrics2.ui.model.FingerprintEnrollIntroStatus.FINGERPRINT_ENROLLABLE_ERROR_REACH_MAX;
+import static com.android.settings.biometrics2.ui.model.FingerprintEnrollIntroStatus.FINGERPRINT_ENROLLABLE_OK;
+import static com.android.settings.biometrics2.ui.model.FingerprintEnrollIntroStatus.FINGERPRINT_ENROLLABLE_UNKNOWN;
+import static com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollIntroViewModel.FINGERPRINT_ENROLL_INTRO_ACTION_CONTINUE_ENROLL;
+import static com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollIntroViewModel.FINGERPRINT_ENROLL_INTRO_ACTION_DONE_AND_FINISH;
+import static com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollIntroViewModel.FINGERPRINT_ENROLL_INTRO_ACTION_SKIP_OR_CANCEL;
+import static com.android.settings.biometrics2.util.EnrollmentRequestUtil.newAllFalseRequest;
+import static com.android.settings.biometrics2.util.EnrollmentRequestUtil.newIsSuwDeferredRequest;
+import static com.android.settings.biometrics2.util.EnrollmentRequestUtil.newIsSuwPortalRequest;
+import static com.android.settings.biometrics2.util.EnrollmentRequestUtil.newIsSuwRequest;
+import static com.android.settings.biometrics2.util.EnrollmentRequestUtil.newIsSuwSuggestedActionFlowRequest;
+import static com.android.settings.biometrics2.util.FingerprintManagerUtil.setupFingerprintEnrolledFingerprints;
+import static com.android.settings.biometrics2.util.FingerprintManagerUtil.setupFingerprintFirstSensor;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import android.app.Application;
+import android.content.res.Resources;
+import android.hardware.fingerprint.FingerprintManager;
+
+import androidx.annotation.NonNull;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import com.android.settings.biometrics2.data.repository.FingerprintRepository;
+import com.android.settings.biometrics2.ui.model.EnrollmentRequest;
+import com.android.settings.biometrics2.ui.model.FingerprintEnrollIntroStatus;
+import com.android.settings.testutils.InstantTaskExecutorRule;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+@RunWith(AndroidJUnit4.class)
+public class FingerprintEnrollIntroViewModelTest {
+
+ @Rule public final MockitoRule mockito = MockitoJUnit.rule();
+ @Rule public final InstantTaskExecutorRule mTaskExecutorRule = new InstantTaskExecutorRule();
+
+ @Mock private Resources mResources;
+ @Mock private LifecycleOwner mLifecycleOwner;
+ @Mock private FingerprintManager mFingerprintManager;
+
+ private Application mApplication;
+ private FingerprintRepository mFingerprintRepository;
+ private FingerprintEnrollIntroViewModel mViewModel;
+
+ @Before
+ public void setUp() {
+ mApplication = ApplicationProvider.getApplicationContext();
+ mFingerprintRepository = new FingerprintRepository(mFingerprintManager);
+ mViewModel = new FingerprintEnrollIntroViewModel(mApplication, mFingerprintRepository);
+ // MediatorLiveData won't update itself unless observed
+ mViewModel.getPageStatusLiveData().observeForever(event -> {});
+ }
+
+ @Test
+ public void testPageStatusLiveDataDefaultValue() {
+ final FingerprintEnrollIntroStatus status = mViewModel.getPageStatusLiveData().getValue();
+ assertThat(status.hasScrollToBottom()).isFalse();
+ assertThat(status.getEnrollableStatus()).isEqualTo(FINGERPRINT_ENROLLABLE_UNKNOWN);
+ }
+
+ @Test
+ public void testGetEnrollmentRequest() {
+ final EnrollmentRequest request = newAllFalseRequest(mApplication);
+
+ mViewModel.setEnrollmentRequest(request);
+
+ assertThat(mViewModel.getEnrollmentRequest()).isEqualTo(request);
+ }
+
+ @Test
+ public void testOnStartToUpdateEnrollableStatus_isSuw() {
+ final int userId = 44;
+ mViewModel.setUserId(userId);
+ mViewModel.setEnrollmentRequest(newIsSuwRequest(mApplication));
+
+ setupFingerprintEnrolledFingerprints(mFingerprintManager, userId, 0);
+ setupSuwMaxFingerprintsEnrollable(mApplication, mResources, 1);
+ mViewModel.onStart(mLifecycleOwner);
+ FingerprintEnrollIntroStatus status = mViewModel.getPageStatusLiveData().getValue();
+ assertThat(status.getEnrollableStatus()).isEqualTo(FINGERPRINT_ENROLLABLE_OK);
+
+ setupFingerprintEnrolledFingerprints(mFingerprintManager, userId, 1);
+ setupSuwMaxFingerprintsEnrollable(mApplication, mResources, 1);
+ mViewModel.onStart(mLifecycleOwner);
+ status = mViewModel.getPageStatusLiveData().getValue();
+ assertThat(status.getEnrollableStatus()).isEqualTo(FINGERPRINT_ENROLLABLE_ERROR_REACH_MAX);
+ }
+
+ @Test
+ public void testOnStartToUpdateEnrollableStatus_isNotSuw() {
+ testOnStartToUpdateEnrollableStatus(newAllFalseRequest(mApplication));
+ }
+
+ @Test
+ public void testOnStartToUpdateEnrollableStatus_isSuwDeferred() {
+ testOnStartToUpdateEnrollableStatus(newIsSuwDeferredRequest(mApplication));
+ }
+
+ @Test
+ public void testOnStartToUpdateEnrollableStatus_isSuwPortal() {
+ testOnStartToUpdateEnrollableStatus(newIsSuwPortalRequest(mApplication));
+ }
+
+ @Test
+ public void testOnStartToUpdateEnrollableStatus_isSuwSuggestedActionFlow() {
+ testOnStartToUpdateEnrollableStatus(newIsSuwSuggestedActionFlowRequest(mApplication));
+ }
+
+ private void testOnStartToUpdateEnrollableStatus(@NonNull EnrollmentRequest request) {
+ final int userId = 45;
+ mViewModel.setUserId(userId);
+ mViewModel.setEnrollmentRequest(request);
+
+ setupFingerprintEnrolledFingerprints(mFingerprintManager, userId, 0);
+ setupFingerprintFirstSensor(mFingerprintManager, TYPE_UDFPS_OPTICAL, 5);
+ mViewModel.onStart(mLifecycleOwner);
+ FingerprintEnrollIntroStatus status = mViewModel.getPageStatusLiveData().getValue();
+ assertThat(status.getEnrollableStatus()).isEqualTo(FINGERPRINT_ENROLLABLE_OK);
+
+ setupFingerprintEnrolledFingerprints(mFingerprintManager, userId, 5);
+ setupFingerprintFirstSensor(mFingerprintManager, TYPE_UDFPS_OPTICAL, 5);
+ mViewModel.onStart(mLifecycleOwner);
+ status = mViewModel.getPageStatusLiveData().getValue();
+ assertThat(status.getEnrollableStatus()).isEqualTo(FINGERPRINT_ENROLLABLE_ERROR_REACH_MAX);
+ }
+
+ @Test
+ public void textCanAssumeUdfps() {
+ setupFingerprintFirstSensor(mFingerprintManager, TYPE_UDFPS_ULTRASONIC, 1);
+ assertThat(mViewModel.canAssumeUdfps()).isEqualTo(true);
+
+ setupFingerprintFirstSensor(mFingerprintManager, TYPE_REAR, 1);
+ assertThat(mViewModel.canAssumeUdfps()).isEqualTo(false);
+ }
+
+ @Test
+ public void testIsParentalConsentRequired() {
+ // We shall not mock FingerprintRepository, but
+ // FingerprintRepository.isParentalConsentRequired() calls static method inside, we can't
+ // mock static method
+ final FingerprintRepository fingerprintRepository = mock(FingerprintRepository.class);
+ mViewModel = new FingerprintEnrollIntroViewModel(mApplication, fingerprintRepository);
+
+ when(fingerprintRepository.isParentalConsentRequired(mApplication)).thenReturn(true);
+ assertThat(mViewModel.isParentalConsentRequired()).isEqualTo(true);
+
+ when(fingerprintRepository.isParentalConsentRequired(mApplication)).thenReturn(false);
+ assertThat(mViewModel.isParentalConsentRequired()).isEqualTo(false);
+ }
+
+ @Test
+ public void testIsBiometricUnlockDisabledByAdmin() {
+ // We shall not mock FingerprintRepository, but
+ // FingerprintRepository.isDisabledByAdmin() calls static method inside, we can't mock
+ // static method
+ final FingerprintRepository fingerprintRepository = mock(FingerprintRepository.class);
+ mViewModel = new FingerprintEnrollIntroViewModel(mApplication, fingerprintRepository);
+
+ final int userId = 33;
+ mViewModel.setUserId(userId);
+
+ when(fingerprintRepository.isDisabledByAdmin(mApplication, userId)).thenReturn(true);
+ assertThat(mViewModel.isBiometricUnlockDisabledByAdmin()).isEqualTo(true);
+
+ when(fingerprintRepository.isDisabledByAdmin(mApplication, userId)).thenReturn(false);
+ assertThat(mViewModel.isBiometricUnlockDisabledByAdmin()).isEqualTo(false);
+ }
+
+ @Test
+ public void testSetHasScrolledToBottom() {
+ mViewModel.setHasScrolledToBottom();
+
+ FingerprintEnrollIntroStatus status = mViewModel.getPageStatusLiveData().getValue();
+
+ assertThat(status.hasScrollToBottom()).isEqualTo(true);
+ }
+
+ @Test
+ public void testOnNextButtonClick_enrollNext() {
+ final int userId = 46;
+ mViewModel.setUserId(userId);
+ mViewModel.setEnrollmentRequest(newIsSuwRequest(mApplication));
+
+ // Set latest status to FINGERPRINT_ENROLLABLE_OK
+ setupFingerprintEnrolledFingerprints(mFingerprintManager, userId, 0);
+ setupSuwMaxFingerprintsEnrollable(mApplication, mResources, 1);
+ mViewModel.onStart(mLifecycleOwner);
+ FingerprintEnrollIntroStatus status = mViewModel.getPageStatusLiveData().getValue();
+ assertThat(status.getEnrollableStatus()).isEqualTo(FINGERPRINT_ENROLLABLE_OK);
+
+ // Perform click on `next`
+ mViewModel.onNextButtonClick(null);
+
+ assertThat(mViewModel.getActionLiveData().getValue())
+ .isEqualTo(FINGERPRINT_ENROLL_INTRO_ACTION_CONTINUE_ENROLL);
+ }
+
+ @Test
+ public void testOnNextButtonClick_doneAndFinish() {
+ final int userId = 46;
+ mViewModel.setUserId(userId);
+ mViewModel.setEnrollmentRequest(newIsSuwRequest(mApplication));
+
+ // Set latest status to FINGERPRINT_ENROLLABLE_ERROR_REACH_MAX
+ setupFingerprintEnrolledFingerprints(mFingerprintManager, userId, 1);
+ setupSuwMaxFingerprintsEnrollable(mApplication, mResources, 1);
+ mViewModel.onStart(mLifecycleOwner);
+ FingerprintEnrollIntroStatus status = mViewModel.getPageStatusLiveData().getValue();
+ assertThat(status.getEnrollableStatus()).isEqualTo(FINGERPRINT_ENROLLABLE_ERROR_REACH_MAX);
+
+ // Perform click on `next`
+ mViewModel.onNextButtonClick(null);
+
+ assertThat(mViewModel.getActionLiveData().getValue())
+ .isEqualTo(FINGERPRINT_ENROLL_INTRO_ACTION_DONE_AND_FINISH);
+ }
+
+ @Test
+ public void testOnSkipOrCancelButtonClick() {
+ mViewModel.onSkipOrCancelButtonClick(null);
+
+ assertThat(mViewModel.getActionLiveData().getValue())
+ .isEqualTo(FINGERPRINT_ENROLL_INTRO_ACTION_SKIP_OR_CANCEL);
+ }
+}
diff --git a/tests/unit/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollmentViewModelTest.java b/tests/unit/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollmentViewModelTest.java
new file mode 100644
index 0000000..b1d55aa
--- /dev/null
+++ b/tests/unit/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollmentViewModelTest.java
@@ -0,0 +1,252 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.biometrics2.ui.viewmodel;
+
+import static com.android.settings.biometrics.BiometricEnrollBase.RESULT_FINISHED;
+import static com.android.settings.biometrics.BiometricEnrollBase.RESULT_SKIP;
+import static com.android.settings.biometrics.BiometricEnrollBase.RESULT_TIMEOUT;
+import static com.android.settings.biometrics.fingerprint.SetupFingerprintEnrollIntroduction.EXTRA_FINGERPRINT_ENROLLED_COUNT;
+import static com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollmentViewModel.SAVED_STATE_IS_WAITING_ACTIVITY_RESULT;
+import static com.android.settings.biometrics2.util.EnrollmentRequestUtil.newAllFalseRequest;
+import static com.android.settings.biometrics2.util.EnrollmentRequestUtil.newIsSuwRequest;
+import static com.android.settings.biometrics2.util.FingerprintManagerUtil.setupFingerprintEnrolledFingerprints;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.when;
+
+import android.app.Application;
+import android.app.KeyguardManager;
+import android.content.Intent;
+import android.hardware.fingerprint.FingerprintManager;
+import android.os.Bundle;
+
+import androidx.activity.result.ActivityResult;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import com.android.settings.biometrics2.data.repository.FingerprintRepository;
+import com.android.settings.biometrics2.ui.model.EnrollmentRequest;
+import com.android.settings.password.SetupSkipDialog;
+import com.android.settings.testutils.InstantTaskExecutorRule;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+@RunWith(AndroidJUnit4.class)
+public class FingerprintEnrollmentViewModelTest {
+
+ @Rule public final MockitoRule mockito = MockitoJUnit.rule();
+ @Rule public final InstantTaskExecutorRule mTaskExecutorRule = new InstantTaskExecutorRule();
+
+ @Mock private FingerprintManager mFingerprintManager;
+ @Mock private KeyguardManager mKeyguardManager;
+
+ private Application mApplication;
+ private FingerprintRepository mFingerprintRepository;
+ private FingerprintEnrollmentViewModel mViewModel;
+
+ @Before
+ public void setUp() {
+ mApplication = ApplicationProvider.getApplicationContext();
+ mFingerprintRepository = new FingerprintRepository(mFingerprintManager);
+ mViewModel = new FingerprintEnrollmentViewModel(mApplication, mFingerprintRepository,
+ mKeyguardManager);
+ }
+
+ @Test
+ public void testGetRequest() {
+ when(mKeyguardManager.isKeyguardSecure()).thenReturn(true);
+ assertThat(mViewModel.getRequest()).isNull();
+
+ final EnrollmentRequest request = newAllFalseRequest(mApplication);
+ mViewModel.setRequest(request);
+ assertThat(mViewModel.getRequest()).isEqualTo(request);
+ }
+
+ @Test
+ public void testGetNextActivityBaseIntentExtras() {
+ mViewModel.setRequest(newAllFalseRequest(mApplication));
+ assertThat(mViewModel.getNextActivityBaseIntentExtras()).isNotNull();
+ }
+
+ @Test
+ public void testOnContinueEnrollActivityResult_shouldRelaySkip1Result() {
+ mViewModel.setRequest(newAllFalseRequest(mApplication));
+ final ActivityResult result = new ActivityResult(RESULT_SKIP, null);
+
+ // Run onContinueEnrollActivityResult
+ mViewModel.onContinueEnrollActivityResult(result, 100);
+
+ assertThat(mViewModel.getSetResultLiveData().getValue()).isEqualTo(result);
+ }
+
+ @Test
+ public void testOnContinueEnrollActivityResult_shouldRelaySkip2Result() {
+ mViewModel.setRequest(newAllFalseRequest(mApplication));
+ final ActivityResult result = new ActivityResult(SetupSkipDialog.RESULT_SKIP, null);
+
+ // Run onContinueEnrollActivityResult
+ mViewModel.onContinueEnrollActivityResult(result, 100);
+
+ assertThat(mViewModel.getSetResultLiveData().getValue()).isEqualTo(result);
+ }
+
+ @Test
+ public void testOnContinueEnrollActivityResult_shouldRelayNullDataTimeoutResult() {
+ mViewModel.setRequest(newAllFalseRequest(mApplication));
+ final ActivityResult result = new ActivityResult(RESULT_TIMEOUT, null);
+
+ // Run onContinueEnrollActivityResult
+ mViewModel.onContinueEnrollActivityResult(result, 100);
+ final ActivityResult setResult = mViewModel.getSetResultLiveData().getValue();
+
+ assertThat(setResult).isNotNull();
+ assertThat(setResult.getResultCode()).isEqualTo(result.getResultCode());
+ assertThat(setResult.getData()).isEqualTo(result.getData());
+ }
+
+ @Test
+ public void testOnContinueEnrollActivityResult_shouldRelayWithDataTimeoutResult() {
+ mViewModel.setRequest(newAllFalseRequest(mApplication));
+ final Intent intent = new Intent("testAction");
+ intent.putExtra("testKey", "testValue");
+ final ActivityResult result = new ActivityResult(RESULT_TIMEOUT, intent);
+
+ // Run onContinueEnrollActivityResult
+ mViewModel.onContinueEnrollActivityResult(result, 100);
+ final ActivityResult setResult = mViewModel.getSetResultLiveData().getValue();
+
+ assertThat(setResult).isNotNull();
+ assertThat(setResult.getResultCode()).isEqualTo(result.getResultCode());
+ assertThat(setResult.getData()).isEqualTo(intent);
+ }
+
+ @Test
+ public void testOnContinueEnrollActivityResult_shouldRelayNullDataFinishResult() {
+ mViewModel.setRequest(newAllFalseRequest(mApplication));
+ final ActivityResult result = new ActivityResult(RESULT_FINISHED, null);
+
+ // Run onContinueEnrollActivityResult
+ mViewModel.onContinueEnrollActivityResult(result, 100);
+ final ActivityResult setResult = mViewModel.getSetResultLiveData().getValue();
+
+ assertThat(setResult).isNotNull();
+ assertThat(setResult.getResultCode()).isEqualTo(result.getResultCode());
+ assertThat(setResult.getData()).isEqualTo(result.getData());
+ }
+
+ @Test
+ public void testOnContinueEnrollActivityResult_shouldRelayWithDataFinishResult() {
+ mViewModel.setRequest(newAllFalseRequest(mApplication));
+ final Intent intent = new Intent("testAction");
+ intent.putExtra("testKey", "testValue");
+ final ActivityResult result = new ActivityResult(RESULT_FINISHED, intent);
+
+ // Run onContinueEnrollActivityResult
+ mViewModel.onContinueEnrollActivityResult(result, 100);
+ final ActivityResult setResult = mViewModel.getSetResultLiveData().getValue();
+
+ assertThat(setResult).isNotNull();
+ assertThat(setResult.getResultCode()).isEqualTo(result.getResultCode());
+ assertThat(setResult.getData()).isEqualTo(intent);
+ }
+
+ @Test
+ public void testOnContinueEnrollActivityResult_shouldRelayNullDataFinishResultAsNewData() {
+ when(mKeyguardManager.isKeyguardSecure()).thenReturn(true);
+ final int userId = 111;
+ final int numOfFp = 4;
+ setupFingerprintEnrolledFingerprints(mFingerprintManager, userId, numOfFp);
+ mViewModel.setRequest(newIsSuwRequest(mApplication));
+ final ActivityResult result = new ActivityResult(RESULT_FINISHED, null);
+
+ // Run onContinueEnrollActivityResult
+ mViewModel.onContinueEnrollActivityResult(result, userId);
+ final ActivityResult setResult = mViewModel.getSetResultLiveData().getValue();
+
+ assertThat(setResult).isNotNull();
+ assertThat(setResult.getResultCode()).isEqualTo(result.getResultCode());
+ assertThat(setResult.getData()).isNotNull();
+ assertThat(setResult.getData().getExtras()).isNotNull();
+ assertThat(setResult.getData().getExtras().getInt(EXTRA_FINGERPRINT_ENROLLED_COUNT, -1))
+ .isEqualTo(numOfFp);
+ }
+
+ @Test
+ public void testOnContinueEnrollActivityResult_shouldRelayWithDataFinishResultAsNewData() {
+ when(mKeyguardManager.isKeyguardSecure()).thenReturn(true);
+ final int userId = 20;
+ final int numOfFp = 9;
+ setupFingerprintEnrolledFingerprints(mFingerprintManager, userId, numOfFp);
+ mViewModel.setRequest(newIsSuwRequest(mApplication));
+ final String action = "testAction";
+ final String key = "testKey";
+ final String value = "testValue";
+ final Intent intent = new Intent(action);
+ intent.putExtra(key, value);
+ final ActivityResult result = new ActivityResult(RESULT_FINISHED, intent);
+
+ // Run onContinueEnrollActivityResult
+ mViewModel.onContinueEnrollActivityResult(result, userId);
+ final ActivityResult setResult = mViewModel.getSetResultLiveData().getValue();
+
+ assertThat(setResult).isNotNull();
+ assertThat(setResult.getResultCode()).isEqualTo(result.getResultCode());
+ assertThat(setResult.getData()).isNotNull();
+ assertThat(setResult.getData().getExtras()).isNotNull();
+ assertThat(setResult.getData().getExtras().getInt(EXTRA_FINGERPRINT_ENROLLED_COUNT, -1))
+ .isEqualTo(numOfFp);
+ assertThat(setResult.getData().getExtras().getString(key)).isEqualTo(value);
+ }
+
+ @Test
+ public void testSetSavedInstanceState() {
+ final Bundle bundle = new Bundle();
+ mViewModel.isWaitingActivityResult().set(true);
+
+ // setSavedInstanceState() as false
+ bundle.putBoolean(SAVED_STATE_IS_WAITING_ACTIVITY_RESULT, false);
+ mViewModel.setSavedInstanceState(bundle);
+ assertThat(mViewModel.isWaitingActivityResult().get()).isFalse();
+
+ // setSavedInstanceState() as false
+ bundle.putBoolean(SAVED_STATE_IS_WAITING_ACTIVITY_RESULT, true);
+ mViewModel.setSavedInstanceState(bundle);
+ assertThat(mViewModel.isWaitingActivityResult().get()).isTrue();
+ }
+
+ @Test
+ public void testOnSaveInstanceState() {
+ final Bundle bundle = new Bundle();
+
+ // setSavedInstanceState() as false
+ mViewModel.isWaitingActivityResult().set(false);
+ mViewModel.onSaveInstanceState(bundle);
+ assertThat(bundle.getBoolean(SAVED_STATE_IS_WAITING_ACTIVITY_RESULT)).isFalse();
+
+ // setSavedInstanceState() as false
+ mViewModel.isWaitingActivityResult().set(true);
+ mViewModel.onSaveInstanceState(bundle);
+ assertThat(bundle.getBoolean(SAVED_STATE_IS_WAITING_ACTIVITY_RESULT)).isTrue();
+ }
+}
diff --git a/tests/unit/src/com/android/settings/biometrics2/util/EnrollmentRequestUtil.java b/tests/unit/src/com/android/settings/biometrics2/util/EnrollmentRequestUtil.java
new file mode 100644
index 0000000..5977c57a
--- /dev/null
+++ b/tests/unit/src/com/android/settings/biometrics2/util/EnrollmentRequestUtil.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.biometrics2.util;
+
+import static com.android.settings.biometrics.BiometricEnrollBase.EXTRA_FROM_SETTINGS_SUMMARY;
+
+import static com.google.android.setupcompat.util.WizardManagerHelper.EXTRA_IS_DEFERRED_SETUP;
+import static com.google.android.setupcompat.util.WizardManagerHelper.EXTRA_IS_FIRST_RUN;
+import static com.google.android.setupcompat.util.WizardManagerHelper.EXTRA_IS_PORTAL_SETUP;
+import static com.google.android.setupcompat.util.WizardManagerHelper.EXTRA_IS_SETUP_FLOW;
+import static com.google.android.setupcompat.util.WizardManagerHelper.EXTRA_IS_SUW_SUGGESTED_ACTION_FLOW;
+import static com.google.android.setupcompat.util.WizardManagerHelper.EXTRA_THEME;
+
+import android.content.Context;
+import android.content.Intent;
+import android.text.TextUtils;
+
+import androidx.annotation.NonNull;
+
+import com.android.settings.biometrics2.ui.model.EnrollmentRequest;
+
+public class EnrollmentRequestUtil {
+
+ @NonNull
+ public static EnrollmentRequest newAllFalseRequest(@NonNull Context context) {
+ return newRequest(context, false, false, false, false, false, false, null);
+ }
+
+ @NonNull
+ public static EnrollmentRequest newIsSuwRequest(@NonNull Context context) {
+ return newRequest(context, true, false, false, false, false, false, null);
+ }
+
+ @NonNull
+ public static EnrollmentRequest newIsSuwDeferredRequest(@NonNull Context context) {
+ return newRequest(context, true, true, false, false, false, false, null);
+ }
+
+ @NonNull
+ public static EnrollmentRequest newIsSuwPortalRequest(@NonNull Context context) {
+ return newRequest(context, true, false, true, false, false, false, null);
+ }
+
+ @NonNull
+ public static EnrollmentRequest newIsSuwSuggestedActionFlowRequest(
+ @NonNull Context context) {
+ return newRequest(context, true, false, false, true, false, false, null);
+ }
+
+ @NonNull
+ public static EnrollmentRequest newRequest(@NonNull Context context, boolean isSuw,
+ boolean isSuwDeferred, boolean isSuwPortal, boolean isSuwSuggestedActionFlow,
+ boolean isSuwFirstRun, boolean isFromSettingsSummery, String theme) {
+ Intent i = new Intent();
+ i.putExtra(EXTRA_IS_SETUP_FLOW, isSuw);
+ i.putExtra(EXTRA_IS_DEFERRED_SETUP, isSuwDeferred);
+ i.putExtra(EXTRA_IS_PORTAL_SETUP, isSuwPortal);
+ i.putExtra(EXTRA_IS_SUW_SUGGESTED_ACTION_FLOW, isSuwSuggestedActionFlow);
+ i.putExtra(EXTRA_IS_FIRST_RUN, isSuwFirstRun);
+ i.putExtra(EXTRA_FROM_SETTINGS_SUMMARY, isFromSettingsSummery);
+ if (!TextUtils.isEmpty(theme)) {
+ i.putExtra(EXTRA_THEME, theme);
+ }
+ return new EnrollmentRequest(i, context);
+ }
+
+}
diff --git a/tests/unit/src/com/android/settings/biometrics2/util/FingerprintManagerUtil.java b/tests/unit/src/com/android/settings/biometrics2/util/FingerprintManagerUtil.java
new file mode 100644
index 0000000..cb45fa4
--- /dev/null
+++ b/tests/unit/src/com/android/settings/biometrics2/util/FingerprintManagerUtil.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.biometrics2.util;
+
+import static org.mockito.Mockito.when;
+
+import android.hardware.biometrics.SensorProperties;
+import android.hardware.fingerprint.Fingerprint;
+import android.hardware.fingerprint.FingerprintManager;
+import android.hardware.fingerprint.FingerprintSensorProperties;
+import android.hardware.fingerprint.FingerprintSensorPropertiesInternal;
+
+import androidx.annotation.NonNull;
+
+import java.util.ArrayList;
+
+public class FingerprintManagerUtil {
+
+ public static void setupFingerprintFirstSensor(
+ @NonNull FingerprintManager mockedFingerprintManager,
+ @FingerprintSensorProperties.SensorType int sensorType,
+ int maxEnrollmentsPerUser) {
+ final ArrayList<FingerprintSensorPropertiesInternal> props = new ArrayList<>();
+ props.add(new FingerprintSensorPropertiesInternal(
+ 0 /* sensorId */,
+ SensorProperties.STRENGTH_STRONG,
+ maxEnrollmentsPerUser,
+ new ArrayList<>() /* componentInfo */,
+ sensorType,
+ true /* resetLockoutRequiresHardwareAuthToken */));
+ when(mockedFingerprintManager.getSensorPropertiesInternal()).thenReturn(props);
+ }
+
+ public static void setupFingerprintEnrolledFingerprints(
+ @NonNull FingerprintManager mockedFingerprintManager,
+ int userId,
+ int enrolledFingerprints) {
+ final ArrayList<Fingerprint> ret = new ArrayList<>();
+ for (int i = 0; i < enrolledFingerprints; ++i) {
+ ret.add(new Fingerprint("name", 0, 0, 0L));
+ }
+ when(mockedFingerprintManager.getEnrolledFingerprints(userId)).thenReturn(ret);
+ }
+}
diff --git a/tests/unit/src/com/android/settings/fuelgauge/batterysaver/BatterySaverScheduleRadioButtonsControllerTest.java b/tests/unit/src/com/android/settings/fuelgauge/batterysaver/BatterySaverScheduleRadioButtonsControllerTest.java
index b807114..f708f6c 100644
--- a/tests/unit/src/com/android/settings/fuelgauge/batterysaver/BatterySaverScheduleRadioButtonsControllerTest.java
+++ b/tests/unit/src/com/android/settings/fuelgauge/batterysaver/BatterySaverScheduleRadioButtonsControllerTest.java
@@ -52,7 +52,7 @@
Settings.Global.putInt(mResolver, Global.AUTOMATIC_POWER_SAVE_MODE,
PowerManager.POWER_SAVE_MODE_TRIGGER_DYNAMIC);
assertThat(mController.getDefaultKey())
- .isEqualTo(BatterySaverScheduleRadioButtonsController.KEY_ROUTINE);
+ .isEqualTo(BatterySaverScheduleRadioButtonsController.KEY_NO_SCHEDULE);
}
@Test
@@ -73,14 +73,6 @@
.isEqualTo(BatterySaverScheduleRadioButtonsController.KEY_NO_SCHEDULE);
}
- @Test
- public void setDefaultKey_any_defaultsToNoScheduleIfWarningNotSeen() {
- Secure.putString(
- mContext.getContentResolver(), Secure.LOW_POWER_WARNING_ACKNOWLEDGED, "null");
- mController.setDefaultKey(BatterySaverScheduleRadioButtonsController.KEY_ROUTINE);
- assertThat(mController.getDefaultKey())
- .isEqualTo(BatterySaverScheduleRadioButtonsController.KEY_NO_SCHEDULE);
- }
@Test
public void setDefaultKey_percentage_shouldSuppressNotification() {
@@ -95,17 +87,4 @@
Secure.SUPPRESS_AUTO_BATTERY_SAVER_SUGGESTION, 0);
assertThat(result).isEqualTo(1);
}
-
- @Test
- public void setDefaultKey_routine_shouldSuppressNotification() {
- Secure.putInt(
- mContext.getContentResolver(), Secure.LOW_POWER_WARNING_ACKNOWLEDGED, 1);
- Settings.Global.putInt(mResolver, Global.AUTOMATIC_POWER_SAVE_MODE,
- PowerManager.POWER_SAVE_MODE_TRIGGER_DYNAMIC);
- mController.setDefaultKey(BatterySaverScheduleRadioButtonsController.KEY_ROUTINE);
-
- final int result = Settings.Secure.getInt(mResolver,
- Secure.SUPPRESS_AUTO_BATTERY_SAVER_SUGGESTION, 0);
- assertThat(result).isEqualTo(1);
- }
}
diff --git a/tests/unit/src/com/android/settings/testutils/FakeFeatureFactory.java b/tests/unit/src/com/android/settings/testutils/FakeFeatureFactory.java
index 8f57f4e..d4127d7 100644
--- a/tests/unit/src/com/android/settings/testutils/FakeFeatureFactory.java
+++ b/tests/unit/src/com/android/settings/testutils/FakeFeatureFactory.java
@@ -25,6 +25,7 @@
import com.android.settings.applications.ApplicationFeatureProvider;
import com.android.settings.aware.AwareFeatureProvider;
import com.android.settings.biometrics.face.FaceFeatureProvider;
+import com.android.settings.biometrics2.factory.BiometricsRepositoryProvider;
import com.android.settings.bluetooth.BluetoothFeatureProvider;
import com.android.settings.dashboard.DashboardFeatureProvider;
import com.android.settings.dashboard.suggestions.SuggestionFeatureProvider;
@@ -73,6 +74,7 @@
public final BluetoothFeatureProvider mBluetoothFeatureProvider;
public final AwareFeatureProvider mAwareFeatureProvider;
public final FaceFeatureProvider mFaceFeatureProvider;
+ public final BiometricsRepositoryProvider mBiometricsRepositoryProvider;
public PanelFeatureProvider panelFeatureProvider;
public SlicesFeatureProvider slicesFeatureProvider;
@@ -120,6 +122,7 @@
mBluetoothFeatureProvider = mock(BluetoothFeatureProvider.class);
mAwareFeatureProvider = mock(AwareFeatureProvider.class);
mFaceFeatureProvider = mock(FaceFeatureProvider.class);
+ mBiometricsRepositoryProvider = mock(BiometricsRepositoryProvider.class);
wifiTrackerLibProvider = mock(WifiTrackerLibProvider.class);
securitySettingsFeatureProvider = mock(SecuritySettingsFeatureProvider.class);
mAccessibilitySearchFeatureProvider = mock(AccessibilitySearchFeatureProvider.class);
@@ -243,6 +246,11 @@
}
@Override
+ public BiometricsRepositoryProvider getBiometricsRepositoryProvider() {
+ return mBiometricsRepositoryProvider;
+ }
+
+ @Override
public WifiTrackerLibProvider getWifiTrackerLibProvider() {
return wifiTrackerLibProvider;
}
diff --git a/tests/unit/src/com/android/settings/testutils/InstantTaskExecutorRule.java b/tests/unit/src/com/android/settings/testutils/InstantTaskExecutorRule.java
new file mode 100644
index 0000000..0a45571
--- /dev/null
+++ b/tests/unit/src/com/android/settings/testutils/InstantTaskExecutorRule.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.testutils;
+
+import androidx.arch.core.executor.ArchTaskExecutor;
+import androidx.arch.core.executor.TaskExecutor;
+
+import org.junit.rules.TestWatcher;
+import org.junit.runner.Description;
+
+/**
+ * A JUnit Test Rule that swaps the background executor used by the Architecture Components with a
+ * different one which executes each task synchronously.
+ *
+ * We can't refer it in prebuilt androidX library.
+ * Copied it from androidx/arch/core/executor/testing/InstantTaskExecutorRule.java
+ */
+public class InstantTaskExecutorRule extends TestWatcher {
+ @Override
+ protected void starting(Description description) {
+ super.starting(description);
+ ArchTaskExecutor.getInstance().setDelegate(new TaskExecutor() {
+ @Override
+ public void executeOnDiskIO(Runnable runnable) {
+ runnable.run();
+ }
+
+ @Override
+ public void postToMainThread(Runnable runnable) {
+ runnable.run();
+ }
+
+ @Override
+ public boolean isMainThread() {
+ return true;
+ }
+ });
+ }
+
+ @Override
+ protected void finished(Description description) {
+ super.finished(description);
+ ArchTaskExecutor.getInstance().setDelegate(null);
+ }
+}