Loading src/com/android/settings/biometrics/BiometricUtils.java +14 −1 Original line number Diff line number Diff line Loading @@ -60,6 +60,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. Loading @@ -69,6 +80,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) { Loading @@ -86,7 +99,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(); } Loading src/com/android/settings/biometrics/face/FaceEnrollIntroduction.java +32 −10 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -37,6 +39,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; Loading @@ -44,7 +47,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; Loading @@ -64,7 +66,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; Loading Loading @@ -145,9 +146,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 Loading @@ -157,11 +156,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); 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(); } }); } } Loading @@ -175,6 +185,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. Loading tests/robotests/src/com/android/settings/biometrics/face/FaceEnrollIntroductionTest.java 0 → 100644 +157 −0 Original line number Diff line number Diff line /* * 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; } } } Loading
src/com/android/settings/biometrics/BiometricUtils.java +14 −1 Original line number Diff line number Diff line Loading @@ -60,6 +60,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. Loading @@ -69,6 +80,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) { Loading @@ -86,7 +99,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(); } Loading
src/com/android/settings/biometrics/face/FaceEnrollIntroduction.java +32 −10 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -37,6 +39,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; Loading @@ -44,7 +47,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; Loading @@ -64,7 +66,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; Loading Loading @@ -145,9 +146,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 Loading @@ -157,11 +156,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); 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(); } }); } } Loading @@ -175,6 +185,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. Loading
tests/robotests/src/com/android/settings/biometrics/face/FaceEnrollIntroductionTest.java 0 → 100644 +157 −0 Original line number Diff line number Diff line /* * 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; } } }