From 8e5780824755ee3983d475a540b20f1a89cc286c Mon Sep 17 00:00:00 2001 From: Milton Wu Date: Mon, 7 Nov 2022 03:33:19 +0000 Subject: [PATCH] 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 --- .../settings/biometrics/BiometricUtils.java | 15 +- .../face/FaceEnrollIntroduction.java | 42 +++-- .../face/FaceEnrollIntroductionTest.java | 157 ++++++++++++++++++ 3 files changed, 203 insertions(+), 11 deletions(-) create mode 100644 tests/robotests/src/com/android/settings/biometrics/face/FaceEnrollIntroductionTest.java diff --git a/src/com/android/settings/biometrics/BiometricUtils.java b/src/com/android/settings/biometrics/BiometricUtils.java index 4cd2f790620..db9465be54d 100644 --- a/src/com/android/settings/biometrics/BiometricUtils.java +++ b/src/com/android/settings/biometrics/BiometricUtils.java @@ -57,6 +57,17 @@ public class BiometricUtils { * 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 @@ public class BiometricUtils { * @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 @@ public class BiometricUtils { 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 ed74d2a550d..0f8bb4355ac 100644 --- a/src/com/android/settings/biometrics/face/FaceEnrollIntroduction.java +++ b/src/com/android/settings/biometrics/face/FaceEnrollIntroduction.java @@ -18,6 +18,8 @@ package com.android.settings.biometrics.face; 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 android.widget.TextView; 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.BiometricEnrollActivity; 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 @@ public class FaceEnrollIntroduction extends BiometricEnrollIntroduction { 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 @@ public class FaceEnrollIntroduction extends BiometricEnrollIntroduction { 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 @@ public class FaceEnrollIntroduction extends BiometricEnrollIntroduction { // 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 @@ public class FaceEnrollIntroduction extends BiometricEnrollIntroduction { 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 00000000000..2e5cc024d3b --- /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 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; + } + } +}