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