Add new "pin complex" type to supported keyguard PINs

This adds a feature to allow DevicePolicyAdmins to prevent using
simple PINs, which are defined as those containing more than 3
repeated values. Examples include '1234', '2468', '1111', '9876', etc.

Bug 12081139

Change-Id: I4ebe1c76a48087dcd7c878e9bd79a4e3ee2a27fe
diff --git a/api/current.txt b/api/current.txt
index 29ec56b..60a18d8 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -5372,6 +5372,7 @@
     field public static final int PASSWORD_QUALITY_BIOMETRIC_WEAK = 32768; // 0x8000
     field public static final int PASSWORD_QUALITY_COMPLEX = 393216; // 0x60000
     field public static final int PASSWORD_QUALITY_NUMERIC = 131072; // 0x20000
+    field public static final int PASSWORD_QUALITY_NUMERIC_COMPLEX = 196608; // 0x30000
     field public static final int PASSWORD_QUALITY_SOMETHING = 65536; // 0x10000
     field public static final int PASSWORD_QUALITY_UNSPECIFIED = 0; // 0x0
     field public static final java.lang.String PROVISIONING_NFC_MIME_TYPE = "application/com.android.managedprovisioning";
diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java
index 49b1d10..2feec1b 100644
--- a/core/java/android/app/admin/DevicePolicyManager.java
+++ b/core/java/android/app/admin/DevicePolicyManager.java
@@ -509,6 +509,14 @@
 
     /**
      * Constant for {@link #setPasswordQuality}: the user must have entered a
+     * password containing at least numeric characters with no repeating (4444)
+     * or ordered (1234, 4321, 2468) sequences.  Note that quality
+     * constants are ordered so that higher values are more restrictive.
+     */
+    public static final int PASSWORD_QUALITY_NUMERIC_COMPLEX = 0x30000;
+
+    /**
+     * Constant for {@link #setPasswordQuality}: the user must have entered a
      * password containing at least alphabetic (or other symbol) characters.
      * Note that quality constants are ordered so that higher values are more
      * restrictive.
@@ -556,8 +564,9 @@
      * @param admin Which {@link DeviceAdminReceiver} this request is associated with.
      * @param quality The new desired quality.  One of
      * {@link #PASSWORD_QUALITY_UNSPECIFIED}, {@link #PASSWORD_QUALITY_SOMETHING},
-     * {@link #PASSWORD_QUALITY_NUMERIC}, {@link #PASSWORD_QUALITY_ALPHABETIC},
-     * {@link #PASSWORD_QUALITY_ALPHANUMERIC} or {@link #PASSWORD_QUALITY_COMPLEX}.
+     * {@link #PASSWORD_QUALITY_NUMERIC}, {@link #PASSWORD_QUALITY_NUMERIC_COMPLEX},
+     * {@link #PASSWORD_QUALITY_ALPHABETIC}, {@link #PASSWORD_QUALITY_ALPHANUMERIC}
+     * or {@link #PASSWORD_QUALITY_COMPLEX}.
      */
     public void setPasswordQuality(ComponentName admin, int quality) {
         if (mService != null) {
@@ -600,9 +609,9 @@
      * take place immediately.  To prompt the user for a new password, use
      * {@link #ACTION_SET_NEW_PASSWORD} after setting this value.  This
      * constraint is only imposed if the administrator has also requested either
-     * {@link #PASSWORD_QUALITY_NUMERIC}, {@link #PASSWORD_QUALITY_ALPHABETIC}
-     * {@link #PASSWORD_QUALITY_ALPHANUMERIC}, or {@link #PASSWORD_QUALITY_COMPLEX}
-     * with {@link #setPasswordQuality}.
+     * {@link #PASSWORD_QUALITY_NUMERIC}, {@link #PASSWORD_QUALITY_NUMERIC_COMPLEX},
+     * {@link #PASSWORD_QUALITY_ALPHABETIC}, {@link #PASSWORD_QUALITY_ALPHANUMERIC},
+     * or {@link #PASSWORD_QUALITY_COMPLEX} with {@link #setPasswordQuality}.
      *
      * <p>The calling device admin must have requested
      * {@link DeviceAdminInfo#USES_POLICY_LIMIT_PASSWORD} to be able to call
@@ -1008,9 +1017,9 @@
    * the change does not take place immediately. To prompt the user for a new
    * password, use {@link #ACTION_SET_NEW_PASSWORD} after setting this value.
    * This constraint is only imposed if the administrator has also requested
-   * either {@link #PASSWORD_QUALITY_NUMERIC},
-   * {@link #PASSWORD_QUALITY_ALPHABETIC}, or
-   * {@link #PASSWORD_QUALITY_ALPHANUMERIC} with {@link #setPasswordQuality}.
+   * either {@link #PASSWORD_QUALITY_NUMERIC}, {@link #PASSWORD_QUALITY_NUMERIC_COMPLEX}
+   * {@link #PASSWORD_QUALITY_ALPHABETIC}, or {@link #PASSWORD_QUALITY_ALPHANUMERIC}
+   * with {@link #setPasswordQuality}.
    *
    * <p>
    * The calling device admin must have requested
diff --git a/core/java/com/android/internal/widget/LockPatternUtils.java b/core/java/com/android/internal/widget/LockPatternUtils.java
index 5409de7..f40b84c 100644
--- a/core/java/com/android/internal/widget/LockPatternUtils.java
+++ b/core/java/com/android/internal/widget/LockPatternUtils.java
@@ -150,6 +150,10 @@
 
     private static final String ENABLED_TRUST_AGENTS = "lockscreen.enabledtrustagents";
 
+    // Maximum allowed number of repeated or ordered characters in a sequence before we'll
+    // consider it a complex PIN/password.
+    public static final int MAX_ALLOWED_SEQUENCE = 3;
+
     private final Context mContext;
     private final ContentResolver mContentResolver;
     private DevicePolicyManager mDevicePolicyManager;
@@ -441,6 +445,11 @@
                     activePasswordQuality = DevicePolicyManager.PASSWORD_QUALITY_NUMERIC;
                 }
                 break;
+            case DevicePolicyManager.PASSWORD_QUALITY_NUMERIC_COMPLEX:
+                if (isLockPasswordEnabled()) {
+                    activePasswordQuality = DevicePolicyManager.PASSWORD_QUALITY_NUMERIC_COMPLEX;
+                }
+                break;
             case DevicePolicyManager.PASSWORD_QUALITY_ALPHABETIC:
                 if (isLockPasswordEnabled()) {
                     activePasswordQuality = DevicePolicyManager.PASSWORD_QUALITY_ALPHABETIC;
@@ -625,11 +634,74 @@
             return DevicePolicyManager.PASSWORD_QUALITY_ALPHABETIC;
         }
         if (hasDigit) {
-            return DevicePolicyManager.PASSWORD_QUALITY_NUMERIC;
+            return maxLengthSequence(password) > MAX_ALLOWED_SEQUENCE
+                    ? DevicePolicyManager.PASSWORD_QUALITY_NUMERIC
+                    : DevicePolicyManager.PASSWORD_QUALITY_NUMERIC_COMPLEX;
         }
         return DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED;
     }
 
+    private static int categoryChar(char c) {
+        if ('a' <= c && c <= 'z') return 0;
+        if ('A' <= c && c <= 'Z') return 1;
+        if ('0' <= c && c <= '9') return 2;
+        return 3;
+    }
+
+    private static int maxDiffCategory(int category) {
+        if (category == 0 || category == 1) return 1;
+        else if (category == 2) return 10;
+        return 0;
+    }
+
+    /*
+     * Returns the maximum length of a sequential characters.  A sequence is defined as
+     * monotonically increasing characters with a constant interval or the same character repeated.
+     *
+     * For example:
+     * maxLengthSequence("1234") == 4
+     * maxLengthSequence("1234abc") == 4
+     * maxLengthSequence("aabc") == 3
+     * maxLengthSequence("qwertyuio") == 1
+     * maxLengthSequence("@ABC") == 3
+     * maxLengthSequence(";;;;") == 4 (anything that repeats)
+     * maxLengthSequence(":;<=>") == 1  (ordered, but not composed of alphas or digits)
+     *
+     * @param string the pass
+     * @return the number of sequential letters or digits
+     */
+    public static int maxLengthSequence(String string) {
+        if (string.length() == 0) return 0;
+        char previousChar = string.charAt(0);
+        int category = categoryChar(previousChar); //current category of the sequence
+        int diff = 0; //difference between two consecutive characters
+        boolean hasDiff = false; //if we are currently targeting a sequence
+        int maxLength = 0; //maximum length of a sequence already found
+        int startSequence = 0; //where the current sequence started
+        for (int current = 1; current < string.length(); current++) {
+            char currentChar = string.charAt(current);
+            int categoryCurrent = categoryChar(currentChar);
+            int currentDiff = (int) currentChar - (int) previousChar;
+            if (categoryCurrent != category || Math.abs(currentDiff) > maxDiffCategory(category)) {
+                maxLength = Math.max(maxLength, current - startSequence);
+                startSequence = current;
+                hasDiff = false;
+                category = categoryCurrent;
+            }
+            else {
+                if(hasDiff && currentDiff != diff) {
+                    maxLength = Math.max(maxLength, current - startSequence);
+                    startSequence = current - 1;
+                }
+                diff = currentDiff;
+                hasDiff = true;
+            }
+            previousChar = currentChar;
+        }
+        maxLength = Math.max(maxLength, string.length() - startSequence);
+        return maxLength;
+    }
+
     /** Update the encryption password if it is enabled **/
     private void updateEncryptionPassword(int type, String password) {
         DevicePolicyManager dpm = getDevicePolicyManager();
@@ -926,10 +998,12 @@
         long backupMode = getLong(PASSWORD_TYPE_ALTERNATE_KEY, 0);
         final boolean passwordEnabled = mode == DevicePolicyManager.PASSWORD_QUALITY_ALPHABETIC
                 || mode == DevicePolicyManager.PASSWORD_QUALITY_NUMERIC
+                || mode == DevicePolicyManager.PASSWORD_QUALITY_NUMERIC_COMPLEX
                 || mode == DevicePolicyManager.PASSWORD_QUALITY_ALPHANUMERIC
                 || mode == DevicePolicyManager.PASSWORD_QUALITY_COMPLEX;
         final boolean backupEnabled = backupMode == DevicePolicyManager.PASSWORD_QUALITY_ALPHABETIC
                 || backupMode == DevicePolicyManager.PASSWORD_QUALITY_NUMERIC
+                || backupMode == DevicePolicyManager.PASSWORD_QUALITY_NUMERIC_COMPLEX
                 || backupMode == DevicePolicyManager.PASSWORD_QUALITY_ALPHANUMERIC
                 || backupMode == DevicePolicyManager.PASSWORD_QUALITY_COMPLEX;
 
@@ -1307,6 +1381,7 @@
         long mode = getKeyguardStoredPasswordQuality();
         final boolean isPattern = mode == DevicePolicyManager.PASSWORD_QUALITY_SOMETHING;
         final boolean isPassword = mode == DevicePolicyManager.PASSWORD_QUALITY_NUMERIC
+                || mode == DevicePolicyManager.PASSWORD_QUALITY_NUMERIC_COMPLEX
                 || mode == DevicePolicyManager.PASSWORD_QUALITY_ALPHABETIC
                 || mode == DevicePolicyManager.PASSWORD_QUALITY_ALPHANUMERIC
                 || mode == DevicePolicyManager.PASSWORD_QUALITY_COMPLEX;
diff --git a/packages/Keyguard/src/com/android/keyguard/KeyguardSecurityModel.java b/packages/Keyguard/src/com/android/keyguard/KeyguardSecurityModel.java
index 5ef41c9..3166ad4 100644
--- a/packages/Keyguard/src/com/android/keyguard/KeyguardSecurityModel.java
+++ b/packages/Keyguard/src/com/android/keyguard/KeyguardSecurityModel.java
@@ -87,6 +87,7 @@
             final int security = mLockPatternUtils.getKeyguardStoredPasswordQuality();
             switch (security) {
                 case DevicePolicyManager.PASSWORD_QUALITY_NUMERIC:
+                case DevicePolicyManager.PASSWORD_QUALITY_NUMERIC_COMPLEX:
                     mode = mLockPatternUtils.isLockPasswordEnabled() ?
                             SecurityMode.PIN : SecurityMode.None;
                     break;
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
index e956dfb..61f09f6 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
@@ -1190,6 +1190,7 @@
             case DevicePolicyManager.PASSWORD_QUALITY_BIOMETRIC_WEAK:
             case DevicePolicyManager.PASSWORD_QUALITY_SOMETHING:
             case DevicePolicyManager.PASSWORD_QUALITY_NUMERIC:
+            case DevicePolicyManager.PASSWORD_QUALITY_NUMERIC_COMPLEX:
             case DevicePolicyManager.PASSWORD_QUALITY_ALPHABETIC:
             case DevicePolicyManager.PASSWORD_QUALITY_ALPHANUMERIC:
             case DevicePolicyManager.PASSWORD_QUALITY_COMPLEX: