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 static android.hardware.biometrics.BiometricAuthenticator.TYPE_FACE; 20 import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FINGERPRINT; 21 import static android.hardware.biometrics.BiometricManager.Authenticators; 22 23 import android.annotation.Nullable; 24 import android.app.ActivityManager; 25 import android.app.ActivityTaskManager; 26 import android.app.IActivityTaskManager; 27 import android.app.TaskStackListener; 28 import android.content.BroadcastReceiver; 29 import android.content.Context; 30 import android.content.Intent; 31 import android.content.IntentFilter; 32 import android.content.res.Configuration; 33 import android.hardware.biometrics.BiometricConstants; 34 import android.hardware.biometrics.BiometricPrompt; 35 import android.hardware.biometrics.IBiometricServiceReceiverInternal; 36 import android.hardware.face.FaceManager; 37 import android.hardware.fingerprint.FingerprintManager; 38 import android.os.Bundle; 39 import android.os.Handler; 40 import android.os.Looper; 41 import android.os.RemoteException; 42 import android.util.Log; 43 import android.view.WindowManager; 44 45 import com.android.internal.R; 46 import com.android.internal.annotations.VisibleForTesting; 47 import com.android.internal.os.SomeArgs; 48 import com.android.systemui.SystemUI; 49 import com.android.systemui.statusbar.CommandQueue; 50 51 import java.util.List; 52 53 import javax.inject.Inject; 54 import javax.inject.Singleton; 55 56 /** 57 * Receives messages sent from {@link com.android.server.biometrics.BiometricService} and shows the 58 * appropriate biometric UI (e.g. BiometricDialogView). 59 */ 60 @Singleton 61 public class AuthController extends SystemUI implements CommandQueue.Callbacks, 62 AuthDialogCallback { 63 64 private static final String TAG = "BiometricPrompt/AuthController"; 65 private static final boolean DEBUG = true; 66 67 private final CommandQueue mCommandQueue; 68 private final Injector mInjector; 69 70 // TODO: These should just be saved from onSaveState 71 private SomeArgs mCurrentDialogArgs; 72 @VisibleForTesting 73 AuthDialog mCurrentDialog; 74 75 private Handler mHandler = new Handler(Looper.getMainLooper()); 76 private WindowManager mWindowManager; 77 @VisibleForTesting 78 IActivityTaskManager mActivityTaskManager; 79 @VisibleForTesting 80 BiometricTaskStackListener mTaskStackListener; 81 @VisibleForTesting 82 IBiometricServiceReceiverInternal mReceiver; 83 84 public class BiometricTaskStackListener extends TaskStackListener { 85 @Override onTaskStackChanged()86 public void onTaskStackChanged() { 87 mHandler.post(mTaskStackChangedRunnable); 88 } 89 } 90 91 @VisibleForTesting 92 final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { 93 @Override 94 public void onReceive(Context context, Intent intent) { 95 if (mCurrentDialog != null 96 && Intent.ACTION_CLOSE_SYSTEM_DIALOGS.equals(intent.getAction())) { 97 Log.w(TAG, "ACTION_CLOSE_SYSTEM_DIALOGS received"); 98 mCurrentDialog.dismissWithoutCallback(true /* animate */); 99 mCurrentDialog = null; 100 101 try { 102 if (mReceiver != null) { 103 mReceiver.onDialogDismissed(BiometricPrompt.DISMISSED_REASON_USER_CANCEL, 104 null /* credentialAttestation */); 105 mReceiver = null; 106 } 107 } catch (RemoteException e) { 108 Log.e(TAG, "Remote exception", e); 109 } 110 } 111 } 112 }; 113 114 private final Runnable mTaskStackChangedRunnable = () -> { 115 if (mCurrentDialog != null) { 116 try { 117 final String clientPackage = mCurrentDialog.getOpPackageName(); 118 Log.w(TAG, "Task stack changed, current client: " + clientPackage); 119 final List<ActivityManager.RunningTaskInfo> runningTasks = 120 mActivityTaskManager.getTasks(1); 121 if (!runningTasks.isEmpty()) { 122 final String topPackage = runningTasks.get(0).topActivity.getPackageName(); 123 if (!topPackage.contentEquals(clientPackage)) { 124 Log.w(TAG, "Evicting client due to: " + topPackage); 125 mCurrentDialog.dismissWithoutCallback(true /* animate */); 126 mCurrentDialog = null; 127 if (mReceiver != null) { 128 mReceiver.onDialogDismissed( 129 BiometricPrompt.DISMISSED_REASON_USER_CANCEL, 130 null /* credentialAttestation */); 131 mReceiver = null; 132 } 133 } 134 } 135 } catch (RemoteException e) { 136 Log.e(TAG, "Remote exception", e); 137 } 138 } 139 }; 140 141 @Override onTryAgainPressed()142 public void onTryAgainPressed() { 143 if (mReceiver == null) { 144 Log.e(TAG, "onTryAgainPressed: Receiver is null"); 145 return; 146 } 147 try { 148 mReceiver.onTryAgainPressed(); 149 } catch (RemoteException e) { 150 Log.e(TAG, "RemoteException when handling try again", e); 151 } 152 } 153 154 @Override onDeviceCredentialPressed()155 public void onDeviceCredentialPressed() { 156 if (mReceiver == null) { 157 Log.e(TAG, "onDeviceCredentialPressed: Receiver is null"); 158 return; 159 } 160 try { 161 mReceiver.onDeviceCredentialPressed(); 162 } catch (RemoteException e) { 163 Log.e(TAG, "RemoteException when handling credential button", e); 164 } 165 } 166 167 @Override onSystemEvent(int event)168 public void onSystemEvent(int event) { 169 if (mReceiver == null) { 170 Log.e(TAG, "onSystemEvent(" + event + "): Receiver is null"); 171 return; 172 } 173 try { 174 mReceiver.onSystemEvent(event); 175 } catch (RemoteException e) { 176 Log.e(TAG, "RemoteException when sending system event", e); 177 } 178 } 179 180 @Override onDismissed(@ismissedReason int reason, @Nullable byte[] credentialAttestation)181 public void onDismissed(@DismissedReason int reason, @Nullable byte[] credentialAttestation) { 182 switch (reason) { 183 case AuthDialogCallback.DISMISSED_USER_CANCELED: 184 sendResultAndCleanUp(BiometricPrompt.DISMISSED_REASON_USER_CANCEL, 185 credentialAttestation); 186 break; 187 188 case AuthDialogCallback.DISMISSED_BUTTON_NEGATIVE: 189 sendResultAndCleanUp(BiometricPrompt.DISMISSED_REASON_NEGATIVE, 190 credentialAttestation); 191 break; 192 193 case AuthDialogCallback.DISMISSED_BUTTON_POSITIVE: 194 sendResultAndCleanUp(BiometricPrompt.DISMISSED_REASON_BIOMETRIC_CONFIRMED, 195 credentialAttestation); 196 break; 197 198 case AuthDialogCallback.DISMISSED_BIOMETRIC_AUTHENTICATED: 199 sendResultAndCleanUp( 200 BiometricPrompt.DISMISSED_REASON_BIOMETRIC_CONFIRM_NOT_REQUIRED, 201 credentialAttestation); 202 break; 203 204 case AuthDialogCallback.DISMISSED_ERROR: 205 sendResultAndCleanUp(BiometricPrompt.DISMISSED_REASON_ERROR, 206 credentialAttestation); 207 break; 208 209 case AuthDialogCallback.DISMISSED_BY_SYSTEM_SERVER: 210 sendResultAndCleanUp(BiometricPrompt.DISMISSED_REASON_SERVER_REQUESTED, 211 credentialAttestation); 212 break; 213 214 case AuthDialogCallback.DISMISSED_CREDENTIAL_AUTHENTICATED: 215 sendResultAndCleanUp(BiometricPrompt.DISMISSED_REASON_CREDENTIAL_CONFIRMED, 216 credentialAttestation); 217 break; 218 219 default: 220 Log.e(TAG, "Unhandled reason: " + reason); 221 break; 222 } 223 } 224 sendResultAndCleanUp(@ismissedReason int reason, @Nullable byte[] credentialAttestation)225 private void sendResultAndCleanUp(@DismissedReason int reason, 226 @Nullable byte[] credentialAttestation) { 227 if (mReceiver == null) { 228 Log.e(TAG, "sendResultAndCleanUp: Receiver is null"); 229 return; 230 } 231 try { 232 mReceiver.onDialogDismissed(reason, credentialAttestation); 233 } catch (RemoteException e) { 234 Log.w(TAG, "Remote exception", e); 235 } 236 onDialogDismissed(reason); 237 } 238 239 public static class Injector { getActivityTaskManager()240 IActivityTaskManager getActivityTaskManager() { 241 return ActivityTaskManager.getService(); 242 } 243 } 244 245 @Inject AuthController(Context context, CommandQueue commandQueue)246 public AuthController(Context context, CommandQueue commandQueue) { 247 this(context, commandQueue, new Injector()); 248 } 249 250 @VisibleForTesting AuthController(Context context, CommandQueue commandQueue, Injector injector)251 AuthController(Context context, CommandQueue commandQueue, Injector injector) { 252 super(context); 253 mCommandQueue = commandQueue; 254 mInjector = injector; 255 256 IntentFilter filter = new IntentFilter(); 257 filter.addAction(Intent.ACTION_CLOSE_SYSTEM_DIALOGS); 258 259 context.registerReceiver(mBroadcastReceiver, filter); 260 } 261 262 @Override start()263 public void start() { 264 mCommandQueue.addCallback(this); 265 mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE); 266 mActivityTaskManager = mInjector.getActivityTaskManager(); 267 268 try { 269 mTaskStackListener = new BiometricTaskStackListener(); 270 mActivityTaskManager.registerTaskStackListener(mTaskStackListener); 271 } catch (RemoteException e) { 272 Log.w(TAG, "Unable to register task stack listener", e); 273 } 274 } 275 276 @Override showAuthenticationDialog(Bundle bundle, IBiometricServiceReceiverInternal receiver, int biometricModality, boolean requireConfirmation, int userId, String opPackageName, long operationId, int sysUiSessionId)277 public void showAuthenticationDialog(Bundle bundle, IBiometricServiceReceiverInternal receiver, 278 int biometricModality, boolean requireConfirmation, int userId, String opPackageName, 279 long operationId, int sysUiSessionId) { 280 final int authenticators = Utils.getAuthenticators(bundle); 281 282 if (DEBUG) { 283 Log.d(TAG, "showAuthenticationDialog, authenticators: " + authenticators 284 + ", biometricModality: " + biometricModality 285 + ", requireConfirmation: " + requireConfirmation 286 + ", operationId: " + operationId 287 + ", sysUiSessionId: " + sysUiSessionId); 288 } 289 SomeArgs args = SomeArgs.obtain(); 290 args.arg1 = bundle; 291 args.arg2 = receiver; 292 args.argi1 = biometricModality; 293 args.arg3 = requireConfirmation; 294 args.argi2 = userId; 295 args.arg4 = opPackageName; 296 args.arg5 = operationId; 297 args.argi3 = sysUiSessionId; 298 299 boolean skipAnimation = false; 300 if (mCurrentDialog != null) { 301 Log.w(TAG, "mCurrentDialog: " + mCurrentDialog); 302 skipAnimation = true; 303 } 304 305 showDialog(args, skipAnimation, null /* savedState */); 306 } 307 308 @Override onBiometricAuthenticated()309 public void onBiometricAuthenticated() { 310 mCurrentDialog.onAuthenticationSucceeded(); 311 } 312 313 @Override onBiometricHelp(String message)314 public void onBiometricHelp(String message) { 315 if (DEBUG) Log.d(TAG, "onBiometricHelp: " + message); 316 317 mCurrentDialog.onHelp(message); 318 } 319 getErrorString(int modality, int error, int vendorCode)320 private String getErrorString(int modality, int error, int vendorCode) { 321 switch (modality) { 322 case TYPE_FACE: 323 return FaceManager.getErrorString(mContext, error, vendorCode); 324 325 case TYPE_FINGERPRINT: 326 return FingerprintManager.getErrorString(mContext, error, vendorCode); 327 328 default: 329 return ""; 330 } 331 } 332 333 @Override onBiometricError(int modality, int error, int vendorCode)334 public void onBiometricError(int modality, int error, int vendorCode) { 335 if (DEBUG) { 336 Log.d(TAG, String.format("onBiometricError(%d, %d, %d)", modality, error, vendorCode)); 337 } 338 339 final boolean isLockout = (error == BiometricConstants.BIOMETRIC_ERROR_LOCKOUT) 340 || (error == BiometricConstants.BIOMETRIC_ERROR_LOCKOUT_PERMANENT); 341 342 // TODO(b/141025588): Create separate methods for handling hard and soft errors. 343 final boolean isSoftError = (error == BiometricConstants.BIOMETRIC_PAUSED_REJECTED 344 || error == BiometricConstants.BIOMETRIC_ERROR_TIMEOUT); 345 346 if (mCurrentDialog.isAllowDeviceCredentials() && isLockout) { 347 if (DEBUG) Log.d(TAG, "onBiometricError, lockout"); 348 mCurrentDialog.animateToCredentialUI(); 349 } else if (isSoftError) { 350 final String errorMessage = (error == BiometricConstants.BIOMETRIC_PAUSED_REJECTED) 351 ? mContext.getString(R.string.biometric_not_recognized) 352 : getErrorString(modality, error, vendorCode); 353 if (DEBUG) Log.d(TAG, "onBiometricError, soft error: " + errorMessage); 354 mCurrentDialog.onAuthenticationFailed(errorMessage); 355 } else { 356 final String errorMessage = getErrorString(modality, error, vendorCode); 357 if (DEBUG) Log.d(TAG, "onBiometricError, hard error: " + errorMessage); 358 mCurrentDialog.onError(errorMessage); 359 } 360 } 361 362 @Override hideAuthenticationDialog()363 public void hideAuthenticationDialog() { 364 if (DEBUG) Log.d(TAG, "hideAuthenticationDialog: " + mCurrentDialog); 365 366 if (mCurrentDialog == null) { 367 // Could be possible if the caller canceled authentication after credential success 368 // but before the client was notified. 369 return; 370 } 371 372 mCurrentDialog.dismissFromSystemServer(); 373 374 // BiometricService will have already sent the callback to the client in this case. 375 // This avoids a round trip to SystemUI. So, just dismiss the dialog and we're done. 376 mCurrentDialog = null; 377 } 378 showDialog(SomeArgs args, boolean skipAnimation, Bundle savedState)379 private void showDialog(SomeArgs args, boolean skipAnimation, Bundle savedState) { 380 mCurrentDialogArgs = args; 381 final int type = args.argi1; 382 final Bundle biometricPromptBundle = (Bundle) args.arg1; 383 final boolean requireConfirmation = (boolean) args.arg3; 384 final int userId = args.argi2; 385 final String opPackageName = (String) args.arg4; 386 final long operationId = (long) args.arg5; 387 final int sysUiSessionId = args.argi3; 388 389 // Create a new dialog but do not replace the current one yet. 390 final AuthDialog newDialog = buildDialog( 391 biometricPromptBundle, 392 requireConfirmation, 393 userId, 394 type, 395 opPackageName, 396 skipAnimation, 397 operationId, 398 sysUiSessionId); 399 400 if (newDialog == null) { 401 Log.e(TAG, "Unsupported type: " + type); 402 return; 403 } 404 405 if (DEBUG) { 406 Log.d(TAG, "userId: " + userId 407 + " savedState: " + savedState 408 + " mCurrentDialog: " + mCurrentDialog 409 + " newDialog: " + newDialog 410 + " type: " + type 411 + " sysUiSessionId: " + sysUiSessionId); 412 } 413 414 if (mCurrentDialog != null) { 415 // If somehow we're asked to show a dialog, the old one doesn't need to be animated 416 // away. This can happen if the app cancels and re-starts auth during configuration 417 // change. This is ugly because we also have to do things on onConfigurationChanged 418 // here. 419 mCurrentDialog.dismissWithoutCallback(false /* animate */); 420 } 421 422 mReceiver = (IBiometricServiceReceiverInternal) args.arg2; 423 mCurrentDialog = newDialog; 424 mCurrentDialog.show(mWindowManager, savedState); 425 } 426 onDialogDismissed(@ismissedReason int reason)427 private void onDialogDismissed(@DismissedReason int reason) { 428 if (DEBUG) Log.d(TAG, "onDialogDismissed: " + reason); 429 if (mCurrentDialog == null) { 430 Log.w(TAG, "Dialog already dismissed"); 431 } 432 mReceiver = null; 433 mCurrentDialog = null; 434 } 435 436 @Override onConfigurationChanged(Configuration newConfig)437 protected void onConfigurationChanged(Configuration newConfig) { 438 super.onConfigurationChanged(newConfig); 439 440 // Save the state of the current dialog (buttons showing, etc) 441 if (mCurrentDialog != null) { 442 final Bundle savedState = new Bundle(); 443 mCurrentDialog.onSaveState(savedState); 444 mCurrentDialog.dismissWithoutCallback(false /* animate */); 445 mCurrentDialog = null; 446 447 // Only show the dialog if necessary. If it was animating out, the dialog is supposed 448 // to send its pending callback immediately. 449 if (savedState.getInt(AuthDialog.KEY_CONTAINER_STATE) 450 != AuthContainerView.STATE_ANIMATING_OUT) { 451 final boolean credentialShowing = 452 savedState.getBoolean(AuthDialog.KEY_CREDENTIAL_SHOWING); 453 if (credentialShowing) { 454 // TODO: Clean this up 455 Bundle bundle = (Bundle) mCurrentDialogArgs.arg1; 456 bundle.putInt(BiometricPrompt.KEY_AUTHENTICATORS_ALLOWED, 457 Authenticators.DEVICE_CREDENTIAL); 458 } 459 460 showDialog(mCurrentDialogArgs, true /* skipAnimation */, savedState); 461 } 462 } 463 } 464 buildDialog(Bundle biometricPromptBundle, boolean requireConfirmation, int userId, int type, String opPackageName, boolean skipIntro, long operationId, int sysUiSessionId)465 protected AuthDialog buildDialog(Bundle biometricPromptBundle, boolean requireConfirmation, 466 int userId, int type, String opPackageName, boolean skipIntro, long operationId, 467 int sysUiSessionId) { 468 return new AuthContainerView.Builder(mContext) 469 .setCallback(this) 470 .setBiometricPromptBundle(biometricPromptBundle) 471 .setRequireConfirmation(requireConfirmation) 472 .setUserId(userId) 473 .setOpPackageName(opPackageName) 474 .setSkipIntro(skipIntro) 475 .setOperationId(operationId) 476 .setSysUiSessionId(sysUiSessionId) 477 .build(type); 478 } 479 } 480