Home | History | Annotate | Download | only in email
      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 
     17 package com.android.email;
     18 
     19 import android.app.admin.DeviceAdminInfo;
     20 import android.app.admin.DeviceAdminReceiver;
     21 import android.app.admin.DevicePolicyManager;
     22 import android.content.ComponentName;
     23 import android.content.ContentProviderOperation;
     24 import android.content.ContentResolver;
     25 import android.content.ContentUris;
     26 import android.content.ContentValues;
     27 import android.content.Context;
     28 import android.content.Intent;
     29 import android.content.OperationApplicationException;
     30 import android.database.Cursor;
     31 import android.net.Uri;
     32 import android.os.Bundle;
     33 import android.os.RemoteException;
     34 
     35 import com.android.email.NotificationController;
     36 import com.android.email.NotificationControllerCreatorHolder;
     37 import com.android.email.provider.AccountReconciler;
     38 import com.android.email.provider.EmailProvider;
     39 import com.android.email.service.EmailBroadcastProcessorService;
     40 import com.android.email.service.EmailServiceUtils;
     41 import com.android.emailcommon.Logging;
     42 import com.android.emailcommon.provider.Account;
     43 import com.android.emailcommon.provider.EmailContent;
     44 import com.android.emailcommon.provider.EmailContent.AccountColumns;
     45 import com.android.emailcommon.provider.EmailContent.PolicyColumns;
     46 import com.android.emailcommon.provider.Policy;
     47 import com.android.emailcommon.utility.TextUtilities;
     48 import com.android.emailcommon.utility.Utility;
     49 import com.android.mail.utils.LogUtils;
     50 import com.google.common.annotations.VisibleForTesting;
     51 
     52 import 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  */
     58 public class SecurityPolicy {
     59     private static final String TAG = "Email";
     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             try {
    442                 // If we are running in a managed policy, it is a securityException to even
    443                 // call setCameraDisabled(), if is disabled is false. We have to swallow
    444                 // the exception here.
    445                 dpm.setCameraDisabled(mAdminName, aggregatePolicy.mDontAllowCamera);
    446             } catch (SecurityException e) {
    447                 LogUtils.d(TAG, "SecurityException in setCameraDisabled, nothing changed");
    448             }
    449 
    450             // encryption required
    451             dpm.setStorageEncryption(mAdminName, aggregatePolicy.mRequireEncryption);
    452         }
    453     }
    454 
    455     /**
    456      * Convenience method; see javadoc below
    457      */
    458     public static void setAccountHoldFlag(Context context, long accountId, boolean newState) {
    459         Account account = Account.restoreAccountWithId(context, accountId);
    460         if (account != null) {
    461             setAccountHoldFlag(context, account, newState);
    462             if (newState) {
    463                 // Make sure there's a notification up
    464                 final NotificationController nc =
    465                         NotificationControllerCreatorHolder.getInstance(context);
    466                 nc.showSecurityNeededNotification(account);
    467             }
    468         }
    469     }
    470 
    471     /**
    472      * API: Set/Clear the "hold" flag in any account.  This flag serves a dual purpose:
    473      * Setting it gives us an indication that it was blocked, and clearing it gives EAS a
    474      * signal to try syncing again.
    475      * @param context context
    476      * @param account the account whose hold flag is to be set/cleared
    477      * @param newState true = security hold, false = free to sync
    478      */
    479     public static void setAccountHoldFlag(Context context, Account account, boolean newState) {
    480         if (newState) {
    481             account.mFlags |= Account.FLAGS_SECURITY_HOLD;
    482         } else {
    483             account.mFlags &= ~Account.FLAGS_SECURITY_HOLD;
    484         }
    485         ContentValues cv = new ContentValues();
    486         cv.put(AccountColumns.FLAGS, account.mFlags);
    487         account.update(context, cv);
    488     }
    489 
    490     /**
    491      * API: Sync service should call this any time a sync fails due to isActive() returning false.
    492      * This will kick off the notify-acquire-admin-state process and/or increase the security level.
    493      * The caller needs to write the required policies into this account before making this call.
    494      * Should not be called from UI thread - uses DB lookups to prepare new notifications
    495      *
    496      * @param accountId the account for which sync cannot proceed
    497      */
    498     public void policiesRequired(long accountId) {
    499         Account account = Account.restoreAccountWithId(mContext, accountId);
    500         // In case the account has been deleted, just return
    501         if (account == null) return;
    502         if (account.mPolicyKey == 0) return;
    503         Policy policy = Policy.restorePolicyWithId(mContext, account.mPolicyKey);
    504         if (policy == null) return;
    505         if (DebugUtils.DEBUG) {
    506             LogUtils.d(TAG, "policiesRequired for " + account.mDisplayName + ": " + policy);
    507         }
    508 
    509         // Mark the account as "on hold".
    510         setAccountHoldFlag(mContext, account, true);
    511 
    512         // Put up an appropriate notification
    513         final NotificationController nc =
    514                 NotificationControllerCreatorHolder.getInstance(mContext);
    515         if (policy.mProtocolPoliciesUnsupported == null) {
    516             nc.showSecurityNeededNotification(account);
    517         } else {
    518             nc.showSecurityUnsupportedNotification(account);
    519         }
    520     }
    521 
    522     public static void clearAccountPolicy(Context context, Account account) {
    523         setAccountPolicy(context, account, null, null);
    524     }
    525 
    526     /**
    527      * Set the policy for an account atomically; this also removes any other policy associated with
    528      * the account and sets the policy key for the account.  If policy is null, the policyKey is
    529      * set to 0 and the securitySyncKey to null.  Also, update the account object to reflect the
    530      * current policyKey and securitySyncKey
    531      * @param context the caller's context
    532      * @param account the account whose policy is to be set
    533      * @param policy the policy to set, or null if we're clearing the policy
    534      * @param securitySyncKey the security sync key for this account (ignored if policy is null)
    535      */
    536     public static void setAccountPolicy(Context context, Account account, Policy policy,
    537             String securitySyncKey) {
    538         ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>();
    539 
    540         // Make sure this is a valid policy set
    541         if (policy != null) {
    542             policy.normalize();
    543             // Add the new policy (no account will yet reference this)
    544             ops.add(ContentProviderOperation.newInsert(
    545                     Policy.CONTENT_URI).withValues(policy.toContentValues()).build());
    546             // Make the policyKey of the account our newly created policy, and set the sync key
    547             ops.add(ContentProviderOperation.newUpdate(
    548                     ContentUris.withAppendedId(Account.CONTENT_URI, account.mId))
    549                     .withValueBackReference(AccountColumns.POLICY_KEY, 0)
    550                     .withValue(AccountColumns.SECURITY_SYNC_KEY, securitySyncKey)
    551                     .build());
    552         } else {
    553             ops.add(ContentProviderOperation.newUpdate(
    554                     ContentUris.withAppendedId(Account.CONTENT_URI, account.mId))
    555                     .withValue(AccountColumns.SECURITY_SYNC_KEY, null)
    556                     .withValue(AccountColumns.POLICY_KEY, 0)
    557                     .build());
    558         }
    559 
    560         // Delete the previous policy associated with this account, if any
    561         if (account.mPolicyKey > 0) {
    562             ops.add(ContentProviderOperation.newDelete(
    563                     ContentUris.withAppendedId(
    564                             Policy.CONTENT_URI, account.mPolicyKey)).build());
    565         }
    566 
    567         try {
    568             context.getContentResolver().applyBatch(EmailContent.AUTHORITY, ops);
    569             account.refresh(context);
    570             syncAccount(context, account);
    571         } catch (RemoteException e) {
    572            // This is fatal to a remote process
    573             throw new IllegalStateException("Exception setting account policy.");
    574         } catch (OperationApplicationException e) {
    575             // Can't happen; our provider doesn't throw this exception
    576         }
    577     }
    578 
    579     private static void syncAccount(final Context context, final Account account) {
    580         final EmailServiceUtils.EmailServiceInfo info =
    581                 EmailServiceUtils.getServiceInfo(context, account.getProtocol(context));
    582         final android.accounts.Account amAccount =
    583                 new android.accounts.Account(account.mEmailAddress, info.accountType);
    584         final Bundle extras = new Bundle(3);
    585         extras.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true);
    586         extras.putBoolean(ContentResolver.SYNC_EXTRAS_DO_NOT_RETRY, true);
    587         extras.putBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, true);
    588         ContentResolver.requestSync(amAccount, EmailContent.AUTHORITY, extras);
    589         LogUtils.i(TAG, "requestSync SecurityPolicy syncAccount %s, %s", account.toString(),
    590                 extras.toString());
    591     }
    592 
    593     public void syncAccount(final Account account) {
    594         syncAccount(mContext, account);
    595     }
    596 
    597     public void setAccountPolicy(long accountId, Policy policy, String securityKey,
    598             boolean notify) {
    599         Account account = Account.restoreAccountWithId(mContext, accountId);
    600         // In case the account has been deleted, just return
    601         if (account == null) {
    602             return;
    603         }
    604         Policy oldPolicy = null;
    605         if (account.mPolicyKey > 0) {
    606             oldPolicy = Policy.restorePolicyWithId(mContext, account.mPolicyKey);
    607         }
    608 
    609         // If attachment policies have changed, fix up any affected attachment records
    610         if (oldPolicy != null && securityKey != null) {
    611             if ((oldPolicy.mDontAllowAttachments != policy.mDontAllowAttachments) ||
    612                     (oldPolicy.mMaxAttachmentSize != policy.mMaxAttachmentSize)) {
    613                 Policy.setAttachmentFlagsForNewPolicy(mContext, account, policy);
    614             }
    615         }
    616 
    617         boolean policyChanged = (oldPolicy == null) || !oldPolicy.equals(policy);
    618         if (!policyChanged && (TextUtilities.stringOrNullEquals(securityKey,
    619                 account.mSecuritySyncKey))) {
    620             LogUtils.d(Logging.LOG_TAG, "setAccountPolicy; policy unchanged");
    621         } else {
    622             setAccountPolicy(mContext, account, policy, securityKey);
    623             policiesUpdated();
    624         }
    625 
    626         boolean setHold = false;
    627         final NotificationController nc =
    628                 NotificationControllerCreatorHolder.getInstance(mContext);
    629         if (policy.mProtocolPoliciesUnsupported != null) {
    630             // We can't support this, reasons in unsupportedRemotePolicies
    631             LogUtils.d(Logging.LOG_TAG,
    632                     "Notify policies for " + account.mDisplayName + " not supported.");
    633             setHold = true;
    634             if (notify) {
    635                 nc.showSecurityUnsupportedNotification(account);
    636             }
    637             // Erase data
    638             Uri uri = EmailProvider.uiUri("uiaccountdata", accountId);
    639             mContext.getContentResolver().delete(uri, null, null);
    640         } else if (isActive(policy)) {
    641             if (policyChanged) {
    642                 LogUtils.d(Logging.LOG_TAG, "Notify policies for " + account.mDisplayName
    643                         + " changed.");
    644                 if (notify) {
    645                     // Notify that policies changed
    646                     nc.showSecurityChangedNotification(account);
    647                 }
    648             } else {
    649                 LogUtils.d(Logging.LOG_TAG, "Policy is active and unchanged; do not notify.");
    650             }
    651         } else {
    652             setHold = true;
    653             LogUtils.d(Logging.LOG_TAG, "Notify policies for " + account.mDisplayName +
    654                     " are not being enforced.");
    655             if (notify) {
    656                 // Put up a notification
    657                 nc.showSecurityNeededNotification(account);
    658             }
    659         }
    660         // Set/clear the account hold.
    661         setAccountHoldFlag(mContext, account, setHold);
    662     }
    663 
    664     /**
    665      * Called from the notification's intent receiver to register that the notification can be
    666      * cleared now.
    667      */
    668     public void clearNotification() {
    669         final NotificationController nc =
    670                 NotificationControllerCreatorHolder.getInstance(mContext);
    671 
    672         nc.cancelSecurityNeededNotification();
    673     }
    674 
    675     /**
    676      * API: Remote wipe (from server).  This is final, there is no confirmation.  It will only
    677      * return to the caller if there is an unexpected failure.  The wipe includes external storage.
    678      */
    679     public void remoteWipe() {
    680         DevicePolicyManager dpm = getDPM();
    681         if (dpm.isAdminActive(mAdminName)) {
    682             dpm.wipeData(DevicePolicyManager.WIPE_EXTERNAL_STORAGE);
    683         } else {
    684             LogUtils.d(Logging.LOG_TAG, "Could not remote wipe because not device admin.");
    685         }
    686     }
    687     /**
    688      * If we are not the active device admin, try to become so.
    689      *
    690      * Also checks for any policies that we have added during the lifetime of this app.
    691      * This catches the case where the user granted an earlier (smaller) set of policies
    692      * but an app upgrade requires that new policies be granted.
    693      *
    694      * @return true if we are already active, false if we are not
    695      */
    696     public boolean isActiveAdmin() {
    697         DevicePolicyManager dpm = getDPM();
    698         return dpm.isAdminActive(mAdminName)
    699                 && dpm.hasGrantedPolicy(mAdminName, DeviceAdminInfo.USES_POLICY_EXPIRE_PASSWORD)
    700                 && dpm.hasGrantedPolicy(mAdminName, DeviceAdminInfo.USES_ENCRYPTED_STORAGE)
    701                 && dpm.hasGrantedPolicy(mAdminName, DeviceAdminInfo.USES_POLICY_DISABLE_CAMERA);
    702     }
    703 
    704     /**
    705      * Report admin component name - for making calls into device policy manager
    706      */
    707     public ComponentName getAdminComponent() {
    708         return mAdminName;
    709     }
    710 
    711     /**
    712      * Delete all accounts whose security flags aren't zero (i.e. they have security enabled).
    713      * This method is synchronous, so it should normally be called within a worker thread (the
    714      * exception being for unit tests)
    715      *
    716      * @param context the caller's context
    717      */
    718     /*package*/ void deleteSecuredAccounts(Context context) {
    719         ContentResolver cr = context.getContentResolver();
    720         // Find all accounts with security and delete them
    721         Cursor c = cr.query(Account.CONTENT_URI, EmailContent.ID_PROJECTION,
    722                 Account.SECURITY_NONZERO_SELECTION, null, null);
    723         try {
    724             LogUtils.w(TAG, "Email administration disabled; deleting " + c.getCount() +
    725                     " secured account(s)");
    726             while (c.moveToNext()) {
    727                 long accountId = c.getLong(EmailContent.ID_PROJECTION_COLUMN);
    728                 Uri uri = EmailProvider.uiUri("uiaccount", accountId);
    729                 cr.delete(uri, null, null);
    730             }
    731         } finally {
    732             c.close();
    733         }
    734         policiesUpdated();
    735         AccountReconciler.reconcileAccounts(context);
    736     }
    737 
    738     /**
    739      * Internal handler for enabled->disabled transitions.  Deletes all secured accounts.
    740      * Must call from worker thread, not on UI thread.
    741      */
    742     /*package*/ void onAdminEnabled(boolean isEnabled) {
    743         if (!isEnabled) {
    744             deleteSecuredAccounts(mContext);
    745         }
    746     }
    747 
    748     /**
    749      * Handle password expiration - if any accounts appear to have triggered this, put up
    750      * warnings, or even shut them down.
    751      *
    752      * NOTE:  If there are multiple accounts with password expiration policies, the device
    753      * password will be set to expire in the shortest required interval (most secure).  The logic
    754      * in this method operates based on the aggregate setting - irrespective of which account caused
    755      * the expiration.  In other words, all accounts (that require expiration) will run/stop
    756      * based on the requirements of the account with the shortest interval.
    757      */
    758     private void onPasswordExpiring(Context context) {
    759         // 1.  Do we have any accounts that matter here?
    760         long nextExpiringAccountId = findShortestExpiration(context);
    761 
    762         // 2.  If not, exit immediately
    763         if (nextExpiringAccountId == -1) {
    764             return;
    765         }
    766 
    767         // 3.  If yes, are we warning or expired?
    768         long expirationDate = getDPM().getPasswordExpiration(mAdminName);
    769         long timeUntilExpiration = expirationDate - System.currentTimeMillis();
    770         boolean expired = timeUntilExpiration < 0;
    771         final NotificationController nc =
    772                 NotificationControllerCreatorHolder.getInstance(context);
    773         if (!expired) {
    774             // 4.  If warning, simply put up a generic notification and report that it came from
    775             // the shortest-expiring account.
    776             nc.showPasswordExpiringNotificationSynchronous(nextExpiringAccountId);
    777         } else {
    778             // 5.  Actually expired - find all accounts that expire passwords, and wipe them
    779             boolean wiped = wipeExpiredAccounts(context);
    780             if (wiped) {
    781                 nc.showPasswordExpiredNotificationSynchronous(nextExpiringAccountId);
    782             }
    783         }
    784     }
    785 
    786     /**
    787      * Find the account with the shortest expiration time.  This is always assumed to be
    788      * the account that forces the password to be refreshed.
    789      * @return -1 if no expirations, or accountId if one is found
    790      */
    791     @VisibleForTesting
    792     /*package*/ static long findShortestExpiration(Context context) {
    793         long policyId = Utility.getFirstRowLong(context, Policy.CONTENT_URI, Policy.ID_PROJECTION,
    794                 HAS_PASSWORD_EXPIRATION, null, PolicyColumns.PASSWORD_EXPIRATION_DAYS + " ASC",
    795                 EmailContent.ID_PROJECTION_COLUMN, -1L);
    796         if (policyId < 0) return -1L;
    797         return Policy.getAccountIdWithPolicyKey(context, policyId);
    798     }
    799 
    800     /**
    801      * For all accounts that require password expiration, put them in security hold and wipe
    802      * their data.
    803      * @param context context
    804      * @return true if one or more accounts were wiped
    805      */
    806     @VisibleForTesting
    807     /*package*/ static boolean wipeExpiredAccounts(Context context) {
    808         boolean result = false;
    809         Cursor c = context.getContentResolver().query(Policy.CONTENT_URI,
    810                 Policy.ID_PROJECTION, HAS_PASSWORD_EXPIRATION, null, null);
    811         if (c == null) {
    812             return false;
    813         }
    814         try {
    815             while (c.moveToNext()) {
    816                 long policyId = c.getLong(Policy.ID_PROJECTION_COLUMN);
    817                 long accountId = Policy.getAccountIdWithPolicyKey(context, policyId);
    818                 if (accountId < 0) continue;
    819                 Account account = Account.restoreAccountWithId(context, accountId);
    820                 if (account != null) {
    821                     // Mark the account as "on hold".
    822                     setAccountHoldFlag(context, account, true);
    823                     // Erase data
    824                     Uri uri = EmailProvider.uiUri("uiaccountdata", accountId);
    825                     context.getContentResolver().delete(uri, null, null);
    826                     // Report one or more were found
    827                     result = true;
    828                 }
    829             }
    830         } finally {
    831             c.close();
    832         }
    833         return result;
    834     }
    835 
    836     /**
    837      * Callback from EmailBroadcastProcessorService.  This provides the workers for the
    838      * DeviceAdminReceiver calls.  These should perform the work directly and not use async
    839      * threads for completion.
    840      */
    841     public static void onDeviceAdminReceiverMessage(Context context, int message) {
    842         SecurityPolicy instance = SecurityPolicy.getInstance(context);
    843         switch (message) {
    844             case DEVICE_ADMIN_MESSAGE_ENABLED:
    845                 instance.onAdminEnabled(true);
    846                 break;
    847             case DEVICE_ADMIN_MESSAGE_DISABLED:
    848                 instance.onAdminEnabled(false);
    849                 break;
    850             case DEVICE_ADMIN_MESSAGE_PASSWORD_CHANGED:
    851                 // TODO make a small helper for this
    852                 // Clear security holds (if any)
    853                 Account.clearSecurityHoldOnAllAccounts(context);
    854                 // Cancel any active notifications (if any are posted)
    855                 final NotificationController nc =
    856                         NotificationControllerCreatorHolder.getInstance(context);
    857 
    858                 nc.cancelPasswordExpirationNotifications();
    859                 break;
    860             case DEVICE_ADMIN_MESSAGE_PASSWORD_EXPIRING:
    861                 instance.onPasswordExpiring(instance.mContext);
    862                 break;
    863         }
    864     }
    865 
    866     /**
    867      * Device Policy administrator.  This is primarily a listener for device state changes.
    868      * Note:  This is instantiated by incoming messages.
    869      * Note:  This is actually a BroadcastReceiver and must remain within the guidelines required
    870      *        for proper behavior, including avoidance of ANRs.
    871      * Note:  We do not implement onPasswordFailed() because the default behavior of the
    872      *        DevicePolicyManager - complete local wipe after 'n' failures - is sufficient.
    873      */
    874     public static class PolicyAdmin extends DeviceAdminReceiver {
    875 
    876         /**
    877          * Called after the administrator is first enabled.
    878          */
    879         @Override
    880         public void onEnabled(Context context, Intent intent) {
    881             EmailBroadcastProcessorService.processDevicePolicyMessage(context,
    882                     DEVICE_ADMIN_MESSAGE_ENABLED);
    883         }
    884 
    885         /**
    886          * Called prior to the administrator being disabled.
    887          */
    888         @Override
    889         public void onDisabled(Context context, Intent intent) {
    890             EmailBroadcastProcessorService.processDevicePolicyMessage(context,
    891                     DEVICE_ADMIN_MESSAGE_DISABLED);
    892         }
    893 
    894         /**
    895          * Called when the user asks to disable administration; we return a warning string that
    896          * will be presented to the user
    897          */
    898         @Override
    899         public CharSequence onDisableRequested(Context context, Intent intent) {
    900             return context.getString(R.string.disable_admin_warning);
    901         }
    902 
    903         /**
    904          * Called after the user has changed their password.
    905          */
    906         @Override
    907         public void onPasswordChanged(Context context, Intent intent) {
    908             EmailBroadcastProcessorService.processDevicePolicyMessage(context,
    909                     DEVICE_ADMIN_MESSAGE_PASSWORD_CHANGED);
    910         }
    911 
    912         /**
    913          * Called when device password is expiring
    914          */
    915         @Override
    916         public void onPasswordExpiring(Context context, Intent intent) {
    917             EmailBroadcastProcessorService.processDevicePolicyMessage(context,
    918                     DEVICE_ADMIN_MESSAGE_PASSWORD_EXPIRING);
    919         }
    920     }
    921 }
    922