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