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