SecurityPolicy.java revision df982bdb471c4465885d05d177d4f8f183ae096f
1/*
2 * Copyright (C) 2010 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.email;
18
19import android.app.admin.DeviceAdminInfo;
20import android.app.admin.DeviceAdminReceiver;
21import android.app.admin.DevicePolicyManager;
22import android.content.ComponentName;
23import android.content.ContentProviderOperation;
24import android.content.ContentResolver;
25import android.content.ContentUris;
26import android.content.ContentValues;
27import android.content.Context;
28import android.content.Intent;
29import android.content.OperationApplicationException;
30import android.database.Cursor;
31import android.os.Build;
32import android.os.RemoteException;
33import android.util.Log;
34
35import com.android.email.service.EmailBroadcastProcessorService;
36import com.android.emailcommon.Logging;
37import com.android.emailcommon.provider.Account;
38import com.android.emailcommon.provider.EmailContent;
39import com.android.emailcommon.provider.EmailContent.AccountColumns;
40import com.android.emailcommon.provider.EmailContent.PolicyColumns;
41import com.android.emailcommon.provider.Policy;
42import com.android.emailcommon.utility.TextUtilities;
43import com.android.emailcommon.utility.Utility;
44import com.google.common.annotations.VisibleForTesting;
45
46import java.util.ArrayList;
47
48/**
49 * Utility functions to support reading and writing security policies, and handshaking the device
50 * into and out of various security states.
51 */
52public class SecurityPolicy {
53    private static final String TAG = "Email/SecurityPolicy";
54    private static SecurityPolicy sInstance = null;
55    private Context mContext;
56    private DevicePolicyManager mDPM;
57    private final ComponentName mAdminName;
58    private Policy mAggregatePolicy;
59
60    // Messages used for DevicePolicyManager callbacks
61    private static final int DEVICE_ADMIN_MESSAGE_ENABLED = 1;
62    private static final int DEVICE_ADMIN_MESSAGE_DISABLED = 2;
63    private static final int DEVICE_ADMIN_MESSAGE_PASSWORD_CHANGED = 3;
64    private static final int DEVICE_ADMIN_MESSAGE_PASSWORD_EXPIRING = 4;
65
66    private static final String HAS_PASSWORD_EXPIRATION =
67            PolicyColumns.PASSWORD_EXPIRATION_DAYS + ">0";
68
69    /**
70     * Get the security policy instance
71     */
72    public synchronized static SecurityPolicy getInstance(Context context) {
73        if (sInstance == null) {
74            sInstance = new SecurityPolicy(context.getApplicationContext());
75        }
76        return sInstance;
77    }
78
79    /**
80     * Private constructor (one time only)
81     */
82    private SecurityPolicy(Context context) {
83        mContext = context.getApplicationContext();
84        mDPM = null;
85        mAdminName = new ComponentName(context, PolicyAdmin.class);
86        mAggregatePolicy = null;
87    }
88
89    /**
90     * For testing only: Inject context into already-created instance
91     */
92    /* package */ void setContext(Context context) {
93        mContext = context;
94    }
95
96    /**
97     * Compute the aggregate policy for all accounts that require it, and record it.
98     *
99     * The business logic is as follows:
100     *  min password length         take the max
101     *  password mode               take the max (strongest mode)
102     *  max password fails          take the min
103     *  max screen lock time        take the min
104     *  require remote wipe         take the max (logical or)
105     *  password history            take the max (strongest mode)
106     *  password expiration         take the min (strongest mode)
107     *  password complex chars      take the max (strongest mode)
108     *  encryption                  take the max (logical or)
109     *
110     * @return a policy representing the strongest aggregate.  If no policy sets are defined,
111     * a lightweight "nothing required" policy will be returned.  Never null.
112     */
113    @VisibleForTesting
114    Policy computeAggregatePolicy() {
115        boolean policiesFound = false;
116        Policy ap = new Policy();
117        ap.mPasswordMinLength = Integer.MIN_VALUE;
118        ap.mPasswordMode = Integer.MIN_VALUE;
119        ap.mPasswordMaxFails = Integer.MAX_VALUE;
120        ap.mPasswordHistory = Integer.MIN_VALUE;
121        ap.mPasswordExpirationDays = Integer.MAX_VALUE;
122        ap.mPasswordComplexChars = Integer.MIN_VALUE;
123        ap.mMaxScreenLockTime = Integer.MAX_VALUE;
124        ap.mRequireRemoteWipe = false;
125        ap.mRequireEncryption = false;
126
127        // This can never be supported at this time. It exists only for historic reasons where
128        // this was able to be supported prior to the introduction of proper removable storage
129        // support for external storage.
130        ap.mRequireEncryptionExternal = false;
131
132        Cursor c = mContext.getContentResolver().query(Policy.CONTENT_URI,
133                Policy.CONTENT_PROJECTION, null, null, null);
134        Policy policy = new Policy();
135        try {
136            while (c.moveToNext()) {
137                policy.restore(c);
138                if (Email.DEBUG) {
139                    Log.d(TAG, "Aggregate from: " + policy);
140                }
141                ap.mPasswordMinLength = Math.max(policy.mPasswordMinLength, ap.mPasswordMinLength);
142                ap.mPasswordMode  = Math.max(policy.mPasswordMode, ap.mPasswordMode);
143                if (policy.mPasswordMaxFails > 0) {
144                    ap.mPasswordMaxFails =
145                            Math.min(policy.mPasswordMaxFails, ap.mPasswordMaxFails);
146                }
147                if (policy.mMaxScreenLockTime > 0) {
148                    ap.mMaxScreenLockTime =
149                            Math.min(policy.mMaxScreenLockTime, ap.mMaxScreenLockTime);
150                }
151                if (policy.mPasswordHistory > 0) {
152                    ap.mPasswordHistory =
153                            Math.max(policy.mPasswordHistory, ap.mPasswordHistory);
154                }
155                if (policy.mPasswordExpirationDays > 0) {
156                    ap.mPasswordExpirationDays =
157                            Math.min(policy.mPasswordExpirationDays, ap.mPasswordExpirationDays);
158                }
159                if (policy.mPasswordComplexChars > 0) {
160                    ap.mPasswordComplexChars =
161                            Math.max(policy.mPasswordComplexChars, ap.mPasswordComplexChars);
162                }
163                ap.mRequireRemoteWipe |= policy.mRequireRemoteWipe;
164                ap.mRequireEncryption |= policy.mRequireEncryption;
165                ap.mDontAllowCamera |= policy.mDontAllowCamera;
166                policiesFound = true;
167            }
168        } finally {
169            c.close();
170        }
171        if (policiesFound) {
172            // final cleanup pass converts any untouched min/max values to zero (not specified)
173            if (ap.mPasswordMinLength == Integer.MIN_VALUE) ap.mPasswordMinLength = 0;
174            if (ap.mPasswordMode == Integer.MIN_VALUE) ap.mPasswordMode = 0;
175            if (ap.mPasswordMaxFails == Integer.MAX_VALUE) ap.mPasswordMaxFails = 0;
176            if (ap.mMaxScreenLockTime == Integer.MAX_VALUE) ap.mMaxScreenLockTime = 0;
177            if (ap.mPasswordHistory == Integer.MIN_VALUE) ap.mPasswordHistory = 0;
178            if (ap.mPasswordExpirationDays == Integer.MAX_VALUE)
179                ap.mPasswordExpirationDays = 0;
180            if (ap.mPasswordComplexChars == Integer.MIN_VALUE)
181                ap.mPasswordComplexChars = 0;
182            if (Email.DEBUG) {
183                Log.d(TAG, "Calculated Aggregate: " + ap);
184            }
185            return ap;
186        }
187        if (Email.DEBUG) {
188            Log.d(TAG, "Calculated Aggregate: no policy");
189        }
190        return Policy.NO_POLICY;
191    }
192
193    /**
194     * Return updated aggregate policy, from cached value if possible
195     */
196    public synchronized Policy getAggregatePolicy() {
197        if (mAggregatePolicy == null) {
198            mAggregatePolicy = computeAggregatePolicy();
199        }
200        return mAggregatePolicy;
201    }
202
203    /**
204     * Get the dpm.  This mainly allows us to make some utility calls without it, for testing.
205     */
206    /* package */ synchronized DevicePolicyManager getDPM() {
207        if (mDPM == null) {
208            mDPM = (DevicePolicyManager) mContext.getSystemService(Context.DEVICE_POLICY_SERVICE);
209        }
210        return mDPM;
211    }
212
213    /**
214     * API: Report that policies may have been updated due to rewriting values in an Account.
215     * @param accountId the account that has been updated, -1 if unknown/deleted
216     */
217    public synchronized void policiesUpdated(long accountId) {
218        mAggregatePolicy = null;
219    }
220
221    /**
222     * API: Report that policies may have been updated *and* the caller vouches that the
223     * change is a reduction in policies.  This forces an immediate change to device state.
224     * Typically used when deleting accounts, although we may use it for server-side policy
225     * rollbacks.
226     */
227    public void reducePolicies() {
228        if (Email.DEBUG) {
229            Log.d(TAG, "reducePolicies");
230        }
231        policiesUpdated(-1);
232        setActivePolicies();
233    }
234
235    /**
236     * API: Query if the proposed set of policies are supported on the device.
237     *
238     * @param policy the polices that were requested
239     * @return boolean if supported
240     */
241    public boolean isSupported(Policy policy) {
242        // IMPLEMENTATION:  At this time, the only policy which might not be supported is
243        // encryption (which requires low-level systems support).  Other policies are fully
244        // supported by the framework and do not need to be checked.
245        if (policy.mRequireEncryption) {
246            int encryptionStatus = getDPM().getStorageEncryptionStatus();
247            if (encryptionStatus == DevicePolicyManager.ENCRYPTION_STATUS_UNSUPPORTED) {
248                return false;
249            }
250        }
251
252        // If we ever support devices that can't disable cameras for any reason, we should
253        // indicate as such in the mDontAllowCamera policy
254
255        return true;
256    }
257
258    /**
259     * API: Remove any unsupported policies
260     *
261     * This is used when we have a set of polices that have been requested, but the server
262     * is willing to allow unsupported policies to be considered optional.
263     *
264     * @param policy the polices that were requested
265     * @return the same PolicySet if all are supported;  A replacement PolicySet if any
266     *   unsupported policies were removed
267     */
268    public Policy clearUnsupportedPolicies(Policy policy) {
269        // IMPLEMENTATION:  At this time, the only policy which might not be supported is
270        // encryption (which requires low-level systems support).  Other policies are fully
271        // supported by the framework and do not need to be checked.
272        if (policy.mRequireEncryption) {
273            int encryptionStatus = getDPM().getStorageEncryptionStatus();
274            if (encryptionStatus == DevicePolicyManager.ENCRYPTION_STATUS_UNSUPPORTED) {
275                policy.mRequireEncryption = false;
276            }
277        }
278
279        // If we ever support devices that can't disable cameras for any reason, we should
280        // clear the mDontAllowCamera policy
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 policy 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 policy 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                    // There's no user action for changes here; this is just a configuration change
408                    reasons |= INACTIVE_NEED_CONFIGURATION;
409                }
410            }
411            if (policy.mPasswordComplexChars > 0) {
412                if (dpm.getPasswordMinimumNonLetter(mAdminName) < policy.mPasswordComplexChars) {
413                    reasons |= INACTIVE_NEED_PASSWORD;
414                }
415            }
416            if (policy.mRequireEncryption) {
417                int encryptionStatus = getDPM().getStorageEncryptionStatus();
418                if (encryptionStatus != DevicePolicyManager.ENCRYPTION_STATUS_ACTIVE) {
419                    reasons |= INACTIVE_NEED_ENCRYPTION;
420                }
421            }
422            if (policy.mDontAllowCamera && !dpm.getCameraDisabled(mAdminName)) {
423                reasons |= INACTIVE_NEED_CONFIGURATION;
424            }
425            // password failures are counted locally - no test required here
426            // no check required for remote wipe (it's supported, if we're the admin)
427
428            // If we made it all the way, reasons == 0 here.  Otherwise it's a list of grievances.
429            return reasons;
430        }
431        // return false, not active
432        return INACTIVE_NEED_ACTIVATION;
433    }
434
435    /**
436     * Set the requested security level based on the aggregate set of requests.
437     * If the set is empty, we release our device administration.  If the set is non-empty,
438     * we only proceed if we are already active as an admin.
439     */
440    public void setActivePolicies() {
441        DevicePolicyManager dpm = getDPM();
442        // compute aggregate set of policies
443        Policy aggregatePolicy = getAggregatePolicy();
444        // if empty set, detach from policy manager
445        if (aggregatePolicy == Policy.NO_POLICY) {
446            if (Email.DEBUG) {
447                Log.d(TAG, "setActivePolicies: none, remove admin");
448            }
449            dpm.removeActiveAdmin(mAdminName);
450        } else if (isActiveAdmin()) {
451            if (Email.DEBUG) {
452                Log.d(TAG, "setActivePolicies: " + aggregatePolicy);
453            }
454            // set each policy in the policy manager
455            // password mode & length
456            dpm.setPasswordQuality(mAdminName, aggregatePolicy.getDPManagerPasswordQuality());
457            dpm.setPasswordMinimumLength(mAdminName, aggregatePolicy.mPasswordMinLength);
458            // screen lock time
459            dpm.setMaximumTimeToLock(mAdminName, aggregatePolicy.mMaxScreenLockTime * 1000);
460            // local wipe (failed passwords limit)
461            dpm.setMaximumFailedPasswordsForWipe(mAdminName, aggregatePolicy.mPasswordMaxFails);
462            // password expiration (days until a password expires).  API takes mSec.
463            long oldExpiration = dpm.getPasswordExpirationTimeout(mAdminName);
464            long newExpiration = aggregatePolicy.getDPManagerPasswordExpirationTimeout();
465            // we only set this if it has changed; otherwise, we're pushing out the existing
466            // expiration time!
467            if (oldExpiration != newExpiration) {
468                dpm.setPasswordExpirationTimeout(mAdminName, newExpiration);
469            }
470            // password history length (number of previous passwords that may not be reused)
471            dpm.setPasswordHistoryLength(mAdminName, aggregatePolicy.mPasswordHistory);
472            // password minimum complex characters.
473            // Note, in Exchange, "complex chars" simply means "non alpha", but in the DPM,
474            // setting the quality to complex also defaults min symbols=1 and min numeric=1.
475            // We always / safely clear minSymbols & minNumeric to zero (there is no policy
476            // configuration in which we explicitly require a minimum number of digits or symbols.)
477            dpm.setPasswordMinimumSymbols(mAdminName, 0);
478            dpm.setPasswordMinimumNumeric(mAdminName, 0);
479            dpm.setPasswordMinimumNonLetter(mAdminName, aggregatePolicy.mPasswordComplexChars);
480            // Device capabilities
481            dpm.setCameraDisabled(mAdminName, aggregatePolicy.mDontAllowCamera);
482
483            // encryption required
484            dpm.setStorageEncryption(mAdminName, aggregatePolicy.mRequireEncryption);
485            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
486                // Disable/re-enable keyguard features as required
487                boolean noKeyguardFeatures =
488                        aggregatePolicy.mPasswordMode != Policy.PASSWORD_MODE_NONE;
489                dpm.setKeyguardDisabledFeatures(mAdminName,
490                        (noKeyguardFeatures ? DevicePolicyManager.KEYGUARD_DISABLE_FEATURES_ALL :
491                            DevicePolicyManager.KEYGUARD_DISABLE_FEATURES_NONE));
492            }
493
494        }
495    }
496
497    /**
498     * Convenience method; see javadoc below
499     */
500    public static void setAccountHoldFlag(Context context, long accountId, boolean newState) {
501        Account account = Account.restoreAccountWithId(context, accountId);
502        if (account != null) {
503            setAccountHoldFlag(context, account, newState);
504        }
505    }
506
507    /**
508     * API: Set/Clear the "hold" flag in any account.  This flag serves a dual purpose:
509     * Setting it gives us an indication that it was blocked, and clearing it gives EAS a
510     * signal to try syncing again.
511     * @param context
512     * @param account the account whose hold flag is to be set/cleared
513     * @param newState true = security hold, false = free to sync
514     */
515    public static void setAccountHoldFlag(Context context, Account account, boolean newState) {
516        if (newState) {
517            account.mFlags |= Account.FLAGS_SECURITY_HOLD;
518        } else {
519            account.mFlags &= ~Account.FLAGS_SECURITY_HOLD;
520        }
521        ContentValues cv = new ContentValues();
522        cv.put(AccountColumns.FLAGS, account.mFlags);
523        account.update(context, cv);
524    }
525
526    public static void clearAccountPolicy(Context context, Account account) {
527        setAccountPolicy(context, account, null, null);
528    }
529
530    /**
531     * Set the policy for an account atomically; this also removes any other policy associated with
532     * the account and sets the policy key for the account.  If policy is null, the policyKey is
533     * set to 0 and the securitySyncKey to null.  Also, update the account object to reflect the
534     * current policyKey and securitySyncKey
535     * @param context the caller's context
536     * @param account the account whose policy is to be set
537     * @param policy the policy to set, or null if we're clearing the policy
538     * @param securitySyncKey the security sync key for this account (ignored if policy is null)
539     */
540    public static void setAccountPolicy(Context context, Account account, Policy policy,
541            String securitySyncKey) {
542        ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>();
543
544        // Make sure this is a valid policy set
545        if (policy != null) {
546            policy.normalize();
547            // Add the new policy (no account will yet reference this)
548            ops.add(ContentProviderOperation.newInsert(
549                    Policy.CONTENT_URI).withValues(policy.toContentValues()).build());
550            // Make the policyKey of the account our newly created policy, and set the sync key
551            ops.add(ContentProviderOperation.newUpdate(
552                    ContentUris.withAppendedId(Account.CONTENT_URI, account.mId))
553                    .withValueBackReference(AccountColumns.POLICY_KEY, 0)
554                    .withValue(AccountColumns.SECURITY_SYNC_KEY, securitySyncKey)
555                    .build());
556        } else {
557            ops.add(ContentProviderOperation.newUpdate(
558                    ContentUris.withAppendedId(Account.CONTENT_URI, account.mId))
559                    .withValue(AccountColumns.SECURITY_SYNC_KEY, null)
560                    .withValue(AccountColumns.POLICY_KEY, 0)
561                    .build());
562        }
563
564        // Delete the previous policy associated with this account, if any
565        if (account.mPolicyKey > 0) {
566            ops.add(ContentProviderOperation.newDelete(
567                    ContentUris.withAppendedId(
568                            Policy.CONTENT_URI, account.mPolicyKey)).build());
569        }
570
571        try {
572            context.getContentResolver().applyBatch(EmailContent.AUTHORITY, ops);
573            account.refresh(context);
574        } catch (RemoteException e) {
575            // This is fatal to a remote process
576            throw new IllegalStateException("Exception setting account policy.");
577        } catch (OperationApplicationException e) {
578            // Can't happen; our provider doesn't throw this exception
579        }
580    }
581
582    /**
583     * API: Report that policies may have been updated due to rewriting values in an Account; we
584     * clear the aggregate policy (so it can be recomputed) and set the policies in the DPM
585     */
586    public synchronized void policiesUpdated() {
587        mAggregatePolicy = null;
588        setActivePolicies();
589    }
590
591    public void setAccountPolicy(long accountId, Policy policy, String securityKey) {
592        Account account = Account.restoreAccountWithId(mContext, accountId);
593        Policy oldPolicy = null;
594        if (account.mPolicyKey > 0) {
595            oldPolicy = Policy.restorePolicyWithId(mContext, account.mPolicyKey);
596        }
597        boolean policyChanged = (oldPolicy == null) || !oldPolicy.equals(policy);
598        if (!policyChanged && (TextUtilities.stringOrNullEquals(securityKey,
599                account.mSecuritySyncKey))) {
600            Log.d(Logging.LOG_TAG, "setAccountPolicy; policy unchanged");
601        } else {
602            setAccountPolicy(mContext, account, policy, securityKey);
603            policiesUpdated();
604        }
605
606        boolean setHold = false;
607        if (isActive(policy)) {
608            // For Email1, ignore; it's really just a courtesy notification
609        } else {
610            setHold = true;
611            Log.d(Logging.LOG_TAG, "Notify policies for " + account.mDisplayName +
612                    " are not being enforced.");
613            // Put up a notification
614            NotificationController.getInstance(mContext).showSecurityNeededNotification(account);
615        }
616        // Set/clear the account hold.
617        setAccountHoldFlag(mContext, account, setHold);
618    }
619
620    /**
621     * API: Sync service should call this any time a sync fails due to isActive() returning false.
622     * This will kick off the notify-acquire-admin-state process and/or increase the security level.
623     * The caller needs to write the required policies into this account before making this call.
624     * Should not be called from UI thread - uses DB lookups to prepare new notifications
625     *
626     * @param accountId the account for which sync cannot proceed
627     */
628    public void policiesRequired(long accountId) {
629        Account account = Account.restoreAccountWithId(mContext, accountId);
630        // In case the account has been deleted, just return
631        if (account == null) return;
632        if (Email.DEBUG) {
633            if (account.mPolicyKey == 0) {
634                Log.d(TAG, "policiesRequired for " + account.mDisplayName + ": none");
635            } else {
636                Policy policy = Policy.restorePolicyWithId(mContext, account.mPolicyKey);
637                if (policy == null) {
638                    Log.w(TAG, "No policy??");
639                } else {
640                    Log.d(TAG, "policiesRequired for " + account.mDisplayName + ": " + policy);
641                }
642            }
643        }
644
645        // Mark the account as "on hold".
646        setAccountHoldFlag(mContext, account, true);
647
648        // Put up a notification
649        NotificationController.getInstance(mContext).showSecurityNeededNotification(account);
650    }
651
652    /**
653     * Called from the notification's intent receiver to register that the notification can be
654     * cleared now.
655     */
656    public void clearNotification() {
657        NotificationController.getInstance(mContext).cancelSecurityNeededNotification();
658    }
659
660    /**
661     * API: Remote wipe (from server).  This is final, there is no confirmation.  It will only
662     * return to the caller if there is an unexpected failure.  The wipe includes external storage.
663     */
664    public void remoteWipe() {
665        DevicePolicyManager dpm = getDPM();
666        if (dpm.isAdminActive(mAdminName)) {
667            dpm.wipeData(DevicePolicyManager.WIPE_EXTERNAL_STORAGE);
668        } else {
669            Log.d(Logging.LOG_TAG, "Could not remote wipe because not device admin.");
670        }
671    }
672    /**
673     * If we are not the active device admin, try to become so.
674     *
675     * Also checks for any policies that we have added during the lifetime of this app.
676     * This catches the case where the user granted an earlier (smaller) set of policies
677     * but an app upgrade requires that new policies be granted.
678     *
679     * @return true if we are already active, false if we are not
680     */
681    public boolean isActiveAdmin() {
682        DevicePolicyManager dpm = getDPM();
683        return dpm.isAdminActive(mAdminName)
684                && dpm.hasGrantedPolicy(mAdminName, DeviceAdminInfo.USES_POLICY_EXPIRE_PASSWORD)
685                && dpm.hasGrantedPolicy(mAdminName, DeviceAdminInfo.USES_ENCRYPTED_STORAGE)
686                && dpm.hasGrantedPolicy(mAdminName, DeviceAdminInfo.USES_POLICY_DISABLE_CAMERA)
687                && dpm.hasGrantedPolicy(mAdminName,
688                        DeviceAdminInfo.USES_POLICY_DISABLE_KEYGUARD_FEATURES);
689    }
690
691    /**
692     * Report admin component name - for making calls into device policy manager
693     */
694    public ComponentName getAdminComponent() {
695        return mAdminName;
696    }
697
698    /**
699     * Delete all accounts whose security flags aren't zero (i.e. they have security enabled).
700     * This method is synchronous, so it should normally be called within a worker thread (the
701     * exception being for unit tests)
702     *
703     * @param context the caller's context
704     */
705    /*package*/ void deleteSecuredAccounts(Context context) {
706        ContentResolver cr = context.getContentResolver();
707        // Find all accounts with security and delete them
708        Cursor c = cr.query(Account.CONTENT_URI, EmailContent.ID_PROJECTION,
709                Account.SECURITY_NONZERO_SELECTION, null, null);
710        try {
711            Log.w(TAG, "Email administration disabled; deleting " + c.getCount() +
712                    " secured account(s)");
713            while (c.moveToNext()) {
714                Controller.getInstance(context).deleteAccountSync(
715                        c.getLong(EmailContent.ID_PROJECTION_COLUMN), context);
716            }
717        } finally {
718            c.close();
719        }
720        policiesUpdated(-1);
721    }
722
723    /**
724     * Internal handler for enabled->disabled transitions.  Deletes all secured accounts.
725     * Must call from worker thread, not on UI thread.
726     */
727    /*package*/ void onAdminEnabled(boolean isEnabled) {
728        if (!isEnabled) {
729            deleteSecuredAccounts(mContext);
730        }
731    }
732
733    /**
734     * Handle password expiration - if any accounts appear to have triggered this, put up
735     * warnings, or even shut them down.
736     *
737     * NOTE:  If there are multiple accounts with password expiration policies, the device
738     * password will be set to expire in the shortest required interval (most secure).  The logic
739     * in this method operates based on the aggregate setting - irrespective of which account caused
740     * the expiration.  In other words, all accounts (that require expiration) will run/stop
741     * based on the requirements of the account with the shortest interval.
742     */
743    private void onPasswordExpiring(Context context) {
744        // 1.  Do we have any accounts that matter here?
745        long nextExpiringAccountId = findShortestExpiration(context);
746
747        // 2.  If not, exit immediately
748        if (nextExpiringAccountId == -1) {
749            return;
750        }
751
752        // 3.  If yes, are we warning or expired?
753        long expirationDate = getDPM().getPasswordExpiration(mAdminName);
754        long timeUntilExpiration = expirationDate - System.currentTimeMillis();
755        boolean expired = timeUntilExpiration < 0;
756        if (!expired) {
757            // 4.  If warning, simply put up a generic notification and report that it came from
758            // the shortest-expiring account.
759            NotificationController.getInstance(mContext).showPasswordExpiringNotification(
760                    nextExpiringAccountId);
761        } else {
762            // 5.  Actually expired - find all accounts that expire passwords, and wipe them
763            boolean wiped = wipeExpiredAccounts(context, Controller.getInstance(context));
764            if (wiped) {
765                NotificationController.getInstance(mContext).showPasswordExpiredNotification(
766                        nextExpiringAccountId);
767            }
768        }
769    }
770
771    /**
772     * Find the account with the shortest expiration time.  This is always assumed to be
773     * the account that forces the password to be refreshed.
774     * @return -1 if no expirations, or accountId if one is found
775     */
776    @VisibleForTesting
777    /*package*/ static long findShortestExpiration(Context context) {
778        long policyId = Utility.getFirstRowLong(context, Policy.CONTENT_URI, Policy.ID_PROJECTION,
779                HAS_PASSWORD_EXPIRATION, null, PolicyColumns.PASSWORD_EXPIRATION_DAYS + " ASC",
780                EmailContent.ID_PROJECTION_COLUMN, -1L);
781        if (policyId < 0) return -1L;
782        return Policy.getAccountIdWithPolicyKey(context, policyId);
783    }
784
785    /**
786     * For all accounts that require password expiration, put them in security hold and wipe
787     * their data.
788     * @param context
789     * @param controller
790     * @return true if one or more accounts were wiped
791     */
792    @VisibleForTesting
793    /*package*/ static boolean wipeExpiredAccounts(Context context, Controller controller) {
794        boolean result = false;
795        Cursor c = context.getContentResolver().query(Policy.CONTENT_URI,
796                Policy.ID_PROJECTION, HAS_PASSWORD_EXPIRATION, null, null);
797        try {
798            while (c.moveToNext()) {
799                long policyId = c.getLong(Policy.ID_PROJECTION_COLUMN);
800                long accountId = Policy.getAccountIdWithPolicyKey(context, policyId);
801                if (accountId < 0) continue;
802                Account account = Account.restoreAccountWithId(context, accountId);
803                if (account != null) {
804                    // Mark the account as "on hold".
805                    setAccountHoldFlag(context, account, true);
806                    // Erase data
807                    controller.deleteSyncedDataSync(accountId);
808                    // Report one or more were found
809                    result = true;
810                }
811            }
812        } finally {
813            c.close();
814        }
815        return result;
816    }
817
818    /**
819     * Callback from EmailBroadcastProcessorService.  This provides the workers for the
820     * DeviceAdminReceiver calls.  These should perform the work directly and not use async
821     * threads for completion.
822     */
823    public static void onDeviceAdminReceiverMessage(Context context, int message) {
824        SecurityPolicy instance = SecurityPolicy.getInstance(context);
825        switch (message) {
826            case DEVICE_ADMIN_MESSAGE_ENABLED:
827                instance.onAdminEnabled(true);
828                break;
829            case DEVICE_ADMIN_MESSAGE_DISABLED:
830                instance.onAdminEnabled(false);
831                break;
832            case DEVICE_ADMIN_MESSAGE_PASSWORD_CHANGED:
833                // TODO make a small helper for this
834                // Clear security holds (if any)
835                Account.clearSecurityHoldOnAllAccounts(context);
836                // Cancel any active notifications (if any are posted)
837                NotificationController.getInstance(context).cancelPasswordExpirationNotifications();
838                break;
839            case DEVICE_ADMIN_MESSAGE_PASSWORD_EXPIRING:
840                instance.onPasswordExpiring(instance.mContext);
841                break;
842        }
843    }
844
845    /**
846     * Device Policy administrator.  This is primarily a listener for device state changes.
847     * Note:  This is instantiated by incoming messages.
848     * Note:  This is actually a BroadcastReceiver and must remain within the guidelines required
849     *        for proper behavior, including avoidance of ANRs.
850     * Note:  We do not implement onPasswordFailed() because the default behavior of the
851     *        DevicePolicyManager - complete local wipe after 'n' failures - is sufficient.
852     */
853    public static class PolicyAdmin extends DeviceAdminReceiver {
854
855        /**
856         * Called after the administrator is first enabled.
857         */
858        @Override
859        public void onEnabled(Context context, Intent intent) {
860            EmailBroadcastProcessorService.processDevicePolicyMessage(context,
861                    DEVICE_ADMIN_MESSAGE_ENABLED);
862        }
863
864        /**
865         * Called prior to the administrator being disabled.
866         */
867        @Override
868        public void onDisabled(Context context, Intent intent) {
869            EmailBroadcastProcessorService.processDevicePolicyMessage(context,
870                    DEVICE_ADMIN_MESSAGE_DISABLED);
871        }
872
873        /**
874         * Called when the user asks to disable administration; we return a warning string that
875         * will be presented to the user
876         */
877        @Override
878        public CharSequence onDisableRequested(Context context, Intent intent) {
879            return context.getString(R.string.disable_admin_warning);
880        }
881
882        /**
883         * Called after the user has changed their password.
884         */
885        @Override
886        public void onPasswordChanged(Context context, Intent intent) {
887            EmailBroadcastProcessorService.processDevicePolicyMessage(context,
888                    DEVICE_ADMIN_MESSAGE_PASSWORD_CHANGED);
889        }
890
891        /**
892         * Called when device password is expiring
893         */
894        @Override
895        public void onPasswordExpiring(Context context, Intent intent) {
896            EmailBroadcastProcessorService.processDevicePolicyMessage(context,
897                    DEVICE_ADMIN_MESSAGE_PASSWORD_EXPIRING);
898        }
899    }
900}
901