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