Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 8e578082 authored by Milton Wu's avatar Milton Wu Committed by lbill
Browse files

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
parent 30783964
Loading
Loading
Loading
Loading
+14 −1
Original line number Diff line number Diff line
@@ -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();
    }
+32 −10
Original line number Diff line number Diff line
@@ -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);
                    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.
+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;
        }
    }
}