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