1 /* 2 * Copyright (C) 2019 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.annotation.IntDef; 20 import android.annotation.NonNull; 21 import android.annotation.Nullable; 22 import android.app.AlertDialog; 23 import android.app.admin.DevicePolicyManager; 24 import android.content.Context; 25 import android.content.pm.UserInfo; 26 import android.graphics.drawable.Drawable; 27 import android.hardware.biometrics.BiometricPrompt; 28 import android.hardware.biometrics.PromptInfo; 29 import android.os.AsyncTask; 30 import android.os.CountDownTimer; 31 import android.os.Handler; 32 import android.os.Looper; 33 import android.os.SystemClock; 34 import android.os.UserManager; 35 import android.text.TextUtils; 36 import android.util.AttributeSet; 37 import android.view.View; 38 import android.view.WindowManager; 39 import android.view.accessibility.AccessibilityManager; 40 import android.widget.ImageView; 41 import android.widget.LinearLayout; 42 import android.widget.TextView; 43 44 import androidx.annotation.StringRes; 45 46 import com.android.internal.widget.LockPatternUtils; 47 import com.android.internal.widget.VerifyCredentialResponse; 48 import com.android.systemui.R; 49 import com.android.systemui.animation.Interpolators; 50 51 import java.lang.annotation.Retention; 52 import java.lang.annotation.RetentionPolicy; 53 54 /** 55 * Abstract base class for Pin, Pattern, or Password authentication, for 56 * {@link BiometricPrompt.Builder#setAllowedAuthenticators(int)}} 57 */ 58 public abstract class AuthCredentialView extends LinearLayout { 59 private static final String TAG = "BiometricPrompt/AuthCredentialView"; 60 private static final int ERROR_DURATION_MS = 3000; 61 62 static final int USER_TYPE_PRIMARY = 1; 63 static final int USER_TYPE_MANAGED_PROFILE = 2; 64 static final int USER_TYPE_SECONDARY = 3; 65 @Retention(RetentionPolicy.SOURCE) 66 @IntDef({USER_TYPE_PRIMARY, USER_TYPE_MANAGED_PROFILE, USER_TYPE_SECONDARY}) 67 private @interface UserType {} 68 69 protected final Handler mHandler; 70 protected final LockPatternUtils mLockPatternUtils; 71 72 private final AccessibilityManager mAccessibilityManager; 73 private final UserManager mUserManager; 74 private final DevicePolicyManager mDevicePolicyManager; 75 76 private PromptInfo mPromptInfo; 77 private AuthPanelController mPanelController; 78 private boolean mShouldAnimatePanel; 79 private boolean mShouldAnimateContents; 80 81 private TextView mTitleView; 82 private TextView mSubtitleView; 83 private TextView mDescriptionView; 84 private ImageView mIconView; 85 protected TextView mErrorView; 86 87 protected @Utils.CredentialType int mCredentialType; 88 protected AuthContainerView mContainerView; 89 protected Callback mCallback; 90 protected AsyncTask<?, ?, ?> mPendingLockCheck; 91 protected int mUserId; 92 protected long mOperationId; 93 protected int mEffectiveUserId; 94 protected ErrorTimer mErrorTimer; 95 96 interface Callback { onCredentialMatched(byte[] attestation)97 void onCredentialMatched(byte[] attestation); 98 } 99 100 protected static class ErrorTimer extends CountDownTimer { 101 private final TextView mErrorView; 102 private final Context mContext; 103 104 /** 105 * @param millisInFuture The number of millis in the future from the call 106 * to {@link #start()} until the countdown is done and {@link 107 * #onFinish()} 108 * is called. 109 * @param countDownInterval The interval along the way to receive 110 * {@link #onTick(long)} callbacks. 111 */ ErrorTimer(Context context, long millisInFuture, long countDownInterval, TextView errorView)112 public ErrorTimer(Context context, long millisInFuture, long countDownInterval, 113 TextView errorView) { 114 super(millisInFuture, countDownInterval); 115 mErrorView = errorView; 116 mContext = context; 117 } 118 119 @Override onTick(long millisUntilFinished)120 public void onTick(long millisUntilFinished) { 121 final int secondsCountdown = (int) (millisUntilFinished / 1000); 122 mErrorView.setText(mContext.getString( 123 R.string.biometric_dialog_credential_too_many_attempts, secondsCountdown)); 124 } 125 126 @Override onFinish()127 public void onFinish() { 128 if (mErrorView != null) { 129 mErrorView.setText(""); 130 } 131 } 132 } 133 134 protected final Runnable mClearErrorRunnable = new Runnable() { 135 @Override 136 public void run() { 137 if (mErrorView != null) { 138 mErrorView.setText(""); 139 } 140 } 141 }; 142 AuthCredentialView(Context context, AttributeSet attrs)143 public AuthCredentialView(Context context, AttributeSet attrs) { 144 super(context, attrs); 145 146 mLockPatternUtils = new LockPatternUtils(mContext); 147 mHandler = new Handler(Looper.getMainLooper()); 148 mAccessibilityManager = mContext.getSystemService(AccessibilityManager.class); 149 mUserManager = mContext.getSystemService(UserManager.class); 150 mDevicePolicyManager = mContext.getSystemService(DevicePolicyManager.class); 151 } 152 showError(String error)153 protected void showError(String error) { 154 if (mHandler != null) { 155 mHandler.removeCallbacks(mClearErrorRunnable); 156 mHandler.postDelayed(mClearErrorRunnable, ERROR_DURATION_MS); 157 } 158 if (mErrorView != null) { 159 mErrorView.setText(error); 160 } 161 } 162 setTextOrHide(TextView view, CharSequence text)163 private void setTextOrHide(TextView view, CharSequence text) { 164 if (TextUtils.isEmpty(text)) { 165 view.setVisibility(View.GONE); 166 } else { 167 view.setText(text); 168 } 169 170 Utils.notifyAccessibilityContentChanged(mAccessibilityManager, this); 171 } 172 setText(TextView view, CharSequence text)173 private void setText(TextView view, CharSequence text) { 174 view.setText(text); 175 } 176 setUserId(int userId)177 void setUserId(int userId) { 178 mUserId = userId; 179 } 180 setOperationId(long operationId)181 void setOperationId(long operationId) { 182 mOperationId = operationId; 183 } 184 setEffectiveUserId(int effectiveUserId)185 void setEffectiveUserId(int effectiveUserId) { 186 mEffectiveUserId = effectiveUserId; 187 } 188 setCredentialType(@tils.CredentialType int credentialType)189 void setCredentialType(@Utils.CredentialType int credentialType) { 190 mCredentialType = credentialType; 191 } 192 setCallback(Callback callback)193 void setCallback(Callback callback) { 194 mCallback = callback; 195 } 196 setPromptInfo(PromptInfo promptInfo)197 void setPromptInfo(PromptInfo promptInfo) { 198 mPromptInfo = promptInfo; 199 } 200 setPanelController(AuthPanelController panelController, boolean animatePanel)201 void setPanelController(AuthPanelController panelController, boolean animatePanel) { 202 mPanelController = panelController; 203 mShouldAnimatePanel = animatePanel; 204 } 205 setShouldAnimateContents(boolean animateContents)206 void setShouldAnimateContents(boolean animateContents) { 207 mShouldAnimateContents = animateContents; 208 } 209 setContainerView(AuthContainerView containerView)210 void setContainerView(AuthContainerView containerView) { 211 mContainerView = containerView; 212 } 213 214 @Override onAttachedToWindow()215 protected void onAttachedToWindow() { 216 super.onAttachedToWindow(); 217 218 final CharSequence title = getTitle(mPromptInfo); 219 setText(mTitleView, title); 220 setTextOrHide(mSubtitleView, getSubtitle(mPromptInfo)); 221 setTextOrHide(mDescriptionView, getDescription(mPromptInfo)); 222 announceForAccessibility(title); 223 224 if (mIconView != null) { 225 final boolean isManagedProfile = Utils.isManagedProfile(mContext, mEffectiveUserId); 226 final Drawable image; 227 if (isManagedProfile) { 228 image = getResources().getDrawable(R.drawable.auth_dialog_enterprise, 229 mContext.getTheme()); 230 } else { 231 image = getResources().getDrawable(R.drawable.auth_dialog_lock, 232 mContext.getTheme()); 233 } 234 mIconView.setImageDrawable(image); 235 } 236 237 // Only animate this if we're transitioning from a biometric view. 238 if (mShouldAnimateContents) { 239 setTranslationY(getResources() 240 .getDimension(R.dimen.biometric_dialog_credential_translation_offset)); 241 setAlpha(0); 242 243 postOnAnimation(() -> { 244 animate().translationY(0) 245 .setDuration(AuthDialog.ANIMATE_CREDENTIAL_INITIAL_DURATION_MS) 246 .alpha(1.f) 247 .setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN) 248 .withLayer() 249 .start(); 250 }); 251 } 252 } 253 254 @Override onDetachedFromWindow()255 protected void onDetachedFromWindow() { 256 super.onDetachedFromWindow(); 257 if (mErrorTimer != null) { 258 mErrorTimer.cancel(); 259 } 260 } 261 262 @Override onFinishInflate()263 protected void onFinishInflate() { 264 super.onFinishInflate(); 265 mTitleView = findViewById(R.id.title); 266 mSubtitleView = findViewById(R.id.subtitle); 267 mDescriptionView = findViewById(R.id.description); 268 mIconView = findViewById(R.id.icon); 269 mErrorView = findViewById(R.id.error); 270 } 271 272 @Override onLayout(boolean changed, int left, int top, int right, int bottom)273 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 274 super.onLayout(changed, left, top, right, bottom); 275 276 if (mShouldAnimatePanel) { 277 // Credential view is always full screen. 278 mPanelController.setUseFullScreen(true); 279 mPanelController.updateForContentDimensions(mPanelController.getContainerWidth(), 280 mPanelController.getContainerHeight(), 0 /* animateDurationMs */); 281 mShouldAnimatePanel = false; 282 } 283 } 284 onErrorTimeoutFinish()285 protected void onErrorTimeoutFinish() {} 286 onCredentialVerified(@onNull VerifyCredentialResponse response, int timeoutMs)287 protected void onCredentialVerified(@NonNull VerifyCredentialResponse response, int timeoutMs) { 288 if (response.isMatched()) { 289 mClearErrorRunnable.run(); 290 mLockPatternUtils.userPresent(mEffectiveUserId); 291 292 // The response passed into this method contains the Gatekeeper Password. We still 293 // have to request Gatekeeper to create a Hardware Auth Token with the 294 // Gatekeeper Password and Challenge (keystore operationId in this case) 295 final long pwHandle = response.getGatekeeperPasswordHandle(); 296 final VerifyCredentialResponse gkResponse = mLockPatternUtils 297 .verifyGatekeeperPasswordHandle(pwHandle, mOperationId, mEffectiveUserId); 298 299 mCallback.onCredentialMatched(gkResponse.getGatekeeperHAT()); 300 mLockPatternUtils.removeGatekeeperPasswordHandle(pwHandle); 301 } else { 302 if (timeoutMs > 0) { 303 mHandler.removeCallbacks(mClearErrorRunnable); 304 long deadline = mLockPatternUtils.setLockoutAttemptDeadline( 305 mEffectiveUserId, timeoutMs); 306 mErrorTimer = new ErrorTimer(mContext, 307 deadline - SystemClock.elapsedRealtime(), 308 LockPatternUtils.FAILED_ATTEMPT_COUNTDOWN_INTERVAL_MS, 309 mErrorView) { 310 @Override 311 public void onFinish() { 312 onErrorTimeoutFinish(); 313 mClearErrorRunnable.run(); 314 } 315 }; 316 mErrorTimer.start(); 317 } else { 318 final boolean didUpdateErrorText = reportFailedAttempt(); 319 if (!didUpdateErrorText) { 320 final @StringRes int errorRes; 321 switch (mCredentialType) { 322 case Utils.CREDENTIAL_PIN: 323 errorRes = R.string.biometric_dialog_wrong_pin; 324 break; 325 case Utils.CREDENTIAL_PATTERN: 326 errorRes = R.string.biometric_dialog_wrong_pattern; 327 break; 328 case Utils.CREDENTIAL_PASSWORD: 329 default: 330 errorRes = R.string.biometric_dialog_wrong_password; 331 break; 332 } 333 showError(getResources().getString(errorRes)); 334 } 335 } 336 } 337 } 338 reportFailedAttempt()339 private boolean reportFailedAttempt() { 340 boolean result = updateErrorMessage( 341 mLockPatternUtils.getCurrentFailedPasswordAttempts(mEffectiveUserId) + 1); 342 mLockPatternUtils.reportFailedPasswordAttempt(mEffectiveUserId); 343 return result; 344 } 345 updateErrorMessage(int numAttempts)346 private boolean updateErrorMessage(int numAttempts) { 347 // Don't show any message if there's no maximum number of attempts. 348 final int maxAttempts = mLockPatternUtils.getMaximumFailedPasswordsForWipe( 349 mEffectiveUserId); 350 if (maxAttempts <= 0 || numAttempts <= 0) { 351 return false; 352 } 353 354 // Update the on-screen error string. 355 if (mErrorView != null) { 356 final String message = getResources().getString( 357 R.string.biometric_dialog_credential_attempts_before_wipe, 358 numAttempts, 359 maxAttempts); 360 showError(message); 361 } 362 363 // Only show dialog if <=1 attempts are left before wiping. 364 final int remainingAttempts = maxAttempts - numAttempts; 365 if (remainingAttempts == 1) { 366 showLastAttemptBeforeWipeDialog(); 367 } else if (remainingAttempts <= 0) { 368 showNowWipingDialog(); 369 } 370 return true; 371 } 372 showLastAttemptBeforeWipeDialog()373 private void showLastAttemptBeforeWipeDialog() { 374 final AlertDialog alertDialog = new AlertDialog.Builder(mContext) 375 .setTitle(R.string.biometric_dialog_last_attempt_before_wipe_dialog_title) 376 .setMessage( 377 getLastAttemptBeforeWipeMessageRes(getUserTypeForWipe(), mCredentialType)) 378 .setPositiveButton(android.R.string.ok, null) 379 .create(); 380 alertDialog.getWindow().setType(WindowManager.LayoutParams.TYPE_STATUS_BAR_SUB_PANEL); 381 alertDialog.show(); 382 } 383 showNowWipingDialog()384 private void showNowWipingDialog() { 385 final AlertDialog alertDialog = new AlertDialog.Builder(mContext) 386 .setMessage(getNowWipingMessageRes(getUserTypeForWipe())) 387 .setPositiveButton(R.string.biometric_dialog_now_wiping_dialog_dismiss, null) 388 .setOnDismissListener( 389 dialog -> mContainerView.animateAway(AuthDialogCallback.DISMISSED_ERROR)) 390 .create(); 391 alertDialog.getWindow().setType(WindowManager.LayoutParams.TYPE_STATUS_BAR_SUB_PANEL); 392 alertDialog.show(); 393 } 394 getUserTypeForWipe()395 private @UserType int getUserTypeForWipe() { 396 final UserInfo userToBeWiped = mUserManager.getUserInfo( 397 mDevicePolicyManager.getProfileWithMinimumFailedPasswordsForWipe(mEffectiveUserId)); 398 if (userToBeWiped == null || userToBeWiped.isPrimary()) { 399 return USER_TYPE_PRIMARY; 400 } else if (userToBeWiped.isManagedProfile()) { 401 return USER_TYPE_MANAGED_PROFILE; 402 } else { 403 return USER_TYPE_SECONDARY; 404 } 405 } 406 getLastAttemptBeforeWipeMessageRes( @serType int userType, @Utils.CredentialType int credentialType)407 private static @StringRes int getLastAttemptBeforeWipeMessageRes( 408 @UserType int userType, @Utils.CredentialType int credentialType) { 409 switch (userType) { 410 case USER_TYPE_PRIMARY: 411 return getLastAttemptBeforeWipeDeviceMessageRes(credentialType); 412 case USER_TYPE_MANAGED_PROFILE: 413 return getLastAttemptBeforeWipeProfileMessageRes(credentialType); 414 case USER_TYPE_SECONDARY: 415 return getLastAttemptBeforeWipeUserMessageRes(credentialType); 416 default: 417 throw new IllegalArgumentException("Unrecognized user type:" + userType); 418 } 419 } 420 getLastAttemptBeforeWipeDeviceMessageRes( @tils.CredentialType int credentialType)421 private static @StringRes int getLastAttemptBeforeWipeDeviceMessageRes( 422 @Utils.CredentialType int credentialType) { 423 switch (credentialType) { 424 case Utils.CREDENTIAL_PIN: 425 return R.string.biometric_dialog_last_pin_attempt_before_wipe_device; 426 case Utils.CREDENTIAL_PATTERN: 427 return R.string.biometric_dialog_last_pattern_attempt_before_wipe_device; 428 case Utils.CREDENTIAL_PASSWORD: 429 default: 430 return R.string.biometric_dialog_last_password_attempt_before_wipe_device; 431 } 432 } 433 getLastAttemptBeforeWipeProfileMessageRes( @tils.CredentialType int credentialType)434 private static @StringRes int getLastAttemptBeforeWipeProfileMessageRes( 435 @Utils.CredentialType int credentialType) { 436 switch (credentialType) { 437 case Utils.CREDENTIAL_PIN: 438 return R.string.biometric_dialog_last_pin_attempt_before_wipe_profile; 439 case Utils.CREDENTIAL_PATTERN: 440 return R.string.biometric_dialog_last_pattern_attempt_before_wipe_profile; 441 case Utils.CREDENTIAL_PASSWORD: 442 default: 443 return R.string.biometric_dialog_last_password_attempt_before_wipe_profile; 444 } 445 } 446 getLastAttemptBeforeWipeUserMessageRes( @tils.CredentialType int credentialType)447 private static @StringRes int getLastAttemptBeforeWipeUserMessageRes( 448 @Utils.CredentialType int credentialType) { 449 switch (credentialType) { 450 case Utils.CREDENTIAL_PIN: 451 return R.string.biometric_dialog_last_pin_attempt_before_wipe_user; 452 case Utils.CREDENTIAL_PATTERN: 453 return R.string.biometric_dialog_last_pattern_attempt_before_wipe_user; 454 case Utils.CREDENTIAL_PASSWORD: 455 default: 456 return R.string.biometric_dialog_last_password_attempt_before_wipe_user; 457 } 458 } 459 getNowWipingMessageRes(@serType int userType)460 private static @StringRes int getNowWipingMessageRes(@UserType int userType) { 461 switch (userType) { 462 case USER_TYPE_PRIMARY: 463 return R.string.biometric_dialog_failed_attempts_now_wiping_device; 464 case USER_TYPE_MANAGED_PROFILE: 465 return R.string.biometric_dialog_failed_attempts_now_wiping_profile; 466 case USER_TYPE_SECONDARY: 467 return R.string.biometric_dialog_failed_attempts_now_wiping_user; 468 default: 469 throw new IllegalArgumentException("Unrecognized user type:" + userType); 470 } 471 } 472 473 @Nullable getTitle(@onNull PromptInfo promptInfo)474 private static CharSequence getTitle(@NonNull PromptInfo promptInfo) { 475 final CharSequence credentialTitle = promptInfo.getDeviceCredentialTitle(); 476 return credentialTitle != null ? credentialTitle : promptInfo.getTitle(); 477 } 478 479 @Nullable getSubtitle(@onNull PromptInfo promptInfo)480 private static CharSequence getSubtitle(@NonNull PromptInfo promptInfo) { 481 final CharSequence credentialSubtitle = promptInfo.getDeviceCredentialSubtitle(); 482 return credentialSubtitle != null ? credentialSubtitle : promptInfo.getSubtitle(); 483 } 484 485 @Nullable getDescription(@onNull PromptInfo promptInfo)486 private static CharSequence getDescription(@NonNull PromptInfo promptInfo) { 487 final CharSequence credentialDescription = promptInfo.getDeviceCredentialDescription(); 488 return credentialDescription != null ? credentialDescription : promptInfo.getDescription(); 489 } 490 } 491