Retry fingerprint or face unlock

When IllegalStateException happens during user enters `Fingerprint
Unlock` or `Face Unlock` page through `Face and Fingerprint Unlock`
page, show ConfirmLock for user to re-enter credential again.
If user fails to pass credential page, finish activity and back to
`Security` page.
If user success to pass credential page, enter `Fingerprint Unlock` or
`Face Unlock` page.

Test: Manully test timeout case on `Face and Fingerprint Unlock` page
Test: robotest for CombinedBiometricProfileSettingsTest
      FaceSettingsLockscreenBypassPreferenceControllerTest
Bug: 248165760
Change-Id: I3361c38d09d14461db8ecf2d89a34ba9604dc7e8
diff --git a/src/com/android/settings/biometrics/combination/BiometricsSettingsBase.java b/src/com/android/settings/biometrics/combination/BiometricsSettingsBase.java
index 404fe6d..4da42d4 100644
--- a/src/com/android/settings/biometrics/combination/BiometricsSettingsBase.java
+++ b/src/com/android/settings/biometrics/combination/BiometricsSettingsBase.java
@@ -28,11 +28,13 @@
 import android.hardware.fingerprint.FingerprintSensorPropertiesInternal;
 import android.os.Bundle;
 import android.os.UserHandle;
+import android.text.TextUtils;
 import android.util.Log;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.StringRes;
+import androidx.annotation.VisibleForTesting;
 import androidx.preference.Preference;
 
 import com.android.settings.R;
@@ -50,11 +52,16 @@
  */
 public abstract class BiometricsSettingsBase extends DashboardFragment {
 
-    private static final int CONFIRM_REQUEST = 2001;
+    @VisibleForTesting
+    static final int CONFIRM_REQUEST = 2001;
     private static final int CHOOSE_LOCK_REQUEST = 2002;
 
     private static final String SAVE_STATE_CONFIRM_CREDETIAL = "confirm_credential";
     private static final String DO_NOT_FINISH_ACTIVITY = "do_not_finish_activity";
+    @VisibleForTesting
+    static final String RETRY_PREFERENCE_KEY = "retry_preference_key";
+    @VisibleForTesting
+    static final String RETRY_PREFERENCE_BUNDLE = "retry_preference_bundle";
 
     protected int mUserId;
     protected long mGkPwHandle;
@@ -63,6 +70,8 @@
     @Nullable private FingerprintManager mFingerprintManager;
     // Do not finish() if choosing/confirming credential, or showing fp/face settings
     private boolean mDoNotFinishActivity;
+    @Nullable private String mRetryPreferenceKey = null;
+    @Nullable private Bundle mRetryPreferenceExtra = null;
 
     @Override
     public void onAttach(Context context) {
@@ -84,6 +93,8 @@
         if (savedInstanceState != null) {
             mConfirmCredential = savedInstanceState.getBoolean(SAVE_STATE_CONFIRM_CREDETIAL);
             mDoNotFinishActivity = savedInstanceState.getBoolean(DO_NOT_FINISH_ACTIVITY);
+            mRetryPreferenceKey = savedInstanceState.getString(RETRY_PREFERENCE_KEY);
+            mRetryPreferenceExtra = savedInstanceState.getBundle(RETRY_PREFERENCE_BUNDLE);
             if (savedInstanceState.containsKey(
                     ChooseLockSettingsHelper.EXTRA_KEY_REQUEST_GK_PW_HANDLE)) {
                 mGkPwHandle = savedInstanceState.getLong(
@@ -124,8 +135,7 @@
         }
     }
 
-    @Override
-    public boolean onPreferenceTreeClick(Preference preference) {
+    private boolean onRetryPreferenceTreeClick(Preference preference, final boolean retry) {
         final String key = preference.getKey();
         final Context context = requireActivity().getApplicationContext();
 
@@ -134,31 +144,77 @@
         if (getFacePreferenceKey().equals(key)) {
             mDoNotFinishActivity = true;
             mFaceManager.generateChallenge(mUserId, (sensorId, userId, challenge) -> {
-                final byte[] token = BiometricUtils.requestGatekeeperHat(context, mGkPwHandle,
-                        mUserId, challenge);
-                final Bundle extras = preference.getExtras();
-                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);
+                try {
+                    final byte[] token = requestGatekeeperHat(context, mGkPwHandle, mUserId,
+                            challenge);
+                    final Bundle extras = preference.getExtras();
+                    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);
+                } catch (IllegalStateException e) {
+                    if (retry) {
+                        mRetryPreferenceKey = preference.getKey();
+                        mRetryPreferenceExtra = preference.getExtras();
+                        mConfirmCredential = true;
+                        launchChooseOrConfirmLock();
+                    } else {
+                        Log.e(getLogTag(), "face generateChallenge fail", e);
+                        mDoNotFinishActivity = false;
+                    }
+                }
             });
-
             return true;
         } else if (getFingerprintPreferenceKey().equals(key)) {
             mDoNotFinishActivity = true;
             mFingerprintManager.generateChallenge(mUserId, (sensorId, userId, challenge) -> {
-                final byte[] token = BiometricUtils.requestGatekeeperHat(context, mGkPwHandle,
-                        mUserId, challenge);
-                final Bundle extras = preference.getExtras();
-                extras.putByteArray(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN, token);
-                extras.putLong(BiometricEnrollBase.EXTRA_KEY_CHALLENGE, challenge);
-                super.onPreferenceTreeClick(preference);
+                try {
+                    final byte[] token = requestGatekeeperHat(context, mGkPwHandle, mUserId,
+                            challenge);
+                    final Bundle extras = preference.getExtras();
+                    extras.putByteArray(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN, token);
+                    extras.putLong(BiometricEnrollBase.EXTRA_KEY_CHALLENGE, challenge);
+                    super.onPreferenceTreeClick(preference);
+                } catch (IllegalStateException e) {
+                    if (retry) {
+                        mRetryPreferenceKey = preference.getKey();
+                        mRetryPreferenceExtra = preference.getExtras();
+                        mConfirmCredential = true;
+                        launchChooseOrConfirmLock();
+                    } else {
+                        Log.e(getLogTag(), "fingerprint generateChallenge fail", e);
+                        mDoNotFinishActivity = false;
+                    }
+                }
             });
-
             return true;
         }
+        return false;
+    }
 
-        return super.onPreferenceTreeClick(preference);
+    @VisibleForTesting
+    protected byte[] requestGatekeeperHat(@NonNull Context context, long gkPwHandle, int userId,
+            long challenge) {
+        return BiometricUtils.requestGatekeeperHat(context, gkPwHandle, userId, challenge);
+    }
+
+    @Override
+    public boolean onPreferenceTreeClick(Preference preference) {
+        return onRetryPreferenceTreeClick(preference, true)
+                || super.onPreferenceTreeClick(preference);
+    }
+
+    private void retryPreferenceKey(@NonNull String key, @Nullable Bundle extras) {
+        final Preference preference = findPreference(key);
+        if (preference == null) {
+            Log.w(getLogTag(), ".retryPreferenceKey, fail to find " + key);
+            return;
+        }
+
+        if (extras != null) {
+            preference.getExtras().putAll(extras);
+        }
+        onRetryPreferenceTreeClick(preference, false);
     }
 
     @Override
@@ -169,6 +225,10 @@
         if (mGkPwHandle != 0L) {
             outState.putLong(ChooseLockSettingsHelper.EXTRA_KEY_REQUEST_GK_PW_HANDLE, mGkPwHandle);
         }
+        if (!TextUtils.isEmpty(mRetryPreferenceKey)) {
+            outState.putString(RETRY_PREFERENCE_KEY, mRetryPreferenceKey);
+            outState.putBundle(RETRY_PREFERENCE_BUNDLE, mRetryPreferenceExtra);
+        }
     }
 
     @Override
@@ -180,6 +240,11 @@
             if (resultCode == RESULT_FINISHED || resultCode == RESULT_OK) {
                 if (BiometricUtils.containsGatekeeperPasswordHandle(data)) {
                     mGkPwHandle = BiometricUtils.getGatekeeperPasswordHandle(data);
+                    if (!TextUtils.isEmpty(mRetryPreferenceKey)) {
+                        getActivity().overridePendingTransition(R.anim.sud_slide_next_in,
+                                R.anim.sud_slide_next_out);
+                        retryPreferenceKey(mRetryPreferenceKey, mRetryPreferenceExtra);
+                    }
                 } else {
                     Log.d(getLogTag(), "Data null or GK PW missing.");
                     finish();
@@ -188,6 +253,8 @@
                 Log.d(getLogTag(), "Password not confirmed.");
                 finish();
             }
+            mRetryPreferenceKey = null;
+            mRetryPreferenceExtra = null;
         }
     }
 
@@ -211,7 +278,8 @@
      */
     public abstract String getUseInAppsPreferenceKey();
 
-    private void launchChooseOrConfirmLock() {
+    @VisibleForTesting
+    protected void launchChooseOrConfirmLock() {
         final ChooseLockSettingsHelper.Builder builder =
                 new ChooseLockSettingsHelper.Builder(getActivity(), this)
                         .setRequestCode(CONFIRM_REQUEST)
diff --git a/tests/robotests/src/com/android/settings/biometrics/combination/CombinedBiometricProfileSettingsTest.java b/tests/robotests/src/com/android/settings/biometrics/combination/CombinedBiometricProfileSettingsTest.java
new file mode 100644
index 0000000..ac8008d
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/biometrics/combination/CombinedBiometricProfileSettingsTest.java
@@ -0,0 +1,358 @@
+/*
+ * 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.biometrics.combination;
+
+import static com.android.settings.biometrics.combination.BiometricsSettingsBase.CONFIRM_REQUEST;
+import static com.android.settings.password.ChooseLockPattern.RESULT_FINISHED;
+
+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.anyLong;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+
+import android.content.Context;
+import android.content.Intent;
+import android.hardware.face.FaceManager;
+import android.hardware.fingerprint.FingerprintManager;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.XmlRes;
+import androidx.fragment.app.FragmentActivity;
+import androidx.preference.Preference;
+import androidx.preference.PreferenceManager;
+import androidx.preference.PreferenceScreen;
+import androidx.test.core.app.ApplicationProvider;
+
+import com.android.settings.R;
+import com.android.settings.password.ChooseLockSettingsHelper;
+import com.android.settings.testutils.FakeFeatureFactory;
+import com.android.settings.testutils.shadow.ShadowFragment;
+import com.android.settings.testutils.shadow.ShadowSettingsPreferenceFragment;
+import com.android.settings.testutils.shadow.ShadowUtils;
+import com.android.settingslib.core.AbstractPreferenceController;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+import org.robolectric.Robolectric;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+import org.robolectric.util.ReflectionHelpers;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+@RunWith(RobolectricTestRunner.class)
+@Config(shadows = {ShadowSettingsPreferenceFragment.class, ShadowUtils.class, ShadowFragment.class})
+public class CombinedBiometricProfileSettingsTest {
+
+    private TestCombinedBiometricProfileSettings mFragment;
+    private Context mContext;
+
+    @Rule
+    public final MockitoRule mMockitoRule = MockitoJUnit.rule();
+    @Captor
+    private ArgumentCaptor<Preference> mPreferenceCaptor;
+    @Mock
+    private FingerprintManager mFingerprintManager;
+    @Mock
+    private BiometricSettingsAppPreferenceController mBiometricSettingsAppPreferenceController;
+    @Mock
+    private FaceManager mFaceManager;
+
+    @Before
+    public void setUp() {
+        ShadowUtils.setFingerprintManager(mFingerprintManager);
+        ShadowUtils.setFaceManager(mFaceManager);
+        FakeFeatureFactory.setupForTest();
+
+        FragmentActivity activity = Robolectric.buildActivity(FragmentActivity.class,
+                new Intent().putExtra(ChooseLockSettingsHelper.EXTRA_KEY_GK_PW_HANDLE, 1L)).get();
+        mContext = spy(ApplicationProvider.getApplicationContext());
+        mFragment = spy(new TestCombinedBiometricProfileSettings(mContext));
+        doReturn(activity).when(mFragment).getActivity();
+
+        ReflectionHelpers.setField(mFragment, "mDashboardFeatureProvider",
+                FakeFeatureFactory.setupForTest().dashboardFeatureProvider);
+
+        final Map<Class<?>, List<AbstractPreferenceController>> preferenceControllers =
+                ReflectionHelpers.getField(mFragment, "mPreferenceControllers");
+        List<AbstractPreferenceController> controllerList = new ArrayList<>();
+        controllerList.add(mBiometricSettingsAppPreferenceController);
+        preferenceControllers.put(BiometricSettingsAppPreferenceController.class, controllerList);
+
+        doAnswer(invocation -> {
+            final CharSequence key = invocation.getArgument(0);
+            final Preference preference = new Preference(mContext);
+            preference.setKey(key.toString());
+            return preference;
+        }).when(mFragment).findPreference(any());
+    }
+
+    @Test
+    public void testClickFingerprintUnlockWithValidGkPwHandle() {
+        doAnswer(invocation -> {
+            final FingerprintManager.GenerateChallengeCallback callback =
+                    invocation.getArgument(1);
+            callback.onChallengeGenerated(0, 0, 1L);
+            return null;
+        }).when(mFingerprintManager).generateChallenge(anyInt(), any());
+        doReturn(new byte[] { 1 }).when(mFragment).requestGatekeeperHat(any(), anyLong(), anyInt(),
+                anyLong());
+
+        // Start fragment
+        mFragment.onAttach(mContext);
+        mFragment.onCreate(null);
+        mFragment.onCreateView(LayoutInflater.from(mContext), mock(ViewGroup.class), Bundle.EMPTY);
+        mFragment.onResume();
+
+        // User clicks on "Fingerprint Unlock"
+        final Preference preference = new Preference(mContext);
+        preference.setKey(mFragment.getFingerprintPreferenceKey());
+        mFragment.onPreferenceTreeClick(preference);
+
+        verify(mBiometricSettingsAppPreferenceController).handlePreferenceTreeClick(
+                mPreferenceCaptor.capture());
+        List<Preference> capturedPreferences = mPreferenceCaptor.getAllValues();
+
+        assertThat(capturedPreferences.size()).isEqualTo(1);
+        assertThat(capturedPreferences.get(0).getKey())
+                .isEqualTo(mFragment.getFingerprintPreferenceKey());
+    }
+
+    @Test
+    public void testClickFingerprintUnlockIfGkPwHandleTimeout() {
+        doAnswer(invocation -> {
+            final FingerprintManager.GenerateChallengeCallback callback =
+                    invocation.getArgument(1);
+            callback.onChallengeGenerated(0, 0, 1L);
+            return null;
+        }).when(mFingerprintManager).generateChallenge(anyInt(), any());
+        doThrow(new IllegalStateException("Test")).when(mFragment).requestGatekeeperHat(any(),
+                anyLong(), anyInt(), anyLong());
+
+        // Start fragment
+        mFragment.onAttach(mContext);
+        mFragment.onCreate(null);
+        mFragment.onCreateView(LayoutInflater.from(mContext), mock(ViewGroup.class), Bundle.EMPTY);
+        mFragment.onResume();
+
+        // User clicks on "Fingerprint Unlock"
+        final Preference preference = new Preference(mContext);
+        preference.setKey(mFragment.getFingerprintPreferenceKey());
+        mFragment.onPreferenceTreeClick(preference);
+
+        verify(mFragment).launchChooseOrConfirmLock();
+    }
+
+    @Test
+    public void testActivityResultLaunchFingerprintUnlock() {
+        doAnswer(invocation -> {
+            final FingerprintManager.GenerateChallengeCallback callback =
+                    invocation.getArgument(1);
+            callback.onChallengeGenerated(0, 0, 1L);
+            return null;
+        }).when(mFingerprintManager).generateChallenge(anyInt(), any());
+        doReturn(new byte[] { 1 }).when(mFragment).requestGatekeeperHat(any(), anyLong(), anyInt(),
+                anyLong());
+
+        // Start fragment
+        mFragment.onAttach(mContext);
+        final Bundle bundle = new Bundle();
+        bundle.putString(BiometricsSettingsBase.RETRY_PREFERENCE_KEY,
+                mFragment.getFingerprintPreferenceKey());
+        final Bundle retryBundle = new Bundle();
+        bundle.putBundle(BiometricsSettingsBase.RETRY_PREFERENCE_BUNDLE, retryBundle);
+        mFragment.onCreate(bundle);
+        mFragment.onCreateView(LayoutInflater.from(mContext), mock(ViewGroup.class), Bundle.EMPTY);
+        mFragment.onResume();
+
+        // onActivityResult
+        mFragment.onActivityResult(CONFIRM_REQUEST, RESULT_FINISHED,
+                new Intent().putExtra(ChooseLockSettingsHelper.EXTRA_KEY_GK_PW_HANDLE, 1L));
+
+        verify(mBiometricSettingsAppPreferenceController).handlePreferenceTreeClick(
+                mPreferenceCaptor.capture());
+        List<Preference> capturedPreferences = mPreferenceCaptor.getAllValues();
+        assertThat(capturedPreferences.size()).isEqualTo(1);
+        assertThat(capturedPreferences.get(0).getKey())
+                .isEqualTo(mFragment.getFingerprintPreferenceKey());
+    }
+
+    @Test
+    public void testClickFaceUnlock() {
+        doAnswer(invocation -> {
+            final FaceManager.GenerateChallengeCallback callback =
+                    invocation.getArgument(1);
+            callback.onGenerateChallengeResult(0, 0, 1L);
+            return null;
+        }).when(mFaceManager).generateChallenge(anyInt(), any());
+        doReturn(new byte[] { 1 }).when(mFragment).requestGatekeeperHat(any(), anyLong(), anyInt(),
+                anyLong());
+
+        // Start fragment
+        mFragment.onAttach(mContext);
+        mFragment.onCreate(null);
+        mFragment.onCreateView(LayoutInflater.from(mContext), mock(ViewGroup.class), Bundle.EMPTY);
+        mFragment.onResume();
+
+        // User clicks on "Face Unlock"
+        final Preference preference = new Preference(mContext);
+        preference.setKey(mFragment.getFacePreferenceKey());
+        mFragment.onPreferenceTreeClick(preference);
+
+        verify(mBiometricSettingsAppPreferenceController).handlePreferenceTreeClick(
+                mPreferenceCaptor.capture());
+        List<Preference> capturedPreferences = mPreferenceCaptor.getAllValues();
+        assertThat(capturedPreferences.size()).isEqualTo(1);
+        assertThat(capturedPreferences.get(0).getKey()).isEqualTo(mFragment.getFacePreferenceKey());
+    }
+
+    @Test
+    public void testClickFaceUnlockIfGkPwHandleTimeout() {
+        doAnswer(invocation -> {
+            final FaceManager.GenerateChallengeCallback callback =
+                    invocation.getArgument(1);
+            callback.onGenerateChallengeResult(0, 0, 1L);
+            return null;
+        }).when(mFaceManager).generateChallenge(anyInt(), any());
+        doThrow(new IllegalStateException("Test")).when(mFragment).requestGatekeeperHat(any(),
+                anyLong(), anyInt(), anyLong());
+
+        // Start fragment
+        mFragment.onAttach(mContext);
+        mFragment.onCreate(null);
+        mFragment.onCreateView(LayoutInflater.from(mContext), mock(ViewGroup.class), Bundle.EMPTY);
+        mFragment.onResume();
+
+        // User clicks on "Face Unlock"
+        final Preference preference = new Preference(mContext);
+        preference.setKey(mFragment.getFacePreferenceKey());
+        mFragment.onPreferenceTreeClick(preference);
+
+        verify(mFragment).launchChooseOrConfirmLock();
+    }
+
+    @Test
+    public void testActivityResultLaunchFaceUnlock() {
+        doAnswer(invocation -> {
+            final FaceManager.GenerateChallengeCallback callback =
+                    invocation.getArgument(1);
+            callback.onGenerateChallengeResult(0, 0, 1L);
+            return null;
+        }).when(mFaceManager).generateChallenge(anyInt(), any());
+        doReturn(new byte[] { 1 }).when(mFragment).requestGatekeeperHat(any(), anyLong(), anyInt(),
+                anyLong());
+
+        // Start fragment
+        mFragment.onAttach(mContext);
+        final Bundle bundle = new Bundle();
+        bundle.putString(BiometricsSettingsBase.RETRY_PREFERENCE_KEY,
+                mFragment.getFingerprintPreferenceKey());
+        final Bundle retryBundle = new Bundle();
+        bundle.putBundle(BiometricsSettingsBase.RETRY_PREFERENCE_BUNDLE, retryBundle);
+        mFragment.onCreate(bundle);
+        mFragment.onCreateView(LayoutInflater.from(mContext), mock(ViewGroup.class), Bundle.EMPTY);
+        mFragment.onResume();
+
+        // User clicks on "Face Unlock"
+        final Preference preference = new Preference(mContext);
+        preference.setKey(mFragment.getFacePreferenceKey());
+        mFragment.onPreferenceTreeClick(preference);
+
+        verify(mBiometricSettingsAppPreferenceController).handlePreferenceTreeClick(
+                mPreferenceCaptor.capture());
+        List<Preference> capturedPreferences = mPreferenceCaptor.getAllValues();
+        assertThat(capturedPreferences.size()).isEqualTo(1);
+        assertThat(capturedPreferences.get(0).getKey()).isEqualTo(mFragment.getFacePreferenceKey());
+    }
+
+    /**
+     * a test fragment that initializes PreferenceScreen for testing.
+     */
+    static class TestCombinedBiometricProfileSettings extends CombinedBiometricProfileSettings {
+
+        private final Context mContext;
+        private final PreferenceManager mPreferenceManager;
+
+        TestCombinedBiometricProfileSettings(Context context) {
+            super();
+            mContext = context;
+            mPreferenceManager = new PreferenceManager(context);
+            mPreferenceManager.setPreferences(mPreferenceManager.createPreferenceScreen(context));
+            setArguments(new Bundle());
+        }
+
+        @Override
+        public int getMetricsCategory() {
+            return 0;
+        }
+
+        @Override
+        public int getPreferenceScreenResId() {
+            return R.xml.placeholder_prefs;
+        }
+
+        @Override
+        public PreferenceScreen getPreferenceScreen() {
+            return mPreferenceManager.getPreferenceScreen();
+        }
+
+        @Override
+        public PreferenceManager getPreferenceManager() {
+            return mPreferenceManager;
+        }
+
+        @Override
+        public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
+            // do nothing
+        }
+
+        @Override
+        public void addPreferencesFromResource(@XmlRes int preferencesResId) {
+            // do nothing
+        }
+
+        @Override
+        public Context getContext() {
+            return mContext;
+        }
+
+        @Override
+        protected void launchChooseOrConfirmLock() {
+            // do nothing
+        }
+    }
+}
diff --git a/tests/robotests/src/com/android/settings/biometrics/face/FaceSettingsLockscreenBypassPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/biometrics/face/FaceSettingsLockscreenBypassPreferenceControllerTest.java
index b9692cf..77a6b02 100644
--- a/tests/robotests/src/com/android/settings/biometrics/face/FaceSettingsLockscreenBypassPreferenceControllerTest.java
+++ b/tests/robotests/src/com/android/settings/biometrics/face/FaceSettingsLockscreenBypassPreferenceControllerTest.java
@@ -31,6 +31,7 @@
 import android.os.UserManager;
 import android.provider.Settings;
 
+import com.android.settings.testutils.FakeFeatureFactory;
 import com.android.settings.testutils.shadow.ShadowUtils;
 import com.android.settingslib.RestrictedLockUtils.EnforcedAdmin;
 import com.android.settingslib.RestrictedSwitchPreference;
@@ -65,6 +66,9 @@
     @Before
     public void setUp() {
         MockitoAnnotations.initMocks(this);
+        ShadowUtils.setFaceManager(mFaceManager);
+        FakeFeatureFactory.setupForTest();
+
         mContext = spy(RuntimeEnvironment.application);
         when(mContext.getSystemService(eq(Context.FACE_SERVICE))).thenReturn(mFaceManager);
         when(mContext.getPackageManager()).thenReturn(mPackageManager);
diff --git a/tests/robotests/src/com/android/settings/testutils/shadow/ShadowUtils.java b/tests/robotests/src/com/android/settings/testutils/shadow/ShadowUtils.java
index 40cb25b..5f8c434 100644
--- a/tests/robotests/src/com/android/settings/testutils/shadow/ShadowUtils.java
+++ b/tests/robotests/src/com/android/settings/testutils/shadow/ShadowUtils.java
@@ -22,6 +22,7 @@
 import android.content.pm.PackageManager;
 import android.graphics.drawable.ColorDrawable;
 import android.graphics.drawable.Drawable;
+import android.hardware.face.FaceManager;
 import android.hardware.fingerprint.FingerprintManager;
 import android.os.UserHandle;
 import android.os.UserManager;
@@ -39,6 +40,7 @@
 public class ShadowUtils {
 
     private static FingerprintManager sFingerprintManager = null;
+    private static FaceManager sFaceManager = null;
     private static boolean sIsUserAMonkey;
     private static boolean sIsDemoUser;
     private static ComponentName sDeviceOwnerComponentName;
@@ -63,6 +65,15 @@
         sFingerprintManager = fingerprintManager;
     }
 
+    @Implementation
+    protected static FaceManager getFaceManagerOrNull(Context context) {
+        return sFaceManager;
+    }
+
+    public static void setFaceManager(FaceManager faceManager) {
+        sFaceManager = faceManager;
+    }
+
     public static void reset() {
         sFingerprintManager = null;
         sIsUserAMonkey = false;