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