SecurityPolicy.java revision e86d8af163ada4d0b3f7c5cb0b32cfeb12da473c
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 android.app.admin.DeviceAdminInfo;
20import android.app.admin.DeviceAdminReceiver;
21import android.app.admin.DevicePolicyManager;
22import android.content.ComponentName;
23import android.content.ContentResolver;
24import android.content.ContentValues;
25import android.content.Context;
26import android.content.Intent;
27import android.database.Cursor;
28import android.util.Log;
29
30import 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
40/**
41 * Utility functions to support reading and writing security policies, and handshaking the device
42 * into and out of various security states.
43 */
44public class SecurityPolicy {
45    private static final String TAG = "Email/SecurityPolicy";
46    private static SecurityPolicy sInstance = null;
47    private Context mContext;
48    private DevicePolicyManager mDPM;
49    private final ComponentName mAdminName;
50    private Policy mAggregatePolicy;
51
52    // 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
58    private static final String HAS_PASSWORD_EXPIRATION =
59        PolicyColumns.PASSWORD_EXPIRATION_DAYS + ">0";
60
61    /**
62     * Get the security policy instance
63     */
64    public synchronized static SecurityPolicy getInstance(Context context) {
65        if (sInstance == null) {
66            sInstance = new SecurityPolicy(context.getApplicationContext());
67        }
68        return sInstance;
69    }
70
71    /**
72     * Private constructor (one time only)
73     */
74    private SecurityPolicy(Context context) {
75        mContext = context.getApplicationContext();
76        mDPM = null;
77        mAdminName = new ComponentName(context, PolicyAdmin.class);
78        mAggregatePolicy = null;
79    }
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)
97     *  password history            take the max (strongest mode)
98     *  password expiration         take the min (strongest mode)
99     *  password complex chars      take the max (strongest mode)
100     *  encryption                  take the max (logical or)
101     *
102     * @return a policy representing the strongest aggregate.  If no policy sets are defined,
103     * a lightweight "nothing required" policy will be returned.  Never null.
104     */
105    @VisibleForTesting
106    Policy computeAggregatePolicy() {
107        boolean policiesFound = false;
108        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;
118
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.
122        aggregate.mRequireEncryptionExternal = false;
123
124        Cursor c = mContext.getContentResolver().query(Policy.CONTENT_URI,
125                Policy.CONTENT_PROJECTION, null, null, null);
126        Policy policy = new Policy();
127        try {
128            while (c.moveToNext()) {
129                policy.restore(c);
130                if (Email.DEBUG) {
131                    Log.d(TAG, "Aggregate from: " + policy);
132                }
133                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;
158                aggregate.mDontAllowCamera |= policy.mDontAllowCamera;
159                policiesFound = true;
160            }
161        } finally {
162            c.close();
163        }
164        if (policiesFound) {
165            // final cleanup pass converts any untouched min/max values to zero (not specified)
166            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;
179        }
180        if (Email.DEBUG) {
181            Log.d(TAG, "Calculated Aggregate: no policy");
182        }
183        return Policy.NO_POLICY;
184    }
185
186    /**
187     * Return updated aggregate policy, from cached value if possible
188     */
189    public synchronized Policy getAggregatePolicy() {
190        if (mAggregatePolicy == null) {
191            mAggregatePolicy = computeAggregatePolicy();
192        }
193        return mAggregatePolicy;
194    }
195
196    /**
197     * Get the dpm.  This mainly allows us to make some utility calls without it, for testing.
198     */
199    /* package */ synchronized DevicePolicyManager getDPM() {
200        if (mDPM == null) {
201            mDPM = (DevicePolicyManager) mContext.getSystemService(Context.DEVICE_POLICY_SERVICE);
202        }
203        return mDPM;
204    }
205
206    /**
207     * API: Report that policies may have been updated due to rewriting values in an Account.
208     * @param accountId the account that has been updated, -1 if unknown/deleted
209     */
210    public synchronized void policiesUpdated(long accountId) {
211        mAggregatePolicy = null;
212    }
213
214    /**
215     * 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() {
221        if (Email.DEBUG) {
222            Log.d(TAG, "reducePolicies");
223        }
224        policiesUpdated(-1);
225        setActivePolicies();
226    }
227
228    /**
229     * API: Query if the proposed set of policies are supported on the device.
230     *
231     * @param policy the polices that were requested
232     * @return boolean if supported
233     */
234    public boolean isSupported(Policy policy) {
235        // 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.
238        if (policy.mRequireEncryption) {
239            int encryptionStatus = getDPM().getStorageEncryptionStatus();
240            if (encryptionStatus == DevicePolicyManager.ENCRYPTION_STATUS_UNSUPPORTED) {
241                return false;
242            }
243        }
244
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
248        return true;
249    }
250
251    /**
252     * 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     *
257     * @param policy the polices that were requested
258     * @return the same PolicySet if all are supported;  A replacement PolicySet if any
259     *   unsupported policies were removed
260     */
261    public Policy clearUnsupportedPolicies(Policy policy) {
262        // 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.
265        if (policy.mRequireEncryption) {
266            int encryptionStatus = getDPM().getStorageEncryptionStatus();
267            if (encryptionStatus == DevicePolicyManager.ENCRYPTION_STATUS_UNSUPPORTED) {
268                policy.mRequireEncryption = false;
269            }
270        }
271
272        // If we ever support devices that can't disable cameras for any reason, we should
273        // clear the mDontAllowCamera policy
274
275        return policy;
276    }
277
278    /**
279     * API: Query used to determine if a given policy is "active" (the device is operating at
280     * the required security level).
281     *
282     * @param policy the policies requested, or null to check aggregate stored policies
283     * @return true if the requested policies are active, false if not.
284     */
285    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        }
308        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    /**
332     * API: Query used to determine if a given policy is "active" (the device is operating at
333     * the required security level).
334     *
335     * 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     *
341     * 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     *
347     * @param policy the policies requested, or null to check aggregate stored policies
348     * @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.
350     */
351    public int getInactiveReasons(Policy policy) {
352        // select aggregate set if needed
353        if (policy == null) {
354            policy = getAggregatePolicy();
355        }
356        // quick check for the "empty set" of no policies
357        if (policy == Policy.NO_POLICY) {
358            return 0;
359        }
360        int reasons = 0;
361        DevicePolicyManager dpm = getDPM();
362        if (isActiveAdmin()) {
363            // check each policy explicitly
364            if (policy.mPasswordMinLength > 0) {
365                if (dpm.getPasswordMinimumLength(mAdminName) < policy.mPasswordMinLength) {
366                    reasons |= INACTIVE_NEED_PASSWORD;
367                }
368            }
369            if (policy.mPasswordMode > 0) {
370                if (dpm.getPasswordQuality(mAdminName) < policy.getDPManagerPasswordQuality()) {
371                    reasons |= INACTIVE_NEED_PASSWORD;
372                }
373                if (!dpm.isActivePasswordSufficient()) {
374                    reasons |= INACTIVE_NEED_PASSWORD;
375                }
376            }
377            if (policy.mMaxScreenLockTime > 0) {
378                // Note, we use seconds, dpm uses milliseconds
379                if (dpm.getMaximumTimeToLock(mAdminName) > policy.mMaxScreenLockTime * 1000) {
380                    reasons |= INACTIVE_NEED_CONFIGURATION;
381                }
382            }
383            if (policy.mPasswordExpirationDays > 0) {
384                // confirm that expirations are currently set
385                long currentTimeout = dpm.getPasswordExpirationTimeout(mAdminName);
386                if (currentTimeout == 0
387                        || currentTimeout > policy.getDPManagerPasswordExpirationTimeout()) {
388                    reasons |= INACTIVE_NEED_PASSWORD;
389                }
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) {
395                    reasons |= INACTIVE_NEED_PASSWORD;
396                }
397            }
398            if (policy.mPasswordHistory > 0) {
399                if (dpm.getPasswordHistoryLength(mAdminName) < policy.mPasswordHistory) {
400                    // There's no user action for changes here; this is just a configuration change
401                    reasons |= INACTIVE_NEED_CONFIGURATION;
402                }
403            }
404            if (policy.mPasswordComplexChars > 0) {
405                if (dpm.getPasswordMinimumNonLetter(mAdminName) < policy.mPasswordComplexChars) {
406                    reasons |= INACTIVE_NEED_PASSWORD;
407                }
408            }
409            if (policy.mRequireEncryption) {
410                int encryptionStatus = getDPM().getStorageEncryptionStatus();
411                if (encryptionStatus != DevicePolicyManager.ENCRYPTION_STATUS_ACTIVE) {
412                    reasons |= INACTIVE_NEED_ENCRYPTION;
413                }
414            }
415            if (policy.mDontAllowCamera && !dpm.getCameraDisabled(mAdminName)) {
416                reasons |= INACTIVE_NEED_CONFIGURATION;
417            }
418            // password failures are counted locally - no test required here
419            // no check required for remote wipe (it's supported, if we're the admin)
420
421            // If we made it all the way, reasons == 0 here.  Otherwise it's a list of grievances.
422            return reasons;
423        }
424        // return false, not active
425        return INACTIVE_NEED_ACTIVATION;
426    }
427
428    /**
429     * 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.
432     */
433    public void setActivePolicies() {
434        DevicePolicyManager dpm = getDPM();
435        // compute aggregate set of policies
436        Policy aggregatePolicy = getAggregatePolicy();
437        // if empty set, detach from policy manager
438        if (aggregatePolicy == Policy.NO_POLICY) {
439            if (Email.DEBUG) {
440                Log.d(TAG, "setActivePolicies: none, remove admin");
441            }
442            dpm.removeActiveAdmin(mAdminName);
443        } else if (isActiveAdmin()) {
444            if (Email.DEBUG) {
445                Log.d(TAG, "setActivePolicies: " + aggregatePolicy);
446            }
447            // set each policy in the policy manager
448            // password mode & length
449            dpm.setPasswordQuality(mAdminName, aggregatePolicy.getDPManagerPasswordQuality());
450            dpm.setPasswordMinimumLength(mAdminName, aggregatePolicy.mPasswordMinLength);
451            // screen lock time
452            dpm.setMaximumTimeToLock(mAdminName, aggregatePolicy.mMaxScreenLockTime * 1000);
453            // local wipe (failed passwords limit)
454            dpm.setMaximumFailedPasswordsForWipe(mAdminName, aggregatePolicy.mPasswordMaxFails);
455            // password expiration (days until a password expires).  API takes mSec.
456            dpm.setPasswordExpirationTimeout(mAdminName,
457                    aggregatePolicy.getDPManagerPasswordExpirationTimeout());
458            // password history length (number of previous passwords that may not be reused)
459            dpm.setPasswordHistoryLength(mAdminName, aggregatePolicy.mPasswordHistory);
460            // 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);
467            dpm.setPasswordMinimumNonLetter(mAdminName, aggregatePolicy.mPasswordComplexChars);
468            // Device capabilities
469            dpm.setCameraDisabled(mAdminName, aggregatePolicy.mDontAllowCamera);
470
471            // encryption required
472            dpm.setStorageEncryption(mAdminName, aggregatePolicy.mRequireEncryption);
473        }
474    }
475
476    /**
477     * 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    /**
487     * 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.
490     * @param context
491     * @param account the account whose hold flag is to be set/cleared
492     * @param newState true = security hold, false = free to sync
493     */
494    public static void setAccountHoldFlag(Context context, Account account, boolean newState) {
495        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);
502        account.update(context, cv);
503    }
504
505    /**
506     * API: Sync service should call this any time a sync fails due to isActive() returning false.
507     * This will kick off the notify-acquire-admin-state process and/or increase the security level.
508     * The caller needs to write the required policies into this account before making this call.
509     * Should not be called from UI thread - uses DB lookups to prepare new notifications
510     *
511     * @param accountId the account for which sync cannot proceed
512     */
513    public void policiesRequired(long accountId) {
514        Account account = Account.restoreAccountWithId(mContext, accountId);
515        // In case the account has been deleted, just return
516        if (account == null) return;
517        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        }
529
530        // Mark the account as "on hold".
531        setAccountHoldFlag(mContext, account, true);
532
533        // Put up a notification
534        NotificationController.getInstance(mContext).showSecurityNeededNotification(account);
535    }
536
537    /**
538     * Called from the notification's intent receiver to register that the notification can be
539     * cleared now.
540     */
541    public void clearNotification() {
542        NotificationController.getInstance(mContext).cancelSecurityNeededNotification();
543    }
544
545    /**
546     * API: Remote wipe (from server).  This is final, there is no confirmation.  It will only
547     * return to the caller if there is an unexpected failure.
548     */
549    public void remoteWipe() {
550        DevicePolicyManager dpm = getDPM();
551        if (dpm.isAdminActive(mAdminName)) {
552            dpm.wipeData(0);
553        } else {
554            Log.d(Logging.LOG_TAG, "Could not remote wipe because not device admin.");
555        }
556    }
557    /**
558     * If we are not the active device admin, try to become so.
559     *
560     * 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     *
564     * @return true if we are already active, false if we are not
565     */
566    public boolean isActiveAdmin() {
567        DevicePolicyManager dpm = getDPM();
568        return dpm.isAdminActive(mAdminName)
569                && dpm.hasGrantedPolicy(mAdminName, DeviceAdminInfo.USES_POLICY_EXPIRE_PASSWORD)
570                && dpm.hasGrantedPolicy(mAdminName, DeviceAdminInfo.USES_ENCRYPTED_STORAGE)
571                && dpm.hasGrantedPolicy(mAdminName, DeviceAdminInfo.USES_POLICY_DISABLE_CAMERA);
572    }
573
574    /**
575     * Report admin component name - for making calls into device policy manager
576     */
577    public ComponentName getAdminComponent() {
578        return mAdminName;
579    }
580
581    /**
582     * 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
587     */
588    /*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,
592                Account.SECURITY_NONZERO_SELECTION, null, null);
593        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        }
603        policiesUpdated(-1);
604    }
605
606    /**
607     * Internal handler for enabled->disabled transitions.  Deletes all secured accounts.
608     * Must call from worker thread, not on UI thread.
609     */
610    /*package*/ void onAdminEnabled(boolean isEnabled) {
611        if (!isEnabled) {
612            deleteSecuredAccounts(mContext);
613        }
614    }
615
616    /**
617     * 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     */
626    private void onPasswordExpiring(Context context) {
627        // 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.
642            NotificationController.getInstance(mContext).showPasswordExpiringNotification(
643                    nextExpiringAccountId);
644        } 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) {
648                NotificationController.getInstance(mContext).showPasswordExpiredNotification(
649                        nextExpiringAccountId);
650            }
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     */
659    @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);
666    }
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     */
675    @VisibleForTesting
676    /*package*/ static boolean wipeExpiredAccounts(Context context, Controller controller) {
677        boolean result = false;
678        Cursor c = context.getContentResolver().query(Policy.CONTENT_URI,
679                Policy.ID_PROJECTION, HAS_PASSWORD_EXPIRATION, null, null);
680        try {
681            while (c.moveToNext()) {
682                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;
693                }
694            }
695        } finally {
696            c.close();
697        }
698        return result;
699    }
700
701    /**
702     * 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)
720                NotificationController.getInstance(context).cancelPasswordExpirationNotifications();
721                break;
722            case DEVICE_ADMIN_MESSAGE_PASSWORD_EXPIRING:
723                instance.onPasswordExpiring(instance.mContext);
724                break;
725        }
726    }
727
728    /**
729     * Device Policy administrator.  This is primarily a listener for device state changes.
730     * Note:  This is instantiated by incoming messages.
731     * Note:  This is actually a BroadcastReceiver and must remain within the guidelines required
732     *        for proper behavior, including avoidance of ANRs.
733     * Note:  We do not implement onPasswordFailed() because the default behavior of the
734     *        DevicePolicyManager - complete local wipe after 'n' failures - is sufficient.
735     */
736    public static class PolicyAdmin extends DeviceAdminReceiver {
737
738        /**
739         * Called after the administrator is first enabled.
740         */
741        @Override
742        public void onEnabled(Context context, Intent intent) {
743            EmailBroadcastProcessorService.processDevicePolicyMessage(context,
744                    DEVICE_ADMIN_MESSAGE_ENABLED);
745        }
746
747        /**
748         * Called prior to the administrator being disabled.
749         */
750        @Override
751        public void onDisabled(Context context, Intent intent) {
752            EmailBroadcastProcessorService.processDevicePolicyMessage(context,
753                    DEVICE_ADMIN_MESSAGE_DISABLED);
754        }
755
756        /**
757         * 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        /**
766         * Called after the user has changed their password.
767         */
768        @Override
769        public void onPasswordChanged(Context context, Intent intent) {
770            EmailBroadcastProcessorService.processDevicePolicyMessage(context,
771                    DEVICE_ADMIN_MESSAGE_PASSWORD_CHANGED);
772        }
773
774        /**
775         * Called when device password is expiring
776         */
777        @Override
778        public void onPasswordExpiring(Context context, Intent intent) {
779            EmailBroadcastProcessorService.processDevicePolicyMessage(context,
780                    DEVICE_ADMIN_MESSAGE_PASSWORD_EXPIRING);
781        }
782    }
783}
784