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