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