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