blob: f571e8ad42d0a5cf0631213561848870ede5e9dc [file] [log] [blame]
Andrew Stadler345fb8b2010-01-26 17:24:15 -08001/*
2 * Copyright (C) 2010 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.email;
18
Andy Stadlere7f4d3e2010-12-08 16:06:16 -080019import android.app.admin.DeviceAdminInfo;
Dianne Hackborn6d001622010-02-26 17:26:45 -080020import android.app.admin.DeviceAdminReceiver;
21import android.app.admin.DevicePolicyManager;
Andrew Stadlerd6286082010-02-01 16:48:16 -080022import android.content.ComponentName;
Marc Blank02d59d22010-10-25 11:49:29 -070023import android.content.ContentResolver;
Andrew Stadler3d2b3b32010-02-05 11:10:39 -080024import android.content.ContentValues;
Andrew Stadler345fb8b2010-01-26 17:24:15 -080025import android.content.Context;
26import android.content.Intent;
27import android.database.Cursor;
Andrew Stadler50d16102010-02-09 11:01:01 -080028import android.util.Log;
Andrew Stadler345fb8b2010-01-26 17:24:15 -080029
Marc Blankc6df1d62011-07-19 14:09:11 -070030import com.android.email.service.EmailBroadcastProcessorService;
31import com.android.emailcommon.Logging;
32import com.android.emailcommon.provider.Account;
33import com.android.emailcommon.provider.EmailContent;
34import com.android.emailcommon.provider.EmailContent.AccountColumns;
35import com.android.emailcommon.provider.EmailContent.PolicyColumns;
36import com.android.emailcommon.provider.Policy;
37import com.android.emailcommon.utility.Utility;
38import com.google.common.annotations.VisibleForTesting;
39
Andrew Stadler345fb8b2010-01-26 17:24:15 -080040/**
Andrew Stadlerd71d0b22010-02-09 17:24:55 -080041 * Utility functions to support reading and writing security policies, and handshaking the device
42 * into and out of various security states.
Andrew Stadler345fb8b2010-01-26 17:24:15 -080043 */
44public class SecurityPolicy {
Marc Blankaeee10e2011-04-27 17:12:06 -070045 private static final String TAG = "Email/SecurityPolicy";
Andrew Stadler345fb8b2010-01-26 17:24:15 -080046 private static SecurityPolicy sInstance = null;
47 private Context mContext;
Andrew Stadlerd6286082010-02-01 16:48:16 -080048 private DevicePolicyManager mDPM;
Ben Komalod09cff02011-05-06 14:57:47 -070049 private final ComponentName mAdminName;
Marc Blankaeee10e2011-04-27 17:12:06 -070050 private Policy mAggregatePolicy;
Andrew Stadler2a5eeea2010-02-08 17:42:42 -080051
Andy Stadlera2269e82010-12-30 00:16:55 -080052 // Messages used for DevicePolicyManager callbacks
53 private static final int DEVICE_ADMIN_MESSAGE_ENABLED = 1;
54 private static final int DEVICE_ADMIN_MESSAGE_DISABLED = 2;
55 private static final int DEVICE_ADMIN_MESSAGE_PASSWORD_CHANGED = 3;
56 private static final int DEVICE_ADMIN_MESSAGE_PASSWORD_EXPIRING = 4;
57
Marc Blankaeee10e2011-04-27 17:12:06 -070058 private static final String HAS_PASSWORD_EXPIRATION =
59 PolicyColumns.PASSWORD_EXPIRATION_DAYS + ">0";
60
Andrew Stadler345fb8b2010-01-26 17:24:15 -080061 /**
62 * Get the security policy instance
63 */
64 public synchronized static SecurityPolicy getInstance(Context context) {
65 if (sInstance == null) {
Andy Stadlera2269e82010-12-30 00:16:55 -080066 sInstance = new SecurityPolicy(context.getApplicationContext());
Andrew Stadler345fb8b2010-01-26 17:24:15 -080067 }
68 return sInstance;
69 }
70
71 /**
72 * Private constructor (one time only)
73 */
74 private SecurityPolicy(Context context) {
Makoto Onuki968be442010-05-20 16:11:26 -070075 mContext = context.getApplicationContext();
Andrew Stadlerd6286082010-02-01 16:48:16 -080076 mDPM = null;
77 mAdminName = new ComponentName(context, PolicyAdmin.class);
78 mAggregatePolicy = null;
Andrew Stadler345fb8b2010-01-26 17:24:15 -080079 }
80
81 /**
82 * For testing only: Inject context into already-created instance
83 */
84 /* package */ void setContext(Context context) {
85 mContext = context;
86 }
87
88 /**
89 * Compute the aggregate policy for all accounts that require it, and record it.
90 *
91 * The business logic is as follows:
92 * min password length take the max
93 * password mode take the max (strongest mode)
94 * max password fails take the min
95 * max screen lock time take the min
96 * require remote wipe take the max (logical or)
Marc Blank9b4988d2010-06-09 16:18:57 -070097 * password history take the max (strongest mode)
Andy Stadler1ca111c2010-12-01 12:58:36 -080098 * password expiration take the min (strongest mode)
Marc Blank9b4988d2010-06-09 16:18:57 -070099 * password complex chars take the max (strongest mode)
Andy Stadler469f2982011-01-13 13:12:55 -0800100 * encryption take the max (logical or)
Makoto Onuki968be442010-05-20 16:11:26 -0700101 *
Andrew Stadlerd6286082010-02-01 16:48:16 -0800102 * @return a policy representing the strongest aggregate. If no policy sets are defined,
103 * a lightweight "nothing required" policy will be returned. Never null.
Andrew Stadler345fb8b2010-01-26 17:24:15 -0800104 */
Ben Komalod09cff02011-05-06 14:57:47 -0700105 @VisibleForTesting
106 Policy computeAggregatePolicy() {
Andrew Stadler345fb8b2010-01-26 17:24:15 -0800107 boolean policiesFound = false;
Marc Blankaeee10e2011-04-27 17:12:06 -0700108 Policy aggregate = new Policy();
109 aggregate.mPasswordMinLength = Integer.MIN_VALUE;
110 aggregate.mPasswordMode = Integer.MIN_VALUE;
111 aggregate.mPasswordMaxFails = Integer.MAX_VALUE;
112 aggregate.mPasswordHistory = Integer.MIN_VALUE;
113 aggregate.mPasswordExpirationDays = Integer.MAX_VALUE;
114 aggregate.mPasswordComplexChars = Integer.MIN_VALUE;
115 aggregate.mMaxScreenLockTime = Integer.MAX_VALUE;
116 aggregate.mRequireRemoteWipe = false;
117 aggregate.mRequireEncryption = false;
Ben Komaloe76962b2011-07-01 12:34:03 -0700118
119 // This can never be supported at this time. It exists only for historic reasons where
120 // this was able to be supported prior to the introduction of proper removable storage
121 // support for external storage.
Marc Blankaeee10e2011-04-27 17:12:06 -0700122 aggregate.mRequireEncryptionExternal = false;
Andrew Stadler345fb8b2010-01-26 17:24:15 -0800123
Marc Blankaeee10e2011-04-27 17:12:06 -0700124 Cursor c = mContext.getContentResolver().query(Policy.CONTENT_URI,
125 Policy.CONTENT_PROJECTION, null, null, null);
126 Policy policy = new Policy();
Andrew Stadler345fb8b2010-01-26 17:24:15 -0800127 try {
128 while (c.moveToNext()) {
Marc Blankaeee10e2011-04-27 17:12:06 -0700129 policy.restore(c);
130 if (Email.DEBUG) {
131 Log.d(TAG, "Aggregate from: " + policy);
Andrew Stadler345fb8b2010-01-26 17:24:15 -0800132 }
Marc Blankaeee10e2011-04-27 17:12:06 -0700133 aggregate.mPasswordMinLength =
134 Math.max(policy.mPasswordMinLength, aggregate.mPasswordMinLength);
135 aggregate.mPasswordMode = Math.max(policy.mPasswordMode, aggregate.mPasswordMode);
136 if (policy.mPasswordMaxFails > 0) {
137 aggregate.mPasswordMaxFails =
138 Math.min(policy.mPasswordMaxFails, aggregate.mPasswordMaxFails);
139 }
140 if (policy.mMaxScreenLockTime > 0) {
141 aggregate.mMaxScreenLockTime = Math.min(policy.mMaxScreenLockTime,
142 aggregate.mMaxScreenLockTime);
143 }
144 if (policy.mPasswordHistory > 0) {
145 aggregate.mPasswordHistory =
146 Math.max(policy.mPasswordHistory, aggregate.mPasswordHistory);
147 }
148 if (policy.mPasswordExpirationDays > 0) {
149 aggregate.mPasswordExpirationDays =
150 Math.min(policy.mPasswordExpirationDays, aggregate.mPasswordExpirationDays);
151 }
152 if (policy.mPasswordComplexChars > 0) {
153 aggregate.mPasswordComplexChars = Math.max(policy.mPasswordComplexChars,
154 aggregate.mPasswordComplexChars);
155 }
156 aggregate.mRequireRemoteWipe |= policy.mRequireRemoteWipe;
157 aggregate.mRequireEncryption |= policy.mRequireEncryption;
Ben Komalod09cff02011-05-06 14:57:47 -0700158 aggregate.mDontAllowCamera |= policy.mDontAllowCamera;
Marc Blankaeee10e2011-04-27 17:12:06 -0700159 policiesFound = true;
Andrew Stadler345fb8b2010-01-26 17:24:15 -0800160 }
161 } finally {
162 c.close();
163 }
164 if (policiesFound) {
Andrew Stadler3d2b3b32010-02-05 11:10:39 -0800165 // final cleanup pass converts any untouched min/max values to zero (not specified)
Marc Blankaeee10e2011-04-27 17:12:06 -0700166 if (aggregate.mPasswordMinLength == Integer.MIN_VALUE) aggregate.mPasswordMinLength = 0;
167 if (aggregate.mPasswordMode == Integer.MIN_VALUE) aggregate.mPasswordMode = 0;
168 if (aggregate.mPasswordMaxFails == Integer.MAX_VALUE) aggregate.mPasswordMaxFails = 0;
169 if (aggregate.mMaxScreenLockTime == Integer.MAX_VALUE) aggregate.mMaxScreenLockTime = 0;
170 if (aggregate.mPasswordHistory == Integer.MIN_VALUE) aggregate.mPasswordHistory = 0;
171 if (aggregate.mPasswordExpirationDays == Integer.MAX_VALUE)
172 aggregate.mPasswordExpirationDays = 0;
173 if (aggregate.mPasswordComplexChars == Integer.MIN_VALUE)
174 aggregate.mPasswordComplexChars = 0;
175 if (Email.DEBUG) {
176 Log.d(TAG, "Calculated Aggregate: " + aggregate);
177 }
178 return aggregate;
Andrew Stadler345fb8b2010-01-26 17:24:15 -0800179 }
Marc Blankaeee10e2011-04-27 17:12:06 -0700180 if (Email.DEBUG) {
181 Log.d(TAG, "Calculated Aggregate: no policy");
182 }
183 return Policy.NO_POLICY;
Andrew Stadler345fb8b2010-01-26 17:24:15 -0800184 }
185
186 /**
Andrew Stadler50d16102010-02-09 11:01:01 -0800187 * Return updated aggregate policy, from cached value if possible
188 */
Marc Blankaeee10e2011-04-27 17:12:06 -0700189 public synchronized Policy getAggregatePolicy() {
Andrew Stadler50d16102010-02-09 11:01:01 -0800190 if (mAggregatePolicy == null) {
191 mAggregatePolicy = computeAggregatePolicy();
192 }
193 return mAggregatePolicy;
194 }
195
196 /**
Andrew Stadlerd6286082010-02-01 16:48:16 -0800197 * Get the dpm. This mainly allows us to make some utility calls without it, for testing.
198 */
Andy Stadlera0d08052011-01-19 11:40:48 -0800199 /* package */ synchronized DevicePolicyManager getDPM() {
Andrew Stadlerd6286082010-02-01 16:48:16 -0800200 if (mDPM == null) {
201 mDPM = (DevicePolicyManager) mContext.getSystemService(Context.DEVICE_POLICY_SERVICE);
202 }
203 return mDPM;
204 }
205
206 /**
Andrew Stadlerd6286082010-02-01 16:48:16 -0800207 * API: Report that policies may have been updated due to rewriting values in an Account.
Andrew Stadler50d16102010-02-09 11:01:01 -0800208 * @param accountId the account that has been updated, -1 if unknown/deleted
Andrew Stadlerd6286082010-02-01 16:48:16 -0800209 */
Marc Blankaeee10e2011-04-27 17:12:06 -0700210 public synchronized void policiesUpdated(long accountId) {
Andrew Stadlerd6286082010-02-01 16:48:16 -0800211 mAggregatePolicy = null;
212 }
213
214 /**
Andrew Stadler50d16102010-02-09 11:01:01 -0800215 * API: Report that policies may have been updated *and* the caller vouches that the
216 * change is a reduction in policies. This forces an immediate change to device state.
217 * Typically used when deleting accounts, although we may use it for server-side policy
218 * rollbacks.
219 */
220 public void reducePolicies() {
Marc Blankaeee10e2011-04-27 17:12:06 -0700221 if (Email.DEBUG) {
222 Log.d(TAG, "reducePolicies");
223 }
224 policiesUpdated(-1);
Andrew Stadler50d16102010-02-09 11:01:01 -0800225 setActivePolicies();
226 }
227
228 /**
Andy Stadler469f2982011-01-13 13:12:55 -0800229 * API: Query if the proposed set of policies are supported on the device.
230 *
Ben Komalod09cff02011-05-06 14:57:47 -0700231 * @param policy the polices that were requested
Andy Stadler469f2982011-01-13 13:12:55 -0800232 * @return boolean if supported
233 */
Marc Blankaeee10e2011-04-27 17:12:06 -0700234 public boolean isSupported(Policy policy) {
Andy Stadler469f2982011-01-13 13:12:55 -0800235 // IMPLEMENTATION: At this time, the only policy which might not be supported is
236 // encryption (which requires low-level systems support). Other policies are fully
237 // supported by the framework and do not need to be checked.
Marc Blankaeee10e2011-04-27 17:12:06 -0700238 if (policy.mRequireEncryption) {
Andy Stadlerc2e63832011-01-17 12:54:40 -0800239 int encryptionStatus = getDPM().getStorageEncryptionStatus();
Andy Stadler469f2982011-01-13 13:12:55 -0800240 if (encryptionStatus == DevicePolicyManager.ENCRYPTION_STATUS_UNSUPPORTED) {
241 return false;
242 }
243 }
Ben Komalod09cff02011-05-06 14:57:47 -0700244
245 // If we ever support devices that can't disable cameras for any reason, we should
246 // indicate as such in the mDontAllowCamera policy
247
Andy Stadler469f2982011-01-13 13:12:55 -0800248 return true;
249 }
250
251 /**
Andy Stadlera0d08052011-01-19 11:40:48 -0800252 * API: Remove any unsupported policies
253 *
254 * This is used when we have a set of polices that have been requested, but the server
255 * is willing to allow unsupported policies to be considered optional.
256 *
Ben Komalod09cff02011-05-06 14:57:47 -0700257 * @param policy the polices that were requested
Andy Stadlera0d08052011-01-19 11:40:48 -0800258 * @return the same PolicySet if all are supported; A replacement PolicySet if any
259 * unsupported policies were removed
260 */
Marc Blankaeee10e2011-04-27 17:12:06 -0700261 public Policy clearUnsupportedPolicies(Policy policy) {
Andy Stadlera0d08052011-01-19 11:40:48 -0800262 // IMPLEMENTATION: At this time, the only policy which might not be supported is
263 // encryption (which requires low-level systems support). Other policies are fully
264 // supported by the framework and do not need to be checked.
Marc Blankaeee10e2011-04-27 17:12:06 -0700265 if (policy.mRequireEncryption) {
Andy Stadlera0d08052011-01-19 11:40:48 -0800266 int encryptionStatus = getDPM().getStorageEncryptionStatus();
267 if (encryptionStatus == DevicePolicyManager.ENCRYPTION_STATUS_UNSUPPORTED) {
Marc Blankaeee10e2011-04-27 17:12:06 -0700268 policy.mRequireEncryption = false;
Andy Stadler7fd14be2011-03-02 16:41:05 -0800269 }
270 }
Ben Komalod09cff02011-05-06 14:57:47 -0700271
272 // If we ever support devices that can't disable cameras for any reason, we should
273 // clear the mDontAllowCamera policy
274
Marc Blankaeee10e2011-04-27 17:12:06 -0700275 return policy;
Andy Stadlera0d08052011-01-19 11:40:48 -0800276 }
277
278 /**
Andy Stadler469f2982011-01-13 13:12:55 -0800279 * API: Query used to determine if a given policy is "active" (the device is operating at
280 * the required security level).
281 *
Ben Komalod09cff02011-05-06 14:57:47 -0700282 * @param policy the policies requested, or null to check aggregate stored policies
Andy Stadler469f2982011-01-13 13:12:55 -0800283 * @return true if the requested policies are active, false if not.
284 */
Marc Blankaeee10e2011-04-27 17:12:06 -0700285 public boolean isActive(Policy policy) {
286 int reasons = getInactiveReasons(policy);
287 if (Email.DEBUG && (reasons != 0)) {
288 StringBuilder sb = new StringBuilder("isActive for " + policy + ": ");
289 if (reasons == 0) {
290 sb.append("true");
291 } else {
292 sb.append("FALSE -> ");
293 }
294 if ((reasons & INACTIVE_NEED_ACTIVATION) != 0) {
295 sb.append("no_admin ");
296 }
297 if ((reasons & INACTIVE_NEED_CONFIGURATION) != 0) {
298 sb.append("config ");
299 }
300 if ((reasons & INACTIVE_NEED_PASSWORD) != 0) {
301 sb.append("password ");
302 }
303 if ((reasons & INACTIVE_NEED_ENCRYPTION) != 0) {
304 sb.append("encryption ");
305 }
306 Log.d(TAG, sb.toString());
307 }
Andy Stadler469f2982011-01-13 13:12:55 -0800308 return reasons == 0;
309 }
310
311 /**
312 * Return bits from isActive: Device Policy Manager has not been activated
313 */
314 public final static int INACTIVE_NEED_ACTIVATION = 1;
315
316 /**
317 * Return bits from isActive: Some required configuration is not correct (no user action).
318 */
319 public final static int INACTIVE_NEED_CONFIGURATION = 2;
320
321 /**
322 * Return bits from isActive: Password needs to be set or updated
323 */
324 public final static int INACTIVE_NEED_PASSWORD = 4;
325
326 /**
327 * Return bits from isActive: Encryption has not be enabled
328 */
329 public final static int INACTIVE_NEED_ENCRYPTION = 8;
330
331 /**
Andrew Stadlerd6286082010-02-01 16:48:16 -0800332 * API: Query used to determine if a given policy is "active" (the device is operating at
Andrew Stadler3d2b3b32010-02-05 11:10:39 -0800333 * the required security level).
Andrew Stadler345fb8b2010-01-26 17:24:15 -0800334 *
Andrew Stadler3d2b3b32010-02-05 11:10:39 -0800335 * This can be used when syncing a specific account, by passing a specific set of policies
336 * for that account. Or, it can be used at any time to compare the device
337 * state against the aggregate set of device policies stored in all accounts.
338 *
339 * This method is for queries only, and does not trigger any change in device state.
340 *
Andy Stadler1ca111c2010-12-01 12:58:36 -0800341 * NOTE: If there are multiple accounts with password expiration policies, the device
342 * password will be set to expire in the shortest required interval (most secure). This method
343 * will return 'false' as soon as the password expires - irrespective of which account caused
344 * the expiration. In other words, all accounts (that require expiration) will run/stop
345 * based on the requirements of the account with the shortest interval.
346 *
Ben Komalod09cff02011-05-06 14:57:47 -0700347 * @param policy the policies requested, or null to check aggregate stored policies
Andy Stadler469f2982011-01-13 13:12:55 -0800348 * @return zero if the requested policies are active, non-zero bits indicates that more work
349 * is needed (typically, by the user) before the required security polices are fully active.
Andrew Stadler345fb8b2010-01-26 17:24:15 -0800350 */
Marc Blankaeee10e2011-04-27 17:12:06 -0700351 public int getInactiveReasons(Policy policy) {
Andrew Stadler50d16102010-02-09 11:01:01 -0800352 // select aggregate set if needed
Marc Blankaeee10e2011-04-27 17:12:06 -0700353 if (policy == null) {
354 policy = getAggregatePolicy();
Andrew Stadler50d16102010-02-09 11:01:01 -0800355 }
356 // quick check for the "empty set" of no policies
Marc Blankaeee10e2011-04-27 17:12:06 -0700357 if (policy == Policy.NO_POLICY) {
Andy Stadler469f2982011-01-13 13:12:55 -0800358 return 0;
Andrew Stadler50d16102010-02-09 11:01:01 -0800359 }
Andy Stadler469f2982011-01-13 13:12:55 -0800360 int reasons = 0;
Andrew Stadlerd6286082010-02-01 16:48:16 -0800361 DevicePolicyManager dpm = getDPM();
Andy Stadlere7f4d3e2010-12-08 16:06:16 -0800362 if (isActiveAdmin()) {
Andrew Stadlerd6286082010-02-01 16:48:16 -0800363 // check each policy explicitly
Marc Blankaeee10e2011-04-27 17:12:06 -0700364 if (policy.mPasswordMinLength > 0) {
365 if (dpm.getPasswordMinimumLength(mAdminName) < policy.mPasswordMinLength) {
Andy Stadler469f2982011-01-13 13:12:55 -0800366 reasons |= INACTIVE_NEED_PASSWORD;
Andrew Stadlerd6286082010-02-01 16:48:16 -0800367 }
368 }
Marc Blankaeee10e2011-04-27 17:12:06 -0700369 if (policy.mPasswordMode > 0) {
370 if (dpm.getPasswordQuality(mAdminName) < policy.getDPManagerPasswordQuality()) {
Andy Stadler469f2982011-01-13 13:12:55 -0800371 reasons |= INACTIVE_NEED_PASSWORD;
Andrew Stadlerd6286082010-02-01 16:48:16 -0800372 }
373 if (!dpm.isActivePasswordSufficient()) {
Andy Stadler469f2982011-01-13 13:12:55 -0800374 reasons |= INACTIVE_NEED_PASSWORD;
Andrew Stadlerd6286082010-02-01 16:48:16 -0800375 }
376 }
Marc Blankaeee10e2011-04-27 17:12:06 -0700377 if (policy.mMaxScreenLockTime > 0) {
Andrew Stadlerd6286082010-02-01 16:48:16 -0800378 // Note, we use seconds, dpm uses milliseconds
Marc Blankaeee10e2011-04-27 17:12:06 -0700379 if (dpm.getMaximumTimeToLock(mAdminName) > policy.mMaxScreenLockTime * 1000) {
Andy Stadler469f2982011-01-13 13:12:55 -0800380 reasons |= INACTIVE_NEED_CONFIGURATION;
Andrew Stadlerd6286082010-02-01 16:48:16 -0800381 }
382 }
Marc Blankaeee10e2011-04-27 17:12:06 -0700383 if (policy.mPasswordExpirationDays > 0) {
Andy Stadler1ca111c2010-12-01 12:58:36 -0800384 // confirm that expirations are currently set
385 long currentTimeout = dpm.getPasswordExpirationTimeout(mAdminName);
386 if (currentTimeout == 0
Marc Blankaeee10e2011-04-27 17:12:06 -0700387 || currentTimeout > policy.getDPManagerPasswordExpirationTimeout()) {
Andy Stadler469f2982011-01-13 13:12:55 -0800388 reasons |= INACTIVE_NEED_PASSWORD;
Andy Stadler1ca111c2010-12-01 12:58:36 -0800389 }
390 // confirm that the current password hasn't expired
391 long expirationDate = dpm.getPasswordExpiration(mAdminName);
392 long timeUntilExpiration = expirationDate - System.currentTimeMillis();
393 boolean expired = timeUntilExpiration < 0;
394 if (expired) {
Andy Stadler469f2982011-01-13 13:12:55 -0800395 reasons |= INACTIVE_NEED_PASSWORD;
Andy Stadler1ca111c2010-12-01 12:58:36 -0800396 }
Marc Blank9b4988d2010-06-09 16:18:57 -0700397 }
Marc Blankaeee10e2011-04-27 17:12:06 -0700398 if (policy.mPasswordHistory > 0) {
399 if (dpm.getPasswordHistoryLength(mAdminName) < policy.mPasswordHistory) {
Marc Blanke86d8af2011-08-28 16:34:28 -0700400 // There's no user action for changes here; this is just a configuration change
401 reasons |= INACTIVE_NEED_CONFIGURATION;
Marc Blank9b4988d2010-06-09 16:18:57 -0700402 }
403 }
Marc Blankaeee10e2011-04-27 17:12:06 -0700404 if (policy.mPasswordComplexChars > 0) {
405 if (dpm.getPasswordMinimumNonLetter(mAdminName) < policy.mPasswordComplexChars) {
Andy Stadler469f2982011-01-13 13:12:55 -0800406 reasons |= INACTIVE_NEED_PASSWORD;
407 }
408 }
Marc Blankaeee10e2011-04-27 17:12:06 -0700409 if (policy.mRequireEncryption) {
Andy Stadlerc2e63832011-01-17 12:54:40 -0800410 int encryptionStatus = getDPM().getStorageEncryptionStatus();
Andy Stadler469f2982011-01-13 13:12:55 -0800411 if (encryptionStatus != DevicePolicyManager.ENCRYPTION_STATUS_ACTIVE) {
412 reasons |= INACTIVE_NEED_ENCRYPTION;
Marc Blank9b4988d2010-06-09 16:18:57 -0700413 }
414 }
Marc Blankce582522011-08-21 21:06:54 -0700415 if (policy.mDontAllowCamera && !dpm.getCameraDisabled(mAdminName)) {
416 reasons |= INACTIVE_NEED_CONFIGURATION;
417 }
Andrew Stadlerd6286082010-02-01 16:48:16 -0800418 // password failures are counted locally - no test required here
419 // no check required for remote wipe (it's supported, if we're the admin)
Andrew Stadler2a5eeea2010-02-08 17:42:42 -0800420
Andy Stadler469f2982011-01-13 13:12:55 -0800421 // If we made it all the way, reasons == 0 here. Otherwise it's a list of grievances.
422 return reasons;
Andrew Stadlerd6286082010-02-01 16:48:16 -0800423 }
Andrew Stadlerd71d0b22010-02-09 17:24:55 -0800424 // return false, not active
Andy Stadler469f2982011-01-13 13:12:55 -0800425 return INACTIVE_NEED_ACTIVATION;
Andrew Stadler345fb8b2010-01-26 17:24:15 -0800426 }
427
428 /**
Andrew Stadler50d16102010-02-09 11:01:01 -0800429 * Set the requested security level based on the aggregate set of requests.
430 * If the set is empty, we release our device administration. If the set is non-empty,
431 * we only proceed if we are already active as an admin.
Andrew Stadler3d2b3b32010-02-05 11:10:39 -0800432 */
433 public void setActivePolicies() {
434 DevicePolicyManager dpm = getDPM();
Andrew Stadler50d16102010-02-09 11:01:01 -0800435 // compute aggregate set of policies
Marc Blankaeee10e2011-04-27 17:12:06 -0700436 Policy aggregatePolicy = getAggregatePolicy();
Andrew Stadler50d16102010-02-09 11:01:01 -0800437 // if empty set, detach from policy manager
Marc Blankaeee10e2011-04-27 17:12:06 -0700438 if (aggregatePolicy == Policy.NO_POLICY) {
439 if (Email.DEBUG) {
440 Log.d(TAG, "setActivePolicies: none, remove admin");
441 }
Andrew Stadler50d16102010-02-09 11:01:01 -0800442 dpm.removeActiveAdmin(mAdminName);
Andy Stadlere7f4d3e2010-12-08 16:06:16 -0800443 } else if (isActiveAdmin()) {
Marc Blankaeee10e2011-04-27 17:12:06 -0700444 if (Email.DEBUG) {
445 Log.d(TAG, "setActivePolicies: " + aggregatePolicy);
446 }
Andrew Stadler3d2b3b32010-02-05 11:10:39 -0800447 // set each policy in the policy manager
448 // password mode & length
Marc Blankaeee10e2011-04-27 17:12:06 -0700449 dpm.setPasswordQuality(mAdminName, aggregatePolicy.getDPManagerPasswordQuality());
450 dpm.setPasswordMinimumLength(mAdminName, aggregatePolicy.mPasswordMinLength);
Andrew Stadler3d2b3b32010-02-05 11:10:39 -0800451 // screen lock time
Marc Blankaeee10e2011-04-27 17:12:06 -0700452 dpm.setMaximumTimeToLock(mAdminName, aggregatePolicy.mMaxScreenLockTime * 1000);
Andrew Stadler3d2b3b32010-02-05 11:10:39 -0800453 // local wipe (failed passwords limit)
Marc Blankaeee10e2011-04-27 17:12:06 -0700454 dpm.setMaximumFailedPasswordsForWipe(mAdminName, aggregatePolicy.mPasswordMaxFails);
Andy Stadler1ca111c2010-12-01 12:58:36 -0800455 // password expiration (days until a password expires). API takes mSec.
456 dpm.setPasswordExpirationTimeout(mAdminName,
Marc Blankaeee10e2011-04-27 17:12:06 -0700457 aggregatePolicy.getDPManagerPasswordExpirationTimeout());
Marc Blank9b4988d2010-06-09 16:18:57 -0700458 // password history length (number of previous passwords that may not be reused)
Marc Blankaeee10e2011-04-27 17:12:06 -0700459 dpm.setPasswordHistoryLength(mAdminName, aggregatePolicy.mPasswordHistory);
Andy Stadler22759ba2011-03-16 09:48:08 -0700460 // password minimum complex characters.
461 // Note, in Exchange, "complex chars" simply means "non alpha", but in the DPM,
462 // setting the quality to complex also defaults min symbols=1 and min numeric=1.
463 // We always / safely clear minSymbols & minNumeric to zero (there is no policy
464 // configuration in which we explicitly require a minimum number of digits or symbols.)
465 dpm.setPasswordMinimumSymbols(mAdminName, 0);
466 dpm.setPasswordMinimumNumeric(mAdminName, 0);
Marc Blankaeee10e2011-04-27 17:12:06 -0700467 dpm.setPasswordMinimumNonLetter(mAdminName, aggregatePolicy.mPasswordComplexChars);
Ben Komalod09cff02011-05-06 14:57:47 -0700468 // Device capabilities
469 dpm.setCameraDisabled(mAdminName, aggregatePolicy.mDontAllowCamera);
470
Andy Stadler469f2982011-01-13 13:12:55 -0800471 // encryption required
Marc Blankaeee10e2011-04-27 17:12:06 -0700472 dpm.setStorageEncryption(mAdminName, aggregatePolicy.mRequireEncryption);
Andrew Stadler3d2b3b32010-02-05 11:10:39 -0800473 }
474 }
475
476 /**
Marc Blank9ba506c2011-02-08 18:54:56 -0800477 * Convenience method; see javadoc below
478 */
479 public static void setAccountHoldFlag(Context context, long accountId, boolean newState) {
480 Account account = Account.restoreAccountWithId(context, accountId);
481 if (account != null) {
482 setAccountHoldFlag(context, account, newState);
483 }
484 }
485
486 /**
Andrew Stadler2a5eeea2010-02-08 17:42:42 -0800487 * API: Set/Clear the "hold" flag in any account. This flag serves a dual purpose:
488 * Setting it gives us an indication that it was blocked, and clearing it gives EAS a
489 * signal to try syncing again.
Andy Stadler1ca111c2010-12-01 12:58:36 -0800490 * @param context
Marc Blank9ba506c2011-02-08 18:54:56 -0800491 * @param account the account whose hold flag is to be set/cleared
Andy Stadler1ca111c2010-12-01 12:58:36 -0800492 * @param newState true = security hold, false = free to sync
Andrew Stadler2a5eeea2010-02-08 17:42:42 -0800493 */
Andy Stadler1ca111c2010-12-01 12:58:36 -0800494 public static void setAccountHoldFlag(Context context, Account account, boolean newState) {
Andrew Stadler2a5eeea2010-02-08 17:42:42 -0800495 if (newState) {
496 account.mFlags |= Account.FLAGS_SECURITY_HOLD;
497 } else {
498 account.mFlags &= ~Account.FLAGS_SECURITY_HOLD;
499 }
500 ContentValues cv = new ContentValues();
501 cv.put(AccountColumns.FLAGS, account.mFlags);
Andy Stadler1ca111c2010-12-01 12:58:36 -0800502 account.update(context, cv);
Andrew Stadler2a5eeea2010-02-08 17:42:42 -0800503 }
504
505 /**
Andrew Stadler3d2b3b32010-02-05 11:10:39 -0800506 * API: Sync service should call this any time a sync fails due to isActive() returning false.
Andrew Stadlerd6286082010-02-01 16:48:16 -0800507 * This will kick off the notify-acquire-admin-state process and/or increase the security level.
Andrew Stadler345fb8b2010-01-26 17:24:15 -0800508 * The caller needs to write the required policies into this account before making this call.
Andrew Stadler3d2b3b32010-02-05 11:10:39 -0800509 * Should not be called from UI thread - uses DB lookups to prepare new notifications
Andrew Stadler345fb8b2010-01-26 17:24:15 -0800510 *
511 * @param accountId the account for which sync cannot proceed
512 */
513 public void policiesRequired(long accountId) {
Marc Blankf5418f12011-06-13 15:32:27 -0700514 Account account = Account.restoreAccountWithId(mContext, accountId);
Marc Blank844b14f2011-01-26 18:18:45 -0800515 // In case the account has been deleted, just return
516 if (account == null) return;
Marc Blankaeee10e2011-04-27 17:12:06 -0700517 if (Email.DEBUG) {
518 if (account.mPolicyKey == 0) {
519 Log.d(TAG, "policiesRequired for " + account.mDisplayName + ": none");
520 } else {
521 Policy policy = Policy.restorePolicyWithId(mContext, account.mPolicyKey);
522 if (policy == null) {
523 Log.w(TAG, "No policy??");
524 } else {
525 Log.d(TAG, "policiesRequired for " + account.mDisplayName + ": " + policy);
526 }
527 }
528 }
Andy Stadler1ca111c2010-12-01 12:58:36 -0800529
Andrew Stadler2a5eeea2010-02-08 17:42:42 -0800530 // Mark the account as "on hold".
Andy Stadler1ca111c2010-12-01 12:58:36 -0800531 setAccountHoldFlag(mContext, account, true);
532
533 // Put up a notification
Makoto Onuki308ce922011-03-21 17:08:16 -0700534 NotificationController.getInstance(mContext).showSecurityNeededNotification(account);
Andrew Stadler3d2b3b32010-02-05 11:10:39 -0800535 }
536
537 /**
538 * Called from the notification's intent receiver to register that the notification can be
539 * cleared now.
540 */
Marc Blankc6df1d62011-07-19 14:09:11 -0700541 public void clearNotification() {
Makoto Onuki308ce922011-03-21 17:08:16 -0700542 NotificationController.getInstance(mContext).cancelSecurityNeededNotification();
Andrew Stadler3d2b3b32010-02-05 11:10:39 -0800543 }
544
545 /**
Andrew Stadler50d16102010-02-09 11:01:01 -0800546 * API: Remote wipe (from server). This is final, there is no confirmation. It will only
Marc Blankc82c1ca2011-09-28 09:41:44 -0700547 * return to the caller if there is an unexpected failure. The wipe includes external storage.
Andrew Stadler3d2b3b32010-02-05 11:10:39 -0800548 */
Andrew Stadler50d16102010-02-09 11:01:01 -0800549 public void remoteWipe() {
Andrew Stadler3d2b3b32010-02-05 11:10:39 -0800550 DevicePolicyManager dpm = getDPM();
551 if (dpm.isAdminActive(mAdminName)) {
Marc Blankc82c1ca2011-09-28 09:41:44 -0700552 dpm.wipeData(DevicePolicyManager.WIPE_EXTERNAL_STORAGE);
Andrew Stadler50d16102010-02-09 11:01:01 -0800553 } else {
Marc Blank31d9acb2011-02-11 15:05:17 -0800554 Log.d(Logging.LOG_TAG, "Could not remote wipe because not device admin.");
Andrew Stadler3d2b3b32010-02-05 11:10:39 -0800555 }
Andrew Stadler345fb8b2010-01-26 17:24:15 -0800556 }
Andrew Stadler345fb8b2010-01-26 17:24:15 -0800557 /**
Andrew Stadler3d2b3b32010-02-05 11:10:39 -0800558 * If we are not the active device admin, try to become so.
559 *
Andy Stadlere7f4d3e2010-12-08 16:06:16 -0800560 * Also checks for any policies that we have added during the lifetime of this app.
561 * This catches the case where the user granted an earlier (smaller) set of policies
562 * but an app upgrade requires that new policies be granted.
563 *
Andrew Stadler3d2b3b32010-02-05 11:10:39 -0800564 * @return true if we are already active, false if we are not
Andrew Stadler345fb8b2010-01-26 17:24:15 -0800565 */
Andrew Stadler3d2b3b32010-02-05 11:10:39 -0800566 public boolean isActiveAdmin() {
567 DevicePolicyManager dpm = getDPM();
Andy Stadlerc2e63832011-01-17 12:54:40 -0800568 return dpm.isAdminActive(mAdminName)
569 && dpm.hasGrantedPolicy(mAdminName, DeviceAdminInfo.USES_POLICY_EXPIRE_PASSWORD)
Ben Komaloaa0a3552011-06-16 14:40:15 -0700570 && dpm.hasGrantedPolicy(mAdminName, DeviceAdminInfo.USES_ENCRYPTED_STORAGE)
571 && dpm.hasGrantedPolicy(mAdminName, DeviceAdminInfo.USES_POLICY_DISABLE_CAMERA);
Andrew Stadler3d2b3b32010-02-05 11:10:39 -0800572 }
Andrew Stadler345fb8b2010-01-26 17:24:15 -0800573
Andrew Stadler3d2b3b32010-02-05 11:10:39 -0800574 /**
575 * Report admin component name - for making calls into device policy manager
576 */
577 public ComponentName getAdminComponent() {
578 return mAdminName;
579 }
580
581 /**
Marc Blank02d59d22010-10-25 11:49:29 -0700582 * Delete all accounts whose security flags aren't zero (i.e. they have security enabled).
583 * This method is synchronous, so it should normally be called within a worker thread (the
584 * exception being for unit tests)
585 *
586 * @param context the caller's context
Andrew Stadler3d2b3b32010-02-05 11:10:39 -0800587 */
Marc Blank02d59d22010-10-25 11:49:29 -0700588 /*package*/ void deleteSecuredAccounts(Context context) {
589 ContentResolver cr = context.getContentResolver();
590 // Find all accounts with security and delete them
591 Cursor c = cr.query(Account.CONTENT_URI, EmailContent.ID_PROJECTION,
Marc Blankaeee10e2011-04-27 17:12:06 -0700592 Account.SECURITY_NONZERO_SELECTION, null, null);
Marc Blank02d59d22010-10-25 11:49:29 -0700593 try {
594 Log.w(TAG, "Email administration disabled; deleting " + c.getCount() +
595 " secured account(s)");
596 while (c.moveToNext()) {
597 Controller.getInstance(context).deleteAccountSync(
598 c.getLong(EmailContent.ID_PROJECTION_COLUMN), context);
599 }
600 } finally {
601 c.close();
602 }
Marc Blankaeee10e2011-04-27 17:12:06 -0700603 policiesUpdated(-1);
Marc Blank02d59d22010-10-25 11:49:29 -0700604 }
605
606 /**
607 * Internal handler for enabled->disabled transitions. Deletes all secured accounts.
Andy Stadlera2269e82010-12-30 00:16:55 -0800608 * Must call from worker thread, not on UI thread.
Marc Blank02d59d22010-10-25 11:49:29 -0700609 */
610 /*package*/ void onAdminEnabled(boolean isEnabled) {
Andrew Stadler856e09d2010-04-06 22:17:21 -0700611 if (!isEnabled) {
Andy Stadlera2269e82010-12-30 00:16:55 -0800612 deleteSecuredAccounts(mContext);
Andrew Stadler3d2b3b32010-02-05 11:10:39 -0800613 }
Andrew Stadler3d2b3b32010-02-05 11:10:39 -0800614 }
615
616 /**
Andy Stadler1ca111c2010-12-01 12:58:36 -0800617 * Handle password expiration - if any accounts appear to have triggered this, put up
618 * warnings, or even shut them down.
619 *
620 * NOTE: If there are multiple accounts with password expiration policies, the device
621 * password will be set to expire in the shortest required interval (most secure). The logic
622 * in this method operates based on the aggregate setting - irrespective of which account caused
623 * the expiration. In other words, all accounts (that require expiration) will run/stop
624 * based on the requirements of the account with the shortest interval.
625 */
Andy Stadlera2269e82010-12-30 00:16:55 -0800626 private void onPasswordExpiring(Context context) {
Andy Stadler1ca111c2010-12-01 12:58:36 -0800627 // 1. Do we have any accounts that matter here?
628 long nextExpiringAccountId = findShortestExpiration(context);
629
630 // 2. If not, exit immediately
631 if (nextExpiringAccountId == -1) {
632 return;
633 }
634
635 // 3. If yes, are we warning or expired?
636 long expirationDate = getDPM().getPasswordExpiration(mAdminName);
637 long timeUntilExpiration = expirationDate - System.currentTimeMillis();
638 boolean expired = timeUntilExpiration < 0;
639 if (!expired) {
640 // 4. If warning, simply put up a generic notification and report that it came from
641 // the shortest-expiring account.
Makoto Onuki308ce922011-03-21 17:08:16 -0700642 NotificationController.getInstance(mContext).showPasswordExpiringNotification(
643 nextExpiringAccountId);
Andy Stadler1ca111c2010-12-01 12:58:36 -0800644 } else {
645 // 5. Actually expired - find all accounts that expire passwords, and wipe them
646 boolean wiped = wipeExpiredAccounts(context, Controller.getInstance(context));
647 if (wiped) {
Makoto Onuki308ce922011-03-21 17:08:16 -0700648 NotificationController.getInstance(mContext).showPasswordExpiredNotification(
649 nextExpiringAccountId);
Andy Stadler1ca111c2010-12-01 12:58:36 -0800650 }
651 }
652 }
653
654 /**
655 * Find the account with the shortest expiration time. This is always assumed to be
656 * the account that forces the password to be refreshed.
657 * @return -1 if no expirations, or accountId if one is found
658 */
Marc Blankaeee10e2011-04-27 17:12:06 -0700659 @VisibleForTesting
660 /*package*/ static long findShortestExpiration(Context context) {
661 long policyId = Utility.getFirstRowLong(context, Policy.CONTENT_URI, Policy.ID_PROJECTION,
662 HAS_PASSWORD_EXPIRATION, null, PolicyColumns.PASSWORD_EXPIRATION_DAYS + " ASC",
663 EmailContent.ID_PROJECTION_COLUMN, -1L);
664 if (policyId < 0) return -1L;
665 return Policy.getAccountIdWithPolicyKey(context, policyId);
Andy Stadler1ca111c2010-12-01 12:58:36 -0800666 }
667
668 /**
669 * For all accounts that require password expiration, put them in security hold and wipe
670 * their data.
671 * @param context
672 * @param controller
673 * @return true if one or more accounts were wiped
674 */
Marc Blankaeee10e2011-04-27 17:12:06 -0700675 @VisibleForTesting
676 /*package*/ static boolean wipeExpiredAccounts(Context context, Controller controller) {
Andy Stadler1ca111c2010-12-01 12:58:36 -0800677 boolean result = false;
Marc Blankaeee10e2011-04-27 17:12:06 -0700678 Cursor c = context.getContentResolver().query(Policy.CONTENT_URI,
679 Policy.ID_PROJECTION, HAS_PASSWORD_EXPIRATION, null, null);
Andy Stadler1ca111c2010-12-01 12:58:36 -0800680 try {
681 while (c.moveToNext()) {
Marc Blankaeee10e2011-04-27 17:12:06 -0700682 long policyId = c.getLong(Policy.ID_PROJECTION_COLUMN);
683 long accountId = Policy.getAccountIdWithPolicyKey(context, policyId);
684 if (accountId < 0) continue;
685 Account account = Account.restoreAccountWithId(context, accountId);
686 if (account != null) {
687 // Mark the account as "on hold".
688 setAccountHoldFlag(context, account, true);
689 // Erase data
690 controller.deleteSyncedDataSync(accountId);
691 // Report one or more were found
692 result = true;
Andy Stadler1ca111c2010-12-01 12:58:36 -0800693 }
694 }
695 } finally {
696 c.close();
697 }
698 return result;
699 }
700
701 /**
Andy Stadlera2269e82010-12-30 00:16:55 -0800702 * Callback from EmailBroadcastProcessorService. This provides the workers for the
703 * DeviceAdminReceiver calls. These should perform the work directly and not use async
704 * threads for completion.
705 */
706 public static void onDeviceAdminReceiverMessage(Context context, int message) {
707 SecurityPolicy instance = SecurityPolicy.getInstance(context);
708 switch (message) {
709 case DEVICE_ADMIN_MESSAGE_ENABLED:
710 instance.onAdminEnabled(true);
711 break;
712 case DEVICE_ADMIN_MESSAGE_DISABLED:
713 instance.onAdminEnabled(false);
714 break;
715 case DEVICE_ADMIN_MESSAGE_PASSWORD_CHANGED:
716 // TODO make a small helper for this
717 // Clear security holds (if any)
718 Account.clearSecurityHoldOnAllAccounts(context);
719 // Cancel any active notifications (if any are posted)
Makoto Onuki308ce922011-03-21 17:08:16 -0700720 NotificationController.getInstance(context).cancelPasswordExpirationNotifications();
Andy Stadlera2269e82010-12-30 00:16:55 -0800721 break;
722 case DEVICE_ADMIN_MESSAGE_PASSWORD_EXPIRING:
723 instance.onPasswordExpiring(instance.mContext);
724 break;
725 }
726 }
727
728 /**
Andrew Stadler3d2b3b32010-02-05 11:10:39 -0800729 * Device Policy administrator. This is primarily a listener for device state changes.
730 * Note: This is instantiated by incoming messages.
Andy Stadlera2269e82010-12-30 00:16:55 -0800731 * Note: This is actually a BroadcastReceiver and must remain within the guidelines required
732 * for proper behavior, including avoidance of ANRs.
Andrew Stadler5893e9e2010-02-08 23:09:05 -0800733 * Note: We do not implement onPasswordFailed() because the default behavior of the
734 * DevicePolicyManager - complete local wipe after 'n' failures - is sufficient.
Andrew Stadler3d2b3b32010-02-05 11:10:39 -0800735 */
Dianne Hackborn4ae83c52010-02-16 20:40:32 -0800736 public static class PolicyAdmin extends DeviceAdminReceiver {
Andrew Stadler345fb8b2010-01-26 17:24:15 -0800737
738 /**
739 * Called after the administrator is first enabled.
740 */
741 @Override
742 public void onEnabled(Context context, Intent intent) {
Andy Stadlera2269e82010-12-30 00:16:55 -0800743 EmailBroadcastProcessorService.processDevicePolicyMessage(context,
744 DEVICE_ADMIN_MESSAGE_ENABLED);
Andrew Stadler345fb8b2010-01-26 17:24:15 -0800745 }
Andrew Stadler856e09d2010-04-06 22:17:21 -0700746
Andrew Stadler345fb8b2010-01-26 17:24:15 -0800747 /**
748 * Called prior to the administrator being disabled.
749 */
750 @Override
751 public void onDisabled(Context context, Intent intent) {
Andy Stadlera2269e82010-12-30 00:16:55 -0800752 EmailBroadcastProcessorService.processDevicePolicyMessage(context,
753 DEVICE_ADMIN_MESSAGE_DISABLED);
Andrew Stadler345fb8b2010-01-26 17:24:15 -0800754 }
Andrew Stadler856e09d2010-04-06 22:17:21 -0700755
Andrew Stadler345fb8b2010-01-26 17:24:15 -0800756 /**
Marc Blank02d59d22010-10-25 11:49:29 -0700757 * Called when the user asks to disable administration; we return a warning string that
758 * will be presented to the user
759 */
760 @Override
761 public CharSequence onDisableRequested(Context context, Intent intent) {
762 return context.getString(R.string.disable_admin_warning);
763 }
764
765 /**
Andrew Stadler345fb8b2010-01-26 17:24:15 -0800766 * Called after the user has changed their password.
767 */
768 @Override
769 public void onPasswordChanged(Context context, Intent intent) {
Andy Stadlera2269e82010-12-30 00:16:55 -0800770 EmailBroadcastProcessorService.processDevicePolicyMessage(context,
771 DEVICE_ADMIN_MESSAGE_PASSWORD_CHANGED);
Andy Stadler1ca111c2010-12-01 12:58:36 -0800772 }
773
774 /**
775 * Called when device password is expiring
776 */
777 @Override
778 public void onPasswordExpiring(Context context, Intent intent) {
Andy Stadlera2269e82010-12-30 00:16:55 -0800779 EmailBroadcastProcessorService.processDevicePolicyMessage(context,
780 DEVICE_ADMIN_MESSAGE_PASSWORD_EXPIRING);
Andrew Stadler345fb8b2010-01-26 17:24:15 -0800781 }
Andrew Stadler345fb8b2010-01-26 17:24:15 -0800782 }
783}