From b6aa1157d6f3e1750317417a8d5f200912598288 Mon Sep 17 00:00:00 2001 From: Joshua McCloskey Date: Thu, 12 Dec 2024 18:25:00 +0000 Subject: [PATCH 1/7] Removed duplicate title in SetupChooseLock Test: Verified manully there is only 1 title Flag: EXEMPT bugfix Fixes: 380881213 Change-Id: Ib310ee5cd7ec1676d411463846fe6d7cd74cd222 --- src/com/android/settings/password/SetupChooseLockGeneric.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/com/android/settings/password/SetupChooseLockGeneric.java b/src/com/android/settings/password/SetupChooseLockGeneric.java index 1b771a2f84a..a5a36da8129 100644 --- a/src/com/android/settings/password/SetupChooseLockGeneric.java +++ b/src/com/android/settings/password/SetupChooseLockGeneric.java @@ -269,9 +269,7 @@ public class SetupChooseLockGeneric extends ChooseLockGeneric { public void onViewCreated(@NotNull View view, Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); GlifPreferenceLayout layout = (GlifPreferenceLayout) view; - int titleResource = R.string.lock_settings_picker_new_lock_title; - layout.setHeaderText(titleResource); setDivider(new ColorDrawable(Color.TRANSPARENT)); setDividerHeight(0); getHeaderView().setVisible(false); From 49d3e348107215920ddf4e18903db96bb1c8768a Mon Sep 17 00:00:00 2001 From: Adam Bookatz Date: Mon, 16 Dec 2024 10:48:31 -0800 Subject: [PATCH 2/7] Expand uninstallForAll to Admins Currently, user 0 can uninstall other users' apps. It really has that ability by virtue of it being an Admin user. But non-user-0 Admins don't currently have this ability, so we expand that here. This also allows this ability on HSUM, where human Admin users aren't user 0. Bug: 384514936 Test: manual confirmation that overflow appears on HSUM device Test: atest SettingsSpaUnitTests:com.android.settings.spa.app.appinfo.AppInfoSettingsMoreOptionsTest Flag: EXEMPT bugfix Change-Id: I976bb63ecced3c128f8109c63c85067f4a0cdf9b --- .../appinfo/AppInfoDashboardFragment.java | 2 +- .../app/appinfo/AppInfoSettingsMoreOptions.kt | 2 +- .../appinfo/AppInfoSettingsMoreOptionsTest.kt | 20 +++++++++++++++++++ 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/src/com/android/settings/applications/appinfo/AppInfoDashboardFragment.java b/src/com/android/settings/applications/appinfo/AppInfoDashboardFragment.java index 1712e85259c..432b71121de 100644 --- a/src/com/android/settings/applications/appinfo/AppInfoDashboardFragment.java +++ b/src/com/android/settings/applications/appinfo/AppInfoDashboardFragment.java @@ -572,7 +572,7 @@ public class AppInfoDashboardFragment extends DashboardFragment showIt = false; } else if (mPackageInfo == null || mDpm.packageHasActiveAdmins(mPackageInfo.packageName)) { showIt = false; - } else if (UserHandle.myUserId() != 0) { + } else if (!mUserManager.isAdminUser()) { showIt = false; } else if (mUserManager.getUsers().size() < 2) { showIt = false; diff --git a/src/com/android/settings/spa/app/appinfo/AppInfoSettingsMoreOptions.kt b/src/com/android/settings/spa/app/appinfo/AppInfoSettingsMoreOptions.kt index e05340271f5..1a3dd6eaa1b 100644 --- a/src/com/android/settings/spa/app/appinfo/AppInfoSettingsMoreOptions.kt +++ b/src/com/android/settings/spa/app/appinfo/AppInfoSettingsMoreOptions.kt @@ -167,7 +167,7 @@ private fun ApplicationInfo.isShowUninstallUpdates(context: Context): Boolean = private fun ApplicationInfo.isShowUninstallForAllUsers( userManager: UserManager, packageManagers: IPackageManagers, -): Boolean = userId == 0 && !isSystemApp && !isInstantApp && +): Boolean = userManager.isUserAdmin(userId) && !isSystemApp && !isInstantApp && isOtherUserHasInstallPackage(userManager, packageManagers) private fun ApplicationInfo.isOtherUserHasInstallPackage( diff --git a/tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppInfoSettingsMoreOptionsTest.kt b/tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppInfoSettingsMoreOptionsTest.kt index d4a989cc3ab..11ba67f75f6 100644 --- a/tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppInfoSettingsMoreOptionsTest.kt +++ b/tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppInfoSettingsMoreOptionsTest.kt @@ -159,6 +159,7 @@ class AppInfoSettingsMoreOptionsTest { packageName = PACKAGE_NAME uid = UID } + whenever(userManager.isUserAdmin(app.userId)).thenReturn(true) whenever(userManager.aliveUsers).thenReturn(listOf(OTHER_USER)) whenever(packageManagers.isPackageInstalledAsUser(PACKAGE_NAME, OTHER_USER_ID)) .thenReturn(true) @@ -171,12 +172,30 @@ class AppInfoSettingsMoreOptionsTest { ) } + @Test + fun uninstallForAllUsers_NotAdminUser_notDisplayed() { + val app = ApplicationInfo().apply { + packageName = PACKAGE_NAME + uid = UID + } + whenever(userManager.isUserAdmin(app.userId)).thenReturn(false) + whenever(userManager.aliveUsers).thenReturn(listOf(OTHER_USER)) + whenever(packageManagers.isPackageInstalledAsUser(PACKAGE_NAME, OTHER_USER_ID)) + .thenReturn(true) + + setContent(app) + composeTestRule.onRoot().performClick() + + composeTestRule.onRoot().assertIsNotDisplayed() + } + @Test fun uninstallForAllUsers_appHiddenNotInQuietModeAndPrimaryUser_displayed() { val app = ApplicationInfo().apply { packageName = PACKAGE_NAME uid = UID } + whenever(userManager.isUserAdmin(app.userId)).thenReturn(true) whenever(userManager.aliveUsers).thenReturn(listOf(OTHER_USER)) whenever(packageManagers .isPackageInstalledAsUser(PACKAGE_NAME, OTHER_USER_ID)) @@ -198,6 +217,7 @@ class AppInfoSettingsMoreOptionsTest { packageName = PACKAGE_NAME uid = UID } + whenever(userManager.isUserAdmin(app.userId)).thenReturn(true) whenever(userManager.aliveUsers).thenReturn(listOf(OTHER_USER)) whenever(packageManagers .isPackageInstalledAsUser(PACKAGE_NAME, OTHER_USER_ID)) From 955c862de173cf105388e040d63d42b18cc2fbe2 Mon Sep 17 00:00:00 2001 From: Weng Su Date: Wed, 25 Dec 2024 22:03:44 +0800 Subject: [PATCH 3/7] Add talkback hint to Wi-Fi hotspot preferences - Set the title as the edit box hint. Bug: 385857484 Flag: EXEMPT bugfix Test: Manual testing atest -c com.android.settings.wifi.tether.WifiTetherPasswordPreferenceControllerTest \ WifiTetherSSIDPreferenceControllerTest Change-Id: Ie7fe1c29d5f45ce47c7f393bf433ed3b2bcacb59 --- ...ifiTetherPasswordPreferenceController.java | 11 +++++++- .../WifiTetherSSIDPreferenceController.java | 12 ++++++++- ...etherPasswordPreferenceControllerTest.java | 22 +++++++++++++++- ...ifiTetherSSIDPreferenceControllerTest.java | 26 ++++++++++++++++--- 4 files changed, 65 insertions(+), 6 deletions(-) diff --git a/src/com/android/settings/wifi/tether/WifiTetherPasswordPreferenceController.java b/src/com/android/settings/wifi/tether/WifiTetherPasswordPreferenceController.java index d61b3d043cd..dbe62d433e9 100644 --- a/src/com/android/settings/wifi/tether/WifiTetherPasswordPreferenceController.java +++ b/src/com/android/settings/wifi/tether/WifiTetherPasswordPreferenceController.java @@ -20,7 +20,9 @@ import android.app.settings.SettingsEnums; import android.content.Context; import android.net.wifi.SoftApConfiguration; import android.text.TextUtils; +import android.widget.EditText; +import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; import androidx.preference.EditTextPreference; import androidx.preference.Preference; @@ -36,7 +38,8 @@ import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; * Controller for logic pertaining to the password of Wi-Fi tethering. */ public class WifiTetherPasswordPreferenceController extends WifiTetherBasePreferenceController - implements ValidatedEditTextPreference.Validator { + implements ValidatedEditTextPreference.Validator, + EditTextPreference.OnBindEditTextListener { private static final String PREF_KEY = "wifi_tether_network_password"; @@ -80,6 +83,7 @@ public class WifiTetherPasswordPreferenceController extends WifiTetherBasePrefer ((ValidatedEditTextPreference) mPreference).setValidator(this); ((ValidatedEditTextPreference) mPreference).setIsPassword(true); ((ValidatedEditTextPreference) mPreference).setIsSummaryPassword(true); + ((EditTextPreference) mPreference).setOnBindEditTextListener(this); updatePasswordDisplay((EditTextPreference) mPreference); } @@ -143,4 +147,9 @@ public class WifiTetherPasswordPreferenceController extends WifiTetherBasePrefer pref.setVisible(false); } } + + @Override + public void onBindEditText(@NonNull EditText editText) { + editText.setHint(R.string.wifi_hotspot_password_title); + } } diff --git a/src/com/android/settings/wifi/tether/WifiTetherSSIDPreferenceController.java b/src/com/android/settings/wifi/tether/WifiTetherSSIDPreferenceController.java index d2d26ab84fa..a57768bbb19 100644 --- a/src/com/android/settings/wifi/tether/WifiTetherSSIDPreferenceController.java +++ b/src/com/android/settings/wifi/tether/WifiTetherSSIDPreferenceController.java @@ -22,18 +22,22 @@ import android.content.Intent; import android.net.wifi.SoftApConfiguration; import android.text.TextUtils; import android.util.Log; +import android.widget.EditText; +import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; import androidx.preference.EditTextPreference; import androidx.preference.Preference; +import com.android.settings.R; import com.android.settings.overlay.FeatureFactory; import com.android.settings.widget.ValidatedEditTextPreference; import com.android.settings.wifi.dpp.WifiDppUtils; import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; public class WifiTetherSSIDPreferenceController extends WifiTetherBasePreferenceController - implements ValidatedEditTextPreference.Validator { + implements ValidatedEditTextPreference.Validator, + EditTextPreference.OnBindEditTextListener { private static final String TAG = "WifiTetherSsidPref"; private static final String PREF_KEY = "wifi_tether_network_name"; @@ -93,6 +97,7 @@ public class WifiTetherSSIDPreferenceController extends WifiTetherBasePreference ((WifiTetherSsidPreference) mPreference).setButtonVisible(false); } + ((EditTextPreference) mPreference).setOnBindEditTextListener(this); updateSsidDisplay((EditTextPreference) mPreference); } @@ -138,4 +143,9 @@ public class WifiTetherSSIDPreferenceController extends WifiTetherBasePreference boolean isQrCodeButtonAvailable() { return ((WifiTetherSsidPreference) mPreference).isQrCodeButtonAvailable(); } + + @Override + public void onBindEditText(@NonNull EditText editText) { + editText.setHint(R.string.wifi_hotspot_name_title); + } } diff --git a/tests/robotests/src/com/android/settings/wifi/tether/WifiTetherPasswordPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/wifi/tether/WifiTetherPasswordPreferenceControllerTest.java index 500e31bc7d6..3dc0a1a5752 100644 --- a/tests/robotests/src/com/android/settings/wifi/tether/WifiTetherPasswordPreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/wifi/tether/WifiTetherPasswordPreferenceControllerTest.java @@ -18,10 +18,13 @@ package com.android.settings.wifi.tether; import static com.google.common.truth.Truth.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -32,6 +35,7 @@ import android.content.Context; import android.net.TetheringManager; import android.net.wifi.SoftApConfiguration; import android.net.wifi.WifiManager; +import android.widget.EditText; import androidx.preference.PreferenceScreen; @@ -81,7 +85,7 @@ public class WifiTetherPasswordPreferenceControllerTest { when(featureFactory.getWifiFeatureProvider().getWifiHotspotRepository()) .thenReturn(mWifiHotspotRepository); - mPreference = new ValidatedEditTextPreference(RuntimeEnvironment.application); + mPreference = spy(new ValidatedEditTextPreference(RuntimeEnvironment.application)); mConfig = new SoftApConfiguration.Builder().setSsid("test_1234") .setPassphrase(INITIAL_PASSWORD, SoftApConfiguration.SECURITY_TYPE_WPA2_PSK) .build(); @@ -179,4 +183,20 @@ public class WifiTetherPasswordPreferenceControllerTest { mController.updateDisplay(); assertThat(mPreference.isPassword()).isTrue(); } + + @Test + public void updateDisplay_shouldSetOnBindEditTextListener() { + mController.displayPreference(mScreen); + + verify(mPreference).setOnBindEditTextListener(any()); + } + + @Test + public void onBindEditText_shouldSetHint() { + EditText editText = mock(EditText.class); + + mController.onBindEditText(editText); + + verify(editText).setHint(anyInt()); + } } diff --git a/tests/robotests/src/com/android/settings/wifi/tether/WifiTetherSSIDPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/wifi/tether/WifiTetherSSIDPreferenceControllerTest.java index 07d57623745..e8ef9366f06 100644 --- a/tests/robotests/src/com/android/settings/wifi/tether/WifiTetherSSIDPreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/wifi/tether/WifiTetherSSIDPreferenceControllerTest.java @@ -18,10 +18,13 @@ package com.android.settings.wifi.tether; import static com.google.common.truth.Truth.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -32,6 +35,7 @@ import android.content.Context; import android.net.TetheringManager; import android.net.wifi.SoftApConfiguration; import android.net.wifi.WifiManager; +import android.widget.EditText; import androidx.preference.PreferenceScreen; @@ -68,7 +72,7 @@ public class WifiTetherSSIDPreferenceControllerTest { @Before public void setUp() { MockitoAnnotations.initMocks(this); - mPreference = new WifiTetherSsidPreference(RuntimeEnvironment.application); + mPreference = spy(new WifiTetherSsidPreference(RuntimeEnvironment.application)); doReturn(mock(DevicePolicyManager.class)).when(mContext) .getSystemService(Context.DEVICE_POLICY_SERVICE); @@ -147,12 +151,19 @@ public class WifiTetherSSIDPreferenceControllerTest { assertThat(mPreference.getSummary()).isEqualTo(config.getSsid()); } + @Test + public void updateDisplay_shouldSetOnBindEditTextListener() { + mController.displayPreference(mScreen); + + verify(mPreference).setOnBindEditTextListener(any()); + } + @Test public void displayPreference_wifiApDisabled_shouldHideQrCodeIcon() { when(mWifiManager.isWifiApEnabled()).thenReturn(false); final SoftApConfiguration config = new SoftApConfiguration.Builder() .setSsid("test_1234").setPassphrase("test_password", - SoftApConfiguration.SECURITY_TYPE_WPA2_PSK).build(); + SoftApConfiguration.SECURITY_TYPE_WPA2_PSK).build(); when(mWifiManager.getSoftApConfiguration()).thenReturn(config); mController.displayPreference(mScreen); @@ -164,10 +175,19 @@ public class WifiTetherSSIDPreferenceControllerTest { when(mWifiManager.isWifiApEnabled()).thenReturn(true); final SoftApConfiguration config = new SoftApConfiguration.Builder() .setSsid("test_1234").setPassphrase("test_password", - SoftApConfiguration.SECURITY_TYPE_WPA2_PSK).build(); + SoftApConfiguration.SECURITY_TYPE_WPA2_PSK).build(); when(mWifiManager.getSoftApConfiguration()).thenReturn(config); mController.displayPreference(mScreen); assertThat(mController.isQrCodeButtonAvailable()).isEqualTo(true); } + + @Test + public void onBindEditText_shouldSetHint() { + EditText editText = mock(EditText.class); + + mController.onBindEditText(editText); + + verify(editText).setHint(anyInt()); + } } From cc28aba208af28821f18f3ddd2404b517b64140a Mon Sep 17 00:00:00 2001 From: Haijie Hong Date: Mon, 30 Dec 2024 15:07:10 +0800 Subject: [PATCH 4/7] Add content description for battery charging status icon BUG: 372622360 Test: atest AdvancedBluetoothDetailsHeaderControllerTest Flag: EXEMPT minor fix Change-Id: I23a889e1576c0625cefb91386987df8826c1935f --- res/values/strings.xml | 5 ++++ ...ancedBluetoothDetailsHeaderController.java | 5 ++++ ...dBluetoothDetailsHeaderControllerTest.java | 28 +++++++++++++------ 3 files changed, 29 insertions(+), 9 deletions(-) diff --git a/res/values/strings.xml b/res/values/strings.xml index 54b6fbc0da2..c707365b150 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -2002,6 +2002,11 @@ %1$s app will no longer connect to your %2$s Experimental. Improves audio quality. + + Battery + + Battery, charging + Forget device diff --git a/src/com/android/settings/bluetooth/AdvancedBluetoothDetailsHeaderController.java b/src/com/android/settings/bluetooth/AdvancedBluetoothDetailsHeaderController.java index 02b8813c225..d8e834dfbf8 100644 --- a/src/com/android/settings/bluetooth/AdvancedBluetoothDetailsHeaderController.java +++ b/src/com/android/settings/bluetooth/AdvancedBluetoothDetailsHeaderController.java @@ -626,6 +626,11 @@ public class AdvancedBluetoothDetailsHeaderController extends BasePreferenceCont imageView.setLayoutParams(layoutParams); } else { imageView.setImageDrawable(createBtBatteryIcon(mContext, level, charging)); + imageView.setContentDescription( + mContext.getString( + charging + ? R.string.device_details_battery_charging + : R.string.device_details_battery)); LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams( ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); imageView.setLayoutParams(layoutParams); diff --git a/tests/robotests/src/com/android/settings/bluetooth/AdvancedBluetoothDetailsHeaderControllerTest.java b/tests/robotests/src/com/android/settings/bluetooth/AdvancedBluetoothDetailsHeaderControllerTest.java index db8c862342f..fc1df5a0f69 100644 --- a/tests/robotests/src/com/android/settings/bluetooth/AdvancedBluetoothDetailsHeaderControllerTest.java +++ b/tests/robotests/src/com/android/settings/bluetooth/AdvancedBluetoothDetailsHeaderControllerTest.java @@ -329,11 +329,16 @@ public class AdvancedBluetoothDetailsHeaderControllerTest { mController.refresh(); - assertBatteryIcon(mLayoutPreference.findViewById(R.id.layout_left), - R.drawable.ic_battery_alert_24dp); - assertBatteryIcon(mLayoutPreference.findViewById(R.id.layout_right), /* resId= */-1); - assertBatteryIcon(mLayoutPreference.findViewById(R.id.layout_middle), - R.drawable.ic_battery_alert_24dp); + assertBatteryIcon( + mLayoutPreference.findViewById(R.id.layout_left), + R.drawable.ic_battery_alert_24dp, + false); + assertBatteryIcon( + mLayoutPreference.findViewById(R.id.layout_right), /* resId= */ -1, true); + assertBatteryIcon( + mLayoutPreference.findViewById(R.id.layout_middle), + R.drawable.ic_battery_alert_24dp, + false); } @Test @@ -546,10 +551,15 @@ public class AdvancedBluetoothDetailsHeaderControllerTest { } } - private void assertBatteryIcon(LinearLayout linearLayout, int resId) { + private void assertBatteryIcon(LinearLayout linearLayout, int resId, boolean charging) { final ImageView imageView = linearLayout.findViewById(R.id.bt_battery_icon); - assertThat(shadowOf(imageView.getDrawable()).getCreatedFromResId()) - .isEqualTo(resId); + if (charging) { + assertThat(imageView.getContentDescription().toString()) + .isEqualTo(mContext.getString(R.string.device_details_battery_charging)); + } else { + assertThat(imageView.getContentDescription().toString()) + .isEqualTo(mContext.getString(R.string.device_details_battery)); + } + assertThat(shadowOf(imageView.getDrawable()).getCreatedFromResId()).isEqualTo(resId); } - } From 4bfbb8782a8046dfdc6c208188f1a938341767a5 Mon Sep 17 00:00:00 2001 From: Shawn Lin Date: Fri, 29 Nov 2024 06:08:06 +0000 Subject: [PATCH 5/7] [Biometric Onboarding & Edu] Support check enrolled fingerprint - Add a new PreferenceItem for check enrolled fingerprint - Create a new DialogFragment for the check enrolled fingerprint with functions: - request an authentication to FingerprintManager - highlight the item when a authentication successes - show error text when authentication fails - close the authentication Bug: 370940762 Test: atest FingerprintSettingsFragmentTest Flag: com.android.settings.flags.biometrics_onboarding_education Change-Id: I90637e4ec20ea46e6f530ffd7ba79df9c31eda6b --- res/drawable/ic_check_list_24dp.xml | 25 +++ .../fingerprint_check_enrolled_dialog.xml | 47 +++++ res/values/strings.xml | 7 +- .../fingerprint/FingerprintSettings.java | 151 ++++++++++++++ .../fingerprint/UdfpsCheckEnrolledView.java | 186 ++++++++++++++++++ .../fingerprint/UdfpsFingerprintDrawable.java | 138 +++++++++++++ .../FingerprintSettingsFragmentTest.java | 43 ++++ 7 files changed, 596 insertions(+), 1 deletion(-) create mode 100644 res/drawable/ic_check_list_24dp.xml create mode 100644 res/layout/fingerprint_check_enrolled_dialog.xml create mode 100644 src/com/android/settings/biometrics/fingerprint/UdfpsCheckEnrolledView.java create mode 100644 src/com/android/settings/biometrics/fingerprint/UdfpsFingerprintDrawable.java diff --git a/res/drawable/ic_check_list_24dp.xml b/res/drawable/ic_check_list_24dp.xml new file mode 100644 index 00000000000..4d8955cbccb --- /dev/null +++ b/res/drawable/ic_check_list_24dp.xml @@ -0,0 +1,25 @@ + + + + diff --git a/res/layout/fingerprint_check_enrolled_dialog.xml b/res/layout/fingerprint_check_enrolled_dialog.xml new file mode 100644 index 00000000000..5565829d825 --- /dev/null +++ b/res/layout/fingerprint_check_enrolled_dialog.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/res/values/strings.xml b/res/values/strings.xml index 7386eabaa38..2ef34a79340 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -921,6 +921,8 @@ When using Fingerprint Unlock Fingerprint for work + + Check enrolled fingerprints Add fingerprint @@ -974,7 +976,10 @@ For best results, use a screen protector that\u2019s Made for Google certified. With other screen protectors, your child\u2019s fingerprint may not work. - + + Touch the fingerprint sensor + + Fingerprint not recognized Watch Unlock diff --git a/src/com/android/settings/biometrics/fingerprint/FingerprintSettings.java b/src/com/android/settings/biometrics/fingerprint/FingerprintSettings.java index d8a14f1e450..223900c466d 100644 --- a/src/com/android/settings/biometrics/fingerprint/FingerprintSettings.java +++ b/src/com/android/settings/biometrics/fingerprint/FingerprintSettings.java @@ -21,6 +21,7 @@ import static android.app.admin.DevicePolicyResources.Strings.Settings.FINGERPRI import static android.app.admin.DevicePolicyResources.Strings.Settings.WORK_PROFILE_FINGERPRINT_LAST_DELETE_MESSAGE; import static android.app.admin.DevicePolicyResources.UNDEFINED; import static android.hardware.biometrics.Flags.screenOffUnlockUdfps; +import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; import static com.android.settings.Utils.SETTINGS_PACKAGE_NAME; import static com.android.settings.Utils.isPrivateProfile; @@ -41,6 +42,7 @@ import android.hardware.fingerprint.Fingerprint; import android.hardware.fingerprint.FingerprintManager; import android.hardware.fingerprint.FingerprintSensorPropertiesInternal; import android.os.Bundle; +import android.os.CancellationSignal; import android.os.Handler; import android.os.UserHandle; import android.os.UserManager; @@ -50,8 +52,15 @@ import android.text.InputFilter; import android.text.Spanned; import android.text.TextUtils; import android.util.Log; +import android.view.LayoutInflater; import android.view.View; +import android.view.ViewGroup; +import android.view.Window; +import android.view.WindowInsets; +import android.view.WindowInsetsController; +import android.view.WindowManager; import android.widget.ImeAwareEditText; +import android.widget.TextView; import android.widget.Toast; import androidx.annotation.NonNull; @@ -77,6 +86,7 @@ import com.android.settings.biometrics.IdentityCheckBiometricErrorDialog; import com.android.settings.core.SettingsBaseActivity; import com.android.settings.core.instrumentation.InstrumentedDialogFragment; import com.android.settings.dashboard.DashboardFragment; +import com.android.settings.flags.Flags; import com.android.settings.overlay.FeatureFactory; import com.android.settings.password.ChooseLockGeneric; import com.android.settings.password.ChooseLockSettingsHelper; @@ -236,6 +246,9 @@ public class FingerprintSettings extends SubSettings { private static final String TAG = "FingerprintSettings"; private static final String KEY_FINGERPRINT_ITEM_PREFIX = "key_fingerprint_item"; + + private static final String KEY_FINGERPRINT_CHECK_ENROLLED = + "key_fingerprint_check_enrolled"; @VisibleForTesting static final String KEY_FINGERPRINT_ADD = "key_fingerprint_add"; private static final String KEY_FINGERPRINT_ENABLE_KEYGUARD_TOGGLE = @@ -697,6 +710,17 @@ public class FingerprintSettings extends SubSettings { mFingerprintsEnrolledCategory.addPreference(pref); pref.setOnPreferenceChangeListener(this); } + if (Flags.biometricsOnboardingEducation() && isUdfps() && fingerprintCount > 0) { + // Setup check enrolled fingerprints preference + Preference pref = new Preference(root.getContext()); + pref.setKey(KEY_FINGERPRINT_CHECK_ENROLLED); + pref.setTitle(root.getContext().getString( + R.string.fingerprint_check_enrolled_title)); + pref.setIcon(R.drawable.ic_check_list_24dp); + pref.setVisible(true); + mFingerprintsEnrolledCategory.addPreference(pref); + pref.setOnPreferenceChangeListener(this); + } mAddFingerprintPreference = findPreference(KEY_FINGERPRINT_ADD); setupAddFingerprintPreference(); return keyToReturn; @@ -922,6 +946,8 @@ public class FingerprintSettings extends SubSettings { FingerprintPreference fpref = (FingerprintPreference) pref; final Fingerprint fp = fpref.getFingerprint(); showRenameDialog(fp); + } else if (KEY_FINGERPRINT_CHECK_ENROLLED.equals(key)) { + showCheckEnrolledDialog(); } return super.onPreferenceTreeClick(pref); } @@ -974,6 +1000,16 @@ public class FingerprintSettings extends SubSettings { mAuthenticateSidecar.stopAuthentication(); } + private void showCheckEnrolledDialog() { + final CheckEnrolledDialog checkEnrolledDialog = new CheckEnrolledDialog(); + final Bundle args = new Bundle(); + args.putInt(CheckEnrolledDialog.KEY_USER_ID, mUserId); + args.putParcelable(CheckEnrolledDialog.KEY_SENSOR_PROPERTIES, mSensorProperties.get(0)); + checkEnrolledDialog.setArguments(args); + checkEnrolledDialog.setTargetFragment(this, 0); + checkEnrolledDialog.show(getFragmentManager(), CheckEnrolledDialog.class.getName()); + } + @Override public boolean onPreferenceChange(Preference preference, Object value) { boolean result = true; @@ -1350,6 +1386,121 @@ public class FingerprintSettings extends SubSettings { return new InputFilter[]{filter}; } + public static class CheckEnrolledDialog extends InstrumentedDialogFragment { + + private static final String KEY_USER_ID = "user_id"; + private static final String KEY_SENSOR_PROPERTIES = "sensor_properties"; + private int mUserId; + private @Nullable CancellationSignal mCancellationSignal; + private @Nullable FingerprintSensorPropertiesInternal mSensorPropertiesInternal; + + @Override + public @NonNull View onCreateView( + @NonNull LayoutInflater inflater, + @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) { + return inflater.inflate( + R.layout.fingerprint_check_enrolled_dialog, container, false); + } + + @Override + public @NonNull Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { + final Dialog dialog = super.onCreateDialog(savedInstanceState); + if (dialog != null) { + mUserId = getArguments().getInt(KEY_USER_ID); + mSensorPropertiesInternal = + getArguments().getParcelable(KEY_SENSOR_PROPERTIES); + + // Remove the default dialog title bar + dialog.getWindow().requestFeature(Window.FEATURE_NO_TITLE); + + dialog.setOnShowListener(dialogInterface -> { + final UdfpsCheckEnrolledView v = + dialog.findViewById(R.id.udfps_check_enrolled_view); + v.setSensorProperties(mSensorPropertiesInternal); + }); + } + + return dialog; + } + + @Override + public void onStart() { + super.onStart(); + if (getDialog() == null) { + return; + } + + final Dialog dialog = getDialog(); + Window window = dialog.getWindow(); + WindowManager.LayoutParams params = window.getAttributes(); + + // Make the dialog fullscreen + params.width = WindowManager.LayoutParams.MATCH_PARENT; + params.height = WindowManager.LayoutParams.MATCH_PARENT; + params.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; + params.setFitInsetsTypes(0); + window.setAttributes(params); + window.getDecorView().getWindowInsetsController().hide( + WindowInsets.Type.statusBars()); + window.getDecorView().getWindowInsetsController().setSystemBarsBehavior( + WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE); + window.setBackgroundDrawableResource(android.R.color.black); + + final TextView message = + dialog.findViewById(R.id.udfps_fingerprint_sensor_message); + final Vibrator vibrator = getContext().getSystemService(Vibrator.class); + final FingerprintManager fpm = Utils.getFingerprintManagerOrNull(getContext()); + mCancellationSignal = new CancellationSignal(); + fpm.authenticate( + null /* crypto */, + mCancellationSignal, + new FingerprintManager.AuthenticationCallback() { + @Override + public void onAuthenticationError( + int errorCode, @NonNull CharSequence errString) { + dialog.dismiss(); + } + + @Override + public void onAuthenticationSucceeded( + @NonNull FingerprintManager.AuthenticationResult result) { + int fingerId = result.getFingerprint().getBiometricId(); + FingerprintSettingsFragment parent = + (FingerprintSettingsFragment) getTargetFragment(); + parent.highlightFingerprintItem(fingerId); + dialog.dismiss(); + } + + @Override + public void onAuthenticationFailed() { + vibrator.vibrate( + VibrationEffect.get(VibrationEffect.EFFECT_DOUBLE_CLICK)); + message.setText(R.string.fingerprint_check_enroll_not_recognized); + message.postDelayed(() -> { + message.setText(R.string.fingerprint_check_enroll_touch_sensor); + }, 2000); + } + }, + null /* handler */, + mUserId); + } + + @Override + public void onDismiss(@NonNull DialogInterface dialog) { + super.onDismiss(dialog); + if (mCancellationSignal != null) { + mCancellationSignal.cancel(); + mCancellationSignal = null; + } + } + + @Override + public int getMetricsCategory() { + return 0; + } + } + public static class RenameDialog extends InstrumentedDialogFragment { private Fingerprint mFp; diff --git a/src/com/android/settings/biometrics/fingerprint/UdfpsCheckEnrolledView.java b/src/com/android/settings/biometrics/fingerprint/UdfpsCheckEnrolledView.java new file mode 100644 index 00000000000..52a28c75ea1 --- /dev/null +++ b/src/com/android/settings/biometrics/fingerprint/UdfpsCheckEnrolledView.java @@ -0,0 +1,186 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.biometrics.fingerprint; + +import android.content.Context; +import android.graphics.Rect; +import android.graphics.RectF; +import android.hardware.fingerprint.FingerprintSensorPropertiesInternal; +import android.util.AttributeSet; +import android.util.Log; +import android.util.RotationUtils; +import android.view.DisplayInfo; +import android.view.Surface; +import android.widget.ImageView; +import android.widget.RelativeLayout; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.android.settings.R; +import com.android.systemui.biometrics.UdfpsUtils; +import com.android.systemui.biometrics.shared.model.UdfpsOverlayParams; + +/** + * View corresponding with fingerprint_check_enrolled_dialog.xml + */ +public class UdfpsCheckEnrolledView extends RelativeLayout { + private static final String TAG = "UdfpsCheckEnrolledView"; + @NonNull + private final UdfpsFingerprintDrawable mFingerprintDrawable; + private ImageView mFingerprintView; + private UdfpsUtils mUdfpsUtils; + + private @Nullable Rect mSensorRect; + private @Nullable UdfpsOverlayParams mOverlayParams; + private @Nullable FingerprintSensorPropertiesInternal mSensorProperties; + + + public UdfpsCheckEnrolledView(@NonNull Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + mFingerprintDrawable = new UdfpsFingerprintDrawable(mContext, attrs); + mUdfpsUtils = new UdfpsUtils(); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + mFingerprintView = findViewById(R.id.udfps_fingerprint_sensor_view); + mFingerprintView.setImageDrawable(mFingerprintDrawable); + } + + /** + * setup SensorProperties + */ + public void setSensorProperties(@Nullable FingerprintSensorPropertiesInternal properties) { + mSensorProperties = properties; + updateOverlayParams(); + } + + private void onSensorRectUpdated() { + updateDimensions(); + + if (mSensorRect == null || mOverlayParams == null) { + Log.e(TAG, "Fail to onSensorRectUpdated, mSensorRect/mOverlayParams null"); + return; + } + + // Updates sensor rect in relation to the overlay view + mSensorRect.set(0, 0, + mOverlayParams.getSensorBounds().width(), + mOverlayParams.getSensorBounds().height()); + mFingerprintDrawable.onSensorRectUpdated(new RectF(mSensorRect)); + } + + private void updateDimensions() { + if (mOverlayParams == null) { + Log.e(TAG, "Fail to updateDimensions for " + this + ", mOverlayParams null"); + return; + } + // Original sensorBounds assume portrait mode. + final Rect rotatedBounds = new Rect(mOverlayParams.getSensorBounds()); + int rotation = mOverlayParams.getRotation(); + if (rotation == Surface.ROTATION_90 || rotation == Surface.ROTATION_270) { + RotationUtils.rotateBounds( + rotatedBounds, + mOverlayParams.getNaturalDisplayWidth(), + mOverlayParams.getNaturalDisplayHeight(), + rotation + ); + } + + RelativeLayout parent = ((RelativeLayout) getParent()); + if (parent == null) { + Log.e(TAG, "Fail to updateDimensions for " + this + ", parent null"); + return; + } + final int[] coords = parent.getLocationOnScreen(); + final int parentLeft = coords[0]; + final int parentTop = coords[1]; + final int parentRight = parentLeft + parent.getWidth(); + + // Update container view LayoutParams + RelativeLayout.LayoutParams checkEnrolledViewLp = + new RelativeLayout.LayoutParams(getWidth(), getHeight()); + checkEnrolledViewLp.addRule(RelativeLayout.ALIGN_PARENT_TOP); + if (rotation == Surface.ROTATION_90) { + checkEnrolledViewLp.addRule(RelativeLayout.ALIGN_PARENT_RIGHT); + checkEnrolledViewLp.width = + rotatedBounds.width() + 2 * (parentRight - rotatedBounds.right); + } else { + checkEnrolledViewLp.addRule(RelativeLayout.ALIGN_PARENT_LEFT); + checkEnrolledViewLp.width = rotatedBounds.width() + 2 * rotatedBounds.left; + } + setLayoutParams(checkEnrolledViewLp); + + // Update fingerprint view LayoutParams + RelativeLayout.LayoutParams fingerprintViewLp = new RelativeLayout.LayoutParams( + rotatedBounds.width(), rotatedBounds.height()); + fingerprintViewLp.addRule(RelativeLayout.ALIGN_PARENT_TOP); + fingerprintViewLp.topMargin = rotatedBounds.top - parentTop; + if (rotation == Surface.ROTATION_90) { + fingerprintViewLp.addRule(RelativeLayout.ALIGN_PARENT_RIGHT); + fingerprintViewLp.rightMargin = parentRight - rotatedBounds.right; + } else { + fingerprintViewLp.addRule(RelativeLayout.ALIGN_PARENT_LEFT); + fingerprintViewLp.leftMargin = rotatedBounds.left - parentLeft; + } + mFingerprintView.setLayoutParams(fingerprintViewLp); + } + + private void updateOverlayParams() { + + if (mSensorProperties == null) { + android.util.Log.e(TAG, "There is no sensor info!"); + return; + } + + DisplayInfo displayInfo = new DisplayInfo(); + if (getDisplay() == null) { + android.util.Log.e(TAG, "Can not get display"); + return; + } + getDisplay().getDisplayInfo(displayInfo); + Rect udfpsBounds = mSensorProperties.getLocation().getRect(); + float scaleFactor = mUdfpsUtils.getScaleFactor(displayInfo); + udfpsBounds.scale(scaleFactor); + + final Rect overlayBounds = new Rect( + 0, /* left */ + displayInfo.getNaturalHeight() / 2, /* top */ + displayInfo.getNaturalWidth(), /* right */ + displayInfo.getNaturalHeight() /* botom */); + + mOverlayParams = new UdfpsOverlayParams( + udfpsBounds, + overlayBounds, + displayInfo.getNaturalWidth(), + displayInfo.getNaturalHeight(), + scaleFactor, + displayInfo.rotation, + mSensorProperties.sensorType); + + post(() -> { + if (mOverlayParams == null) { + Log.e(TAG, "Fail to updateOverlayParams, mOverlayParams null"); + return; + } + mSensorRect = new Rect(mOverlayParams.getSensorBounds()); + onSensorRectUpdated(); + }); + } +} diff --git a/src/com/android/settings/biometrics/fingerprint/UdfpsFingerprintDrawable.java b/src/com/android/settings/biometrics/fingerprint/UdfpsFingerprintDrawable.java new file mode 100644 index 00000000000..e5ed6e16cc6 --- /dev/null +++ b/src/com/android/settings/biometrics/fingerprint/UdfpsFingerprintDrawable.java @@ -0,0 +1,138 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.biometrics.fingerprint; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.ColorFilter; +import android.graphics.Paint; +import android.graphics.PixelFormat; +import android.graphics.Rect; +import android.graphics.RectF; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.ShapeDrawable; +import android.graphics.drawable.shapes.PathShape; +import android.util.AttributeSet; +import android.util.PathParser; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.android.settings.R; + +/** + * UDFPS fingerprint drawable + */ +public class UdfpsFingerprintDrawable extends Drawable { + private static final String TAG = "UdfpsFingerprintDrawable"; + + private static final float DEFAULT_STROKE_WIDTH = 3f; + + @NonNull + private final Paint mSensorOutlinePaint; + @NonNull + private final ShapeDrawable mFingerprintDrawable; + private int mAlpha; + + @Nullable + private RectF mSensorRect; + private int mEnrollIcon; + private int mOutlineColor; + + UdfpsFingerprintDrawable(@NonNull Context context, @Nullable AttributeSet attrs) { + mFingerprintDrawable = defaultFactory(context); + + loadResources(context, attrs); + mSensorOutlinePaint = new Paint(0 /* flags */); + mSensorOutlinePaint.setAntiAlias(true); + mSensorOutlinePaint.setColor(mOutlineColor); + mSensorOutlinePaint.setStyle(Paint.Style.FILL); + + mFingerprintDrawable.setTint(mEnrollIcon); + + setAlpha(255); + } + + /** The [sensorRect] coordinates for the sensor area. */ + void onSensorRectUpdated(@NonNull RectF sensorRect) { + int margin = ((int) sensorRect.height()) / 8; + Rect bounds = new Rect((int) (sensorRect.left) + margin, (int) (sensorRect.top) + margin, + (int) (sensorRect.right) - margin, (int) (sensorRect.bottom) - margin); + updateFingerprintIconBounds(bounds); + mSensorRect = sensorRect; + } + + void updateFingerprintIconBounds(@NonNull Rect bounds) { + mFingerprintDrawable.setBounds(bounds); + invalidateSelf(); + } + + @Override + public void draw(@NonNull Canvas canvas) { + if (mSensorRect != null) { + canvas.drawOval(mSensorRect, mSensorOutlinePaint); + } + mFingerprintDrawable.draw(canvas); + mFingerprintDrawable.setAlpha(getAlpha()); + mSensorOutlinePaint.setAlpha(getAlpha()); + } + + @Override + public void setAlpha(int alpha) { + mAlpha = alpha; + mFingerprintDrawable.setAlpha(alpha); + mSensorOutlinePaint.setAlpha(alpha); + invalidateSelf(); + } + + @Override + public int getAlpha() { + return mAlpha; + } + + @Override + public void setColorFilter(@Nullable ColorFilter colorFilter) { + } + + @Override + public int getOpacity() { + return PixelFormat.OPAQUE; + } + + private ShapeDrawable defaultFactory(Context context) { + String fpPath = context.getResources().getString(R.string.config_udfpsIcon); + ShapeDrawable drawable = new ShapeDrawable( + new PathShape(PathParser.createPathFromPathData(fpPath), 72f, 72f) + ); + drawable.mutate(); + drawable.getPaint().setStyle(Paint.Style.STROKE); + drawable.getPaint().setStrokeCap(Paint.Cap.ROUND); + drawable.getPaint().setStrokeWidth(DEFAULT_STROKE_WIDTH); + return drawable; + } + + private void loadResources(Context context, @Nullable AttributeSet attrs) { + final TypedArray ta = context.obtainStyledAttributes(attrs, + R.styleable.BiometricsEnrollView, R.attr.biometricsEnrollStyle, + R.style.BiometricsEnrollStyle); + mEnrollIcon = ta.getColor(R.styleable.BiometricsEnrollView_biometricsEnrollIcon, 0); + mOutlineColor = ta.getColor( + R.styleable.BiometricsEnrollView_biometricsMovingTargetFill, 0); + ta.recycle(); + } +} diff --git a/tests/robotests/src/com/android/settings/biometrics/fingerprint/FingerprintSettingsFragmentTest.java b/tests/robotests/src/com/android/settings/biometrics/fingerprint/FingerprintSettingsFragmentTest.java index 1086f85d45c..a570baadd59 100644 --- a/tests/robotests/src/com/android/settings/biometrics/fingerprint/FingerprintSettingsFragmentTest.java +++ b/tests/robotests/src/com/android/settings/biometrics/fingerprint/FingerprintSettingsFragmentTest.java @@ -17,6 +17,7 @@ package com.android.settings.biometrics.fingerprint; 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 com.android.settings.biometrics.BiometricEnrollBase.BIOMETRIC_AUTH_REQUEST; @@ -354,6 +355,48 @@ public class FingerprintSettingsFragmentTest { assertThat(addPref.isEnabled()).isTrue(); } + @Test + @EnableFlags(com.android.settings.flags.Flags.FLAG_BIOMETRICS_ONBOARDING_EDUCATION) + public void testCheckEnrolledShown_whenAtLeastOneFingerprintEnrolled_Udfps() { + final Fingerprint fingerprint = new Fingerprint("Test", 0, 0); + doReturn(List.of(fingerprint)).when(mFingerprintManager).getEnrolledFingerprints(anyInt()); + setUpFragment(false, PRIMARY_USER_ID, TYPE_UDFPS_OPTICAL, 5); + + shadowOf(Looper.getMainLooper()).idle(); + + final Preference checkEnrolledPerf = + mFragment.findPreference("key_fingerprint_check_enrolled"); + assertThat(checkEnrolledPerf).isNotNull(); + assertThat(checkEnrolledPerf.isVisible()).isTrue(); + } + + @Test + @EnableFlags(com.android.settings.flags.Flags.FLAG_BIOMETRICS_ONBOARDING_EDUCATION) + public void testCheckEnrolledHide_whenNoFingerprintEnrolled_Udfps() { + doReturn(List.of()).when(mFingerprintManager).getEnrolledFingerprints(anyInt()); + setUpFragment(false, PRIMARY_USER_ID, TYPE_UDFPS_OPTICAL, 5); + + shadowOf(Looper.getMainLooper()).idle(); + + final Preference checkEnrolledPerf = + mFragment.findPreference("key_fingerprint_check_enrolled"); + assertThat(checkEnrolledPerf).isNull(); + } + + @Test + @EnableFlags(com.android.settings.flags.Flags.FLAG_BIOMETRICS_ONBOARDING_EDUCATION) + public void testCheckEnrolledHide_nonUdfps() { + final Fingerprint fingerprint = new Fingerprint("Test", 0, 0); + doReturn(List.of(fingerprint)).when(mFingerprintManager).getEnrolledFingerprints(anyInt()); + setUpFragment(false, PRIMARY_USER_ID, TYPE_REAR, 5); + + shadowOf(Looper.getMainLooper()).idle(); + + final Preference checkEnrolledPerf = + mFragment.findPreference("key_fingerprint_check_enrolled"); + assertThat(checkEnrolledPerf).isNull(); + } + private void setSensor(@FingerprintSensorProperties.SensorType int sensorType, int maxFingerprints) { final ArrayList props = new ArrayList<>(); From 440c3c2779aea5b6de71d732f20e8cf18c9d1fb7 Mon Sep 17 00:00:00 2001 From: Chaohui Wang Date: Tue, 31 Dec 2024 11:17:42 +0800 Subject: [PATCH 6/7] Reduce Mobile data switch flaky Set initial value to null, so no animation when the actual value true is emitted. Bug: 329584989 Flag: EXEMPT bug fix Test: manual - on SIMs Test: unit test Change-Id: I3eea55115f02e65dcdcc44ccf917f9083622b723 --- .../spa/network/MobileDataSwitchPreference.kt | 83 ++++++++++++ .../network/MobileDataSwitchingPreference.kt | 48 ------- .../network/NetworkCellularGroupProvider.kt | 119 +++++++----------- .../network/MobileDataSwitchPreferenceTest.kt | 101 +++++++++++++++ 4 files changed, 228 insertions(+), 123 deletions(-) create mode 100644 src/com/android/settings/spa/network/MobileDataSwitchPreference.kt delete mode 100644 src/com/android/settings/spa/network/MobileDataSwitchingPreference.kt create mode 100644 tests/spa_unit/src/com/android/settings/spa/network/MobileDataSwitchPreferenceTest.kt diff --git a/src/com/android/settings/spa/network/MobileDataSwitchPreference.kt b/src/com/android/settings/spa/network/MobileDataSwitchPreference.kt new file mode 100644 index 00000000000..e178dc378a6 --- /dev/null +++ b/src/com/android/settings/spa/network/MobileDataSwitchPreference.kt @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.spa.network + +import androidx.annotation.VisibleForTesting +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.android.settings.R +import com.android.settings.network.telephony.MobileDataRepository +import com.android.settings.network.telephony.subscriptionManager +import com.android.settingslib.spa.framework.compose.rememberContext +import com.android.settingslib.spa.widget.preference.SwitchPreference +import com.android.settingslib.spa.widget.preference.SwitchPreferenceModel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch + +@Composable +fun MobileDataSwitchPreference(subId: Int) { + MobileDataSwitchPreference( + subId = subId, + mobileDataRepository = rememberContext(::MobileDataRepository), + setMobileData = setMobileDataImpl(subId), + ) +} + +@VisibleForTesting +@Composable +fun MobileDataSwitchPreference( + subId: Int, + mobileDataRepository: MobileDataRepository, + setMobileData: (newChecked: Boolean) -> Unit, +) { + val mobileDataSummary = stringResource(id = R.string.mobile_data_settings_summary) + val isMobileDataEnabled by + remember(subId) { mobileDataRepository.isMobileDataEnabledFlow(subId) } + .collectAsStateWithLifecycle(initialValue = null) + + SwitchPreference( + object : SwitchPreferenceModel { + override val title = stringResource(id = R.string.mobile_data_settings_title) + override val summary = { mobileDataSummary } + override val checked = { isMobileDataEnabled } + override val onCheckedChange = setMobileData + } + ) +} + +@Composable +private fun setMobileDataImpl(subId: Int): (newChecked: Boolean) -> Unit { + val context = LocalContext.current + val coroutineScope = rememberCoroutineScope() + val wifiPickerTrackerHelper = rememberWifiPickerTrackerHelper() + return { newEnabled -> + coroutineScope.launch(Dispatchers.Default) { + setMobileData( + context = context, + subscriptionManager = context.subscriptionManager, + wifiPickerTrackerHelper = wifiPickerTrackerHelper, + subId = subId, + enabled = newEnabled, + ) + } + } +} diff --git a/src/com/android/settings/spa/network/MobileDataSwitchingPreference.kt b/src/com/android/settings/spa/network/MobileDataSwitchingPreference.kt deleted file mode 100644 index 4b95d448b5f..00000000000 --- a/src/com/android/settings/spa/network/MobileDataSwitchingPreference.kt +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.settings.spa.network - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.ui.res.stringResource -import com.android.settings.R -import com.android.settingslib.spa.widget.preference.SwitchPreference -import com.android.settingslib.spa.widget.preference.SwitchPreferenceModel -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch - -@Composable -fun MobileDataSwitchingPreference( - isMobileDataEnabled: () -> Boolean?, - setMobileDataEnabled: (newEnabled: Boolean) -> Unit, -) { - val mobileDataSummary = stringResource(id = R.string.mobile_data_settings_summary) - val coroutineScope = rememberCoroutineScope() - SwitchPreference( - object : SwitchPreferenceModel { - override val title = stringResource(id = R.string.mobile_data_settings_title) - override val summary = { mobileDataSummary } - override val checked = { isMobileDataEnabled() } - override val onCheckedChange: (Boolean) -> Unit = { newEnabled -> - coroutineScope.launch(Dispatchers.Default) { - setMobileDataEnabled(newEnabled) - } - } - override val changeable:() -> Boolean = {true} - } - ) -} diff --git a/src/com/android/settings/spa/network/NetworkCellularGroupProvider.kt b/src/com/android/settings/spa/network/NetworkCellularGroupProvider.kt index f60ba81fc4c..4bdb0442f1d 100644 --- a/src/com/android/settings/spa/network/NetworkCellularGroupProvider.kt +++ b/src/com/android/settings/spa/network/NetworkCellularGroupProvider.kt @@ -29,6 +29,7 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.outlined.Message import androidx.compose.material.icons.outlined.DataUsage import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableIntState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf @@ -40,7 +41,6 @@ import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.vectorResource -import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleRegistry import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -60,7 +60,6 @@ import com.android.settingslib.spa.framework.common.SettingsPageProvider import com.android.settingslib.spa.framework.common.createSettingsPage import com.android.settingslib.spa.framework.compose.navigator import com.android.settingslib.spa.framework.compose.rememberContext -import com.android.settingslib.spa.framework.util.collectLatestWithLifecycle import com.android.settingslib.spa.widget.preference.Preference import com.android.settingslib.spa.widget.preference.PreferenceModel import com.android.settingslib.spa.widget.scaffold.RegularScaffold @@ -110,51 +109,48 @@ open class NetworkCellularGroupProvider : SettingsPageProvider, SearchablePage { var textsSelectedId = rememberSaveable { mutableIntStateOf(SubscriptionManager.INVALID_SUBSCRIPTION_ID) } - var mobileDataSelectedId = rememberSaveable { - mutableIntStateOf(SubscriptionManager.INVALID_SUBSCRIPTION_ID) - } + val mobileDataSelectedId = rememberSaveable { mutableStateOf(null) } var nonDdsRemember = rememberSaveable { mutableIntStateOf(SubscriptionManager.INVALID_SUBSCRIPTION_ID) } - var showMobileDataSection = rememberSaveable { - mutableStateOf(false) - } val subscriptionViewModel = viewModel() CollectAirplaneModeAndFinishIfOn() - remember { - allOfFlows(context, subscriptionViewModel.selectableSubscriptionInfoListFlow) - }.collectLatestWithLifecycle(LocalLifecycleOwner.current) { - callsSelectedId.intValue = defaultVoiceSubId - textsSelectedId.intValue = defaultSmsSubId - mobileDataSelectedId.intValue = defaultDataSubId - nonDdsRemember.intValue = nonDds + LaunchedEffect(Unit) { + allOfFlows(context, subscriptionViewModel.selectableSubscriptionInfoListFlow).collect { + callsSelectedId.intValue = defaultVoiceSubId + textsSelectedId.intValue = defaultSmsSubId + mobileDataSelectedId.value = defaultDataSubId + nonDdsRemember.intValue = nonDds + } } val selectableSubscriptionInfoList by subscriptionViewModel .selectableSubscriptionInfoListFlow .collectAsStateWithLifecycle(initialValue = emptyList()) - showMobileDataSection.value = selectableSubscriptionInfoList - .filter { subInfo -> subInfo.simSlotIndex > -1 } - .size > 0 - val stringSims = stringResource(R.string.provider_network_settings_title) - RegularScaffold(title = stringSims) { + + RegularScaffold(title = stringResource(R.string.provider_network_settings_title)) { SimsSection(selectableSubscriptionInfoList) - if(showMobileDataSection.value) { - MobileDataSectionImpl( - mobileDataSelectedId, - nonDdsRemember, + val mobileDataSelectedIdValue = mobileDataSelectedId.value + // Avoid draw mobile data UI before data ready to reduce flaky + if (mobileDataSelectedIdValue != null) { + val showMobileDataSection = + selectableSubscriptionInfoList.any { subInfo -> subInfo.simSlotIndex > -1 } + if (showMobileDataSection) { + MobileDataSectionImpl(mobileDataSelectedIdValue, nonDdsRemember.intValue) + } + + PrimarySimSectionImpl( + subscriptionViewModel.selectableSubscriptionInfoListFlow, + callsSelectedId, + textsSelectedId, + remember(mobileDataSelectedIdValue) { + mutableIntStateOf(mobileDataSelectedIdValue) + }, ) } - PrimarySimSectionImpl( - subscriptionViewModel.selectableSubscriptionInfoListFlow, - callsSelectedId, - textsSelectedId, - mobileDataSelectedId, - ) - OtherSection() } } @@ -217,46 +213,23 @@ open class NetworkCellularGroupProvider : SettingsPageProvider, SearchablePage { } @Composable -fun MobileDataSectionImpl( - mobileDataSelectedId: MutableIntState, - nonDds: MutableIntState, -) { - val context = LocalContext.current - val localLifecycleOwner = LocalLifecycleOwner.current +fun MobileDataSectionImpl(mobileDataSelectedId: Int, nonDds: Int) { val mobileDataRepository = rememberContext(::MobileDataRepository) Category(title = stringResource(id = R.string.mobile_data_settings_title)) { - val isAutoDataEnabled by remember(nonDds.intValue) { + MobileDataSwitchPreference(subId = mobileDataSelectedId) + + val isAutoDataEnabled by remember(nonDds) { mobileDataRepository.isMobileDataPolicyEnabledFlow( - subId = nonDds.intValue, + subId = nonDds, policy = TelephonyManager.MOBILE_DATA_POLICY_AUTO_DATA_SWITCH ) }.collectAsStateWithLifecycle(initialValue = null) - - val mobileDataStateChanged by remember(mobileDataSelectedId.intValue) { - mobileDataRepository.isMobileDataEnabledFlow(mobileDataSelectedId.intValue) - }.collectAsStateWithLifecycle(initialValue = false) - val coroutineScope = rememberCoroutineScope() - - MobileDataSwitchingPreference( - isMobileDataEnabled = { mobileDataStateChanged }, - setMobileDataEnabled = { newEnabled -> - coroutineScope.launch { - setMobileData( - context, - context.getSystemService(SubscriptionManager::class.java), - getWifiPickerTrackerHelper(context, localLifecycleOwner), - mobileDataSelectedId.intValue, - newEnabled - ) - } - }, - ) - if (nonDds.intValue != SubscriptionManager.INVALID_SUBSCRIPTION_ID) { + if (SubscriptionManager.isValidSubscriptionId(nonDds)) { AutomaticDataSwitchingPreference( isAutoDataEnabled = { isAutoDataEnabled }, setAutoDataEnabled = { newEnabled -> - mobileDataRepository.setAutoDataSwitch(nonDds.intValue, newEnabled) + mobileDataRepository.setAutoDataSwitch(nonDds, newEnabled) }, ) } @@ -328,9 +301,6 @@ fun PrimarySimSectionImpl( mobileDataSelectedId: MutableIntState, ) { val context = LocalContext.current - val localLifecycleOwner = LocalLifecycleOwner.current - val wifiPickerTrackerHelper = getWifiPickerTrackerHelper(context, localLifecycleOwner) - val primarySimInfo = remember(subscriptionInfoListFlow) { subscriptionInfoListFlow .map { subscriptionInfoList -> @@ -346,7 +316,7 @@ fun PrimarySimSectionImpl( callsSelectedId, textsSelectedId, mobileDataSelectedId, - wifiPickerTrackerHelper + rememberWifiPickerTrackerHelper() ) } } @@ -354,22 +324,21 @@ fun PrimarySimSectionImpl( @Composable fun CollectAirplaneModeAndFinishIfOn() { val context = LocalContext.current - context.settingsGlobalBooleanFlow(Settings.Global.AIRPLANE_MODE_ON) - .collectLatestWithLifecycle(LocalLifecycleOwner.current) { isAirplaneModeOn -> + LaunchedEffect(Unit) { + context.settingsGlobalBooleanFlow(Settings.Global.AIRPLANE_MODE_ON).collect { + isAirplaneModeOn -> if (isAirplaneModeOn) { context.getActivity()?.finish() } } + } } -private fun getWifiPickerTrackerHelper( - context: Context, - lifecycleOwner: LifecycleOwner -): WifiPickerTrackerHelper { - return WifiPickerTrackerHelper( - LifecycleRegistry(lifecycleOwner), context, - null /* WifiPickerTrackerCallback */ - ) +@Composable +fun rememberWifiPickerTrackerHelper(): WifiPickerTrackerHelper { + val context = LocalContext.current + val lifecycleOwner = LocalLifecycleOwner.current + return remember { WifiPickerTrackerHelper(LifecycleRegistry(lifecycleOwner), context, null) } } private fun Context.defaultVoiceSubscriptionFlow(): Flow = diff --git a/tests/spa_unit/src/com/android/settings/spa/network/MobileDataSwitchPreferenceTest.kt b/tests/spa_unit/src/com/android/settings/spa/network/MobileDataSwitchPreferenceTest.kt new file mode 100644 index 00000000000..3334db9fd95 --- /dev/null +++ b/tests/spa_unit/src/com/android/settings/spa/network/MobileDataSwitchPreferenceTest.kt @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.spa.network + +import android.content.Context +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.settings.R +import com.android.settings.network.telephony.MobileDataRepository +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.flowOf +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.any +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import org.mockito.kotlin.spy +import org.mockito.kotlin.stub + +@RunWith(AndroidJUnit4::class) +class MobileDataSwitchPreferenceTest { + @get:Rule val composeTestRule = createComposeRule() + + private val context: Context = spy(ApplicationProvider.getApplicationContext()) {} + + private val mockMobileDataRepository = + mock { on { isMobileDataEnabledFlow(any()) } doReturn emptyFlow() } + + @Test + fun title_displayed() { + composeTestRule.setContent { + CompositionLocalProvider(LocalContext provides context) { + MobileDataSwitchPreference(SUB_ID, mockMobileDataRepository) {} + } + } + + composeTestRule + .onNodeWithText(context.getString(R.string.mobile_data_settings_title)) + .assertIsDisplayed() + } + + @Test + fun summary_displayed() { + composeTestRule.setContent { + CompositionLocalProvider(LocalContext provides context) { + MobileDataSwitchPreference(SUB_ID, mockMobileDataRepository) {} + } + } + + composeTestRule + .onNodeWithText(context.getString(R.string.mobile_data_settings_summary)) + .assertIsDisplayed() + } + + @Test + fun onClick_whenOff_turnedOn() { + mockMobileDataRepository.stub { + on { isMobileDataEnabledFlow(SUB_ID) } doReturn flowOf(false) + } + var newCheckedCalled: Boolean? = null + composeTestRule.setContent { + CompositionLocalProvider(LocalContext provides context) { + MobileDataSwitchPreference(SUB_ID, mockMobileDataRepository) { + newCheckedCalled = it + } + } + } + + composeTestRule + .onNodeWithText(context.getString(R.string.mobile_data_settings_title)) + .performClick() + + assertThat(newCheckedCalled).isTrue() + } + + private companion object { + const val SUB_ID = 12 + } +} From 16cc1a1f24fd3493a626fd6e9a5365d445833c14 Mon Sep 17 00:00:00 2001 From: Jacky Wang Date: Tue, 31 Dec 2024 21:59:16 +0800 Subject: [PATCH 7/7] [Catalyst] Rename PreferenceScreenMetadata{Creator,Factory} Bug: 386179791 Flag: com.android.settings.flags.catalyst Test: manual Change-Id: Id932b2555bd72f3635ad1a866c866b5d5535ce86 --- src/com/android/settings/SettingsApplication.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/com/android/settings/SettingsApplication.java b/src/com/android/settings/SettingsApplication.java index f2257d4dfdf..442e3c2559a 100644 --- a/src/com/android/settings/SettingsApplication.java +++ b/src/com/android/settings/SettingsApplication.java @@ -44,7 +44,7 @@ import com.android.settings.spa.SettingsSpaEnvironment; import com.android.settingslib.applications.AppIconCacheManager; import com.android.settingslib.datastore.BackupRestoreStorageManager; import com.android.settingslib.metadata.FixedArrayMap; -import com.android.settingslib.metadata.PreferenceScreenMetadataCreator; +import com.android.settingslib.metadata.PreferenceScreenMetadataFactory; import com.android.settingslib.metadata.PreferenceScreenRegistry; import com.android.settingslib.metadata.ProvidePreferenceScreenOptions; import com.android.settingslib.preference.PreferenceBindingFactory; @@ -75,8 +75,8 @@ public class SettingsApplication extends Application { super.onCreate(); if (Flags.catalyst()) { - PreferenceScreenRegistry.INSTANCE.setPreferenceScreenMetadataCreators( - getPreferenceScreenCreators()); + PreferenceScreenRegistry.INSTANCE.setPreferenceScreenMetadataFactories( + preferenceScreenFactories()); PreferenceBindingFactory.setDefaultFactory(new SettingsPreferenceBindingFactory()); } @@ -106,8 +106,8 @@ public class SettingsApplication extends Application { registerActivityLifecycleCallbacks(new DeveloperOptionsActivityLifecycle()); } - /** Returns the creators of preference screen metadata. */ - protected FixedArrayMap getPreferenceScreenCreators() { + /** Returns the factories of preference screen metadata. */ + protected FixedArrayMap preferenceScreenFactories() { // PreferenceScreenCollector is generated by annotation processor from classes annotated // with @ProvidePreferenceScreen return PreferenceScreenCollector.get();