【Android】BiometricPrompt R 原生bug及解决方案
BiometricPrompt 在R版本进行了大量调整,并且历次的codebase升级都包含有对这块逻辑的修改,可能是Google未对此部分做足够的测试验证,目前实际开发中依然发现了较多的原生的严重bug。下面就遇到的几个进行整理并提供解决方案。
一. 类型转换错误(开发者版本中存在)
文本密码中输入密码的view 在竖屏的xml文件下是ImeAwareEditText,但在横屏下是EditText。而ImeAwareEditText是EditText的子类,横竖屏转换时会出现类型转换错误。
<ImeAwareEditText android:id="@+id/lockPassword" android:layout_width="208dp" android:layout_height="wrap_content" android:layout_gravity="center_horizontal" android:minHeight="48dp" android:gravity="center" android:inputType="textPassword" android:maxLength="500" android:imeOptions="actionNext|flagNoFullscreen|flagForceAscii" style="@style/TextAppearance.AuthCredential.PasswordEntry"/>
在正式版本中,这类的低级问题已经解决。
二. 退出类错误
2.1 退出时序异常
场景举例:使用官网提供的模板:https://developer.android.com/training/sign-in/biometric-auth,可以轻松的自己实现一个demo。不断尝试点击系统的返回键来退出BiometricPrompt,会低概率出现crash。
原因:监听按键的animateAway和dismissFromSystemserver同时回调,且低概率因时序mContainerState状态没有拦截removeView导致问题。
private void removeWindowIfAttached(boolean sendReason) { if (sendReason) { sendPendingCallbackIfNotNull(); } if (mContainerState == STATE_GONE) { //animateAway和dismissFromSystemserver同时回调时,mContainerState状态有概率更新错误,没有成功拦截。 Log.w(TAG, "Container already STATE_GONE, mSysUiSessionId: " + mConfig.mSysUiSessionId); return; } Log.d(TAG, "Removing container, mSysUiSessionId: " + mConfig.mSysUiSessionId); mContainerState = STATE_GONE; mWindowManager.removeView(this); //问题点:当AuthContainerView已经remove后还会进行调用。 }
解决方案:加入isattachedtowindow()的判断。
if(isattachedtowindow()) { mWindowManager.removeView(this); }
2.2 退出逻辑异常
- 场景举例:前提:录入指纹。在唤起BiometricPrompt的瞬间(还在执行入场动画时),点击返回键退出。 会较高概率的复现问题。
- 原因:原生流程没有将View入场动画未执行完就退出的场景作为用户取消场景,在animateAway时直接退出,并且不上报给框架,导致指纹没有被取消注册。指纹异常后,按压指纹验证,CommandQueue仍然接收到到MSG_BIOMETRIC_AUTHENTICATED执行到错误逻辑导致SystemUI崩溃。
private void onDialogAnimatedIn() { if (mContainerState == STATE_PENDING_DISMISS) { Log.d(TAG, "onDialogAnimatedIn(): mPendingDismissDialog=true, dismissing now"); animateAway(false /* sendReason */, 0); //此处reason传入的0,会导致后续逻辑不会去取消指纹的注册。 return; } mContainerState = STATE_SHOWING; if (mBiometricView != null) { mBiometricView.onDialogAnimatedIn(); } }
- 解决方案:animateaway的原因从0改成onDialogAnimatedIn。
animateAway(true/* sendReason */, DISSMISSED_USER_CANCELED); //DISSMISSED_USER_CANCELED = 1
三. 唤起类错误
场景举例:在BiometricPrompt显示的时候进入近期任务,再触发唤起BiometricPrompt。低概率无法唤起。
原因:时序问题,下面的广播方法和hideAuthenticationDialog方法同时执行会出现时序问题,因为hideAuthenticationDialog会将mCurrentDialog置空。导致如果后执行广播中的方法时候,因为针对mCurrentDialog做了非空判断,未通知服务取消上次的认证回话,最终无法再次唤起,从而引发异常。
@VisibleForTesting final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { // mCurrentDialog != null的逻辑放在了整个判断的最外层 if (mCurrentDialog != null && Intent.ACTION_CLOSE_SYSTEM_DIALOGS.equals(intent.getAction())) { Log.w(TAG, "ACTION_CLOSE_SYSTEM_DIALOGS received"); mCurrentDialog.dismissWithoutCallback(true /* animate */); mCurrentDialog = null; try { if (mReceiver != null) { mReceiver.onDialogDismissed(BiometricPrompt.DISMISSED_REASON_USER_CANCEL, null /* credentialAttestation */); mReceiver = null; } } catch (RemoteException e) { Log.e(TAG, "Remote exception", e); } } } };
解决方案:
if ( Intent.ACTION_CLOSE_SYSTEM_DIALOGS.equals(intent.getAction())) { // 将判空放在方法内部,仅控制view的逻辑。 // 不影响mReceiver.onDialogDismissed和服务交互的逻辑。 if (mCurrentDialog != null) { mCurrentDialog.dismissWithoutCallback(true /* animate */); mCurrentDialog = null; } }
四. 整体性问题
此部分无法确认是原生设计如此,还是整个方案的异常。因为如果当作问题解决,需要修改从上层接口到系统服务到系统界面的整体逻辑。
- 正常情况下,密码校验错误5次后,无法使用指纹/人脸。但当使用密码识别错误5次后,再次唤起BiometricPrompt,发现依然可以使用指纹和人脸校验,并且可以校验成功。此部分逻辑涉及BiometricPrompt整个校验体系。当密码校验错误5次后,上层应用调用方依然可以注册生物识别校验;生物识别服务依然给systmeui发送参数显示指纹/人脸弹框。各个模块的逻辑都未对此种情况进行任何处理,应该为设计如此。
- 正常情况下,密码校验错误5次后,密码校验页面会进入30s倒计时。但是再次唤起BiometricPrompt时其密码页面不会进入倒计时状态,只有输入一次密码后才会触发成倒计时状态。 这种现象是可以只改systmeui处理完成并且明显是违背逻辑的。修改方案为每次唤起BiometricPrompt的密码校验页面时先判断当前状态,如果在倒计时时,则直接根据相应的时间展示对应的页面。
五. 总结
- 本文提到的若干问题均是提到了问题最直观的现象和原因以及对应的解决方案,至于错误逻辑是怎么一步步导致问题现象的并没有逐句展示,如果有需要,后续会补充完整。
- 可以看到,大部分问题都是两种逻辑同时触发,当某些状态量的变更不符合预期时序时造成的低概率问题。