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