Adding support for minimum number of non letter characters.

Change-Id: If54cb7209d65eef826d474d7e0dbbef63d2f2b47
diff --git a/api/current.xml b/api/current.xml
index 5a0a619..0ffbb1b 100644
--- a/api/current.xml
+++ b/api/current.xml
@@ -31871,6 +31871,19 @@
 <parameter name="admin" type="android.content.ComponentName">
 </parameter>
 </method>
+<method name="getPasswordMinimumNonLetter"
+ return="int"
+ abstract="false"
+ native="false"
+ synchronized="false"
+ static="false"
+ final="false"
+ deprecated="not deprecated"
+ visibility="public"
+>
+<parameter name="admin" type="android.content.ComponentName">
+</parameter>
+</method>
 <method name="getPasswordMinimumNumeric"
  return="int"
  abstract="false"
@@ -32076,6 +32089,21 @@
 <parameter name="length" type="int">
 </parameter>
 </method>
+<method name="setPasswordMinimumNonLetter"
+ return="void"
+ abstract="false"
+ native="false"
+ synchronized="false"
+ static="false"
+ final="false"
+ deprecated="not deprecated"
+ visibility="public"
+>
+<parameter name="admin" type="android.content.ComponentName">
+</parameter>
+<parameter name="length" type="int">
+</parameter>
+</method>
 <method name="setPasswordMinimumNumeric"
  return="void"
  abstract="false"
diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java
index ef63175..3066f5c 100644
--- a/core/java/android/app/admin/DevicePolicyManager.java
+++ b/core/java/android/app/admin/DevicePolicyManager.java
@@ -215,9 +215,13 @@
 
     /**
      * Constant for {@link #setPasswordQuality}: the user must have entered a
-     * password containing numeric <em>and</em> alphabetic characters,
-     * <em>and</em> special symbols. Note that quality constants are ordered so
-     * that higher values are more restrictive.
+     * password containing at least a letter, a numerical digit and a special
+     * symbol, by default. With this password quality, passwords can be
+     * restricted to contain various sets of characters, like at least an
+     * uppercase letter, etc. These are specified using various methods,
+     * like {@link #setPasswordMinimumLowerCase(ComponentName, int)}. Note
+     * that quality constants are ordered so that higher values are more
+     * restrictive.
      */
     public static final int PASSWORD_QUALITY_COMPLEX = 0x60000;
 
@@ -329,7 +333,8 @@
      * 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
-     * {@link #PASSWORD_QUALITY_COMPLEX} with {@link #setPasswordQuality}.
+     * {@link #PASSWORD_QUALITY_COMPLEX} with {@link #setPasswordQuality}. The
+     * default value is 0.
      * <p>
      * The calling device admin must have requested
      * {@link DeviceAdminInfo#USES_POLICY_LIMIT_PASSWORD} to be able to call
@@ -353,7 +358,10 @@
 
     /**
      * Retrieve the current number of upper case letters required in the
-     * password for all admins or a particular one.
+     * password for all admins or a particular one. This is the same value as
+     * set by {#link {@link #setPasswordMinimumUpperCase(ComponentName, int)}
+     * and only applies when the password quality is
+     * {@link #PASSWORD_QUALITY_COMPLEX}.
      *
      * @param admin The name of the admin component to check, or null to
      *            aggregate all admins.
@@ -380,7 +388,8 @@
      * 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
-     * {@link #PASSWORD_QUALITY_COMPLEX} with {@link #setPasswordQuality}.
+     * {@link #PASSWORD_QUALITY_COMPLEX} with {@link #setPasswordQuality}. The
+     * default value is 0.
      * <p>
      * The calling device admin must have requested
      * {@link DeviceAdminInfo#USES_POLICY_LIMIT_PASSWORD} to be able to call
@@ -404,7 +413,10 @@
 
     /**
      * Retrieve the current number of lower case letters required in the
-     * password for all admins or a particular one.
+     * password for all admins or a particular one. This is the same value as
+     * set by {#link {@link #setPasswordMinimumLowerCase(ComponentName, int)}
+     * and only applies when the password quality is
+     * {@link #PASSWORD_QUALITY_COMPLEX}.
      *
      * @param admin The name of the admin component to check, or null to
      *            aggregate all admins.
@@ -431,7 +443,8 @@
      * 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
-     * {@link #PASSWORD_QUALITY_COMPLEX} with {@link #setPasswordQuality}.
+     * {@link #PASSWORD_QUALITY_COMPLEX} with {@link #setPasswordQuality}. The
+     * default value is 1.
      * <p>
      * The calling device admin must have requested
      * {@link DeviceAdminInfo#USES_POLICY_LIMIT_PASSWORD} to be able to call
@@ -454,7 +467,10 @@
 
     /**
      * Retrieve the current number of letters required in the password for all
-     * admins or a particular one.
+     * admins or a particular one. This is the same value as
+     * set by {#link {@link #setPasswordMinimumLetters(ComponentName, int)}
+     * and only applies when the password quality is
+     * {@link #PASSWORD_QUALITY_COMPLEX}.
      *
      * @param admin The name of the admin component to check, or null to
      *            aggregate all admins.
@@ -480,7 +496,8 @@
      * 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
-     * {@link #PASSWORD_QUALITY_COMPLEX} with {@link #setPasswordQuality}.
+     * {@link #PASSWORD_QUALITY_COMPLEX} with {@link #setPasswordQuality}. The
+     * default value is 1.
      * <p>
      * The calling device admin must have requested
      * {@link DeviceAdminInfo#USES_POLICY_LIMIT_PASSWORD} to be able to call
@@ -503,7 +520,10 @@
 
     /**
      * Retrieve the current number of numerical digits required in the password
-     * for all admins or a particular one.
+     * for all admins or a particular one. This is the same value as
+     * set by {#link {@link #setPasswordMinimumNumeric(ComponentName, int)}
+     * and only applies when the password quality is
+     * {@link #PASSWORD_QUALITY_COMPLEX}.
      *
      * @param admin The name of the admin component to check, or null to
      *            aggregate all admins.
@@ -529,7 +549,8 @@
      * 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
-     * {@link #PASSWORD_QUALITY_COMPLEX} with {@link #setPasswordQuality}.
+     * {@link #PASSWORD_QUALITY_COMPLEX} with {@link #setPasswordQuality}. The
+     * default value is 1.
      * <p>
      * The calling device admin must have requested
      * {@link DeviceAdminInfo#USES_POLICY_LIMIT_PASSWORD} to be able to call
@@ -552,7 +573,10 @@
 
     /**
      * Retrieve the current number of symbols required in the password for all
-     * admins or a particular one.
+     * admins or a particular one. This is the same value as
+     * set by {#link {@link #setPasswordMinimumSymbols(ComponentName, int)}
+     * and only applies when the password quality is
+     * {@link #PASSWORD_QUALITY_COMPLEX}.
      *
      * @param admin The name of the admin component to check, or null to
      *            aggregate all admins.
@@ -569,6 +593,59 @@
         return 0;
     }
 
+    /**
+     * Called by an application that is administering the device to set the
+     * minimum number of non-letter characters (numerical digits or symbols)
+     * required in the password. After setting this, the user will not be able
+     * to enter a new password that is not at least as restrictive as what has
+     * been set. Note that the current password will remain until the user has
+     * set a new one, so 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 {@link #PASSWORD_QUALITY_COMPLEX} with
+     * {@link #setPasswordQuality}. The default value is 0.
+     * <p>
+     * The calling device admin must have requested
+     * {@link DeviceAdminInfo#USES_POLICY_LIMIT_PASSWORD} to be able to call
+     * this method; if it has not, a security exception will be thrown.
+     *
+     * @param admin Which {@link DeviceAdminReceiver} this request is associated
+     *            with.
+     * @param length The new desired minimum number of letters required in the
+     *            password. A value of 0 means there is no restriction.
+     */
+    public void setPasswordMinimumNonLetter(ComponentName admin, int length) {
+        if (mService != null) {
+            try {
+                mService.setPasswordMinimumNonLetter(admin, length);
+            } catch (RemoteException e) {
+                Log.w(TAG, "Failed talking with device policy service", e);
+            }
+        }
+    }
+
+    /**
+     * Retrieve the current number of non-letter characters required in the
+     * password for all admins or a particular one. This is the same value as
+     * set by {#link {@link #setPasswordMinimumNonLetter(ComponentName, int)}
+     * and only applies when the password quality is
+     * {@link #PASSWORD_QUALITY_COMPLEX}.
+     *
+     * @param admin The name of the admin component to check, or null to
+     *            aggregate all admins.
+     * @return The minimum number of letters required in the password.
+     */
+    public int getPasswordMinimumNonLetter(ComponentName admin) {
+        if (mService != null) {
+            try {
+                return mService.getPasswordMinimumNonLetter(admin);
+            } catch (RemoteException e) {
+                Log.w(TAG, "Failed talking with device policy service", e);
+            }
+        }
+        return 0;
+    }
+
   /**
    * Called by an application that is administering the device to set the length
    * of the password history. After setting this, the user will not be able to
@@ -894,11 +971,11 @@
      * @hide
      */
     public void setActivePasswordState(int quality, int length, int letters, int uppercase,
-            int lowercase, int numbers, int symbols) {
+            int lowercase, int numbers, int symbols, int nonletter) {
         if (mService != null) {
             try {
                 mService.setActivePasswordState(quality, length, letters, uppercase, lowercase,
-                        numbers, symbols);
+                        numbers, symbols, nonletter);
             } catch (RemoteException e) {
                 Log.w(TAG, "Failed talking with device policy service", e);
             }
diff --git a/core/java/android/app/admin/IDevicePolicyManager.aidl b/core/java/android/app/admin/IDevicePolicyManager.aidl
index fa31a37..3ada95c 100644
--- a/core/java/android/app/admin/IDevicePolicyManager.aidl
+++ b/core/java/android/app/admin/IDevicePolicyManager.aidl
@@ -45,6 +45,9 @@
 
     void setPasswordMinimumSymbols(in ComponentName who, int length);
     int getPasswordMinimumSymbols(in ComponentName who);
+
+    void setPasswordMinimumNonLetter(in ComponentName who, int length);
+    int getPasswordMinimumNonLetter(in ComponentName who);
     
     void setPasswordHistoryLength(in ComponentName who, int length);
     int getPasswordHistoryLength(in ComponentName who);
@@ -71,7 +74,8 @@
     void getRemoveWarning(in ComponentName policyReceiver, in RemoteCallback result);
     void removeActiveAdmin(in ComponentName policyReceiver);
     
-    void setActivePasswordState(int quality, int length, int letters, int uppercase, int lowercase, int numbers, int symbols);
+    void setActivePasswordState(int quality, int length, int letters, int uppercase, int lowercase,
+        int numbers, int symbols, int nonletter);
     void reportFailedPasswordAttempt();
     void reportSuccessfulPasswordAttempt();
 }
diff --git a/core/java/com/android/internal/widget/LockPatternUtils.java b/core/java/com/android/internal/widget/LockPatternUtils.java
index 9983c02..0b62a67 100644
--- a/core/java/com/android/internal/widget/LockPatternUtils.java
+++ b/core/java/com/android/internal/widget/LockPatternUtils.java
@@ -164,6 +164,9 @@
         return getDevicePolicyManager().getPasswordMinimumSymbols(null);
     }
 
+    public int getRequestedPasswordMinimumNonLetter() {
+        return getDevicePolicyManager().getPasswordMinimumNonLetter(null);
+    }
     /**
      * Returns the actual password mode, as set by keyguard after updating the password.
      *
@@ -369,10 +372,10 @@
                 setBoolean(PATTERN_EVER_CHOSEN_KEY, true);
                 setLong(PASSWORD_TYPE_KEY, DevicePolicyManager.PASSWORD_QUALITY_SOMETHING);
                 dpm.setActivePasswordState(DevicePolicyManager.PASSWORD_QUALITY_SOMETHING, pattern
-                        .size(), 0, 0, 0, 0, 0);
+                        .size(), 0, 0, 0, 0, 0, 0);
             } else {
                 dpm.setActivePasswordState(DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED, 0, 0,
-                        0, 0, 0, 0);
+                        0, 0, 0, 0, 0);
             }
         } catch (FileNotFoundException fnfe) {
             // Cant do much, unless we want to fail over to using the settings
@@ -441,6 +444,7 @@
                     int lowercase = 0;
                     int numbers = 0;
                     int symbols = 0;
+                    int nonletter = 0;
                     for (int i = 0; i < password.length(); i++) {
                         char c = password.charAt(i);
                         if (c >= 'A' && c <= 'Z') {
@@ -451,16 +455,18 @@
                             lowercase++;
                         } else if (c >= '0' && c <= '9') {
                             numbers++;
+                            nonletter++;
                         } else {
                             symbols++;
+                            nonletter++;
                         }
                     }
                     dpm.setActivePasswordState(Math.max(quality, computedQuality), password
-                            .length(), letters, uppercase, lowercase, numbers, symbols);
+                            .length(), letters, uppercase, lowercase, numbers, symbols, nonletter);
                 } else {
                     // The password is not anything.
                     dpm.setActivePasswordState(
-                            DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED, 0, 0, 0, 0, 0, 0);
+                            DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED, 0, 0, 0, 0, 0, 0, 0);
                 }
                 // Add the password to the password history. We assume all
                 // password
@@ -483,7 +489,7 @@
                 setString(PASSWORD_HISTORY_KEY, passwordHistory);
             } else {
                 dpm.setActivePasswordState(
-                        DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED, 0, 0, 0, 0, 0, 0);
+                        DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED, 0, 0, 0, 0, 0, 0, 0);
             }
         } catch (FileNotFoundException fnfe) {
             // Cant do much, unless we want to fail over to using the settings provider
diff --git a/services/java/com/android/server/DevicePolicyManagerService.java b/services/java/com/android/server/DevicePolicyManagerService.java
index 1e1c416..c0ea68d 100644
--- a/services/java/com/android/server/DevicePolicyManagerService.java
+++ b/services/java/com/android/server/DevicePolicyManagerService.java
@@ -80,6 +80,7 @@
     int mActivePasswordLetters = 0;
     int mActivePasswordNumeric = 0;
     int mActivePasswordSymbols = 0;
+    int mActivePasswordNonLetter = 0;
     int mFailedPasswordAttempts = 0;
 
     int mPasswordOwner = -1;
@@ -100,6 +101,7 @@
         int minimumPasswordLetters = 1;
         int minimumPasswordNumeric = 1;
         int minimumPasswordSymbols = 1;
+        int minimumPasswordNonLetter = 0;
         long maximumTimeToUnlock = 0;
         int maximumFailedPasswordsForWipe = 0;
 
@@ -153,6 +155,11 @@
                     out.attribute(null, "value", Integer.toString(minimumPasswordSymbols));
                     out.endTag(null, "min-password-symbols");
                 }
+                if (minimumPasswordNonLetter > 0) {
+                    out.startTag(null, "min-password-nonletter");
+                    out.attribute(null, "value", Integer.toString(minimumPasswordNonLetter));
+                    out.endTag(null, "min-password-nonletter");
+                }
             }
             if (maximumTimeToUnlock != DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED) {
                 out.startTag(null, "max-time-to-unlock");
@@ -202,6 +209,9 @@
                 } else if ("min-password-symbols".equals(tag)) {
                     minimumPasswordSymbols = Integer.parseInt(
                             parser.getAttributeValue(null, "value"));
+                } else if ("min-password-nonletter".equals(tag)) {
+                    minimumPasswordNonLetter = Integer.parseInt(
+                            parser.getAttributeValue(null, "value"));
                 } else if ("max-time-to-unlock".equals(tag)) {
                     maximumTimeToUnlock = Long.parseLong(
                             parser.getAttributeValue(null, "value"));
@@ -240,6 +250,8 @@
                     pw.println(minimumPasswordNumeric);
             pw.print(prefix); pw.print("minimumPasswordSymbols=");
                     pw.println(minimumPasswordSymbols);
+            pw.print(prefix); pw.print("minimumPasswordNonLetter=");
+                    pw.println(minimumPasswordNonLetter);
             pw.print(prefix); pw.print("maximumTimeToUnlock=");
                     pw.println(maximumTimeToUnlock);
             pw.print(prefix); pw.print("maximumFailedPasswordsForWipe=");
@@ -429,7 +441,7 @@
             if (mActivePasswordQuality != 0 || mActivePasswordLength != 0
                     || mActivePasswordUpperCase != 0 || mActivePasswordLowerCase != 0
                     || mActivePasswordLetters != 0 || mActivePasswordNumeric != 0
-                    || mActivePasswordSymbols != 0) {
+                    || mActivePasswordSymbols != 0 || mActivePasswordNonLetter != 0) {
                 out.startTag(null, "active-password");
                 out.attribute(null, "quality", Integer.toString(mActivePasswordQuality));
                 out.attribute(null, "length", Integer.toString(mActivePasswordLength));
@@ -439,6 +451,7 @@
                 out.attribute(null, "numeric", Integer
                         .toString(mActivePasswordNumeric));
                 out.attribute(null, "symbols", Integer.toString(mActivePasswordSymbols));
+                out.attribute(null, "nonletter", Integer.toString(mActivePasswordNonLetter));
                 out.endTag(null, "active-password");
             }
 
@@ -529,6 +542,8 @@
                             parser.getAttributeValue(null, "numeric"));
                     mActivePasswordSymbols = Integer.parseInt(
                             parser.getAttributeValue(null, "symbols"));
+                    mActivePasswordNonLetter = Integer.parseInt(
+                            parser.getAttributeValue(null, "nonletter"));
                     XmlUtils.skipCurrentTag(parser);
                 } else {
                     Slog.w(TAG, "Unknown tag: " + tag);
@@ -571,6 +586,7 @@
             mActivePasswordLetters = 0;
             mActivePasswordNumeric = 0;
             mActivePasswordSymbols = 0;
+            mActivePasswordNonLetter = 0;
         }
 
         validatePasswordOwnerLocked();
@@ -974,6 +990,40 @@
         }
     }
 
+    public void setPasswordMinimumNonLetter(ComponentName who, int length) {
+        synchronized (this) {
+            if (who == null) {
+                throw new NullPointerException("ComponentName is null");
+            }
+            ActiveAdmin ap = getActiveAdminForCallerLocked(who,
+                    DeviceAdminInfo.USES_POLICY_LIMIT_PASSWORD);
+            if (ap.minimumPasswordNonLetter != length) {
+                ap.minimumPasswordNonLetter = length;
+                saveSettingsLocked();
+            }
+        }
+    }
+
+    public int getPasswordMinimumNonLetter(ComponentName who) {
+        synchronized (this) {
+            int length = 0;
+
+            if (who != null) {
+                ActiveAdmin admin = getActiveAdminUncheckedLocked(who);
+                return admin != null ? admin.minimumPasswordNonLetter : length;
+            }
+
+            final int N = mAdminList.size();
+            for (int i=0; i<N; i++) {
+                ActiveAdmin admin = mAdminList.get(i);
+                if (length < admin.minimumPasswordNonLetter) {
+                    length = admin.minimumPasswordNonLetter;
+                }
+            }
+            return length;
+        }
+    }
+
     public boolean isActivePasswordSufficient() {
         synchronized (this) {
             // This API can only be called by an active device admin,
@@ -991,7 +1041,8 @@
                     && mActivePasswordLowerCase >= getPasswordMinimumLowerCase(null)
                     && mActivePasswordLetters >= getPasswordMinimumLetters(null)
                     && mActivePasswordNumeric >= getPasswordMinimumNumeric(null)
-                    && mActivePasswordSymbols >= getPasswordMinimumSymbols(null);
+                    && mActivePasswordSymbols >= getPasswordMinimumSymbols(null)
+                    && mActivePasswordNonLetter >= getPasswordMinimumNonLetter(null);
         }
     }
 
@@ -1075,6 +1126,7 @@
                 int lowercase = 0;
                 int numbers = 0;
                 int symbols = 0;
+                int nonletter = 0;
                 for (int i = 0; i < password.length(); i++) {
                     char c = password.charAt(i);
                     if (c >= 'A' && c <= 'Z') {
@@ -1085,8 +1137,10 @@
                         lowercase++;
                     } else if (c >= '0' && c <= '9') {
                         numbers++;
+                        nonletter++;
                     } else {
                         symbols++;
+                        nonletter++;
                     }
                 }
                 int neededLetters = getPasswordMinimumLetters(null);
@@ -1123,6 +1177,13 @@
                             + " does not meet required number of special symbols " + neededSymbols);
                     return false;
                 }
+                int neededNonLetter = getPasswordMinimumNonLetter(null);
+                if (nonletter < neededNonLetter) {
+                    Slog.w(TAG, "resetPassword: number of non-letter characters " + nonletter
+                            + " does not meet required number of non-letter characters "
+                            + neededNonLetter);
+                    return false;
+                }
             }
 
             LockPatternUtils utils = new LockPatternUtils(mContext);
@@ -1282,7 +1343,7 @@
     }
 
     public void setActivePasswordState(int quality, int length, int letters, int uppercase,
-            int lowercase, int numbers, int symbols) {
+            int lowercase, int numbers, int symbols, int nonletter) {
         mContext.enforceCallingOrSelfPermission(
                 android.Manifest.permission.BIND_DEVICE_ADMIN, null);
 
@@ -1293,7 +1354,7 @@
                     || mFailedPasswordAttempts != 0 || mActivePasswordLetters != letters
                     || mActivePasswordUpperCase != uppercase
                     || mActivePasswordLowerCase != lowercase || mActivePasswordNumeric != numbers
-                    || mActivePasswordSymbols != symbols) {
+                    || mActivePasswordSymbols != symbols || mActivePasswordNonLetter != nonletter) {
                 long ident = Binder.clearCallingIdentity();
                 try {
                     mActivePasswordQuality = quality;
@@ -1303,6 +1364,7 @@
                     mActivePasswordUpperCase = uppercase;
                     mActivePasswordNumeric = numbers;
                     mActivePasswordSymbols = symbols;
+                    mActivePasswordNonLetter = nonletter;
                     mFailedPasswordAttempts = 0;
                     saveSettingsLocked();
                     sendAdminCommandLocked(DeviceAdminReceiver.ACTION_PASSWORD_CHANGED,
@@ -1391,6 +1453,7 @@
             pw.print("  mActivePasswordLetters="); pw.println(mActivePasswordLetters);
             pw.print("  mActivePasswordNumeric="); pw.println(mActivePasswordNumeric);
             pw.print("  mActivePasswordSymbols="); pw.println(mActivePasswordSymbols);
+            pw.print("  mActivePasswordNonLetter="); pw.println(mActivePasswordNonLetter);
             pw.print("  mFailedPasswordAttempts="); pw.println(mFailedPasswordAttempts);
             pw.print("  mPasswordOwner="); pw.println(mPasswordOwner);
         }