Add password expiration support to DevicePolicyManager.

Change-Id: Ib2629ec547c123ac489d7f4cbd4e0a1d4aa07620
diff --git a/api/current.xml b/api/current.xml
index bad739f0..6daeca9 100644
--- a/api/current.xml
+++ b/api/current.xml
@@ -34318,6 +34318,17 @@
  visibility="public"
 >
 </field>
+<field name="USES_POLICY_EXPIRE_PASSWORD"
+ type="int"
+ transient="false"
+ volatile="false"
+ value="6"
+ static="true"
+ final="true"
+ deprecated="not deprecated"
+ visibility="public"
+>
+</field>
 <field name="USES_POLICY_FORCE_LOCK"
  type="int"
  transient="false"
@@ -34487,6 +34498,21 @@
 <parameter name="intent" type="android.content.Intent">
 </parameter>
 </method>
+<method name="onPasswordExpiring"
+ return="void"
+ abstract="false"
+ native="false"
+ synchronized="false"
+ static="false"
+ final="false"
+ deprecated="not deprecated"
+ visibility="public"
+>
+<parameter name="context" type="android.content.Context">
+</parameter>
+<parameter name="intent" type="android.content.Intent">
+</parameter>
+</method>
 <method name="onPasswordFailed"
  return="void"
  abstract="false"
@@ -34576,6 +34602,17 @@
  visibility="public"
 >
 </field>
+<field name="ACTION_PASSWORD_EXPIRING"
+ type="java.lang.String"
+ transient="false"
+ volatile="false"
+ value="&quot;android.app.action.ACTION_PASSWORD_EXPIRING&quot;"
+ static="true"
+ final="true"
+ deprecated="not deprecated"
+ visibility="public"
+>
+</field>
 <field name="ACTION_PASSWORD_FAILED"
  type="java.lang.String"
  transient="false"
@@ -34688,6 +34725,32 @@
 <parameter name="admin" type="android.content.ComponentName">
 </parameter>
 </method>
+<method name="getPasswordExpiration"
+ return="long"
+ 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="getPasswordExpirationTimeout"
+ return="long"
+ 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="getPasswordHistoryLength"
  return="int"
  abstract="false"
@@ -34928,6 +34991,21 @@
 <parameter name="timeMs" type="long">
 </parameter>
 </method>
+<method name="setPasswordExpirationTimeout"
+ 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="timeout" type="long">
+</parameter>
+</method>
 <method name="setPasswordHistoryLength"
  return="void"
  abstract="false"
@@ -246305,7 +246383,7 @@
  deprecated="not deprecated"
  visibility="public"
 >
-<parameter name="arg0" type="T">
+<parameter name="t" type="T">
 </parameter>
 </method>
 </interface>
diff --git a/core/java/android/app/admin/DeviceAdminInfo.java b/core/java/android/app/admin/DeviceAdminInfo.java
index 2237c82..2bb0e33 100644
--- a/core/java/android/app/admin/DeviceAdminInfo.java
+++ b/core/java/android/app/admin/DeviceAdminInfo.java
@@ -112,6 +112,15 @@
      */
     public static final int USES_POLICY_SETS_GLOBAL_PROXY = 5;
 
+    /**
+     * A type of policy that this device admin can use: force the user to
+     * change their password after an administrator-defined time limit.
+     *
+     * <p>To control this policy, the device admin must have an "expire-password"
+     * tag in the "uses-policies" section of its meta-data.
+     */
+    public static final int USES_POLICY_EXPIRE_PASSWORD = 6;
+
     /** @hide */
     public static class PolicyInfo {
         public final int ident;
@@ -150,7 +159,10 @@
         sPoliciesDisplayOrder.add(new PolicyInfo(USES_POLICY_SETS_GLOBAL_PROXY, "set-global-proxy",
                 com.android.internal.R.string.policylab_setGlobalProxy,
                 com.android.internal.R.string.policydesc_setGlobalProxy));
-        
+        sPoliciesDisplayOrder.add(new PolicyInfo(USES_POLICY_EXPIRE_PASSWORD, "expire-password",
+                com.android.internal.R.string.policylab_expirePassword,
+                com.android.internal.R.string.policydesc_expirePassword));
+
         for (int i=0; i<sPoliciesDisplayOrder.size(); i++) {
             PolicyInfo pi = sPoliciesDisplayOrder.get(i);
             sRevKnownPolicies.put(pi.ident, pi);
diff --git a/core/java/android/app/admin/DeviceAdminReceiver.java b/core/java/android/app/admin/DeviceAdminReceiver.java
index b4dd9e7..eccd7c9 100644
--- a/core/java/android/app/admin/DeviceAdminReceiver.java
+++ b/core/java/android/app/admin/DeviceAdminReceiver.java
@@ -145,7 +145,19 @@
     @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
     public static final String ACTION_PASSWORD_SUCCEEDED
             = "android.app.action.ACTION_PASSWORD_SUCCEEDED";
-    
+
+    /**
+     * Action periodically sent to a device administrator when the device password
+     * is expiring. 
+     *
+     * <p>The calling device admin must have requested
+     * {@link DeviceAdminInfo#USES_POLICY_EXPIRE_PASSWORD} to receive
+     * this broadcast.
+     */
+    @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+    public static final String ACTION_PASSWORD_EXPIRING
+            = "android.app.action.ACTION_PASSWORD_EXPIRING";
+
     /**
      * Name under which an DevicePolicy component publishes information
      * about itself.  This meta-data must reference an XML resource containing
@@ -251,7 +263,28 @@
      */
     public void onPasswordSucceeded(Context context, Intent intent) {
     }
-    
+
+    /**
+     * Called periodically when the password is about to expire or has expired.  It will typically
+     * be called on device boot, once per day before the password expires and at the time when it
+     * expires.
+     *
+     * <p>If the password is not updated by the user, this method will continue to be called
+     * once per day until the password is changed or the device admin disables password expiration.
+     *
+     * <p>The admin will typically post a notification requesting the user to change their password
+     * in response to this call. The actual password expiration time can be obtained by calling
+     * {@link DevicePolicyManager#getPasswordExpiration(ComponentName) }
+     *
+     * <p>The admin should be sure to take down any notifications it posted in response to this call
+     * when it receives {@link DeviceAdminReceiver#onPasswordChanged(Context, Intent) }.
+     *
+     * @param context The running context as per {@link #onReceive}.
+     * @param intent The received intent as per {@link #onReceive}.
+     */
+    public void onPasswordExpiring(Context context, Intent intent) {
+    }
+
     /**
      * Intercept standard device administrator broadcasts.  Implementations
      * should not override this method; it is better to implement the
@@ -276,6 +309,8 @@
             }
         } else if (ACTION_DEVICE_ADMIN_DISABLED.equals(action)) {
             onDisabled(context, intent);
+        } else if (ACTION_PASSWORD_EXPIRING.equals(action)) {
+            onPasswordExpiring(context, intent);
         }
     }
 }
diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java
index ca27010..a18fdac 100644
--- a/core/java/android/app/admin/DevicePolicyManager.java
+++ b/core/java/android/app/admin/DevicePolicyManager.java
@@ -681,6 +681,73 @@
     }
 
     /**
+     * Called by a device admin to set the password expiration timeout. Calling this method
+     * will restart the countdown for password expiration for the given admin, as will changing
+     * the device password (for all admins).
+     *
+     * <p>The provided timeout is the time delta in ms and will be added to the current time.
+     * For example, to have the password expire 5 days from now, timeout would be
+     * 5 * 86400 * 1000 = 432000000 ms for timeout.
+     *
+     * <p>To disable password expiration, a value of 0 may be used for timeout.
+     *
+     * <p>Timeout must be at least 1 day or IllegalArgumentException will be thrown.
+     *
+     * <p>The calling device admin must have requested
+     * {@link DeviceAdminInfo#USES_POLICY_EXPIRE_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 timeout The limit (in ms) that a password can remain in effect. A value of 0
+     *        means there is no restriction (unlimited).
+     */
+    public void setPasswordExpirationTimeout(ComponentName admin, long timeout) {
+        if (mService != null) {
+            try {
+                mService.setPasswordExpirationTimeout(admin, timeout);
+            } catch (RemoteException e) {
+                Log.w(TAG, "Failed talking with device policy service", e);
+            }
+        }
+    }
+
+    /**
+     * Get the current password expiration timeout for the given admin or the aggregate
+     * of all admins if admin is null.
+     *
+     * @param admin The name of the admin component to check, or null to aggregate all admins.
+     * @return The timeout for the given admin or the minimum of all timeouts
+     */
+    public long getPasswordExpirationTimeout(ComponentName admin) {
+        if (mService != null) {
+            try {
+                return mService.getPasswordExpirationTimeout(admin);
+            } catch (RemoteException e) {
+                Log.w(TAG, "Failed talking with device policy service", e);
+            }
+        }
+        return 0;
+    }
+
+    /**
+     * Get the current password expiration time for the given admin or an aggregate of
+     * all admins if admin is null.
+     *
+     * @param admin The name of the admin component to check, or null to aggregate all admins.
+     * @return The password expiration time, in ms.
+     */
+    public long getPasswordExpiration(ComponentName admin) {
+        if (mService != null) {
+            try {
+                return mService.getPasswordExpiration(admin);
+            } catch (RemoteException e) {
+                Log.w(TAG, "Failed talking with device policy service", e);
+            }
+        }
+        return 0;
+    }
+
+    /**
      * Retrieve the current password history length for all admins
      * or a particular one.
      * @param admin The name of the admin component to check, or null to aggregate
diff --git a/core/java/android/app/admin/IDevicePolicyManager.aidl b/core/java/android/app/admin/IDevicePolicyManager.aidl
index 3fcd6fc..7acc83e 100644
--- a/core/java/android/app/admin/IDevicePolicyManager.aidl
+++ b/core/java/android/app/admin/IDevicePolicyManager.aidl
@@ -52,6 +52,11 @@
     void setPasswordHistoryLength(in ComponentName who, int length);
     int getPasswordHistoryLength(in ComponentName who);
 
+    void setPasswordExpirationTimeout(in ComponentName who, long expiration);
+    long getPasswordExpirationTimeout(in ComponentName who);
+
+    long getPasswordExpiration(in ComponentName who);
+
     boolean isActivePasswordSufficient();
     int getCurrentFailedPasswordAttempts();
     
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index 3b01890..64cd00a 100755
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -1396,6 +1396,11 @@
     <string name="policydesc_setGlobalProxy">Set the device global proxy
         to be used while policy is enabled. Only the first device admin
         sets the effective global proxy.</string>
+    <!-- Title of policy access to enforce password expiration [CHAR LIMIT=30]-->
+    <string name="policylab_expirePassword">Set password expiration</string>
+    <!-- Description of policy access to enforce password expiration [CHAR LIMIT=110]-->
+    <string name="policydesc_expirePassword">Control how long before lockscreen password needs to be
+    changed</string>
 
     <!-- The order of these is important, don't reorder without changing Contacts.java --> <skip />
     <!-- Phone number types from android.provider.Contacts. This could be used when adding a new phone number for a contact, for example. -->
diff --git a/services/java/com/android/server/DevicePolicyManagerService.java b/services/java/com/android/server/DevicePolicyManagerService.java
index 68aa8e3..3dcad38 100644
--- a/services/java/com/android/server/DevicePolicyManagerService.java
+++ b/services/java/com/android/server/DevicePolicyManagerService.java
@@ -28,6 +28,8 @@
 import org.xmlpull.v1.XmlSerializer;
 
 import android.app.Activity;
+import android.app.AlarmManager;
+import android.app.PendingIntent;
 import android.app.admin.DeviceAdminInfo;
 import android.app.admin.DeviceAdminReceiver;
 import android.app.admin.DevicePolicyManager;
@@ -37,10 +39,13 @@
 import android.content.ContentResolver;
 import android.content.Context;
 import android.content.Intent;
+import android.content.IntentFilter;
 import android.content.pm.PackageManager;
 import android.content.pm.ResolveInfo;
 import android.content.pm.PackageManager.NameNotFoundException;
+import android.net.ConnectivityManager;
 import android.os.Binder;
+import android.os.Handler;
 import android.os.IBinder;
 import android.os.IPowerManager;
 import android.os.PowerManager;
@@ -49,7 +54,6 @@
 import android.os.RemoteException;
 import android.os.ServiceManager;
 import android.os.SystemClock;
-import android.net.Proxy;
 import android.provider.Settings;
 import android.util.Slog;
 import android.util.PrintWriterPrinter;
@@ -64,8 +68,9 @@
 import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.PrintWriter;
-import java.net.InetSocketAddress;
+import java.text.DateFormat;
 import java.util.ArrayList;
+import java.util.Date;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Set;
@@ -74,8 +79,20 @@
  * Implementation of the device policy APIs.
  */
 public class DevicePolicyManagerService extends IDevicePolicyManager.Stub {
+    private static final int REQUEST_EXPIRE_PASSWORD = 5571;
+
     static final String TAG = "DevicePolicyManagerService";
 
+    private static final long EXPIRATION_GRACE_PERIOD_MS = 5 * 86400 * 1000; // 5 days, in ms
+
+    protected static final String ACTION_EXPIRED_PASSWORD_NOTIFICATION
+            = "com.android.server.ACTION_EXPIRED_PASSWORD_NOTIFICATION";
+
+    private static final long MS_PER_DAY = 86400 * 1000;
+    private static final long MS_PER_HOUR = 3600 * 1000;
+    private static final long MS_PER_MINUTE = 60 * 1000;
+    private static final long MIN_TIMEOUT = 86400 * 1000; // minimum expiration timeout is 1 day
+
     final Context mContext;
     final MyPackageMonitor mMonitor;
     final PowerManager.WakeLock mWakeLock;
@@ -93,12 +110,29 @@
     int mFailedPasswordAttempts = 0;
 
     int mPasswordOwner = -1;
+    Handler mHandler = new Handler();
 
     final HashMap<ComponentName, ActiveAdmin> mAdminMap
             = new HashMap<ComponentName, ActiveAdmin>();
     final ArrayList<ActiveAdmin> mAdminList
             = new ArrayList<ActiveAdmin>();
 
+    BroadcastReceiver mReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            String action = intent.getAction();
+            if (Intent.ACTION_BOOT_COMPLETED.equals(action)
+                    || ACTION_EXPIRED_PASSWORD_NOTIFICATION.equals(action)) {
+                Slog.v(TAG, "Sending password expiration notifications for action " + action);
+                mHandler.post(new Runnable() {
+                    public void run() {
+                        handlePasswordExpirationNotification();
+                    }
+                });
+            }
+        }
+    };
+
     static class ActiveAdmin {
         final DeviceAdminInfo info;
 
@@ -113,6 +147,8 @@
         int minimumPasswordNonLetter = 0;
         long maximumTimeToUnlock = 0;
         int maximumFailedPasswordsForWipe = 0;
+        long passwordExpirationTimeout = 0L;
+        long passwordExpirationDate = 0L;
 
         // TODO: review implementation decisions with frameworks team
         boolean specifiesGlobalProxy = false;
@@ -200,6 +236,16 @@
                     out.endTag(null, "global-proxy-exclusion-list");
                 }
             }
+            if (passwordExpirationTimeout != 0L) {
+                out.startTag(null, "password-expiration-timeout");
+                out.attribute(null, "value", Long.toString(passwordExpirationTimeout));
+                out.endTag(null, "password-expiration-timeout");
+            }
+            if (passwordExpirationDate != 0L) {
+                out.startTag(null, "password-expiration-date");
+                out.attribute(null, "value", Long.toString(passwordExpirationDate));
+                out.endTag(null, "password-expiration-date");
+            }
         }
 
         void readFromXml(XmlPullParser parser)
@@ -256,6 +302,12 @@
                 } else if ("global-proxy-exclusion-list".equals(tag)) {
                     globalProxyExclusionList =
                         parser.getAttributeValue(null, "value");
+                } else if ("password-expiration-timeout".equals(tag)) {
+                    passwordExpirationTimeout = Long.parseLong(
+                            parser.getAttributeValue(null, "value"));
+                } else if ("password-expiration-date".equals(tag)) {
+                    passwordExpirationDate = Long.parseLong(
+                            parser.getAttributeValue(null, "value"));
                 } else {
                     Slog.w(TAG, "Unknown admin tag: " + tag);
                 }
@@ -296,6 +348,10 @@
                     pw.println(maximumFailedPasswordsForWipe);
             pw.print(prefix); pw.print("specifiesGlobalProxy=");
                     pw.println(specifiesGlobalProxy);
+            pw.print(prefix); pw.print("passwordExpirationTimeout=");
+                    pw.println(passwordExpirationTimeout);
+            pw.print(prefix); pw.print("passwordExpirationDate=");
+                    pw.println(passwordExpirationDate);
             if (globalProxySpec != null) {
                 pw.print(prefix); pw.print("globalProxySpec=");
                         pw.println(globalProxySpec);
@@ -348,6 +404,38 @@
         mMonitor.register(context, true);
         mWakeLock = ((PowerManager)context.getSystemService(Context.POWER_SERVICE))
                 .newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "DPM");
+        IntentFilter filter = new IntentFilter();
+        filter.addAction(Intent.ACTION_BOOT_COMPLETED);
+        filter.addAction(ACTION_EXPIRED_PASSWORD_NOTIFICATION);
+        context.registerReceiver(mReceiver, filter);
+    }
+
+    static String countdownString(long time) {
+        long days = time / MS_PER_DAY;
+        long hours = (time / MS_PER_HOUR) % 24;
+        long minutes = (time / MS_PER_MINUTE) % 60;
+        return days + "d" + hours + "h" + minutes + "m";
+    }
+
+    protected void setExpirationAlarmCheckLocked(Context context) {
+        final long expiration = getPasswordExpirationLocked(null);
+        final long now = System.currentTimeMillis();
+        final long timeToExpire = expiration - now;
+        final long alarmTime;
+        if (timeToExpire > 0L && timeToExpire < MS_PER_DAY) {
+            // Next expiration is less than a day, set alarm for exact expiration time
+            alarmTime = now + timeToExpire;
+        } else {
+            // Check again in 24 hours...
+            alarmTime = now + MS_PER_DAY;
+        }
+
+        AlarmManager am = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
+        PendingIntent pi = PendingIntent.getBroadcast(context, REQUEST_EXPIRE_PASSWORD,
+                new Intent(ACTION_EXPIRED_PASSWORD_NOTIFICATION),
+                PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_UPDATE_CURRENT);
+        am.cancel(pi);
+        am.set(AlarmManager.RTC, alarmTime, pi);
     }
 
     private IPowerManager getIPowerManager() {
@@ -402,6 +490,9 @@
     void sendAdminCommandLocked(ActiveAdmin admin, String action) {
         Intent intent = new Intent(action);
         intent.setComponent(admin.info.getComponent());
+        if (action.equals(DeviceAdminReceiver.ACTION_PASSWORD_EXPIRING)) {
+            intent.putExtra("expiration", admin.passwordExpirationDate);
+        }
         mContext.sendBroadcast(intent);
     }
 
@@ -696,6 +787,26 @@
         }
     }
 
+    private void handlePasswordExpirationNotification() {
+        synchronized (this) {
+            final long now = System.currentTimeMillis();
+            final int N = mAdminList.size();
+            if (N <= 0) {
+                return;
+            }
+            for (int i=0; i < N; i++) {
+                ActiveAdmin admin = mAdminList.get(i);
+                if (admin.info.usesPolicy(DeviceAdminInfo.USES_POLICY_EXPIRE_PASSWORD)
+                        && admin.passwordExpirationTimeout > 0L
+                        && admin.passwordExpirationDate > 0L
+                        && now > admin.passwordExpirationDate - EXPIRATION_GRACE_PERIOD_MS) {
+                    sendAdminCommandLocked(admin, DeviceAdminReceiver.ACTION_PASSWORD_EXPIRING);
+                }
+            }
+            setExpirationAlarmCheckLocked(mContext);
+        }
+    }
+
     public void setActiveAdmin(ComponentName adminReceiver) {
         mContext.enforceCallingOrSelfPermission(
                 android.Manifest.permission.BIND_DEVICE_ADMIN, null);
@@ -877,6 +988,74 @@
         }
     }
 
+    public void setPasswordExpirationTimeout(ComponentName who, long timeout) {
+        synchronized (this) {
+            if (who == null) {
+                throw new NullPointerException("ComponentName is null");
+            }
+            if (timeout != 0L && timeout < MIN_TIMEOUT) {
+                throw new IllegalArgumentException("Timeout must be > " + MIN_TIMEOUT + "ms");
+            }
+            ActiveAdmin ap = getActiveAdminForCallerLocked(who,
+                    DeviceAdminInfo.USES_POLICY_EXPIRE_PASSWORD);
+            // Calling this API automatically bumps the expiration date
+            final long expiration = timeout > 0L ? (timeout + System.currentTimeMillis()) : 0L;
+            ap.passwordExpirationDate = expiration;
+            ap.passwordExpirationTimeout = timeout;
+            if (timeout > 0L) {
+                Slog.w(TAG, "setPasswordExpiration(): password will expire on "
+                        + DateFormat.getDateTimeInstance(DateFormat.DEFAULT, DateFormat.DEFAULT)
+                        .format(new Date(expiration)));
+            }
+            saveSettingsLocked();
+            setExpirationAlarmCheckLocked(mContext); // in case this is the first one
+        }
+    }
+
+    public long getPasswordExpirationTimeout(ComponentName who) {
+        synchronized (this) {
+            long timeout = 0L;
+            if (who != null) {
+                ActiveAdmin admin = getActiveAdminUncheckedLocked(who);
+                return admin != null ? admin.passwordExpirationTimeout : timeout;
+            }
+
+            final int N = mAdminList.size();
+            for (int i = 0; i < N; i++) {
+                ActiveAdmin admin = mAdminList.get(i);
+                if (timeout == 0L || (admin.passwordExpirationTimeout != 0L
+                        && timeout > admin.passwordExpirationTimeout)) {
+                    timeout = admin.passwordExpirationTimeout;
+                }
+            }
+            return timeout;
+        }
+    }
+
+    private long getPasswordExpirationLocked(ComponentName who) {
+        long timeout = 0L;
+        if (who != null) {
+            ActiveAdmin admin = getActiveAdminUncheckedLocked(who);
+            return admin != null ? admin.passwordExpirationDate : timeout;
+        }
+
+        final int N = mAdminList.size();
+        for (int i = 0; i < N; i++) {
+            ActiveAdmin admin = mAdminList.get(i);
+            if (timeout == 0L || (admin.passwordExpirationDate != 0
+                    && timeout > admin.passwordExpirationDate)) {
+                timeout = admin.passwordExpirationDate;
+            }
+        }
+        return timeout;
+    }
+
+    public long getPasswordExpiration(ComponentName who) {
+        synchronized (this) {
+            return getPasswordExpirationLocked(who);
+        }
+    }
+
     public void setPasswordMinimumUpperCase(ComponentName who, int length) {
         synchronized (this) {
             if (who == null) {
@@ -1431,6 +1610,7 @@
                     mActivePasswordNonLetter = nonletter;
                     mFailedPasswordAttempts = 0;
                     saveSettingsLocked();
+                    updatePasswordExpirationsLocked();
                     sendAdminCommandLocked(DeviceAdminReceiver.ACTION_PASSWORD_CHANGED,
                             DeviceAdminInfo.USES_POLICY_LIMIT_PASSWORD);
                 } finally {
@@ -1440,6 +1620,20 @@
         }
     }
 
+    private void updatePasswordExpirationsLocked() {
+        final int N = mAdminList.size();
+        if (N > 0) {
+            for (int i=0; i<N; i++) {
+                ActiveAdmin admin = mAdminList.get(i);
+                if (admin.info.usesPolicy(DeviceAdminInfo.USES_POLICY_EXPIRE_PASSWORD)) {
+                    admin.passwordExpirationDate = System.currentTimeMillis()
+                            + admin.passwordExpirationTimeout;
+                }
+            }
+            saveSettingsLocked();
+        }
+    }
+
     public void reportFailedPasswordAttempt() {
         mContext.enforceCallingOrSelfPermission(
                 android.Manifest.permission.BIND_DEVICE_ADMIN, null);