Merge "Make BiometricPrompt honor max attempts before wipe" into rvc-dev
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 3662877..ae38e34 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -374,6 +374,17 @@
     <string name="biometric_dialog_wrong_password">Wrong password</string>
     <!-- Error string shown when the user enters too many incorrect attempts [CHAR LIMIT=120]-->
     <string name="biometric_dialog_credential_too_many_attempts">Too many incorrect attempts.\nTry again in <xliff:g id="number">%d</xliff:g> seconds.</string>
+    <!-- Error string shown when the user enters an incorrect PIN/pattern/password and it counts towards the max attempts before the data on the device is wiped. [CHAR LIMIT=NONE]-->
+    <string name="biometric_dialog_credential_attempts_before_wipe">Try again. Attempt <xliff:g id="attempts" example="1">%1$d</xliff:g> of <xliff:g id="max_attempts" example="3">%2$d</xliff:g>.</string>
+
+    <!-- Content of a dialog shown when the user has failed to provide the device lock too many times and the device is wiped. [CHAR LIMIT=NONE] -->
+    <string name="biometric_dialog_failed_attempts_now_wiping_device">Too many incorrect attempts. This device\u2019s data will be deleted.</string>
+    <!-- Content of a dialog shown when the user has failed to provide the user lock too many times and the user is removed. [CHAR LIMIT=NONE] -->
+    <string name="biometric_dialog_failed_attempts_now_wiping_user">Too many incorrect attempts. This user will be deleted.</string>
+    <!-- Content of a dialog shown when the user has failed to provide the work lock too many times and the work profile is removed. [CHAR LIMIT=NONE] -->
+    <string name="biometric_dialog_failed_attempts_now_wiping_profile">Too many incorrect attempts. This work profile and its data will be deleted.</string>
+    <!-- Button label to dismiss the dialog telling the user the device, user, or work profile has been wiped. [CHAR LIMIT=40] -->
+    <string name="biometric_dialog_now_wiping_dialog_dismiss">Dismiss</string>
 
     <!-- Message shown when the system-provided fingerprint dialog is shown, asking for authentication -->
     <string name="fingerprint_dialog_touch_sensor">Touch the fingerprint sensor</string>
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialView.java
index 0d9d426..13f3c0f 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialView.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialView.java
@@ -16,8 +16,10 @@
 
 package com.android.systemui.biometrics;
 
-import android.annotation.NonNull;
+import android.app.AlertDialog;
+import android.app.admin.DevicePolicyManager;
 import android.content.Context;
+import android.content.pm.UserInfo;
 import android.graphics.drawable.Drawable;
 import android.hardware.biometrics.BiometricPrompt;
 import android.os.AsyncTask;
@@ -26,32 +28,49 @@
 import android.os.Handler;
 import android.os.Looper;
 import android.os.SystemClock;
+import android.os.UserManager;
 import android.text.TextUtils;
 import android.util.AttributeSet;
 import android.view.View;
+import android.view.WindowManager;
 import android.view.accessibility.AccessibilityManager;
 import android.widget.ImageView;
 import android.widget.LinearLayout;
 import android.widget.TextView;
 
+import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.annotation.StringRes;
 
 import com.android.internal.widget.LockPatternUtils;
 import com.android.systemui.Interpolators;
 import com.android.systemui.R;
 
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
 /**
  * Abstract base class for Pin, Pattern, or Password authentication, for
  * {@link BiometricPrompt.Builder#setAllowedAuthenticators(int)}}
  */
 public abstract class AuthCredentialView extends LinearLayout {
-
     private static final String TAG = "BiometricPrompt/AuthCredentialView";
     private static final int ERROR_DURATION_MS = 3000;
 
-    private final AccessibilityManager mAccessibilityManager;
+    static final int USER_TYPE_PRIMARY = 1;
+    static final int USER_TYPE_MANAGED_PROFILE = 2;
+    static final int USER_TYPE_SECONDARY = 3;
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef({USER_TYPE_PRIMARY, USER_TYPE_MANAGED_PROFILE, USER_TYPE_SECONDARY})
+    private @interface UserType {}
 
     protected final Handler mHandler;
+    protected final LockPatternUtils mLockPatternUtils;
+
+    private final AccessibilityManager mAccessibilityManager;
+    private final UserManager mUserManager;
+    private final DevicePolicyManager mDevicePolicyManager;
 
     private Bundle mBiometricPromptBundle;
     private AuthPanelController mPanelController;
@@ -65,7 +84,6 @@
     protected TextView mErrorView;
 
     protected @Utils.CredentialType int mCredentialType;
-    protected final LockPatternUtils mLockPatternUtils;
     protected AuthContainerView mContainerView;
     protected Callback mCallback;
     protected AsyncTask<?, ?, ?> mPendingLockCheck;
@@ -106,14 +124,18 @@
 
         @Override
         public void onFinish() {
-            mErrorView.setText("");
+            if (mErrorView != null) {
+                mErrorView.setText("");
+            }
         }
     }
 
     protected final Runnable mClearErrorRunnable = new Runnable() {
         @Override
         public void run() {
-            mErrorView.setText("");
+            if (mErrorView != null) {
+                mErrorView.setText("");
+            }
         }
     };
 
@@ -123,12 +145,18 @@
         mLockPatternUtils = new LockPatternUtils(mContext);
         mHandler = new Handler(Looper.getMainLooper());
         mAccessibilityManager = mContext.getSystemService(AccessibilityManager.class);
+        mUserManager = mContext.getSystemService(UserManager.class);
+        mDevicePolicyManager = mContext.getSystemService(DevicePolicyManager.class);
     }
 
     protected void showError(String error) {
-        mHandler.removeCallbacks(mClearErrorRunnable);
-        mErrorView.setText(error);
-        mHandler.postDelayed(mClearErrorRunnable, ERROR_DURATION_MS);
+        if (mHandler != null) {
+            mHandler.removeCallbacks(mClearErrorRunnable);
+            mHandler.postDelayed(mClearErrorRunnable, ERROR_DURATION_MS);
+        }
+        if (mErrorView != null) {
+            mErrorView.setText(error);
+        }
     }
 
     private void setTextOrHide(TextView view, CharSequence text) {
@@ -274,26 +302,94 @@
                 };
                 mErrorTimer.start();
             } else {
-                final int error;
-                switch (mCredentialType) {
-                    case Utils.CREDENTIAL_PIN:
-                        error = R.string.biometric_dialog_wrong_pin;
-                        break;
-                    case Utils.CREDENTIAL_PATTERN:
-                        error = R.string.biometric_dialog_wrong_pattern;
-                        break;
-                    case Utils.CREDENTIAL_PASSWORD:
-                        error = R.string.biometric_dialog_wrong_password;
-                        break;
-                    default:
-                        error = R.string.biometric_dialog_wrong_password;
-                        break;
+                final boolean didUpdateErrorText = reportFailedAttempt();
+                if (!didUpdateErrorText) {
+                    final @StringRes int errorRes;
+                    switch (mCredentialType) {
+                        case Utils.CREDENTIAL_PIN:
+                            errorRes = R.string.biometric_dialog_wrong_pin;
+                            break;
+                        case Utils.CREDENTIAL_PATTERN:
+                            errorRes = R.string.biometric_dialog_wrong_pattern;
+                            break;
+                        case Utils.CREDENTIAL_PASSWORD:
+                        default:
+                            errorRes = R.string.biometric_dialog_wrong_password;
+                            break;
+                    }
+                    showError(getResources().getString(errorRes));
                 }
-                showError(getResources().getString(error));
             }
         }
     }
 
+    private boolean reportFailedAttempt() {
+        boolean result = updateErrorMessage(
+                mLockPatternUtils.getCurrentFailedPasswordAttempts(mEffectiveUserId) + 1);
+        mLockPatternUtils.reportFailedPasswordAttempt(mEffectiveUserId);
+        return result;
+    }
+
+    private boolean updateErrorMessage(int numAttempts) {
+        // Don't show any message if there's no maximum number of attempts.
+        final int maxAttempts = mLockPatternUtils.getMaximumFailedPasswordsForWipe(
+                mEffectiveUserId);
+        if (maxAttempts <= 0 || numAttempts <= 0) {
+            return false;
+        }
+
+        // Update the on-screen error string.
+        if (mErrorView != null) {
+            final String message = getResources().getString(
+                    R.string.biometric_dialog_credential_attempts_before_wipe,
+                    numAttempts,
+                    maxAttempts);
+            showError(message);
+        }
+
+        // Only show popup dialog before wipe.
+        final int remainingAttempts = maxAttempts - numAttempts;
+        if (remainingAttempts <= 0) {
+            showNowWipingMessage();
+            mContainerView.animateAway(AuthDialogCallback.DISMISSED_ERROR);
+        }
+        return true;
+    }
+
+    private void showNowWipingMessage() {
+        final AlertDialog alertDialog = new AlertDialog.Builder(mContext)
+                .setMessage(getNowWipingMessageRes(getUserTypeForWipe()))
+                .setPositiveButton(R.string.biometric_dialog_now_wiping_dialog_dismiss, null)
+                .create();
+        alertDialog.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT);
+        alertDialog.show();
+    }
+
+    private @UserType int getUserTypeForWipe() {
+        final UserInfo userToBeWiped = mUserManager.getUserInfo(
+                mDevicePolicyManager.getProfileWithMinimumFailedPasswordsForWipe(mEffectiveUserId));
+        if (userToBeWiped == null || userToBeWiped.isPrimary()) {
+            return USER_TYPE_PRIMARY;
+        } else if (userToBeWiped.isManagedProfile()) {
+            return USER_TYPE_MANAGED_PROFILE;
+        } else {
+            return USER_TYPE_SECONDARY;
+        }
+    }
+
+    private static @StringRes int getNowWipingMessageRes(@UserType int userType) {
+        switch (userType) {
+            case USER_TYPE_PRIMARY:
+                return R.string.biometric_dialog_failed_attempts_now_wiping_device;
+            case USER_TYPE_MANAGED_PROFILE:
+                return R.string.biometric_dialog_failed_attempts_now_wiping_profile;
+            case USER_TYPE_SECONDARY:
+                return R.string.biometric_dialog_failed_attempts_now_wiping_user;
+            default:
+                throw new IllegalArgumentException("Unrecognized user type:" + userType);
+        }
+    }
+
     @Nullable
     private static CharSequence getTitle(@NonNull Bundle bundle) {
         final CharSequence credentialTitle =