/* * Copyright (C) 2018 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.systemui.biometrics; import android.content.Context; import android.content.pm.PackageManager; import android.content.res.Configuration; import android.hardware.biometrics.BiometricAuthenticator; import android.hardware.biometrics.BiometricPrompt; import android.hardware.biometrics.IBiometricServiceReceiverInternal; import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.os.Message; import android.os.RemoteException; import android.util.Log; import android.view.WindowManager; import com.android.internal.os.SomeArgs; import com.android.systemui.Dependency; import com.android.systemui.SystemUI; import com.android.systemui.keyguard.WakefulnessLifecycle; import com.android.systemui.statusbar.CommandQueue; /** * Receives messages sent from AuthenticationClient and shows the appropriate biometric UI (e.g. * BiometricDialogView). */ public class BiometricDialogImpl extends SystemUI implements CommandQueue.Callbacks { private static final String TAG = "BiometricDialogImpl"; private static final boolean DEBUG = true; private static final int MSG_SHOW_DIALOG = 1; private static final int MSG_BIOMETRIC_AUTHENTICATED = 2; private static final int MSG_BIOMETRIC_HELP = 3; private static final int MSG_BIOMETRIC_ERROR = 4; private static final int MSG_HIDE_DIALOG = 5; private static final int MSG_BUTTON_NEGATIVE = 6; private static final int MSG_USER_CANCELED = 7; private static final int MSG_BUTTON_POSITIVE = 8; private static final int MSG_TRY_AGAIN_PRESSED = 9; private SomeArgs mCurrentDialogArgs; private BiometricDialogView mCurrentDialog; private WindowManager mWindowManager; private IBiometricServiceReceiverInternal mReceiver; private boolean mDialogShowing; private Callback mCallback = new Callback(); private WakefulnessLifecycle mWakefulnessLifecycle; private Handler mHandler = new Handler(Looper.getMainLooper()) { @Override public void handleMessage(Message msg) { switch(msg.what) { case MSG_SHOW_DIALOG: handleShowDialog((SomeArgs) msg.obj, false /* skipAnimation */, null /* savedState */); break; case MSG_BIOMETRIC_AUTHENTICATED: { SomeArgs args = (SomeArgs) msg.obj; handleBiometricAuthenticated((boolean) args.arg1 /* authenticated */, (String) args.arg2 /* failureReason */); args.recycle(); break; } case MSG_BIOMETRIC_HELP: { SomeArgs args = (SomeArgs) msg.obj; handleBiometricHelp((String) args.arg1 /* message */); args.recycle(); break; } case MSG_BIOMETRIC_ERROR: handleBiometricError((String) msg.obj); break; case MSG_HIDE_DIALOG: handleHideDialog((Boolean) msg.obj); break; case MSG_BUTTON_NEGATIVE: handleButtonNegative(); break; case MSG_USER_CANCELED: handleUserCanceled(); break; case MSG_BUTTON_POSITIVE: handleButtonPositive(); break; case MSG_TRY_AGAIN_PRESSED: handleTryAgainPressed(); break; default: Log.w(TAG, "Unknown message: " + msg.what); break; } } }; private class Callback implements DialogViewCallback { @Override public void onUserCanceled() { mHandler.obtainMessage(MSG_USER_CANCELED).sendToTarget(); } @Override public void onErrorShown() { mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_HIDE_DIALOG, false /* userCanceled */), BiometricPrompt.HIDE_DIALOG_DELAY); } @Override public void onNegativePressed() { mHandler.obtainMessage(MSG_BUTTON_NEGATIVE).sendToTarget(); } @Override public void onPositivePressed() { mHandler.obtainMessage(MSG_BUTTON_POSITIVE).sendToTarget(); } @Override public void onTryAgainPressed() { mHandler.obtainMessage(MSG_TRY_AGAIN_PRESSED).sendToTarget(); } } final WakefulnessLifecycle.Observer mWakefulnessObserver = new WakefulnessLifecycle.Observer() { @Override public void onStartedGoingToSleep() { if (mDialogShowing) { if (DEBUG) Log.d(TAG, "User canceled due to screen off"); mHandler.obtainMessage(MSG_USER_CANCELED).sendToTarget(); } } }; @Override public void start() { final PackageManager pm = mContext.getPackageManager(); if (pm.hasSystemFeature(PackageManager.FEATURE_FINGERPRINT) || pm.hasSystemFeature(PackageManager.FEATURE_FACE) || pm.hasSystemFeature(PackageManager.FEATURE_IRIS)) { getComponent(CommandQueue.class).addCallback(this); mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE); mWakefulnessLifecycle = Dependency.get(WakefulnessLifecycle.class); mWakefulnessLifecycle.addObserver(mWakefulnessObserver); } } @Override public void showBiometricDialog(Bundle bundle, IBiometricServiceReceiverInternal receiver, int type, boolean requireConfirmation, int userId) { if (DEBUG) { Log.d(TAG, "showBiometricDialog, type: " + type + ", requireConfirmation: " + requireConfirmation); } // Remove these messages as they are part of the previous client mHandler.removeMessages(MSG_BIOMETRIC_ERROR); mHandler.removeMessages(MSG_BIOMETRIC_HELP); mHandler.removeMessages(MSG_BIOMETRIC_AUTHENTICATED); mHandler.removeMessages(MSG_HIDE_DIALOG); SomeArgs args = SomeArgs.obtain(); args.arg1 = bundle; args.arg2 = receiver; args.argi1 = type; args.arg3 = requireConfirmation; args.argi2 = userId; mHandler.obtainMessage(MSG_SHOW_DIALOG, args).sendToTarget(); } @Override public void onBiometricAuthenticated(boolean authenticated, String failureReason) { if (DEBUG) Log.d(TAG, "onBiometricAuthenticated: " + authenticated + " reason: " + failureReason); SomeArgs args = SomeArgs.obtain(); args.arg1 = authenticated; args.arg2 = failureReason; mHandler.obtainMessage(MSG_BIOMETRIC_AUTHENTICATED, args).sendToTarget(); } @Override public void onBiometricHelp(String message) { if (DEBUG) Log.d(TAG, "onBiometricHelp: " + message); SomeArgs args = SomeArgs.obtain(); args.arg1 = message; mHandler.obtainMessage(MSG_BIOMETRIC_HELP, args).sendToTarget(); } @Override public void onBiometricError(String error) { if (DEBUG) Log.d(TAG, "onBiometricError: " + error); mHandler.obtainMessage(MSG_BIOMETRIC_ERROR, error).sendToTarget(); } @Override public void hideBiometricDialog() { if (DEBUG) Log.d(TAG, "hideBiometricDialog"); mHandler.obtainMessage(MSG_HIDE_DIALOG, false /* userCanceled */).sendToTarget(); } private void handleShowDialog(SomeArgs args, boolean skipAnimation, Bundle savedState) { mCurrentDialogArgs = args; final int type = args.argi1; // Create a new dialog but do not replace the current one yet. BiometricDialogView newDialog; if (type == BiometricAuthenticator.TYPE_FINGERPRINT) { newDialog = new FingerprintDialogView(mContext, mCallback); } else if (type == BiometricAuthenticator.TYPE_FACE) { newDialog = new FaceDialogView(mContext, mCallback); } else { Log.e(TAG, "Unsupported type: " + type); return; } if (DEBUG) Log.d(TAG, "handleShowDialog, " + " savedState: " + savedState + " mCurrentDialog: " + mCurrentDialog + " newDialog: " + newDialog + " type: " + type); if (savedState != null) { // SavedState is only non-null if it's from onConfigurationChanged. Restore the state // even though it may be removed / re-created again newDialog.restoreState(savedState); } else if (mCurrentDialog != null && mDialogShowing) { // If somehow we're asked to show a dialog, the old one doesn't need to be animated // away. This can happen if the app cancels and re-starts auth during configuration // change. This is ugly because we also have to do things on onConfigurationChanged // here. mCurrentDialog.forceRemove(); } mReceiver = (IBiometricServiceReceiverInternal) args.arg2; newDialog.setBundle((Bundle) args.arg1); newDialog.setRequireConfirmation((boolean) args.arg3); newDialog.setUserId(args.argi2); newDialog.setSkipIntro(skipAnimation); mCurrentDialog = newDialog; mWindowManager.addView(mCurrentDialog, mCurrentDialog.getLayoutParams()); mDialogShowing = true; } private void handleBiometricAuthenticated(boolean authenticated, String failureReason) { if (DEBUG) Log.d(TAG, "handleBiometricAuthenticated: " + authenticated); if (authenticated) { mCurrentDialog.announceForAccessibility( mContext.getResources() .getText(mCurrentDialog.getAuthenticatedAccessibilityResourceId())); if (mCurrentDialog.requiresConfirmation()) { mCurrentDialog.updateState(BiometricDialogView.STATE_PENDING_CONFIRMATION); } else { mCurrentDialog.updateState(BiometricDialogView.STATE_AUTHENTICATED); mHandler.postDelayed(() -> { handleHideDialog(false /* userCanceled */); }, mCurrentDialog.getDelayAfterAuthenticatedDurationMs()); } } else { mCurrentDialog.onAuthenticationFailed(failureReason); } } private void handleBiometricHelp(String message) { if (DEBUG) Log.d(TAG, "handleBiometricHelp: " + message); mCurrentDialog.onHelpReceived(message); } private void handleBiometricError(String error) { if (DEBUG) Log.d(TAG, "handleBiometricError: " + error); if (!mDialogShowing) { if (DEBUG) Log.d(TAG, "Dialog already dismissed"); return; } mCurrentDialog.onErrorReceived(error); } private void handleHideDialog(boolean userCanceled) { if (DEBUG) Log.d(TAG, "handleHideDialog, userCanceled: " + userCanceled); if (!mDialogShowing) { // This can happen if there's a race and we get called from both // onAuthenticated and onError, etc. Log.w(TAG, "Dialog already dismissed, userCanceled: " + userCanceled); return; } if (userCanceled) { try { mReceiver.onDialogDismissed(BiometricPrompt.DISMISSED_REASON_USER_CANCEL); } catch (RemoteException e) { Log.e(TAG, "RemoteException when hiding dialog", e); } } mReceiver = null; mDialogShowing = false; mCurrentDialog.startDismiss(); } private void handleButtonNegative() { if (mReceiver == null) { Log.e(TAG, "Receiver is null"); return; } try { mReceiver.onDialogDismissed(BiometricPrompt.DISMISSED_REASON_NEGATIVE); } catch (RemoteException e) { Log.e(TAG, "Remote exception when handling negative button", e); } handleHideDialog(false /* userCanceled */); } private void handleButtonPositive() { if (mReceiver == null) { Log.e(TAG, "Receiver is null"); return; } try { mReceiver.onDialogDismissed(BiometricPrompt.DISMISSED_REASON_POSITIVE); } catch (RemoteException e) { Log.e(TAG, "Remote exception when handling positive button", e); } handleHideDialog(false /* userCanceled */); } private void handleUserCanceled() { handleHideDialog(true /* userCanceled */); } private void handleTryAgainPressed() { try { mReceiver.onTryAgainPressed(); } catch (RemoteException e) { Log.e(TAG, "RemoteException when handling try again", e); } } @Override protected void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); final boolean wasShowing = mDialogShowing; // Save the state of the current dialog (buttons showing, etc) final Bundle savedState = new Bundle(); if (mCurrentDialog != null) { mCurrentDialog.onSaveState(savedState); } if (mDialogShowing) { mCurrentDialog.forceRemove(); mDialogShowing = false; } if (wasShowing) { handleShowDialog(mCurrentDialogArgs, true /* skipAnimation */, savedState); } } }