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