Fix face enroll introduction crash after 10mins

When requestGatekeeperHat() throws exception in FaceEnrollIntroduction
page, remove gk_pw_handle and recreate activity to trigger confirmLock.

Test: robotest for FaceEnrollIntroductionTest
Bug: 234437174
Change-Id: Ie1dd6f36e4deb3f776e3b39acd165fc47d04f526
Merged-In: Ie1dd6f36e4deb3f776e3b39acd165fc47d04f526
diff --git a/src/com/android/settings/biometrics/BiometricUtils.java b/src/com/android/settings/biometrics/BiometricUtils.java
index 4cd2f790..db9465b 100644
--- a/src/com/android/settings/biometrics/BiometricUtils.java
+++ b/src/com/android/settings/biometrics/BiometricUtils.java
@@ -57,6 +57,17 @@
      * enrolled biometric of the same type.
      */
     public static int REQUEST_ADD_ANOTHER = 7;
+
+    /**
+     * Gatekeeper credential not match exception, it throws if VerifyCredentialResponse is not
+     * matched in requestGatekeeperHat().
+     */
+    public static class GatekeeperCredentialNotMatchException extends IllegalStateException {
+        public GatekeeperCredentialNotMatchException(String s) {
+            super(s);
+        }
+    };
+
     /**
      * Given the result from confirming or choosing a credential, request Gatekeeper to generate
      * a HardwareAuthToken with the Gatekeeper Password together with a biometric challenge.
@@ -66,6 +77,8 @@
      * @param userId User ID that the credential/biometric operation applies to
      * @param challenge Unique biometric challenge from FingerprintManager/FaceManager
      * @return
+     * @throws GatekeeperCredentialNotMatchException if Gatekeeper response is not match
+     * @throws IllegalStateException if Gatekeeper Password is missing
      */
     public static byte[] requestGatekeeperHat(@NonNull Context context, @NonNull Intent result,
             int userId, long challenge) {
@@ -83,7 +96,7 @@
         final VerifyCredentialResponse response = utils.verifyGatekeeperPasswordHandle(gkPwHandle,
                 challenge, userId);
         if (!response.isMatched()) {
-            throw new IllegalStateException("Unable to request Gatekeeper HAT");
+            throw new GatekeeperCredentialNotMatchException("Unable to request Gatekeeper HAT");
         }
         return response.getGatekeeperHAT();
     }
diff --git a/src/com/android/settings/biometrics/face/FaceEnrollIntroduction.java b/src/com/android/settings/biometrics/face/FaceEnrollIntroduction.java
index ed74d2a..0f8bb43 100644
--- a/src/com/android/settings/biometrics/face/FaceEnrollIntroduction.java
+++ b/src/com/android/settings/biometrics/face/FaceEnrollIntroduction.java
@@ -18,6 +18,8 @@
 
 import static android.app.admin.DevicePolicyResources.Strings.Settings.FACE_UNLOCK_DISABLED;
 
+import static com.android.settings.biometrics.BiometricUtils.GatekeeperCredentialNotMatchException;
+
 import android.app.admin.DevicePolicyManager;
 import android.app.settings.SettingsEnums;
 import android.content.Intent;
@@ -36,6 +38,7 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.StringRes;
+import androidx.annotation.VisibleForTesting;
 
 import com.android.settings.R;
 import com.android.settings.Utils;
@@ -43,7 +46,6 @@
 import com.android.settings.biometrics.BiometricEnrollIntroduction;
 import com.android.settings.biometrics.BiometricUtils;
 import com.android.settings.biometrics.MultiBiometricEnrollHelper;
-import com.android.settings.overlay.FeatureFactory;
 import com.android.settings.password.ChooseLockSettingsHelper;
 import com.android.settings.password.SetupSkipDialog;
 import com.android.settings.utils.SensorPrivacyManagerHelper;
@@ -61,7 +63,6 @@
     private static final String TAG = "FaceEnrollIntroduction";
 
     private FaceManager mFaceManager;
-    private FaceFeatureProvider mFaceFeatureProvider;
     @Nullable private FooterButton mPrimaryFooterButton;
     @Nullable private FooterButton mSecondaryFooterButton;
     @Nullable private SensorPrivacyManager mSensorPrivacyManager;
@@ -142,9 +143,7 @@
             infoMessageRequireEyes.setText(getInfoMessageRequireEyes());
         }
 
-        mFaceManager = Utils.getFaceManagerOrNull(this);
-        mFaceFeatureProvider = FeatureFactory.getFactory(getApplicationContext())
-                .getFaceFeatureProvider();
+        mFaceManager = getFaceManager();
 
         // This path is an entry point for SetNewPasswordController, e.g.
         // adb shell am start -a android.app.action.SET_NEW_PASSWORD
@@ -154,11 +153,22 @@
                 // We either block on generateChallenge, or need to gray out the "next" button until
                 // the challenge is ready. Let's just do this for now.
                 mFaceManager.generateChallenge(mUserId, (sensorId, userId, challenge) -> {
-                    mToken = BiometricUtils.requestGatekeeperHat(this, getIntent(), mUserId,
-                            challenge);
-                    mSensorId = sensorId;
-                    mChallenge = challenge;
-                    mFooterBarMixin.getPrimaryButton().setEnabled(true);
+                    if (isFinishing()) {
+                        // Do nothing if activity is finishing
+                        Log.w(TAG, "activity finished before challenge callback launched.");
+                        return;
+                    }
+
+                    try {
+                        mToken = requestGatekeeperHat(challenge);
+                        mSensorId = sensorId;
+                        mChallenge = challenge;
+                        mFooterBarMixin.getPrimaryButton().setEnabled(true);
+                    } catch (GatekeeperCredentialNotMatchException e) {
+                        // Let BiometricEnrollBase#onCreate() to trigger confirmLock()
+                        getIntent().removeExtra(ChooseLockSettingsHelper.EXTRA_KEY_GK_PW_HANDLE);
+                        recreate();
+                    }
                 });
             }
         }
@@ -172,6 +182,18 @@
         Log.v(TAG, "cameraPrivacyEnabled : " + cameraPrivacyEnabled);
     }
 
+    @VisibleForTesting
+    @Nullable
+    protected FaceManager getFaceManager() {
+        return Utils.getFaceManagerOrNull(this);
+    }
+
+    @VisibleForTesting
+    @Nullable
+    protected byte[] requestGatekeeperHat(long challenge) {
+        return BiometricUtils.requestGatekeeperHat(this, getIntent(), mUserId, challenge);
+    }
+
     @Override
     protected void onActivityResult(int requestCode, int resultCode, Intent data) {
         // If user has skipped or finished enrolling, don't restart enrollment.
diff --git a/tests/robotests/src/com/android/settings/biometrics/face/FaceEnrollIntroductionTest.java b/tests/robotests/src/com/android/settings/biometrics/face/FaceEnrollIntroductionTest.java
new file mode 100644
index 0000000..2e5cc02
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/biometrics/face/FaceEnrollIntroductionTest.java
@@ -0,0 +1,157 @@
+/*
+ * 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.face;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.doAnswer;
+
+import android.content.Intent;
+import android.hardware.face.FaceManager;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.settings.biometrics.BiometricUtils;
+import com.android.settings.password.ChooseLockSettingsHelper;
+import com.android.settings.testutils.shadow.ShadowLockPatternUtils;
+import com.android.settings.testutils.shadow.ShadowSensorPrivacyManager;
+import com.android.settings.testutils.shadow.ShadowUserManager;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.Robolectric;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.Shadows;
+import org.robolectric.android.controller.ActivityController;
+import org.robolectric.annotation.Config;
+import org.robolectric.shadows.ShadowActivity;
+
+@RunWith(RobolectricTestRunner.class)
+@Config(shadows = {
+        ShadowLockPatternUtils.class,
+        ShadowUserManager.class,
+        ShadowSensorPrivacyManager.class
+})
+public class FaceEnrollIntroductionTest {
+
+    @Mock private FaceManager mFaceManager;
+
+    private ActivityController<TestFaceEnrollIntroduction> mController;
+    private TestFaceEnrollIntroduction mActivity;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+    }
+
+    private void setupActivity(@NonNull Intent intent) {
+        doAnswer(invocation -> {
+            final FaceManager.GenerateChallengeCallback callback =
+                    invocation.getArgument(1);
+            callback.onGenerateChallengeResult(0, 0, 1L);
+            return null;
+        }).when(mFaceManager).generateChallenge(anyInt(), any());
+        mController = Robolectric.buildActivity(TestFaceEnrollIntroduction.class, intent);
+        mActivity = mController.get();
+        mActivity.mOverrideFaceManager = mFaceManager;
+    }
+
+    @Test
+    public void testOnCreate() {
+        setupActivity(new Intent());
+        mController.create();
+    }
+
+    @Test
+    public void testOnCreateToGenerateChallenge() {
+        setupActivity(new Intent().putExtra(ChooseLockSettingsHelper.EXTRA_KEY_GK_PW_HANDLE, 1L));
+        mActivity.mGateKeeperAction = GateKeeperAction.RETURN_BYTE_ARRAY;
+        mController.create();
+    }
+
+    @Test
+    public void testGenerateChallengeFailThenRecreate() {
+        setupActivity(new Intent().putExtra(ChooseLockSettingsHelper.EXTRA_KEY_GK_PW_HANDLE, 1L));
+        mActivity.mGateKeeperAction = GateKeeperAction.THROW_CREDENTIAL_NOT_MATCH;
+        mController.create();
+
+        // Make sure recreate() is called on original activity
+        assertThat(mActivity.getRecreateCount()).isEqualTo(1);
+
+        // Simulate recreate() action
+        setupActivity(mActivity.getIntent());
+        mController.create();
+
+        // Verify confirmLock()
+        assertThat(mActivity.getConfirmingCredentials()).isTrue();
+        ShadowActivity shadowActivity = Shadows.shadowOf(mActivity);
+        ShadowActivity.IntentForResult startedActivity =
+                shadowActivity.getNextStartedActivityForResult();
+        assertWithMessage("Next activity 1").that(startedActivity).isNotNull();
+    }
+
+    enum GateKeeperAction { CALL_SUPER, RETURN_BYTE_ARRAY, THROW_CREDENTIAL_NOT_MATCH }
+
+    public static class TestFaceEnrollIntroduction extends FaceEnrollIntroduction {
+
+        private int mRecreateCount = 0;
+
+        public int getRecreateCount() {
+            return mRecreateCount;
+        }
+
+        @Override
+        public void recreate() {
+            mRecreateCount++;
+            // Do nothing
+        }
+
+        public boolean getConfirmingCredentials() {
+            return mConfirmingCredentials;
+        }
+
+        public FaceManager mOverrideFaceManager = null;
+        @NonNull public GateKeeperAction mGateKeeperAction = GateKeeperAction.CALL_SUPER;
+
+        @Nullable
+        @Override
+        public byte[] requestGatekeeperHat(long challenge) {
+            switch (mGateKeeperAction) {
+                case RETURN_BYTE_ARRAY:
+                    return new byte[] { 1 };
+                case THROW_CREDENTIAL_NOT_MATCH:
+                    throw new BiometricUtils.GatekeeperCredentialNotMatchException("test");
+                case CALL_SUPER:
+                default:
+                    return super.requestGatekeeperHat(challenge);
+            }
+        }
+
+        @Nullable
+        @Override
+        protected FaceManager getFaceManager() {
+            return mOverrideFaceManager;
+        }
+    }
+}