R版本 Google对生物识别弹框 BiometricPrompt的逻辑进行了重构,尤其是框架服务和systemui部分。本文从systemui角度来了解下相比于Q版本的变化,其中会涉及部分的框架的调整。

一. 概述

下图为systemui的类图(取自博客:https://blog.csdn.net/u013398960/article/details/105630411),侵删。
图片说明

更直观的代码结构目录如下:
图片说明

可以对比下Q版本上的区别:https://blog.nowcoder.net/n/da445eff609041ab957221ddb08f00bb

整体逻辑可概述为如下内容:

  1. com.android.systemui.biometrics.AuthController 作为总的控制类,接收框架服务的回调,并提供与服务交互的接口。
  2. com.android.systemui.biometrics.AuthContainerView 作为整个布局的容器,与AuthController交互并拉起其它的子布局。整个view相关的逻辑多在其中。
  3. com.android.systemui.biometrics.AuthPanelController 控制整体弹框背景部分的视图。
  4. com.android.systemui.biometrics.AuthCredentialView 新增非生物识别类型的view,如图案密码和文本密码。此部分是完全新增的内容,需重点介绍。

二. 布局

整体为覆盖整个窗口的底层布局:AuthContainerView;控制背景的PanelView。 两者不论是在生物识别(指纹、人脸)还是非生物识别(图案密码、文本密码)等类型中均存在且逻辑相同。

2.1 生物识别布局

对于生物识别类型BiometricView,又专门封装了一个ScrollView来装载后再指纹或人脸的具体元素。

2.1.1 结构视图

图片说明

2.1.2 代码实现

  1. 在AuthContainerView构造时先inflate 具体的BiometricView
// Inflate biometric view only if necessary.
        if (Utils.isBiometricAllowed(mConfig.mBiometricPromptBundle)) {
            if (config.mModalityMask == BiometricAuthenticator.TYPE_FINGERPRINT) {
                mBiometricView = (AuthBiometricFingerprintView)
                        factory.inflate(R.layout.auth_biometric_fingerprint_view, null, false);
            } else if (config.mModalityMask == BiometricAuthenticator.TYPE_FACE) {
                mBiometricView = (AuthBiometricFaceView)
                        factory.inflate(R.layout.auth_biometric_face_view, null, false);
            } else {
                Log.e(TAG, "Unsupported biometric modality: " + config.mModalityMask);
                mBiometricView = null;
                mBackgroundView = null;
                mBiometricScrollView = null;
                return;
            }
  1. 在onAttachedToWindow 的时候addview
private void addBiometricView() {
        mBiometricView.setRequireConfirmation(mConfig.mRequireConfirmation);
        mBiometricView.setPanelController(mPanelController);
        mBiometricView.setBiometricPromptBundle(mConfig.mBiometricPromptBundle);
        mBiometricView.setCallback(mBiometricCallback);
        mBiometricView.setBackgroundView(mBackgroundView);
        mBiometricView.setUserId(mConfig.mUserId);
        mBiometricView.setEffectiveUserId(mEffectiveUserId);
        mBiometricScrollView.addView(mBiometricView);
    }

2.2 密码认证

密码部分相对简单,满足条件时会在AuthContainerView onAttachedToWindow的时候inflate,并直接add进AuthContainerView。

private void addCredentialView(boolean animatePanel, boolean animateContents) {
        final LayoutInflater factory = LayoutInflater.from(mContext);

        final @Utils.CredentialType int credentialType = mInjector.getCredentialType(
                mContext, mEffectiveUserId);

        switch (credentialType) {
            case Utils.CREDENTIAL_PATTERN:
                mCredentialView = (AuthCredentialView) factory.inflate(
                        R.layout.auth_credential_pattern_view, null, false);
                break;
            case Utils.CREDENTIAL_PIN:
            case Utils.CREDENTIAL_PASSWORD:
                mCredentialView = (AuthCredentialView) factory.inflate(
                        R.layout.auth_credential_password_view, null, false);
                break;
            default:
                throw new IllegalStateException("Unknown credential type: " + credentialType);
        }

        // The background is used for detecting taps / cancelling authentication. Since the
        // credential view is full-screen and should not be canceled from background taps,
        // disable it.
        mBackgroundView.setOnClickListener(null);
        mBackgroundView.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO);

        mCredentialView.setContainerView(this);
        mCredentialView.setUserId(mConfig.mUserId);
        mCredentialView.setOperationId(mConfig.mOperationId);
        mCredentialView.setEffectiveUserId(mEffectiveUserId);
        mCredentialView.setCredentialType(credentialType);
        mCredentialView.setCallback(mCredentialCallback);
        mCredentialView.setBiometricPromptBundle(mConfig.mBiometricPromptBundle);
        mCredentialView.setPanelController(mPanelController, animatePanel);
        mCredentialView.setShouldAnimateContents(animateContents);
        mFrameLayout.addView(mCredentialView);
    }

view内部的具体内容不再展开。

三. 密码认证逻辑

R版本与Q最大的不同就是R上面systemui集成进了密码认证的部分。 在Q版本上生物识别弹框功能也能使用密码校验的功能,但是由BiometricService去拉的settings的密码模块,但凡涉及密码校验的部分都与systemui无关。 但在R版本上原生将密码校验的功能和view全部集成进了systemui,不再有设置模块的参与。

3.1 密码类型

密码共有4位/6位数字密码、图案密码、复杂密码、pin码等多种类型,从设置密码录入页和锁屏密码中就可以看出。 BiometricPrompt中对密码类型进行了整合:将数字密码、复杂密码等类型统一划分为文本密码类型,与图案密码共同作为使用的两种密码类型。 个人理解:设置密码和锁屏密码属于两个大模块,而生物弹框的密码部分只是一个小组件,没有必要做成和这两个模块一样大规模的逻辑和复杂的view体系。

3.2 文本密码

AuthCredentialPasswordView 本身比较简单,主要是通过输入域来和用户交互。 其输入文本的控件为ImeAwareEditText类型,在onEditorAction回调中,当判断满足条件后会将密码输入底层进行校验:com.android.systemui.biometrics.AuthCredentialPasswordView#checkPasswordAndUnlock。 和锁屏密码、设置密码一样,会有一个回调onCredentialVerified来接校验的结果。

@Override
    protected void onCredentialVerified(byte[] attestation, int timeoutMs) {
        super.onCredentialVerified(attestation, timeoutMs);

        final boolean matched = attestation != null;

        if (matched) {
            mImm.hideSoftInputFromWindow(getWindowToken(), 0 /* flags */);
        } else {
            mPasswordField.setText("");
        }
    }

从上面代码可以看出,对于校验结果AuthCredentialPasswordView本身只处理部分和view相关的内容,其他具体的逻辑交由父类AuthCredentialView执行。

3.3 图案密码

AuthCredentialPatternView 和文本密码逻辑一样,只不过和用户交互的控件变成了LockPatternView类型。

3.4 密码验证处理流程

protected void onCredentialVerified(byte[] attestation, int timeoutMs) {

        final boolean matched = attestation != null;

        if (matched) {
            mClearErrorRunnable.run();
            mLockPatternUtils.userPresent(mEffectiveUserId);
            **mCallback.onCredentialMatched(attestation); //重点内容**
        } else {
            if (timeoutMs > 0) {
                mHandler.removeCallbacks(mClearErrorRunnable);
                long deadline = mLockPatternUtils.setLockoutAttemptDeadline(
                        mEffectiveUserId, timeoutMs);
                mErrorTimer = new ErrorTimer(mContext,
                        deadline - SystemClock.elapsedRealtime(),
                        LockPatternUtils.FAILED_ATTEMPT_COUNTDOWN_INTERVAL_MS,
                        mErrorView) {
                    @Override
                    public void onFinish() {
                        onErrorTimeoutFinish();
                        mClearErrorRunnable.run();
                    }
                };
                mErrorTimer.start();
            } else {
                final boolean didUpdateErrorText = reportFailedAttempt();
                if (!didUpdateErrorText) {
                    final @StringRes int errorRes;
                    switch (mCredentialType) {
                        case Utils.CREDENTIAL_PIN:
                            errorRes = R.string.biometric_dialog_wrong_pin;
                            break;
                        case Utils.CREDENTIAL_PATTERN:
                            errorRes = R.string.biometric_dialog_wrong_pattern;
                            break;
                        case Utils.CREDENTIAL_PASSWORD:
                        default:
                            errorRes = R.string.biometric_dialog_wrong_password;
                            break;
                    }
                    showError(getResources().getString(errorRes));
                }
            }
        }
    }

整体分为两个方向:当校验成功时会通过onCredentialMatched接口来和服务通信;当校验失败的时候只是会对view本身做出调整。下面看一下onCredentialMatched的传递流程。

final class CredentialCallback implements AuthCredentialView.Callback {
        @Override
        public void onCredentialMatched(byte[] attestation) {
            mCredentialAttestation = attestation;
            **animateAway(AuthDialogCallback.DISMISSED_CREDENTIAL_AUTHENTICATED);**
        }
    }

com.android.systemui.biometrics.AuthContainerView.CredentialCallback AuthContainerView的内部类实现该接口,从而调用到animateAway方法。值得注意的是,不论是密码识别和生物识别,与服务交互的路径均需要通过animateAway方法。animateAway - removeWindowIfAttached - sendPendingCallbackIfNotNull.

private void sendPendingCallbackIfNotNull() {
        Log.d(TAG, "pendingCallback: " + mPendingCallbackReason
                + " sysUISessionId: " + mConfig.mSysUiSessionId);
        if (mPendingCallbackReason != null) {
            **mConfig.mCallback.onDismissed(mPendingCallbackReason, mCredentialAttestation);**
            mPendingCallbackReason = null;
        }
    }

最终会走到onDismissed方法,而这就是与服务交互的最终入口。

@Override
    public void onDismissed(@DismissedReason int reason, @Nullable byte[] credentialAttestation) {
        switch (reason) {
            case AuthDialogCallback.DISMISSED_USER_CANCELED:
                sendResultAndCleanUp(BiometricPrompt.DISMISSED_REASON_USER_CANCEL,
                        credentialAttestation);
                break;

            case AuthDialogCallback.DISMISSED_BUTTON_NEGATIVE:
                sendResultAndCleanUp(BiometricPrompt.DISMISSED_REASON_NEGATIVE,
                        credentialAttestation);
                break;

            case AuthDialogCallback.DISMISSED_BUTTON_POSITIVE:
                sendResultAndCleanUp(BiometricPrompt.DISMISSED_REASON_BIOMETRIC_CONFIRMED,
                        credentialAttestation);
                break;

            case AuthDialogCallback.DISMISSED_BIOMETRIC_AUTHENTICATED:
                sendResultAndCleanUp(
                        BiometricPrompt.DISMISSED_REASON_BIOMETRIC_CONFIRM_NOT_REQUIRED,
                        credentialAttestation);
                break;

            case AuthDialogCallback.DISMISSED_ERROR:
                sendResultAndCleanUp(BiometricPrompt.DISMISSED_REASON_ERROR,
                        credentialAttestation);
                break;

            case AuthDialogCallback.DISMISSED_BY_SYSTEM_SERVER:
                sendResultAndCleanUp(BiometricPrompt.DISMISSED_REASON_SERVER_REQUESTED,
                        credentialAttestation);
                break;

            case AuthDialogCallback.DISMISSED_CREDENTIAL_AUTHENTICATED:
                sendResultAndCleanUp(BiometricPrompt.DISMISSED_REASON_CREDENTIAL_CONFIRMED,
                        credentialAttestation);
                break;

            default:
                Log.e(TAG, "Unhandled reason: " + reason);
                break;
        }
    }

上述分别定义了校验成功、失败等各种情况。在服务的com.android.server.biometrics.BiometricService#handleOnDismissed方法中会有一一对应的处理操作。 上层应用的对应回调结果也是在这里触发。

private void handleOnDismissed(int reason, @Nullable byte[] credentialAttestation) {
        if (mCurrentAuthSession == null) {
            Slog.e(TAG, "onDismissed: " + reason + ", auth session null");
            return;
        }

        logDialogDismissed(reason);

        try {
            switch (reason) {
                case BiometricPrompt.DISMISSED_REASON_CREDENTIAL_CONFIRMED:
                    if (credentialAttestation != null) {
                        mKeyStore.addAuthToken(credentialAttestation);
                    } else {
                        Slog.e(TAG, "Credential confirmed but attestation is null");
                    }
                case BiometricPrompt.DISMISSED_REASON_BIOMETRIC_CONFIRMED:
                case BiometricPrompt.DISMISSED_REASON_BIOMETRIC_CONFIRM_NOT_REQUIRED:
                    if (mCurrentAuthSession.mTokenEscrow != null) {
                        mKeyStore.addAuthToken(mCurrentAuthSession.mTokenEscrow);
                    } else {
                        Slog.e(TAG, "mTokenEscrow is null");
                    }
                    mCurrentAuthSession.mClientReceiver.onAuthenticationSucceeded(
                            Utils.getAuthenticationTypeForResult(reason));
                    break;

                case BiometricPrompt.DISMISSED_REASON_NEGATIVE:
                    mCurrentAuthSession.mClientReceiver.onDialogDismissed(reason);
                    // Cancel authentication. Skip the token/package check since we are cancelling
                    // from system server. The interface is permission protected so this is fine.
                    cancelInternal(null /* token */, null /* package */,
                            mCurrentAuthSession.mCallingUid, mCurrentAuthSession.mCallingPid,
                            mCurrentAuthSession.mCallingUserId, false /* fromClient */);
                    break;

                case BiometricPrompt.DISMISSED_REASON_USER_CANCEL:
                    mCurrentAuthSession.mClientReceiver.onError(
                            mCurrentAuthSession.mModality,
                            BiometricConstants.BIOMETRIC_ERROR_USER_CANCELED,
                            0 /* vendorCode */
                    );
                    // Cancel authentication. Skip the token/package check since we are cancelling
                    // from system server. The interface is permission protected so this is fine.
                    cancelInternal(null /* token */, null /* package */, Binder.getCallingUid(),
                            Binder.getCallingPid(), UserHandle.getCallingUserId(),
                            false /* fromClient */);
                    break;

                case BiometricPrompt.DISMISSED_REASON_SERVER_REQUESTED:
                case BiometricPrompt.DISMISSED_REASON_ERROR:
                    mCurrentAuthSession.mClientReceiver.onError(
                            mCurrentAuthSession.mModality,
                            mCurrentAuthSession.mErrorEscrow,
                            mCurrentAuthSession.mVendorCodeEscrow
                    );
                    break;

                default:
                    Slog.w(TAG, "Unhandled reason: " + reason);
                    break;
            }

            // Dialog is gone, auth session is done.
            mCurrentAuthSession = null;

        } catch (RemoteException e) {
            Slog.e(TAG, "Remote exception", e);
        }
    }

三. 总结

  1. R版本BiometricPrompt的服务和systemui模块的变化很大,主要是集成了密码校验的模块和重构了两者的通信逻辑。
  2. 生物校验的流程和Q版本整体上保持一致:由上层应用调用方自行注册指纹/人脸服务 ,systemui只提供对应的view界面。并且在某些情况下通知服务取消注册。
  3. 密码校验的发起和回调接受均在systmeui完成。

四. 参考资料

  1. https://blog.csdn.net/u013398960/article/details/105630411