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