diff --git a/api/current.xml b/api/current.xml index bad739f00f509..6daeca9d2aa4a 100644 --- a/api/current.xml +++ b/api/current.xml @@ -34318,6 +34318,17 @@ visibility="public" > + + + + + + + + + + + + + + + + + + + + + + + + - + diff --git a/core/java/android/app/admin/DeviceAdminInfo.java b/core/java/android/app/admin/DeviceAdminInfo.java index 2237c821b234d..2bb0e33b2b3fd 100644 --- a/core/java/android/app/admin/DeviceAdminInfo.java +++ b/core/java/android/app/admin/DeviceAdminInfo.java @@ -112,6 +112,15 @@ public final class DeviceAdminInfo implements Parcelable { */ 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. + * + *

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 @@ public final class DeviceAdminInfo implements Parcelable { 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; iThe 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 class DeviceAdminReceiver extends BroadcastReceiver { */ 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. + * + *

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. + * + *

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) } + * + *

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 @@ public class DeviceAdminReceiver extends BroadcastReceiver { } } 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 ca270103b172d..a18fdacca6181 100644 --- a/core/java/android/app/admin/DevicePolicyManager.java +++ b/core/java/android/app/admin/DevicePolicyManager.java @@ -680,6 +680,73 @@ public class DevicePolicyManager { } } + /** + * 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). + * + *

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. + * + *

To disable password expiration, a value of 0 may be used for timeout. + * + *

Timeout must be at least 1 day or IllegalArgumentException will be thrown. + * + *

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. diff --git a/core/java/android/app/admin/IDevicePolicyManager.aidl b/core/java/android/app/admin/IDevicePolicyManager.aidl index 3fcd6fce5140b..7acc83e2d375a 100644 --- a/core/java/android/app/admin/IDevicePolicyManager.aidl +++ b/core/java/android/app/admin/IDevicePolicyManager.aidl @@ -52,6 +52,11 @@ interface IDevicePolicyManager { 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 3b01890b22b05..64cd00a6678ad 100755 --- a/core/res/res/values/strings.xml +++ b/core/res/res/values/strings.xml @@ -1396,6 +1396,11 @@ Set the device global proxy to be used while policy is enabled. Only the first device admin sets the effective global proxy. + + Set password expiration + + Control how long before lockscreen password needs to be + changed diff --git a/services/java/com/android/server/DevicePolicyManagerService.java b/services/java/com/android/server/DevicePolicyManagerService.java index 68aa8e37ba7c7..3dcad38558792 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.XmlPullParserException; 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.ComponentName; 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.RemoteCallback; 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.FileNotFoundException; 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 @@ import java.util.Set; * 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 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { int mFailedPasswordAttempts = 0; int mPasswordOwner = -1; + Handler mHandler = new Handler(); final HashMap mAdminMap = new HashMap(); final ArrayList mAdminList = new ArrayList(); + 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 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { 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 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { 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 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { } 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 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { 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 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { 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 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { 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 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { } } + 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 class DevicePolicyManagerService extends IDevicePolicyManager.Stub { } } + 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 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { mActivePasswordNonLetter = nonletter; mFailedPasswordAttempts = 0; saveSettingsLocked(); + updatePasswordExpirationsLocked(); sendAdminCommandLocked(DeviceAdminReceiver.ACTION_PASSWORD_CHANGED, DeviceAdminInfo.USES_POLICY_LIMIT_PASSWORD); } finally { @@ -1440,6 +1620,20 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { } } + private void updatePasswordExpirationsLocked() { + final int N = mAdminList.size(); + if (N > 0) { + for (int i=0; i