From 52a46d0a85f9b602c2039c55cb240a110d496011 Mon Sep 17 00:00:00 2001 From: Milton Wu Date: Fri, 31 Mar 2023 19:49:46 +0800 Subject: [PATCH] Pass Timeout back to upper biometric preference When FaceSettings or FingerprintSettings are closed because of onStop(), this information can't been passed back to previous Preference screen, CombinedBiometricSettings, because handlePreferenceTreeClick() from AbstractPreferenceController class only can launchActivity() throguh preference's Context. In order to recevice the activity result code from FaceSettings or FingerprintSettings, add handleBiometricPreferenceTreeClick() method in BiometricStatusPreferenceController. Then CombinedBiometricSettings uses this method to show FaceSettings or FingerprintSettings through launchActivityForResult(). Bug: 263057093 Test: atest BiometricNavigationUtilsTest Test: Manually open camera through double-click power key on different pages inside "Face & Fingerprint Unlock" Change-Id: I99167739766ad5ea5f204b0f0543ba6ad18fac31 --- .../biometrics/BiometricNavigationUtils.java | 15 +++++- .../BiometricStatusPreferenceController.java | 29 ++++++++++- .../combination/BiometricsSettingsBase.java | 51 ++++++++++++++++++- .../biometrics/face/FaceSettings.java | 2 + .../fingerprint/FingerprintSettings.java | 7 +++ .../BiometricNavigationUtilsTest.java | 38 +++++++++++--- 6 files changed, 130 insertions(+), 12 deletions(-) diff --git a/src/com/android/settings/biometrics/BiometricNavigationUtils.java b/src/com/android/settings/biometrics/BiometricNavigationUtils.java index 32d3a328d95..2d0744b5b3f 100644 --- a/src/com/android/settings/biometrics/BiometricNavigationUtils.java +++ b/src/com/android/settings/biometrics/BiometricNavigationUtils.java @@ -26,6 +26,9 @@ import android.os.Bundle; import android.os.UserHandle; import android.os.UserManager; +import androidx.activity.result.ActivityResultLauncher; +import androidx.annotation.Nullable; + import com.android.internal.app.UnlaunchableAppActivity; import com.android.settings.core.SettingsBaseActivity; import com.android.settingslib.RestrictedLockUtils; @@ -49,15 +52,23 @@ public class BiometricNavigationUtils { * * @param className The class name of Settings screen to launch. * @param extras Extras to put into the launching {@link Intent}. + * @param launcher Launcher to launch activity if non-quiet mode * @return true if the Settings screen is launching. */ - public boolean launchBiometricSettings(Context context, String className, Bundle extras) { + public boolean launchBiometricSettings(Context context, String className, Bundle extras, + @Nullable ActivityResultLauncher launcher) { final Intent quietModeDialogIntent = getQuietModeDialogIntent(context); if (quietModeDialogIntent != null) { context.startActivity(quietModeDialogIntent); return false; } - context.startActivity(getSettingsPageIntent(className, extras)); + + final Intent settingsPageIntent = getSettingsPageIntent(className, extras); + if (launcher != null) { + launcher.launch(settingsPageIntent); + } else { + context.startActivity(settingsPageIntent); + } return true; } diff --git a/src/com/android/settings/biometrics/BiometricStatusPreferenceController.java b/src/com/android/settings/biometrics/BiometricStatusPreferenceController.java index 76a23a5f059..2f9ae6b8117 100644 --- a/src/com/android/settings/biometrics/BiometricStatusPreferenceController.java +++ b/src/com/android/settings/biometrics/BiometricStatusPreferenceController.java @@ -17,10 +17,14 @@ package com.android.settings.biometrics; import android.content.Context; +import android.content.Intent; import android.os.UserHandle; import android.os.UserManager; import android.text.TextUtils; +import androidx.activity.result.ActivityResultLauncher; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.preference.Preference; import com.android.internal.widget.LockPatternUtils; @@ -29,6 +33,8 @@ import com.android.settings.biometrics.activeunlock.ActiveUnlockStatusUtils; import com.android.settings.core.BasePreferenceController; import com.android.settings.overlay.FeatureFactory; +import java.lang.ref.WeakReference; + public abstract class BiometricStatusPreferenceController extends BasePreferenceController { protected final UserManager mUm; @@ -39,6 +45,8 @@ public abstract class BiometricStatusPreferenceController extends BasePreference private final BiometricNavigationUtils mBiometricNavigationUtils; private final ActiveUnlockStatusUtils mActiveUnlockStatusUtils; + @NonNull private WeakReference> mLauncherWeakReference = + new WeakReference<>(null); /** * @return true if the controller should be shown exclusively. @@ -118,14 +126,31 @@ public abstract class BiometricStatusPreferenceController extends BasePreference preference.setSummary(getSummaryText()); } + /** + * Set ActivityResultLauncher that will be used later during handlePreferenceTreeClick() + * + * @param preference the preference being compared + * @param launcher the ActivityResultLauncher + * @return {@code true} if matched preference. + */ + public boolean setPreferenceTreeClickLauncher(@NonNull Preference preference, + @Nullable ActivityResultLauncher launcher) { + if (!TextUtils.equals(preference.getKey(), getPreferenceKey())) { + return false; + } + + mLauncherWeakReference = new WeakReference<>(launcher); + return true; + } + @Override public boolean handlePreferenceTreeClick(Preference preference) { if (!TextUtils.equals(preference.getKey(), getPreferenceKey())) { return super.handlePreferenceTreeClick(preference); } - return mBiometricNavigationUtils.launchBiometricSettings( - preference.getContext(), getSettingsClassName(), preference.getExtras()); + return mBiometricNavigationUtils.launchBiometricSettings(preference.getContext(), + getSettingsClassName(), preference.getExtras(), mLauncherWeakReference.get()); } protected int getUserId() { diff --git a/src/com/android/settings/biometrics/combination/BiometricsSettingsBase.java b/src/com/android/settings/biometrics/combination/BiometricsSettingsBase.java index 7b02b9dabe2..052b8cd4787 100644 --- a/src/com/android/settings/biometrics/combination/BiometricsSettingsBase.java +++ b/src/com/android/settings/biometrics/combination/BiometricsSettingsBase.java @@ -33,6 +33,9 @@ import android.os.UserHandle; import android.text.TextUtils; import android.util.Log; +import androidx.activity.result.ActivityResult; +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.StringRes; @@ -42,14 +45,19 @@ import androidx.preference.Preference; import com.android.settings.R; import com.android.settings.Utils; import com.android.settings.biometrics.BiometricEnrollBase; +import com.android.settings.biometrics.BiometricStatusPreferenceController; import com.android.settings.biometrics.BiometricUtils; import com.android.settings.biometrics.BiometricsSplitScreenDialog; import com.android.settings.core.SettingsBaseActivity; import com.android.settings.dashboard.DashboardFragment; import com.android.settings.password.ChooseLockGeneric; import com.android.settings.password.ChooseLockSettingsHelper; +import com.android.settingslib.core.AbstractPreferenceController; import com.android.settingslib.transition.SettingsTransitionHelper; +import java.util.Collection; +import java.util.List; + /** * Base fragment with the confirming credential functionality for combined biometrics settings. */ @@ -78,6 +86,18 @@ public abstract class BiometricsSettingsBase extends DashboardFragment { @Nullable private String mRetryPreferenceKey = null; @Nullable private Bundle mRetryPreferenceExtra = null; + private final ActivityResultLauncher mFaceOrFingerprintPreferenceLauncher = + registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), + this::onFaceOrFingerprintPreferenceResult); + + private void onFaceOrFingerprintPreferenceResult(@Nullable ActivityResult result) { + if (result != null && result.getResultCode() == BiometricEnrollBase.RESULT_TIMEOUT) { + // When "Face Unlock" or "Fingerprint Unlock" is closed due to entering onStop(), + // "Face & Fingerprint Unlock" shall also close itself and back to "Security" page. + finish(); + } + } + @Override public void onAttach(Context context) { super.onAttach(context); @@ -165,7 +185,7 @@ public abstract class BiometricsSettingsBase extends DashboardFragment { extras.putByteArray(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN, token); extras.putInt(BiometricEnrollBase.EXTRA_KEY_SENSOR_ID, sensorId); extras.putLong(BiometricEnrollBase.EXTRA_KEY_CHALLENGE, challenge); - super.onPreferenceTreeClick(preference); + onFaceOrFingerprintPreferenceTreeClick(preference); } catch (IllegalStateException e) { if (retry) { mRetryPreferenceKey = preference.getKey(); @@ -200,7 +220,7 @@ public abstract class BiometricsSettingsBase extends DashboardFragment { final Bundle extras = preference.getExtras(); extras.putByteArray(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN, token); extras.putLong(BiometricEnrollBase.EXTRA_KEY_CHALLENGE, challenge); - super.onPreferenceTreeClick(preference); + onFaceOrFingerprintPreferenceTreeClick(preference); } catch (IllegalStateException e) { if (retry) { mRetryPreferenceKey = preference.getKey(); @@ -224,6 +244,33 @@ public abstract class BiometricsSettingsBase extends DashboardFragment { return BiometricUtils.requestGatekeeperHat(context, gkPwHandle, userId, challenge); } + /** + * Handle preference tree click action for "Face Unlock" or "Fingerprint Unlock" with a launcher + * because "Face & Fingerprint Unlock" has to close itself when it gets a specific activity + * error code. + * + * @param preference "Face Unlock" or "Fingerprint Unlock" preference. + */ + private void onFaceOrFingerprintPreferenceTreeClick(@NonNull Preference preference) { + Collection> controllers = getPreferenceControllers(); + for (List controllerList : controllers) { + for (AbstractPreferenceController controller : controllerList) { + if (controller instanceof BiometricStatusPreferenceController) { + final BiometricStatusPreferenceController biometricController = + (BiometricStatusPreferenceController) controller; + if (biometricController.setPreferenceTreeClickLauncher(preference, + mFaceOrFingerprintPreferenceLauncher)) { + if (biometricController.handlePreferenceTreeClick(preference)) { + writePreferenceClickMetric(preference); + } + biometricController.setPreferenceTreeClickLauncher(preference, null); + return; + } + } + } + } + } + @Override public boolean onPreferenceTreeClick(Preference preference) { return onRetryPreferenceTreeClick(preference, true) diff --git a/src/com/android/settings/biometrics/face/FaceSettings.java b/src/com/android/settings/biometrics/face/FaceSettings.java index cf2c09bc930..c300bae8ebc 100644 --- a/src/com/android/settings/biometrics/face/FaceSettings.java +++ b/src/com/android/settings/biometrics/face/FaceSettings.java @@ -325,6 +325,8 @@ public class FaceSettings extends DashboardFragment { mFaceManager.revokeChallenge(mSensorId, mUserId, mChallenge); mToken = null; } + // Let parent "Face & Fingerprint Unlock" can use this error code to close itself. + setResult(RESULT_TIMEOUT); finish(); } } diff --git a/src/com/android/settings/biometrics/fingerprint/FingerprintSettings.java b/src/com/android/settings/biometrics/fingerprint/FingerprintSettings.java index 88a05c3d037..f60cd0cd66b 100644 --- a/src/com/android/settings/biometrics/fingerprint/FingerprintSettings.java +++ b/src/com/android/settings/biometrics/fingerprint/FingerprintSettings.java @@ -669,6 +669,7 @@ public class FingerprintSettings extends SubSettings { public void onStop() { super.onStop(); if (!getActivity().isChangingConfigurations() && !mLaunchedConfirm && !mIsEnrolling) { + setResult(RESULT_TIMEOUT); getActivity().finish(); } } @@ -874,6 +875,12 @@ public class FingerprintSettings extends SubSettings { } else if (requestCode == AUTO_ADD_FIRST_FINGERPRINT_REQUEST) { if (resultCode != RESULT_FINISHED || data == null) { Log.d(TAG, "Add first fingerprint, fail or null data, result:" + resultCode); + if (resultCode == BiometricEnrollBase.RESULT_TIMEOUT) { + // If "Fingerprint Unlock" is closed because of timeout, notify result code + // back because "Face & Fingerprint Unlock" has to close itself for timeout + // case. + setResult(resultCode); + } finish(); return; } diff --git a/tests/unit/src/com/android/settings/biometrics/BiometricNavigationUtilsTest.java b/tests/unit/src/com/android/settings/biometrics/BiometricNavigationUtilsTest.java index 395f88fec2d..90652f0d937 100644 --- a/tests/unit/src/com/android/settings/biometrics/BiometricNavigationUtilsTest.java +++ b/tests/unit/src/com/android/settings/biometrics/BiometricNavigationUtilsTest.java @@ -20,6 +20,7 @@ import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -33,6 +34,7 @@ import android.os.UserHandle; import android.os.UserManager; import android.provider.Settings; +import androidx.activity.result.ActivityResultLauncher; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; @@ -55,6 +57,8 @@ public class BiometricNavigationUtilsTest { @Mock private UserManager mUserManager; + @Mock + private ActivityResultLauncher mLauncher; private Context mContext; private BiometricNavigationUtils mBiometricNavigationUtils; @@ -72,7 +76,7 @@ public class BiometricNavigationUtilsTest { when(mUserManager.isQuietModeEnabled(any())).thenReturn(true); mBiometricNavigationUtils.launchBiometricSettings(mContext, SETTINGS_CLASS_NAME, - Bundle.EMPTY); + Bundle.EMPTY, null); assertQuietModeDialogLaunchRequested(); } @@ -82,7 +86,17 @@ public class BiometricNavigationUtilsTest { when(mUserManager.isQuietModeEnabled(any())).thenReturn(true); assertThat(mBiometricNavigationUtils.launchBiometricSettings( - mContext, SETTINGS_CLASS_NAME, Bundle.EMPTY)).isFalse(); + mContext, SETTINGS_CLASS_NAME, Bundle.EMPTY, null)).isFalse(); + } + + @Test + public void launchBiometricSettings_quietMode_withLauncher_notThroughLauncher() { + when(mUserManager.isQuietModeEnabled(any())).thenReturn(true); + + mBiometricNavigationUtils.launchBiometricSettings(mContext, SETTINGS_CLASS_NAME, + Bundle.EMPTY, mLauncher); + + verify(mLauncher, never()).launch(any(Intent.class)); } @Test @@ -90,7 +104,7 @@ public class BiometricNavigationUtilsTest { when(mUserManager.isQuietModeEnabled(any())).thenReturn(false); mBiometricNavigationUtils.launchBiometricSettings( - mContext, SETTINGS_CLASS_NAME, Bundle.EMPTY); + mContext, SETTINGS_CLASS_NAME, Bundle.EMPTY, null); assertSettingsPageLaunchRequested(false /* shouldContainExtras */); } @@ -100,7 +114,7 @@ public class BiometricNavigationUtilsTest { when(mUserManager.isQuietModeEnabled(any())).thenReturn(false); assertThat(mBiometricNavigationUtils.launchBiometricSettings( - mContext, SETTINGS_CLASS_NAME, Bundle.EMPTY)).isTrue(); + mContext, SETTINGS_CLASS_NAME, Bundle.EMPTY, null)).isTrue(); } @Test @@ -108,17 +122,29 @@ public class BiometricNavigationUtilsTest { when(mUserManager.isQuietModeEnabled(any())).thenReturn(false); final Bundle extras = createNotEmptyExtras(); - mBiometricNavigationUtils.launchBiometricSettings(mContext, SETTINGS_CLASS_NAME, extras); + mBiometricNavigationUtils.launchBiometricSettings( + mContext, SETTINGS_CLASS_NAME, extras, null); assertSettingsPageLaunchRequested(true /* shouldContainExtras */); } + @Test + public void launchBiometricSettings_noQuietMode_withLauncher_launchesThroughLauncher() { + when(mUserManager.isQuietModeEnabled(any())).thenReturn(false); + + final Bundle extras = createNotEmptyExtras(); + mBiometricNavigationUtils.launchBiometricSettings( + mContext, SETTINGS_CLASS_NAME, extras, mLauncher); + + verify(mLauncher).launch(any(Intent.class)); + } + @Test public void launchBiometricSettings_noQuietMode_withExtras_returnsTrue() { when(mUserManager.isQuietModeEnabled(any())).thenReturn(false); assertThat(mBiometricNavigationUtils.launchBiometricSettings( - mContext, SETTINGS_CLASS_NAME, createNotEmptyExtras())).isTrue(); + mContext, SETTINGS_CLASS_NAME, createNotEmptyExtras(), null)).isTrue(); } @Test