1 /* 2 * Copyright (C) 2018 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License 15 */ 16 17 package com.android.systemui.biometrics; 18 19 import android.content.Context; 20 import android.content.pm.PackageManager; 21 import android.content.res.Configuration; 22 import android.hardware.biometrics.BiometricAuthenticator; 23 import android.hardware.biometrics.BiometricPrompt; 24 import android.hardware.biometrics.IBiometricServiceReceiverInternal; 25 import android.os.Bundle; 26 import android.os.Handler; 27 import android.os.Looper; 28 import android.os.Message; 29 import android.os.RemoteException; 30 import android.util.Log; 31 import android.view.WindowManager; 32 33 import com.android.internal.os.SomeArgs; 34 import com.android.systemui.Dependency; 35 import com.android.systemui.SystemUI; 36 import com.android.systemui.keyguard.WakefulnessLifecycle; 37 import com.android.systemui.statusbar.CommandQueue; 38 39 /** 40 * Receives messages sent from AuthenticationClient and shows the appropriate biometric UI (e.g. 41 * BiometricDialogView). 42 */ 43 public class BiometricDialogImpl extends SystemUI implements CommandQueue.Callbacks { 44 private static final String TAG = "BiometricDialogImpl"; 45 private static final boolean DEBUG = true; 46 47 private static final int MSG_SHOW_DIALOG = 1; 48 private static final int MSG_BIOMETRIC_AUTHENTICATED = 2; 49 private static final int MSG_BIOMETRIC_HELP = 3; 50 private static final int MSG_BIOMETRIC_ERROR = 4; 51 private static final int MSG_HIDE_DIALOG = 5; 52 private static final int MSG_BUTTON_NEGATIVE = 6; 53 private static final int MSG_USER_CANCELED = 7; 54 private static final int MSG_BUTTON_POSITIVE = 8; 55 private static final int MSG_TRY_AGAIN_PRESSED = 9; 56 57 private SomeArgs mCurrentDialogArgs; 58 private BiometricDialogView mCurrentDialog; 59 private WindowManager mWindowManager; 60 private IBiometricServiceReceiverInternal mReceiver; 61 private boolean mDialogShowing; 62 private Callback mCallback = new Callback(); 63 private WakefulnessLifecycle mWakefulnessLifecycle; 64 65 private Handler mHandler = new Handler(Looper.getMainLooper()) { 66 @Override 67 public void handleMessage(Message msg) { 68 switch(msg.what) { 69 case MSG_SHOW_DIALOG: 70 handleShowDialog((SomeArgs) msg.obj, false /* skipAnimation */, 71 null /* savedState */); 72 break; 73 case MSG_BIOMETRIC_AUTHENTICATED: { 74 SomeArgs args = (SomeArgs) msg.obj; 75 handleBiometricAuthenticated((boolean) args.arg1 /* authenticated */, 76 (String) args.arg2 /* failureReason */); 77 args.recycle(); 78 break; 79 } 80 case MSG_BIOMETRIC_HELP: { 81 SomeArgs args = (SomeArgs) msg.obj; 82 handleBiometricHelp((String) args.arg1 /* message */); 83 args.recycle(); 84 break; 85 } 86 case MSG_BIOMETRIC_ERROR: 87 handleBiometricError((String) msg.obj); 88 break; 89 case MSG_HIDE_DIALOG: 90 handleHideDialog((Boolean) msg.obj); 91 break; 92 case MSG_BUTTON_NEGATIVE: 93 handleButtonNegative(); 94 break; 95 case MSG_USER_CANCELED: 96 handleUserCanceled(); 97 break; 98 case MSG_BUTTON_POSITIVE: 99 handleButtonPositive(); 100 break; 101 case MSG_TRY_AGAIN_PRESSED: 102 handleTryAgainPressed(); 103 break; 104 default: 105 Log.w(TAG, "Unknown message: " + msg.what); 106 break; 107 } 108 } 109 }; 110 111 private class Callback implements DialogViewCallback { 112 @Override onUserCanceled()113 public void onUserCanceled() { 114 mHandler.obtainMessage(MSG_USER_CANCELED).sendToTarget(); 115 } 116 117 @Override onErrorShown()118 public void onErrorShown() { 119 mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_HIDE_DIALOG, 120 false /* userCanceled */), BiometricPrompt.HIDE_DIALOG_DELAY); 121 } 122 123 @Override onNegativePressed()124 public void onNegativePressed() { 125 mHandler.obtainMessage(MSG_BUTTON_NEGATIVE).sendToTarget(); 126 } 127 128 @Override onPositivePressed()129 public void onPositivePressed() { 130 mHandler.obtainMessage(MSG_BUTTON_POSITIVE).sendToTarget(); 131 } 132 133 @Override onTryAgainPressed()134 public void onTryAgainPressed() { 135 mHandler.obtainMessage(MSG_TRY_AGAIN_PRESSED).sendToTarget(); 136 } 137 } 138 139 final WakefulnessLifecycle.Observer mWakefulnessObserver = new WakefulnessLifecycle.Observer() { 140 @Override 141 public void onStartedGoingToSleep() { 142 if (mDialogShowing) { 143 if (DEBUG) Log.d(TAG, "User canceled due to screen off"); 144 mHandler.obtainMessage(MSG_USER_CANCELED).sendToTarget(); 145 } 146 } 147 }; 148 149 @Override start()150 public void start() { 151 final PackageManager pm = mContext.getPackageManager(); 152 if (pm.hasSystemFeature(PackageManager.FEATURE_FINGERPRINT) 153 || pm.hasSystemFeature(PackageManager.FEATURE_FACE) 154 || pm.hasSystemFeature(PackageManager.FEATURE_IRIS)) { 155 getComponent(CommandQueue.class).addCallback(this); 156 mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE); 157 mWakefulnessLifecycle = Dependency.get(WakefulnessLifecycle.class); 158 mWakefulnessLifecycle.addObserver(mWakefulnessObserver); 159 } 160 } 161 162 @Override showBiometricDialog(Bundle bundle, IBiometricServiceReceiverInternal receiver, int type, boolean requireConfirmation, int userId)163 public void showBiometricDialog(Bundle bundle, IBiometricServiceReceiverInternal receiver, 164 int type, boolean requireConfirmation, int userId) { 165 if (DEBUG) { 166 Log.d(TAG, "showBiometricDialog, type: " + type 167 + ", requireConfirmation: " + requireConfirmation); 168 } 169 // Remove these messages as they are part of the previous client 170 mHandler.removeMessages(MSG_BIOMETRIC_ERROR); 171 mHandler.removeMessages(MSG_BIOMETRIC_HELP); 172 mHandler.removeMessages(MSG_BIOMETRIC_AUTHENTICATED); 173 mHandler.removeMessages(MSG_HIDE_DIALOG); 174 SomeArgs args = SomeArgs.obtain(); 175 args.arg1 = bundle; 176 args.arg2 = receiver; 177 args.argi1 = type; 178 args.arg3 = requireConfirmation; 179 args.argi2 = userId; 180 mHandler.obtainMessage(MSG_SHOW_DIALOG, args).sendToTarget(); 181 } 182 183 @Override onBiometricAuthenticated(boolean authenticated, String failureReason)184 public void onBiometricAuthenticated(boolean authenticated, String failureReason) { 185 if (DEBUG) Log.d(TAG, "onBiometricAuthenticated: " + authenticated 186 + " reason: " + failureReason); 187 188 SomeArgs args = SomeArgs.obtain(); 189 args.arg1 = authenticated; 190 args.arg2 = failureReason; 191 mHandler.obtainMessage(MSG_BIOMETRIC_AUTHENTICATED, args).sendToTarget(); 192 } 193 194 @Override onBiometricHelp(String message)195 public void onBiometricHelp(String message) { 196 if (DEBUG) Log.d(TAG, "onBiometricHelp: " + message); 197 SomeArgs args = SomeArgs.obtain(); 198 args.arg1 = message; 199 mHandler.obtainMessage(MSG_BIOMETRIC_HELP, args).sendToTarget(); 200 } 201 202 @Override onBiometricError(String error)203 public void onBiometricError(String error) { 204 if (DEBUG) Log.d(TAG, "onBiometricError: " + error); 205 mHandler.obtainMessage(MSG_BIOMETRIC_ERROR, error).sendToTarget(); 206 } 207 208 @Override hideBiometricDialog()209 public void hideBiometricDialog() { 210 if (DEBUG) Log.d(TAG, "hideBiometricDialog"); 211 mHandler.obtainMessage(MSG_HIDE_DIALOG, false /* userCanceled */).sendToTarget(); 212 } 213 handleShowDialog(SomeArgs args, boolean skipAnimation, Bundle savedState)214 private void handleShowDialog(SomeArgs args, boolean skipAnimation, Bundle savedState) { 215 mCurrentDialogArgs = args; 216 final int type = args.argi1; 217 218 // Create a new dialog but do not replace the current one yet. 219 BiometricDialogView newDialog; 220 if (type == BiometricAuthenticator.TYPE_FINGERPRINT) { 221 newDialog = new FingerprintDialogView(mContext, mCallback); 222 } else if (type == BiometricAuthenticator.TYPE_FACE) { 223 newDialog = new FaceDialogView(mContext, mCallback); 224 } else { 225 Log.e(TAG, "Unsupported type: " + type); 226 return; 227 } 228 229 if (DEBUG) Log.d(TAG, "handleShowDialog, " 230 + " savedState: " + savedState 231 + " mCurrentDialog: " + mCurrentDialog 232 + " newDialog: " + newDialog 233 + " type: " + type); 234 235 if (savedState != null) { 236 // SavedState is only non-null if it's from onConfigurationChanged. Restore the state 237 // even though it may be removed / re-created again 238 newDialog.restoreState(savedState); 239 } else if (mCurrentDialog != null && mDialogShowing) { 240 // If somehow we're asked to show a dialog, the old one doesn't need to be animated 241 // away. This can happen if the app cancels and re-starts auth during configuration 242 // change. This is ugly because we also have to do things on onConfigurationChanged 243 // here. 244 mCurrentDialog.forceRemove(); 245 } 246 247 mReceiver = (IBiometricServiceReceiverInternal) args.arg2; 248 newDialog.setBundle((Bundle) args.arg1); 249 newDialog.setRequireConfirmation((boolean) args.arg3); 250 newDialog.setUserId(args.argi2); 251 newDialog.setSkipIntro(skipAnimation); 252 mCurrentDialog = newDialog; 253 mWindowManager.addView(mCurrentDialog, mCurrentDialog.getLayoutParams()); 254 mDialogShowing = true; 255 } 256 handleBiometricAuthenticated(boolean authenticated, String failureReason)257 private void handleBiometricAuthenticated(boolean authenticated, String failureReason) { 258 if (DEBUG) Log.d(TAG, "handleBiometricAuthenticated: " + authenticated); 259 260 if (authenticated) { 261 mCurrentDialog.announceForAccessibility( 262 mContext.getResources() 263 .getText(mCurrentDialog.getAuthenticatedAccessibilityResourceId())); 264 if (mCurrentDialog.requiresConfirmation()) { 265 mCurrentDialog.updateState(BiometricDialogView.STATE_PENDING_CONFIRMATION); 266 } else { 267 mCurrentDialog.updateState(BiometricDialogView.STATE_AUTHENTICATED); 268 mHandler.postDelayed(() -> { 269 handleHideDialog(false /* userCanceled */); 270 }, mCurrentDialog.getDelayAfterAuthenticatedDurationMs()); 271 } 272 } else { 273 mCurrentDialog.onAuthenticationFailed(failureReason); 274 } 275 } 276 handleBiometricHelp(String message)277 private void handleBiometricHelp(String message) { 278 if (DEBUG) Log.d(TAG, "handleBiometricHelp: " + message); 279 mCurrentDialog.onHelpReceived(message); 280 } 281 handleBiometricError(String error)282 private void handleBiometricError(String error) { 283 if (DEBUG) Log.d(TAG, "handleBiometricError: " + error); 284 if (!mDialogShowing) { 285 if (DEBUG) Log.d(TAG, "Dialog already dismissed"); 286 return; 287 } 288 mCurrentDialog.onErrorReceived(error); 289 } 290 handleHideDialog(boolean userCanceled)291 private void handleHideDialog(boolean userCanceled) { 292 if (DEBUG) Log.d(TAG, "handleHideDialog, userCanceled: " + userCanceled); 293 if (!mDialogShowing) { 294 // This can happen if there's a race and we get called from both 295 // onAuthenticated and onError, etc. 296 Log.w(TAG, "Dialog already dismissed, userCanceled: " + userCanceled); 297 return; 298 } 299 if (userCanceled) { 300 try { 301 mReceiver.onDialogDismissed(BiometricPrompt.DISMISSED_REASON_USER_CANCEL); 302 } catch (RemoteException e) { 303 Log.e(TAG, "RemoteException when hiding dialog", e); 304 } 305 } 306 mReceiver = null; 307 mDialogShowing = false; 308 mCurrentDialog.startDismiss(); 309 } 310 handleButtonNegative()311 private void handleButtonNegative() { 312 if (mReceiver == null) { 313 Log.e(TAG, "Receiver is null"); 314 return; 315 } 316 try { 317 mReceiver.onDialogDismissed(BiometricPrompt.DISMISSED_REASON_NEGATIVE); 318 } catch (RemoteException e) { 319 Log.e(TAG, "Remote exception when handling negative button", e); 320 } 321 handleHideDialog(false /* userCanceled */); 322 } 323 handleButtonPositive()324 private void handleButtonPositive() { 325 if (mReceiver == null) { 326 Log.e(TAG, "Receiver is null"); 327 return; 328 } 329 try { 330 mReceiver.onDialogDismissed(BiometricPrompt.DISMISSED_REASON_POSITIVE); 331 } catch (RemoteException e) { 332 Log.e(TAG, "Remote exception when handling positive button", e); 333 } 334 handleHideDialog(false /* userCanceled */); 335 } 336 handleUserCanceled()337 private void handleUserCanceled() { 338 handleHideDialog(true /* userCanceled */); 339 } 340 handleTryAgainPressed()341 private void handleTryAgainPressed() { 342 try { 343 mReceiver.onTryAgainPressed(); 344 } catch (RemoteException e) { 345 Log.e(TAG, "RemoteException when handling try again", e); 346 } 347 } 348 349 @Override onConfigurationChanged(Configuration newConfig)350 protected void onConfigurationChanged(Configuration newConfig) { 351 super.onConfigurationChanged(newConfig); 352 final boolean wasShowing = mDialogShowing; 353 354 // Save the state of the current dialog (buttons showing, etc) 355 final Bundle savedState = new Bundle(); 356 if (mCurrentDialog != null) { 357 mCurrentDialog.onSaveState(savedState); 358 } 359 360 if (mDialogShowing) { 361 mCurrentDialog.forceRemove(); 362 mDialogShowing = false; 363 } 364 365 if (wasShowing) { 366 handleShowDialog(mCurrentDialogArgs, true /* skipAnimation */, savedState); 367 } 368 } 369 } 370