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