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