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