MailService.java revision f5418f1f93b02e7fab9f15eb201800b65510998e
1/* 2 * Copyright (C) 2008 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.service; 18 19import com.android.email.Controller; 20import com.android.email.Email; 21import com.android.email.Preferences; 22import com.android.email.SecurityPolicy; 23import com.android.email.SingleRunningTask; 24import com.android.email.provider.AccountBackupRestore; 25import com.android.emailcommon.AccountManagerTypes; 26import com.android.emailcommon.mail.MessagingException; 27import com.android.emailcommon.provider.Account; 28import com.android.emailcommon.provider.EmailContent; 29import com.android.emailcommon.provider.HostAuth; 30import com.android.emailcommon.provider.Mailbox; 31import com.android.emailcommon.utility.AccountReconciler; 32import com.android.emailcommon.utility.EmailAsyncTask; 33import com.google.common.annotations.VisibleForTesting; 34 35import android.accounts.AccountManager; 36import android.accounts.AccountManagerCallback; 37import android.app.AlarmManager; 38import android.app.PendingIntent; 39import android.app.Service; 40import android.content.ContentResolver; 41import android.content.ContentUris; 42import android.content.Context; 43import android.content.Intent; 44import android.content.SyncStatusObserver; 45import android.database.Cursor; 46import android.net.ConnectivityManager; 47import android.net.Uri; 48import android.os.Bundle; 49import android.os.IBinder; 50import android.os.SystemClock; 51import android.text.TextUtils; 52import android.util.Log; 53 54import java.util.ArrayList; 55import java.util.HashMap; 56import java.util.List; 57 58/** 59 * Background service for refreshing non-push email accounts. 60 * 61 * TODO: Convert to IntentService to move *all* work off the UI thread, serialize work, and avoid 62 * possible problems with out-of-order startId processing. 63 */ 64public class MailService extends Service { 65 private static final String LOG_TAG = "Email-MailService"; 66 67 private static final String ACTION_CHECK_MAIL = 68 "com.android.email.intent.action.MAIL_SERVICE_WAKEUP"; 69 private static final String ACTION_RESCHEDULE = 70 "com.android.email.intent.action.MAIL_SERVICE_RESCHEDULE"; 71 private static final String ACTION_CANCEL = 72 "com.android.email.intent.action.MAIL_SERVICE_CANCEL"; 73 private static final String ACTION_SEND_PENDING_MAIL = 74 "com.android.email.intent.action.MAIL_SERVICE_SEND_PENDING"; 75 private static final String ACTION_DELETE_EXCHANGE_ACCOUNTS = 76 "com.android.email.intent.action.MAIL_SERVICE_DELETE_EXCHANGE_ACCOUNTS"; 77 78 private static final String EXTRA_ACCOUNT = "com.android.email.intent.extra.ACCOUNT"; 79 private static final String EXTRA_ACCOUNT_INFO = "com.android.email.intent.extra.ACCOUNT_INFO"; 80 private static final String EXTRA_DEBUG_WATCHDOG = "com.android.email.intent.extra.WATCHDOG"; 81 82 /** Time between watchdog checks; in milliseconds */ 83 private static final long WATCHDOG_DELAY = 10 * 60 * 1000; // 10 minutes 84 85 /** Sentinel value asking to update mSyncReports if it's currently empty */ 86 @VisibleForTesting 87 static final int SYNC_REPORTS_ALL_ACCOUNTS_IF_EMPTY = -1; 88 /** Sentinel value asking that mSyncReports be rebuilt */ 89 @VisibleForTesting 90 static final int SYNC_REPORTS_RESET = -2; 91 92 @VisibleForTesting 93 Controller mController; 94 private final Controller.Result mControllerCallback = new ControllerResults(); 95 private ContentResolver mContentResolver; 96 private Context mContext; 97 98 private int mStartId; 99 100 /** 101 * Access must be synchronized, because there are accesses from the Controller callback 102 */ 103 /*package*/ static HashMap<Long,AccountSyncReport> mSyncReports = 104 new HashMap<Long,AccountSyncReport>(); 105 106 public static void actionReschedule(Context context) { 107 Intent i = new Intent(); 108 i.setClass(context, MailService.class); 109 i.setAction(MailService.ACTION_RESCHEDULE); 110 context.startService(i); 111 } 112 113 public static void actionCancel(Context context) { 114 Intent i = new Intent(); 115 i.setClass(context, MailService.class); 116 i.setAction(MailService.ACTION_CANCEL); 117 context.startService(i); 118 } 119 120 public static void actionDeleteExchangeAccounts(Context context) { 121 Intent i = new Intent(); 122 i.setClass(context, MailService.class); 123 i.setAction(MailService.ACTION_DELETE_EXCHANGE_ACCOUNTS); 124 context.startService(i); 125 } 126 127 /** 128 * Entry point for AttachmentDownloadService to ask that pending mail be sent 129 * @param context the caller's context 130 * @param accountId the account whose pending mail should be sent 131 */ 132 public static void actionSendPendingMail(Context context, long accountId) { 133 Intent i = new Intent(); 134 i.setClass(context, MailService.class); 135 i.setAction(MailService.ACTION_SEND_PENDING_MAIL); 136 i.putExtra(MailService.EXTRA_ACCOUNT, accountId); 137 context.startService(i); 138 } 139 140 @Override 141 public int onStartCommand(final Intent intent, int flags, final int startId) { 142 super.onStartCommand(intent, flags, startId); 143 144 // Restore accounts, if it has not happened already 145 AccountBackupRestore.restoreIfNeeded(this); 146 147 EmailAsyncTask.runAsyncParallel(new Runnable() { 148 @Override 149 public void run() { 150 reconcilePopImapAccountsSync(MailService.this); 151 } 152 }); 153 154 // TODO this needs to be passed through the controller and back to us 155 mStartId = startId; 156 String action = intent.getAction(); 157 final long accountId = intent.getLongExtra(EXTRA_ACCOUNT, -1); 158 159 mController = Controller.getInstance(this); 160 mController.addResultCallback(mControllerCallback); 161 mContentResolver = getContentResolver(); 162 mContext = this; 163 164 final AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE); 165 166 if (ACTION_CHECK_MAIL.equals(action)) { 167 // DB access required to satisfy this intent, so offload from UI thread 168 EmailAsyncTask.runAsyncParallel(new Runnable() { 169 @Override 170 public void run() { 171 // If we have the data, restore the last-sync-times for each account 172 // These are cached in the wakeup intent in case the process was killed. 173 restoreSyncReports(intent); 174 175 // Sync a specific account if given 176 if (Email.DEBUG) { 177 Log.d(LOG_TAG, "action: check mail for id=" + accountId); 178 } 179 if (accountId >= 0) { 180 setWatchdog(accountId, alarmManager); 181 } 182 183 // Start sync if account is given && bg data enabled && account has sync enabled 184 boolean syncStarted = false; 185 if (accountId != -1 && isBackgroundDataEnabled()) { 186 synchronized(mSyncReports) { 187 for (AccountSyncReport report: mSyncReports.values()) { 188 if (report.accountId == accountId) { 189 if (report.syncEnabled) { 190 syncStarted = syncOneAccount(mController, accountId, 191 startId); 192 } 193 break; 194 } 195 } 196 } 197 } 198 199 // Reschedule if we didn't start sync. 200 if (!syncStarted) { 201 // Prevent runaway on the current account by pretending it updated 202 if (accountId != -1) { 203 updateAccountReport(accountId, 0); 204 } 205 // Find next account to sync, and reschedule 206 reschedule(alarmManager); 207 // Stop the service, unless actually syncing (which will stop the service) 208 stopSelf(startId); 209 } 210 } 211 }); 212 } 213 else if (ACTION_CANCEL.equals(action)) { 214 if (Email.DEBUG) { 215 Log.d(LOG_TAG, "action: cancel"); 216 } 217 cancel(); 218 stopSelf(startId); 219 } 220 else if (ACTION_DELETE_EXCHANGE_ACCOUNTS.equals(action)) { 221 if (Email.DEBUG) { 222 Log.d(LOG_TAG, "action: delete exchange accounts"); 223 } 224 EmailAsyncTask.runAsyncParallel(new Runnable() { 225 @Override 226 public void run() { 227 Cursor c = mContentResolver.query(Account.CONTENT_URI, Account.ID_PROJECTION, 228 null, null, null); 229 try { 230 while (c.moveToNext()) { 231 long accountId = c.getLong(Account.ID_PROJECTION_COLUMN); 232 if ("eas".equals(Account.getProtocol(mContext, accountId))) { 233 // Always log this 234 Log.d(LOG_TAG, "Deleting EAS account: " + accountId); 235 mController.deleteAccountSync(accountId, mContext); 236 } 237 } 238 } finally { 239 c.close(); 240 } 241 } 242 }); 243 stopSelf(startId); 244 } 245 else if (ACTION_SEND_PENDING_MAIL.equals(action)) { 246 if (Email.DEBUG) { 247 Log.d(LOG_TAG, "action: send pending mail"); 248 } 249 EmailAsyncTask.runAsyncParallel(new Runnable() { 250 @Override 251 public void run() { 252 mController.sendPendingMessages(accountId); 253 } 254 }); 255 stopSelf(startId); 256 } 257 else if (ACTION_RESCHEDULE.equals(action)) { 258 if (Email.DEBUG) { 259 Log.d(LOG_TAG, "action: reschedule"); 260 } 261 // DB access required to satisfy this intent, so offload from UI thread 262 EmailAsyncTask.runAsyncParallel(new Runnable() { 263 @Override 264 public void run() { 265 // When called externally, we refresh the sync reports table to pick up 266 // any changes in the account list or account settings 267 refreshSyncReports(); 268 // Finally, scan for the next needing update, and set an alarm for it 269 reschedule(alarmManager); 270 stopSelf(startId); 271 } 272 }); 273 } 274 275 // Returning START_NOT_STICKY means that if a mail check is killed (e.g. due to memory 276 // pressure, there will be no explicit restart. This is OK; Note that we set a watchdog 277 // alarm before each mailbox check. If the mailbox check never completes, the watchdog 278 // will fire and get things running again. 279 return START_NOT_STICKY; 280 } 281 282 @Override 283 public IBinder onBind(Intent intent) { 284 return null; 285 } 286 287 @Override 288 public void onDestroy() { 289 super.onDestroy(); 290 Controller.getInstance(getApplication()).removeResultCallback(mControllerCallback); 291 } 292 293 private void cancel() { 294 AlarmManager alarmMgr = (AlarmManager)getSystemService(Context.ALARM_SERVICE); 295 PendingIntent pi = createAlarmIntent(-1, null, false); 296 alarmMgr.cancel(pi); 297 } 298 299 /** 300 * Refresh the sync reports, to pick up any changes in the account list or account settings. 301 */ 302 private void refreshSyncReports() { 303 synchronized (mSyncReports) { 304 // Make shallow copy of sync reports so we can recover the prev sync times 305 HashMap<Long,AccountSyncReport> oldSyncReports = 306 new HashMap<Long,AccountSyncReport>(mSyncReports); 307 308 // Delete the sync reports to force a refresh from live account db data 309 setupSyncReportsLocked(SYNC_REPORTS_RESET, this); 310 311 // Restore prev-sync & next-sync times for any reports in the new list 312 for (AccountSyncReport newReport : mSyncReports.values()) { 313 AccountSyncReport oldReport = oldSyncReports.get(newReport.accountId); 314 if (oldReport != null) { 315 newReport.prevSyncTime = oldReport.prevSyncTime; 316 newReport.setNextSyncTime(); 317 } 318 } 319 } 320 } 321 322 /** 323 * Create and send an alarm with the entire list. This also sends a list of known last-sync 324 * times with the alarm, so if we are killed between alarms, we don't lose this info. 325 * 326 * @param alarmMgr passed in so we can mock for testing. 327 */ 328 private void reschedule(AlarmManager alarmMgr) { 329 // restore the reports if lost 330 setupSyncReports(SYNC_REPORTS_ALL_ACCOUNTS_IF_EMPTY); 331 synchronized (mSyncReports) { 332 int numAccounts = mSyncReports.size(); 333 long[] accountInfo = new long[numAccounts * 2]; // pairs of { accountId, lastSync } 334 int accountInfoIndex = 0; 335 336 long nextCheckTime = Long.MAX_VALUE; 337 AccountSyncReport nextAccount = null; 338 long timeNow = SystemClock.elapsedRealtime(); 339 340 for (AccountSyncReport report : mSyncReports.values()) { 341 if (report.syncInterval <= 0) { // no timed checks - skip 342 continue; 343 } 344 long prevSyncTime = report.prevSyncTime; 345 long nextSyncTime = report.nextSyncTime; 346 347 // select next account to sync 348 if ((prevSyncTime == 0) || (nextSyncTime < timeNow)) { // never checked, or overdue 349 nextCheckTime = 0; 350 nextAccount = report; 351 } else if (nextSyncTime < nextCheckTime) { // next to be checked 352 nextCheckTime = nextSyncTime; 353 nextAccount = report; 354 } 355 // collect last-sync-times for all accounts 356 // this is using pairs of {long,long} to simplify passing in a bundle 357 accountInfo[accountInfoIndex++] = report.accountId; 358 accountInfo[accountInfoIndex++] = report.prevSyncTime; 359 } 360 361 // Clear out any unused elements in the array 362 while (accountInfoIndex < accountInfo.length) { 363 accountInfo[accountInfoIndex++] = -1; 364 } 365 366 // set/clear alarm as needed 367 long idToCheck = (nextAccount == null) ? -1 : nextAccount.accountId; 368 PendingIntent pi = createAlarmIntent(idToCheck, accountInfo, false); 369 370 if (nextAccount == null) { 371 alarmMgr.cancel(pi); 372 if (Email.DEBUG) { 373 Log.d(LOG_TAG, "reschedule: alarm cancel - no account to check"); 374 } 375 } else { 376 alarmMgr.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, nextCheckTime, pi); 377 if (Email.DEBUG) { 378 Log.d(LOG_TAG, "reschedule: alarm set at " + nextCheckTime 379 + " for " + nextAccount); 380 } 381 } 382 } 383 } 384 385 /** 386 * Create a watchdog alarm and set it. This is used in case a mail check fails (e.g. we are 387 * killed by the system due to memory pressure.) Normally, a mail check will complete and 388 * the watchdog will be replaced by the call to reschedule(). 389 * @param accountId the account we were trying to check 390 * @param alarmMgr system alarm manager 391 */ 392 private void setWatchdog(long accountId, AlarmManager alarmMgr) { 393 PendingIntent pi = createAlarmIntent(accountId, null, true); 394 long timeNow = SystemClock.elapsedRealtime(); 395 long nextCheckTime = timeNow + WATCHDOG_DELAY; 396 alarmMgr.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, nextCheckTime, pi); 397 } 398 399 /** 400 * Return a pending intent for use by this alarm. Most of the fields must be the same 401 * (in order for the intent to be recognized by the alarm manager) but the extras can 402 * be different, and are passed in here as parameters. 403 */ 404 private PendingIntent createAlarmIntent(long checkId, long[] accountInfo, boolean isWatchdog) { 405 Intent i = new Intent(); 406 i.setClass(this, MailService.class); 407 i.setAction(ACTION_CHECK_MAIL); 408 i.putExtra(EXTRA_ACCOUNT, checkId); 409 i.putExtra(EXTRA_ACCOUNT_INFO, accountInfo); 410 if (isWatchdog) { 411 i.putExtra(EXTRA_DEBUG_WATCHDOG, true); 412 } 413 PendingIntent pi = PendingIntent.getService(this, 0, i, PendingIntent.FLAG_UPDATE_CURRENT); 414 return pi; 415 } 416 417 /** 418 * Start a controller sync for a specific account 419 * 420 * @param controller The controller to do the sync work 421 * @param checkAccountId the account Id to try and check 422 * @param startId the id of this service launch 423 * @return true if mail checking has started, false if it could not (e.g. bad account id) 424 */ 425 private boolean syncOneAccount(Controller controller, long checkAccountId, int startId) { 426 long inboxId = Mailbox.findMailboxOfType(this, checkAccountId, Mailbox.TYPE_INBOX); 427 if (inboxId == Mailbox.NO_MAILBOX) { 428 return false; 429 } else { 430 controller.serviceCheckMail(checkAccountId, inboxId, startId); 431 return true; 432 } 433 } 434 435 /** 436 * Note: Times are relative to SystemClock.elapsedRealtime() 437 * 438 * TODO: Look more closely at syncEnabled and see if we can simply coalesce it into 439 * syncInterval (e.g. if !syncEnabled, set syncInterval to -1). 440 */ 441 @VisibleForTesting 442 static class AccountSyncReport { 443 long accountId; 444 /** The time of the last sync, or, {@code 0}, the last sync time is unknown. */ 445 long prevSyncTime; 446 /** The time of the next sync. If {@code 0}, sync ASAP. If {@code 1}, don't sync. */ 447 long nextSyncTime; 448 /** Minimum time between syncs; in minutes. */ 449 int syncInterval; 450 /** If {@code true}, auto sync is enabled. */ 451 boolean syncEnabled; 452 453 /** 454 * Sets the next sync time using the previous sync time and sync interval. 455 */ 456 private void setNextSyncTime() { 457 if (syncInterval > 0 && prevSyncTime != 0) { 458 nextSyncTime = prevSyncTime + (syncInterval * 1000 * 60); 459 } 460 } 461 462 @Override 463 public String toString() { 464 return "id=" + accountId + " prevSync=" + prevSyncTime + " nextSync=" + nextSyncTime; 465 } 466 } 467 468 /** 469 * scan accounts to create a list of { acct, prev sync, next sync, #new } 470 * use this to create a fresh copy. assumes all accounts need sync 471 * 472 * @param accountId -1 will rebuild the list if empty. other values will force loading 473 * of a single account (e.g if it was created after the original list population) 474 */ 475 private void setupSyncReports(long accountId) { 476 synchronized (mSyncReports) { 477 setupSyncReportsLocked(accountId, mContext); 478 } 479 } 480 481 /** 482 * Handle the work of setupSyncReports. Must be synchronized on mSyncReports. 483 */ 484 @VisibleForTesting 485 void setupSyncReportsLocked(long accountId, Context context) { 486 ContentResolver resolver = context.getContentResolver(); 487 if (accountId == SYNC_REPORTS_RESET) { 488 // For test purposes, force refresh of mSyncReports 489 mSyncReports.clear(); 490 accountId = SYNC_REPORTS_ALL_ACCOUNTS_IF_EMPTY; 491 } else if (accountId == SYNC_REPORTS_ALL_ACCOUNTS_IF_EMPTY) { 492 // -1 == reload the list if empty, otherwise exit immediately 493 if (mSyncReports.size() > 0) { 494 return; 495 } 496 } else { 497 // load a single account if it doesn't already have a sync record 498 if (mSyncReports.containsKey(accountId)) { 499 return; 500 } 501 } 502 503 // setup to add a single account or all accounts 504 Uri uri; 505 if (accountId == SYNC_REPORTS_ALL_ACCOUNTS_IF_EMPTY) { 506 uri = Account.CONTENT_URI; 507 } else { 508 uri = ContentUris.withAppendedId(Account.CONTENT_URI, accountId); 509 } 510 511 final boolean oneMinuteRefresh 512 = Preferences.getPreferences(this).getForceOneMinuteRefresh(); 513 if (oneMinuteRefresh) { 514 Log.w(LOG_TAG, "One-minute refresh enabled."); 515 } 516 517 // We use a full projection here because we'll restore each account object from it 518 Cursor c = resolver.query(uri, Account.CONTENT_PROJECTION, null, null, null); 519 try { 520 while (c.moveToNext()) { 521 Account account = Account.getContent(c, Account.class); 522 // The following sanity checks are primarily for the sake of ignoring non-user 523 // accounts that may have been left behind e.g. by failed unit tests. 524 // Properly-formed accounts will always pass these simple checks. 525 if (TextUtils.isEmpty(account.mEmailAddress) 526 || account.mHostAuthKeyRecv <= 0 527 || account.mHostAuthKeySend <= 0) { 528 continue; 529 } 530 531 // The account is OK, so proceed 532 AccountSyncReport report = new AccountSyncReport(); 533 int syncInterval = account.mSyncInterval; 534 535 // If we're not using MessagingController (EAS at this point), don't schedule syncs 536 if (!mController.isMessagingController(account.mId)) { 537 syncInterval = Account.CHECK_INTERVAL_NEVER; 538 } else if (oneMinuteRefresh && syncInterval >= 0) { 539 syncInterval = 1; 540 } 541 542 report.accountId = account.mId; 543 report.prevSyncTime = 0; 544 report.nextSyncTime = (syncInterval > 0) ? 0 : -1; // 0 == ASAP -1 == no sync 545 546 report.syncInterval = syncInterval; 547 548 // See if the account is enabled for sync in AccountManager 549 android.accounts.Account accountManagerAccount = 550 new android.accounts.Account(account.mEmailAddress, 551 AccountManagerTypes.TYPE_POP_IMAP); 552 report.syncEnabled = ContentResolver.getSyncAutomatically(accountManagerAccount, 553 EmailContent.AUTHORITY); 554 555 // TODO lookup # new in inbox 556 mSyncReports.put(report.accountId, report); 557 } 558 } finally { 559 c.close(); 560 } 561 } 562 563 /** 564 * Update list with a single account's sync times and unread count 565 * 566 * @param accountId the account being updated 567 * @param newCount the number of new messages, or -1 if not being reported (don't update) 568 * @return the report for the updated account, or null if it doesn't exist (e.g. deleted) 569 */ 570 private AccountSyncReport updateAccountReport(long accountId, int newCount) { 571 // restore the reports if lost 572 setupSyncReports(accountId); 573 synchronized (mSyncReports) { 574 AccountSyncReport report = mSyncReports.get(accountId); 575 if (report == null) { 576 // discard result - there is no longer an account with this id 577 Log.d(LOG_TAG, "No account to update for id=" + Long.toString(accountId)); 578 return null; 579 } 580 581 // report found - update it (note - editing the report while in-place in the hashmap) 582 report.prevSyncTime = SystemClock.elapsedRealtime(); 583 report.setNextSyncTime(); 584 if (Email.DEBUG) { 585 Log.d(LOG_TAG, "update account " + report.toString()); 586 } 587 return report; 588 } 589 } 590 591 /** 592 * when we receive an alarm, update the account sync reports list if necessary 593 * this will be the case when if we have restarted the process and lost the data 594 * in the global. 595 * 596 * @param restoreIntent the intent with the list 597 */ 598 private void restoreSyncReports(Intent restoreIntent) { 599 // restore the reports if lost 600 setupSyncReports(SYNC_REPORTS_ALL_ACCOUNTS_IF_EMPTY); 601 synchronized (mSyncReports) { 602 long[] accountInfo = restoreIntent.getLongArrayExtra(EXTRA_ACCOUNT_INFO); 603 if (accountInfo == null) { 604 Log.d(LOG_TAG, "no data in intent to restore"); 605 return; 606 } 607 int accountInfoIndex = 0; 608 int accountInfoLimit = accountInfo.length; 609 while (accountInfoIndex < accountInfoLimit) { 610 long accountId = accountInfo[accountInfoIndex++]; 611 long prevSync = accountInfo[accountInfoIndex++]; 612 AccountSyncReport report = mSyncReports.get(accountId); 613 if (report != null) { 614 if (report.prevSyncTime == 0) { 615 report.prevSyncTime = prevSync; 616 report.setNextSyncTime(); 617 } 618 } 619 } 620 } 621 } 622 623 class ControllerResults extends Controller.Result { 624 @Override 625 public void updateMailboxCallback(MessagingException result, long accountId, 626 long mailboxId, int progress, int numNewMessages, 627 ArrayList<Long> addedMessages) { 628 // First, look for authentication failures and notify 629 //checkAuthenticationStatus(result, accountId); 630 if (result != null || progress == 100) { 631 // We only track the inbox here in the service - ignore other mailboxes 632 long inboxId = Mailbox.findMailboxOfType(MailService.this, 633 accountId, Mailbox.TYPE_INBOX); 634 if (mailboxId == inboxId) { 635 if (progress == 100) { 636 updateAccountReport(accountId, numNewMessages); 637 } else { 638 updateAccountReport(accountId, -1); 639 } 640 } 641 } 642 } 643 644 @Override 645 public void serviceCheckMailCallback(MessagingException result, long accountId, 646 long mailboxId, int progress, long tag) { 647 if (result != null || progress == 100) { 648 if (result != null) { 649 // the checkmail ended in an error. force an update of the refresh 650 // time, so we don't just spin on this account 651 updateAccountReport(accountId, -1); 652 } 653 AlarmManager alarmManager = (AlarmManager)getSystemService(Context.ALARM_SERVICE); 654 reschedule(alarmManager); 655 int serviceId = MailService.this.mStartId; 656 if (tag != 0) { 657 serviceId = (int) tag; 658 } 659 stopSelf(serviceId); 660 } 661 } 662 } 663 664 /** 665 * @see ConnectivityManager#getBackgroundDataSetting() 666 */ 667 private boolean isBackgroundDataEnabled() { 668 ConnectivityManager cm = 669 (ConnectivityManager)getSystemService(Context.CONNECTIVITY_SERVICE); 670 return cm.getBackgroundDataSetting(); 671 } 672 673 public class EmailSyncStatusObserver implements SyncStatusObserver { 674 @Override 675 public void onStatusChanged(int which) { 676 // We ignore the argument (we can only get called in one case - when settings change) 677 } 678 } 679 680 public static ArrayList<Account> getPopImapAccountList(Context context) { 681 ArrayList<Account> providerAccounts = new ArrayList<Account>(); 682 Cursor c = context.getContentResolver().query(Account.CONTENT_URI, Account.ID_PROJECTION, 683 null, null, null); 684 try { 685 while (c.moveToNext()) { 686 long accountId = c.getLong(Account.CONTENT_ID_COLUMN); 687 String protocol = Account.getProtocol(context, accountId); 688 if ((protocol != null) && ("pop3".equals(protocol) || "imap".equals(protocol))) { 689 Account account = Account.restoreAccountWithId(context, accountId); 690 if (account != null) { 691 providerAccounts.add(account); 692 } 693 } 694 } 695 } finally { 696 c.close(); 697 } 698 return providerAccounts; 699 } 700 701 private static final SingleRunningTask<Context> sReconcilePopImapAccountsSyncExecutor = 702 new SingleRunningTask<Context>("ReconcilePopImapAccountsSync") { 703 @Override 704 protected void runInternal(Context context) { 705 android.accounts.Account[] accountManagerAccounts = AccountManager.get(context) 706 .getAccountsByType(AccountManagerTypes.TYPE_POP_IMAP); 707 ArrayList<Account> providerAccounts = getPopImapAccountList(context); 708 MailService.reconcileAccountsWithAccountManager(context, providerAccounts, 709 accountManagerAccounts, false, context.getContentResolver()); 710 711 } 712 }; 713 714 /** 715 * Reconcile POP/IMAP accounts. 716 */ 717 public static void reconcilePopImapAccountsSync(Context context) { 718 sReconcilePopImapAccountsSyncExecutor.run(context); 719 } 720 721 /** 722 * Handles a variety of cleanup actions that must be performed when an account has been deleted. 723 * This includes triggering an account backup, ensuring that security policies are properly 724 * reset, if necessary, notifying the UI of the change, and resetting scheduled syncs and 725 * notifications. 726 * @param context the caller's context 727 */ 728 public static void accountDeleted(Context context) { 729 AccountBackupRestore.backup(context); 730 SecurityPolicy.getInstance(context).reducePolicies(); 731 Email.setNotifyUiAccountsChanged(true); 732 MailService.actionReschedule(context); 733 } 734 735 /** 736 * See Utility.reconcileAccounts for details 737 * @param context The context in which to operate 738 * @param emailProviderAccounts the exchange provider accounts to work from 739 * @param accountManagerAccounts The account manager accounts to work from 740 * @param blockExternalChanges FOR TESTING ONLY - block backups, security changes, etc. 741 * @param resolver the content resolver for making provider updates (injected for testability) 742 */ 743 @VisibleForTesting 744 public static void reconcileAccountsWithAccountManager(Context context, 745 List<Account> emailProviderAccounts, android.accounts.Account[] accountManagerAccounts, 746 boolean blockExternalChanges, ContentResolver resolver) { 747 boolean accountsDeleted = AccountReconciler.reconcileAccounts(context, 748 emailProviderAccounts, accountManagerAccounts, resolver); 749 // If we changed the list of accounts, refresh the backup & security settings 750 if (!blockExternalChanges && accountsDeleted) { 751 accountDeleted(context); 752 } 753 } 754 755 public static void setupAccountManagerAccount(Context context, Account account, 756 boolean email, boolean calendar, boolean contacts, 757 AccountManagerCallback<Bundle> callback) { 758 Bundle options = new Bundle(); 759 HostAuth hostAuthRecv = HostAuth.restoreHostAuthWithId(context, account.mHostAuthKeyRecv); 760 // Set up username/password 761 options.putString(EasAuthenticatorService.OPTIONS_USERNAME, account.mEmailAddress); 762 options.putString(EasAuthenticatorService.OPTIONS_PASSWORD, hostAuthRecv.mPassword); 763 options.putBoolean(EasAuthenticatorService.OPTIONS_CONTACTS_SYNC_ENABLED, contacts); 764 options.putBoolean(EasAuthenticatorService.OPTIONS_CALENDAR_SYNC_ENABLED, calendar); 765 options.putBoolean(EasAuthenticatorService.OPTIONS_EMAIL_SYNC_ENABLED, email); 766 String accountType = hostAuthRecv.mProtocol.equals("eas") ? 767 AccountManagerTypes.TYPE_EXCHANGE : 768 AccountManagerTypes.TYPE_POP_IMAP; 769 AccountManager.get(context).addAccount(accountType, null, null, options, null, callback, 770 null); 771 } 772} 773