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