SecurityPolicy.java revision 9ba506c4dd498150555f6c59aa758f7467bf9236
1/*
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
19import com.android.email.activity.setup.AccountSecurity;
20import com.android.email.provider.EmailContent;
21import com.android.email.provider.EmailContent.Account;
22import com.android.email.provider.EmailContent.AccountColumns;
23import com.android.email.service.EmailBroadcastProcessorService;
24import com.android.emailcommon.service.PolicySet;
25
26import android.app.admin.DeviceAdminInfo;
27import android.app.admin.DeviceAdminReceiver;
28import android.app.admin.DevicePolicyManager;
29import android.content.ComponentName;
30import android.content.ContentResolver;
31import android.content.ContentValues;
32import android.content.Context;
33import android.content.Intent;
34import android.database.Cursor;
35import android.util.Log;
36
37/**
38 * Utility functions to support reading and writing security policies, and handshaking the device
39 * into and out of various security states.
40 */
41public class SecurityPolicy {
42    private static final String TAG = "SecurityPolicy";
43    private static SecurityPolicy sInstance = null;
44    private Context mContext;
45    private DevicePolicyManager mDPM;
46    private ComponentName mAdminName;
47    private PolicySet mAggregatePolicy;
48
49    /* package */ static final PolicySet NO_POLICY_SET =
50            new PolicySet(0, PolicySet.PASSWORD_MODE_NONE, 0, 0, false, 0, 0, 0, false);
51
52    /**
53     * This projection on Account is for scanning/reading
54     */
55    private static final String[] ACCOUNT_SECURITY_PROJECTION = new String[] {
56        AccountColumns.ID, AccountColumns.SECURITY_FLAGS
57    };
58    private static final int ACCOUNT_SECURITY_COLUMN_ID = 0;
59    private static final int ACCOUNT_SECURITY_COLUMN_FLAGS = 1;
60
61    // Messages used for DevicePolicyManager callbacks
62    private static final int DEVICE_ADMIN_MESSAGE_ENABLED = 1;
63    private static final int DEVICE_ADMIN_MESSAGE_DISABLED = 2;
64    private static final int DEVICE_ADMIN_MESSAGE_PASSWORD_CHANGED = 3;
65    private static final int DEVICE_ADMIN_MESSAGE_PASSWORD_EXPIRING = 4;
66
67    /**
68     * Get the security policy instance
69     */
70    public synchronized static SecurityPolicy getInstance(Context context) {
71        if (sInstance == null) {
72            sInstance = new SecurityPolicy(context.getApplicationContext());
73        }
74        return sInstance;
75    }
76
77    /**
78     * Private constructor (one time only)
79     */
80    private SecurityPolicy(Context context) {
81        mContext = context.getApplicationContext();
82        mDPM = null;
83        mAdminName = new ComponentName(context, PolicyAdmin.class);
84        mAggregatePolicy = null;
85    }
86
87    /**
88     * For testing only: Inject context into already-created instance
89     */
90    /* package */ void setContext(Context context) {
91        mContext = context;
92    }
93
94    /**
95     * Compute the aggregate policy for all accounts that require it, and record it.
96     *
97     * The business logic is as follows:
98     *  min password length         take the max
99     *  password mode               take the max (strongest mode)
100     *  max password fails          take the min
101     *  max screen lock time        take the min
102     *  require remote wipe         take the max (logical or)
103     *  password history            take the max (strongest mode)
104     *  password expiration         take the min (strongest mode)
105     *  password complex chars      take the max (strongest mode)
106     *  encryption                  take the max (logical or)
107     *
108     * @return a policy representing the strongest aggregate.  If no policy sets are defined,
109     * a lightweight "nothing required" policy will be returned.  Never null.
110     */
111    /*package*/ PolicySet computeAggregatePolicy() {
112        boolean policiesFound = false;
113
114        int minPasswordLength = Integer.MIN_VALUE;
115        int passwordMode = Integer.MIN_VALUE;
116        int maxPasswordFails = Integer.MAX_VALUE;
117        int maxScreenLockTime = Integer.MAX_VALUE;
118        boolean requireRemoteWipe = false;
119        int passwordHistory = Integer.MIN_VALUE;
120        int passwordExpirationDays = Integer.MAX_VALUE;
121        int passwordComplexChars = Integer.MIN_VALUE;
122        boolean requireEncryption = false;
123
124        Cursor c = mContext.getContentResolver().query(Account.CONTENT_URI,
125                ACCOUNT_SECURITY_PROJECTION, Account.SECURITY_NONZERO_SELECTION, null, null);
126        try {
127            while (c.moveToNext()) {
128                long flags = c.getLong(ACCOUNT_SECURITY_COLUMN_FLAGS);
129                if (flags != 0) {
130                    PolicySet p = new PolicySet(flags);
131                    minPasswordLength = Math.max(p.mMinPasswordLength, minPasswordLength);
132                    passwordMode  = Math.max(p.mPasswordMode, passwordMode);
133                    if (p.mMaxPasswordFails > 0) {
134                        maxPasswordFails = Math.min(p.mMaxPasswordFails, maxPasswordFails);
135                    }
136                    if (p.mMaxScreenLockTime > 0) {
137                        maxScreenLockTime = Math.min(p.mMaxScreenLockTime, maxScreenLockTime);
138                    }
139                    if (p.mPasswordHistory > 0) {
140                        passwordHistory = Math.max(p.mPasswordHistory, passwordHistory);
141                    }
142                    if (p.mPasswordExpirationDays > 0) {
143                        passwordExpirationDays =
144                                Math.min(p.mPasswordExpirationDays, passwordExpirationDays);
145                    }
146                    if (p.mPasswordComplexChars > 0) {
147                        passwordComplexChars = Math.max(p.mPasswordComplexChars,
148                                passwordComplexChars);
149                    }
150                    requireRemoteWipe |= p.mRequireRemoteWipe;
151                    requireEncryption |= p.mRequireEncryption;
152                    policiesFound = true;
153                }
154            }
155        } finally {
156            c.close();
157        }
158        if (policiesFound) {
159            // final cleanup pass converts any untouched min/max values to zero (not specified)
160            if (minPasswordLength == Integer.MIN_VALUE) minPasswordLength = 0;
161            if (passwordMode == Integer.MIN_VALUE) passwordMode = 0;
162            if (maxPasswordFails == Integer.MAX_VALUE) maxPasswordFails = 0;
163            if (maxScreenLockTime == Integer.MAX_VALUE) maxScreenLockTime = 0;
164            if (passwordHistory == Integer.MIN_VALUE) passwordHistory = 0;
165            if (passwordExpirationDays == Integer.MAX_VALUE) passwordExpirationDays = 0;
166            if (passwordComplexChars == Integer.MIN_VALUE) passwordComplexChars = 0;
167
168            return new PolicySet(minPasswordLength, passwordMode, maxPasswordFails,
169                    maxScreenLockTime, requireRemoteWipe, passwordExpirationDays, passwordHistory,
170                    passwordComplexChars, requireEncryption);
171        } else {
172            return NO_POLICY_SET;
173        }
174    }
175
176    /**
177     * Return updated aggregate policy, from cached value if possible
178     */
179    public synchronized PolicySet getAggregatePolicy() {
180        if (mAggregatePolicy == null) {
181            mAggregatePolicy = computeAggregatePolicy();
182        }
183        return mAggregatePolicy;
184    }
185
186    /**
187     * Get the dpm.  This mainly allows us to make some utility calls without it, for testing.
188     */
189    /* package */ synchronized DevicePolicyManager getDPM() {
190        if (mDPM == null) {
191            mDPM = (DevicePolicyManager) mContext.getSystemService(Context.DEVICE_POLICY_SERVICE);
192        }
193        return mDPM;
194    }
195
196    /**
197     * API: Report that policies may have been updated due to rewriting values in an Account.
198     * @param accountId the account that has been updated, -1 if unknown/deleted
199     */
200    public synchronized void updatePolicies(long accountId) {
201        mAggregatePolicy = null;
202    }
203
204    /**
205     * API: Report that policies may have been updated *and* the caller vouches that the
206     * change is a reduction in policies.  This forces an immediate change to device state.
207     * Typically used when deleting accounts, although we may use it for server-side policy
208     * rollbacks.
209     */
210    public void reducePolicies() {
211        updatePolicies(-1);
212        setActivePolicies();
213    }
214
215    /**
216     * API: Query if the proposed set of policies are supported on the device.
217     *
218     * @param policies the polices that were requested
219     * @return boolean if supported
220     */
221    public boolean isSupported(PolicySet policies) {
222        // IMPLEMENTATION:  At this time, the only policy which might not be supported is
223        // encryption (which requires low-level systems support).  Other policies are fully
224        // supported by the framework and do not need to be checked.
225        if (policies.mRequireEncryption) {
226            int encryptionStatus = getDPM().getStorageEncryptionStatus();
227            if (encryptionStatus == DevicePolicyManager.ENCRYPTION_STATUS_UNSUPPORTED) {
228                return false;
229            }
230        }
231        return true;
232    }
233
234    /**
235     * API: Remove any unsupported policies
236     *
237     * This is used when we have a set of polices that have been requested, but the server
238     * is willing to allow unsupported policies to be considered optional.
239     *
240     * @param policies the polices that were requested
241     * @return the same PolicySet if all are supported;  A replacement PolicySet if any
242     *   unsupported policies were removed
243     */
244    public PolicySet clearUnsupportedPolicies(PolicySet policies) {
245        PolicySet result = policies;
246        // IMPLEMENTATION:  At this time, the only policy which might not be supported is
247        // encryption (which requires low-level systems support).  Other policies are fully
248        // supported by the framework and do not need to be checked.
249        if (policies.mRequireEncryption) {
250            int encryptionStatus = getDPM().getStorageEncryptionStatus();
251            if (encryptionStatus == DevicePolicyManager.ENCRYPTION_STATUS_UNSUPPORTED) {
252                // Make new PolicySet w/o encryption
253                result = new PolicySet(policies.mMinPasswordLength, policies.mPasswordMode,
254                        policies.mMaxPasswordFails, policies.mMaxScreenLockTime,
255                        policies.mRequireRemoteWipe, policies.mPasswordExpirationDays,
256                        policies.mPasswordHistory, policies.mPasswordComplexChars, false);
257            }
258        }
259        return result;
260    }
261
262    /**
263     * API: Query used to determine if a given policy is "active" (the device is operating at
264     * the required security level).
265     *
266     * @param policies the policies requested, or null to check aggregate stored policies
267     * @return true if the requested policies are active, false if not.
268     */
269    public boolean isActive(PolicySet policies) {
270        int reasons = getInactiveReasons(policies);
271        return reasons == 0;
272    }
273
274    /**
275     * Return bits from isActive:  Device Policy Manager has not been activated
276     */
277    public final static int INACTIVE_NEED_ACTIVATION = 1;
278
279    /**
280     * Return bits from isActive:  Some required configuration is not correct (no user action).
281     */
282    public final static int INACTIVE_NEED_CONFIGURATION = 2;
283
284    /**
285     * Return bits from isActive:  Password needs to be set or updated
286     */
287    public final static int INACTIVE_NEED_PASSWORD = 4;
288
289    /**
290     * Return bits from isActive:  Encryption has not be enabled
291     */
292    public final static int INACTIVE_NEED_ENCRYPTION = 8;
293
294    /**
295     * API: Query used to determine if a given policy is "active" (the device is operating at
296     * the required security level).
297     *
298     * This can be used when syncing a specific account, by passing a specific set of policies
299     * for that account.  Or, it can be used at any time to compare the device
300     * state against the aggregate set of device policies stored in all accounts.
301     *
302     * This method is for queries only, and does not trigger any change in device state.
303     *
304     * NOTE:  If there are multiple accounts with password expiration policies, the device
305     * password will be set to expire in the shortest required interval (most secure).  This method
306     * will return 'false' as soon as the password expires - irrespective of which account caused
307     * the expiration.  In other words, all accounts (that require expiration) will run/stop
308     * based on the requirements of the account with the shortest interval.
309     *
310     * @param policies the policies requested, or null to check aggregate stored policies
311     * @return zero if the requested policies are active, non-zero bits indicates that more work
312     * is needed (typically, by the user) before the required security polices are fully active.
313     */
314    public int getInactiveReasons(PolicySet policies) {
315        // select aggregate set if needed
316        if (policies == null) {
317            policies = getAggregatePolicy();
318        }
319        // quick check for the "empty set" of no policies
320        if (policies == NO_POLICY_SET) {
321            return 0;
322        }
323        int reasons = 0;
324        DevicePolicyManager dpm = getDPM();
325        if (isActiveAdmin()) {
326            // check each policy explicitly
327            if (policies.mMinPasswordLength > 0) {
328                if (dpm.getPasswordMinimumLength(mAdminName) < policies.mMinPasswordLength) {
329                    reasons |= INACTIVE_NEED_PASSWORD;
330                }
331            }
332            if (policies.mPasswordMode > 0) {
333                if (dpm.getPasswordQuality(mAdminName) < policies.getDPManagerPasswordQuality()) {
334                    reasons |= INACTIVE_NEED_PASSWORD;
335                }
336                if (!dpm.isActivePasswordSufficient()) {
337                    reasons |= INACTIVE_NEED_PASSWORD;
338                }
339            }
340            if (policies.mMaxScreenLockTime > 0) {
341                // Note, we use seconds, dpm uses milliseconds
342                if (dpm.getMaximumTimeToLock(mAdminName) > policies.mMaxScreenLockTime * 1000) {
343                    reasons |= INACTIVE_NEED_CONFIGURATION;
344                }
345            }
346            if (policies.mPasswordExpirationDays > 0) {
347                // confirm that expirations are currently set
348                long currentTimeout = dpm.getPasswordExpirationTimeout(mAdminName);
349                if (currentTimeout == 0
350                        || currentTimeout > policies.getDPManagerPasswordExpirationTimeout()) {
351                    reasons |= INACTIVE_NEED_PASSWORD;
352                }
353                // confirm that the current password hasn't expired
354                long expirationDate = dpm.getPasswordExpiration(mAdminName);
355                long timeUntilExpiration = expirationDate - System.currentTimeMillis();
356                boolean expired = timeUntilExpiration < 0;
357                if (expired) {
358                    reasons |= INACTIVE_NEED_PASSWORD;
359                }
360            }
361            if (policies.mPasswordHistory > 0) {
362                if (dpm.getPasswordHistoryLength(mAdminName) < policies.mPasswordHistory) {
363                    reasons |= INACTIVE_NEED_PASSWORD;
364                }
365            }
366            if (policies.mPasswordComplexChars > 0) {
367                if (dpm.getPasswordMinimumNonLetter(mAdminName) < policies.mPasswordComplexChars) {
368                    reasons |= INACTIVE_NEED_PASSWORD;
369                }
370            }
371            if (policies.mRequireEncryption) {
372                int encryptionStatus = getDPM().getStorageEncryptionStatus();
373                if (encryptionStatus != DevicePolicyManager.ENCRYPTION_STATUS_ACTIVE) {
374                    reasons |= INACTIVE_NEED_ENCRYPTION;
375                }
376            }
377            // password failures are counted locally - no test required here
378            // no check required for remote wipe (it's supported, if we're the admin)
379
380            // If we made it all the way, reasons == 0 here.  Otherwise it's a list of grievances.
381            return reasons;
382        }
383        // return false, not active
384        return INACTIVE_NEED_ACTIVATION;
385    }
386
387    /**
388     * Set the requested security level based on the aggregate set of requests.
389     * If the set is empty, we release our device administration.  If the set is non-empty,
390     * we only proceed if we are already active as an admin.
391     */
392    public void setActivePolicies() {
393        DevicePolicyManager dpm = getDPM();
394        // compute aggregate set of policies
395        PolicySet policies = getAggregatePolicy();
396        // if empty set, detach from policy manager
397        if (policies == NO_POLICY_SET) {
398            dpm.removeActiveAdmin(mAdminName);
399        } else if (isActiveAdmin()) {
400            // set each policy in the policy manager
401            // password mode & length
402            dpm.setPasswordQuality(mAdminName, policies.getDPManagerPasswordQuality());
403            dpm.setPasswordMinimumLength(mAdminName, policies.mMinPasswordLength);
404            // screen lock time
405            dpm.setMaximumTimeToLock(mAdminName, policies.mMaxScreenLockTime * 1000);
406            // local wipe (failed passwords limit)
407            dpm.setMaximumFailedPasswordsForWipe(mAdminName, policies.mMaxPasswordFails);
408            // password expiration (days until a password expires).  API takes mSec.
409            dpm.setPasswordExpirationTimeout(mAdminName,
410                    policies.getDPManagerPasswordExpirationTimeout());
411            // password history length (number of previous passwords that may not be reused)
412            dpm.setPasswordHistoryLength(mAdminName, policies.mPasswordHistory);
413            // password minimum complex characters
414            dpm.setPasswordMinimumNonLetter(mAdminName, policies.mPasswordComplexChars);
415            // encryption required
416            dpm.setStorageEncryption(mAdminName, policies.mRequireEncryption);
417        }
418    }
419
420    /**
421     * Convenience method; see javadoc below
422     */
423    public static void setAccountHoldFlag(Context context, long accountId, boolean newState) {
424        Account account = Account.restoreAccountWithId(context, accountId);
425        if (account != null) {
426            setAccountHoldFlag(context, account, newState);
427        }
428    }
429
430    /**
431     * API: Set/Clear the "hold" flag in any account.  This flag serves a dual purpose:
432     * Setting it gives us an indication that it was blocked, and clearing it gives EAS a
433     * signal to try syncing again.
434     * @param context
435     * @param account the account whose hold flag is to be set/cleared
436     * @param newState true = security hold, false = free to sync
437     */
438    public static void setAccountHoldFlag(Context context, Account account, boolean newState) {
439        if (newState) {
440            account.mFlags |= Account.FLAGS_SECURITY_HOLD;
441        } else {
442            account.mFlags &= ~Account.FLAGS_SECURITY_HOLD;
443        }
444        ContentValues cv = new ContentValues();
445        cv.put(AccountColumns.FLAGS, account.mFlags);
446        account.update(context, cv);
447    }
448
449    /**
450     * API: Sync service should call this any time a sync fails due to isActive() returning false.
451     * This will kick off the notify-acquire-admin-state process and/or increase the security level.
452     * The caller needs to write the required policies into this account before making this call.
453     * Should not be called from UI thread - uses DB lookups to prepare new notifications
454     *
455     * @param accountId the account for which sync cannot proceed
456     */
457    public void policiesRequired(long accountId) {
458        Account account = EmailContent.Account.restoreAccountWithId(mContext, accountId);
459        // In case the account has been deleted, just return
460        if (account == null) return;
461
462        // Mark the account as "on hold".
463        setAccountHoldFlag(mContext, account, true);
464
465        // Put up a notification
466        String tickerText = mContext.getString(R.string.security_notification_ticker_fmt,
467                account.getDisplayName());
468        String contentTitle = mContext.getString(R.string.security_notification_content_title);
469        String contentText = account.getDisplayName();
470        Intent intent = AccountSecurity.actionUpdateSecurityIntent(mContext, accountId);
471        NotificationController.getInstance(mContext).postAccountNotification(
472                account, tickerText, contentTitle, contentText, intent,
473                NotificationController.NOTIFICATION_ID_SECURITY_NEEDED);
474    }
475
476    /**
477     * Called from the notification's intent receiver to register that the notification can be
478     * cleared now.
479     */
480    public void clearNotification(long accountId) {
481        NotificationController.getInstance(mContext).cancelNotification(
482                NotificationController.NOTIFICATION_ID_SECURITY_NEEDED);
483    }
484
485    /**
486     * API: Remote wipe (from server).  This is final, there is no confirmation.  It will only
487     * return to the caller if there is an unexpected failure.
488     */
489    public void remoteWipe() {
490        DevicePolicyManager dpm = getDPM();
491        if (dpm.isAdminActive(mAdminName)) {
492            dpm.wipeData(0);
493        } else {
494            Log.d(Email.LOG_TAG, "Could not remote wipe because not device admin.");
495        }
496    }
497    /**
498     * If we are not the active device admin, try to become so.
499     *
500     * Also checks for any policies that we have added during the lifetime of this app.
501     * This catches the case where the user granted an earlier (smaller) set of policies
502     * but an app upgrade requires that new policies be granted.
503     *
504     * @return true if we are already active, false if we are not
505     */
506    public boolean isActiveAdmin() {
507        DevicePolicyManager dpm = getDPM();
508        return dpm.isAdminActive(mAdminName)
509                && dpm.hasGrantedPolicy(mAdminName, DeviceAdminInfo.USES_POLICY_EXPIRE_PASSWORD)
510                && dpm.hasGrantedPolicy(mAdminName, DeviceAdminInfo.USES_ENCRYPTED_STORAGE);
511    }
512
513    /**
514     * Report admin component name - for making calls into device policy manager
515     */
516    public ComponentName getAdminComponent() {
517        return mAdminName;
518    }
519
520    /**
521     * Delete all accounts whose security flags aren't zero (i.e. they have security enabled).
522     * This method is synchronous, so it should normally be called within a worker thread (the
523     * exception being for unit tests)
524     *
525     * @param context the caller's context
526     */
527    /*package*/ void deleteSecuredAccounts(Context context) {
528        ContentResolver cr = context.getContentResolver();
529        // Find all accounts with security and delete them
530        Cursor c = cr.query(Account.CONTENT_URI, EmailContent.ID_PROJECTION,
531                AccountColumns.SECURITY_FLAGS + "!=0", null, null);
532        try {
533            Log.w(TAG, "Email administration disabled; deleting " + c.getCount() +
534                    " secured account(s)");
535            while (c.moveToNext()) {
536                Controller.getInstance(context).deleteAccountSync(
537                        c.getLong(EmailContent.ID_PROJECTION_COLUMN), context);
538            }
539        } finally {
540            c.close();
541        }
542        updatePolicies(-1);
543    }
544
545    /**
546     * Internal handler for enabled->disabled transitions.  Deletes all secured accounts.
547     * Must call from worker thread, not on UI thread.
548     */
549    /*package*/ void onAdminEnabled(boolean isEnabled) {
550        if (!isEnabled) {
551            deleteSecuredAccounts(mContext);
552        }
553    }
554
555    /**
556     * Handle password expiration - if any accounts appear to have triggered this, put up
557     * warnings, or even shut them down.
558     *
559     * NOTE:  If there are multiple accounts with password expiration policies, the device
560     * password will be set to expire in the shortest required interval (most secure).  The logic
561     * in this method operates based on the aggregate setting - irrespective of which account caused
562     * the expiration.  In other words, all accounts (that require expiration) will run/stop
563     * based on the requirements of the account with the shortest interval.
564     */
565    private void onPasswordExpiring(Context context) {
566        // 1.  Do we have any accounts that matter here?
567        long nextExpiringAccountId = findShortestExpiration(context);
568
569        // 2.  If not, exit immediately
570        if (nextExpiringAccountId == -1) {
571            return;
572        }
573
574        // 3.  If yes, are we warning or expired?
575        long expirationDate = getDPM().getPasswordExpiration(mAdminName);
576        long timeUntilExpiration = expirationDate - System.currentTimeMillis();
577        boolean expired = timeUntilExpiration < 0;
578        if (!expired) {
579            // 4.  If warning, simply put up a generic notification and report that it came from
580            // the shortest-expiring account.
581            Account account = Account.restoreAccountWithId(context, nextExpiringAccountId);
582            if (account == null) return;
583            Intent intent = new Intent(DevicePolicyManager.ACTION_SET_NEW_PASSWORD);
584            String ticker = context.getString(
585                    R.string.password_expire_warning_ticker_fmt, account.getDisplayName());
586            String contentTitle = context.getString(
587                    R.string.password_expire_warning_content_title);
588            String contentText = context.getString(
589                    R.string.password_expire_warning_content_text_fmt, account.getDisplayName());
590            NotificationController nc = NotificationController.getInstance(mContext);
591            nc.postAccountNotification(account, ticker, contentTitle, contentText, intent,
592                    NotificationController.NOTIFICATION_ID_PASSWORD_EXPIRING);
593        } else {
594            // 5.  Actually expired - find all accounts that expire passwords, and wipe them
595            boolean wiped = wipeExpiredAccounts(context, Controller.getInstance(context));
596            if (wiped) {
597                // Post notification
598                Account account = Account.restoreAccountWithId(context, nextExpiringAccountId);
599                if (account == null) return;
600                Intent intent =
601                    new Intent(DevicePolicyManager.ACTION_SET_NEW_PASSWORD);
602                String ticker = context.getString(R.string.password_expired_ticker);
603                String contentTitle = context.getString(R.string.password_expired_content_title);
604                String contentText = context.getString(R.string.password_expired_content_text);
605                NotificationController nc = NotificationController.getInstance(mContext);
606                nc.postAccountNotification(account, ticker, contentTitle,
607                        contentText, intent,
608                        NotificationController.NOTIFICATION_ID_PASSWORD_EXPIRED);
609            }
610        }
611    }
612
613    /**
614     * Find the account with the shortest expiration time.  This is always assumed to be
615     * the account that forces the password to be refreshed.
616     * @return -1 if no expirations, or accountId if one is found
617     */
618    /* package */ static long findShortestExpiration(Context context) {
619        long nextExpiringAccountId = -1;
620        long shortestExpiration = Long.MAX_VALUE;
621        Cursor c = context.getContentResolver().query(Account.CONTENT_URI,
622                ACCOUNT_SECURITY_PROJECTION, Account.SECURITY_NONZERO_SELECTION, null, null);
623        try {
624            while (c.moveToNext()) {
625                long flags = c.getLong(ACCOUNT_SECURITY_COLUMN_FLAGS);
626                if (flags != 0) {
627                    PolicySet p = new PolicySet(flags);
628                    if (p.mPasswordExpirationDays > 0 &&
629                            p.mPasswordExpirationDays < shortestExpiration) {
630                        nextExpiringAccountId = c.getLong(ACCOUNT_SECURITY_COLUMN_ID);
631                        shortestExpiration = p.mPasswordExpirationDays;
632                    }
633                }
634            }
635        } finally {
636            c.close();
637        }
638        return nextExpiringAccountId;
639    }
640
641    /**
642     * For all accounts that require password expiration, put them in security hold and wipe
643     * their data.
644     * @param context
645     * @param controller
646     * @return true if one or more accounts were wiped
647     */
648    /* package */ static boolean wipeExpiredAccounts(Context context, Controller controller) {
649        boolean result = false;
650        Cursor c = context.getContentResolver().query(Account.CONTENT_URI,
651                ACCOUNT_SECURITY_PROJECTION, Account.SECURITY_NONZERO_SELECTION, null, null);
652        try {
653            while (c.moveToNext()) {
654                long flags = c.getLong(ACCOUNT_SECURITY_COLUMN_FLAGS);
655                if (flags != 0) {
656                    PolicySet p = new PolicySet(flags);
657                    if (p.mPasswordExpirationDays > 0) {
658                        long accountId = c.getLong(ACCOUNT_SECURITY_COLUMN_ID);
659                        Account account = Account.restoreAccountWithId(context, accountId);
660                        if (account != null) {
661                            // Mark the account as "on hold".
662                            setAccountHoldFlag(context, account, true);
663                            // Erase data
664                            controller.deleteSyncedDataSync(accountId);
665                            // Report one or more were found
666                            result = true;
667                        }
668                    }
669                }
670            }
671        } finally {
672            c.close();
673        }
674        return result;
675    }
676
677    /**
678     * Callback from EmailBroadcastProcessorService.  This provides the workers for the
679     * DeviceAdminReceiver calls.  These should perform the work directly and not use async
680     * threads for completion.
681     */
682    public static void onDeviceAdminReceiverMessage(Context context, int message) {
683        SecurityPolicy instance = SecurityPolicy.getInstance(context);
684        switch (message) {
685            case DEVICE_ADMIN_MESSAGE_ENABLED:
686                instance.onAdminEnabled(true);
687                break;
688            case DEVICE_ADMIN_MESSAGE_DISABLED:
689                instance.onAdminEnabled(false);
690                break;
691            case DEVICE_ADMIN_MESSAGE_PASSWORD_CHANGED:
692                // TODO make a small helper for this
693                // Clear security holds (if any)
694                Account.clearSecurityHoldOnAllAccounts(context);
695                // Cancel any active notifications (if any are posted)
696                NotificationController nc = NotificationController.getInstance(context);
697                nc.cancelNotification(NotificationController.NOTIFICATION_ID_PASSWORD_EXPIRING);
698                nc.cancelNotification(NotificationController.NOTIFICATION_ID_PASSWORD_EXPIRED);
699                break;
700            case DEVICE_ADMIN_MESSAGE_PASSWORD_EXPIRING:
701                instance.onPasswordExpiring(instance.mContext);
702                break;
703        }
704    }
705
706    /**
707     * Device Policy administrator.  This is primarily a listener for device state changes.
708     * Note:  This is instantiated by incoming messages.
709     * Note:  This is actually a BroadcastReceiver and must remain within the guidelines required
710     *        for proper behavior, including avoidance of ANRs.
711     * Note:  We do not implement onPasswordFailed() because the default behavior of the
712     *        DevicePolicyManager - complete local wipe after 'n' failures - is sufficient.
713     */
714    public static class PolicyAdmin extends DeviceAdminReceiver {
715
716        /**
717         * Called after the administrator is first enabled.
718         */
719        @Override
720        public void onEnabled(Context context, Intent intent) {
721            EmailBroadcastProcessorService.processDevicePolicyMessage(context,
722                    DEVICE_ADMIN_MESSAGE_ENABLED);
723        }
724
725        /**
726         * Called prior to the administrator being disabled.
727         */
728        @Override
729        public void onDisabled(Context context, Intent intent) {
730            EmailBroadcastProcessorService.processDevicePolicyMessage(context,
731                    DEVICE_ADMIN_MESSAGE_DISABLED);
732        }
733
734        /**
735         * Called when the user asks to disable administration; we return a warning string that
736         * will be presented to the user
737         */
738        @Override
739        public CharSequence onDisableRequested(Context context, Intent intent) {
740            return context.getString(R.string.disable_admin_warning);
741        }
742
743        /**
744         * Called after the user has changed their password.
745         */
746        @Override
747        public void onPasswordChanged(Context context, Intent intent) {
748            EmailBroadcastProcessorService.processDevicePolicyMessage(context,
749                    DEVICE_ADMIN_MESSAGE_PASSWORD_CHANGED);
750        }
751
752        /**
753         * Called when device password is expiring
754         */
755        @Override
756        public void onPasswordExpiring(Context context, Intent intent) {
757            EmailBroadcastProcessorService.processDevicePolicyMessage(context,
758                    DEVICE_ADMIN_MESSAGE_PASSWORD_EXPIRING);
759        }
760    }
761}
762