R版本 Google对生物识别弹框 BiometricPrompt的逻辑进行了重构,尤其是框架服务和systemui部分。本文从systemui角度来了解下相比于Q版本的变化,其中会涉及部分的框架的调整。
一. 概述
下图为systemui的类图(取自博客:https://blog.csdn.net/u013398960/article/details/105630411),侵删。
更直观的代码结构目录如下:
可以对比下Q版本上的区别:https://blog.nowcoder.net/n/da445eff609041ab957221ddb08f00bb
整体逻辑可概述为如下内容:
- com.android.systemui.biometrics.AuthController 作为总的控制类,接收框架服务的回调,并提供与服务交互的接口。
- com.android.systemui.biometrics.AuthContainerView 作为整个布局的容器,与AuthController交互并拉起其它的子布局。整个view相关的逻辑多在其中。
- com.android.systemui.biometrics.AuthPanelController 控制整体弹框背景部分的视图。
- com.android.systemui.biometrics.AuthCredentialView 新增非生物识别类型的view,如图案密码和文本密码。此部分是完全新增的内容,需重点介绍。
二. 布局
整体为覆盖整个窗口的底层布局:AuthContainerView;控制背景的PanelView。 两者不论是在生物识别(指纹、人脸)还是非生物识别(图案密码、文本密码)等类型中均存在且逻辑相同。
2.1 生物识别布局
对于生物识别类型BiometricView,又专门封装了一个ScrollView来装载后再指纹或人脸的具体元素。
2.1.1 结构视图
2.1.2 代码实现
- 在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; }
- 在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); } }
三. 总结
- R版本BiometricPrompt的服务和systemui模块的变化很大,主要是集成了密码校验的模块和重构了两者的通信逻辑。
- 生物校验的流程和Q版本整体上保持一致:由上层应用调用方自行注册指纹/人脸服务 ,systemui只提供对应的view界面。并且在某些情况下通知服务取消注册。
- 密码校验的发起和回调接受均在systmeui完成。