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