MailService.java revision 34e6205d4432c8b591c35fbcfc504852608f35a6
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.AccountBackupRestore; 20import com.android.email.Controller; 21import com.android.email.Email; 22import com.android.email.NotificationController; 23import com.android.email.SecurityPolicy; 24import com.android.email.Utility; 25import com.android.email.mail.MessagingException; 26import com.android.email.provider.EmailContent; 27import com.android.email.provider.EmailProvider; 28import com.android.email.provider.EmailContent.Account; 29import com.android.email.provider.EmailContent.AccountColumns; 30import com.android.email.provider.EmailContent.HostAuth; 31import com.android.email.provider.EmailContent.Mailbox; 32 33import android.accounts.AccountManager; 34import android.accounts.AccountManagerCallback; 35import android.accounts.AccountManagerFuture; 36import android.accounts.AuthenticatorException; 37import android.accounts.OnAccountsUpdateListener; 38import android.accounts.OperationCanceledException; 39import android.app.AlarmManager; 40import android.app.PendingIntent; 41import android.app.Service; 42import android.content.ContentResolver; 43import android.content.ContentUris; 44import android.content.Context; 45import android.content.Intent; 46import android.content.SyncStatusObserver; 47import android.database.Cursor; 48import android.net.ConnectivityManager; 49import android.net.Uri; 50import android.os.AsyncTask; 51import android.os.Bundle; 52import android.os.Handler; 53import android.os.IBinder; 54import android.os.SystemClock; 55import android.util.Config; 56import android.util.Log; 57 58import java.io.IOException; 59import java.util.ArrayList; 60import java.util.HashMap; 61import java.util.List; 62import java.util.concurrent.atomic.AtomicInteger; 63 64/** 65 * Background service for refreshing non-push email accounts. 66 */ 67public class MailService extends Service { 68 /** DO NOT CHECK IN "TRUE" */ 69 private static final boolean DEBUG_FORCE_QUICK_REFRESH = false; // force 1-minute refresh 70 71 private static final String LOG_TAG = "Email-MailService"; 72 73 private static final String ACTION_CHECK_MAIL = 74 "com.android.email.intent.action.MAIL_SERVICE_WAKEUP"; 75 private static final String ACTION_RESCHEDULE = 76 "com.android.email.intent.action.MAIL_SERVICE_RESCHEDULE"; 77 private static final String ACTION_CANCEL = 78 "com.android.email.intent.action.MAIL_SERVICE_CANCEL"; 79 private static final String ACTION_NOTIFY_MAIL = 80 "com.android.email.intent.action.MAIL_SERVICE_NOTIFY"; 81 private static final String ACTION_SEND_PENDING_MAIL = 82 "com.android.email.intent.action.MAIL_SERVICE_SEND_PENDING"; 83 84 private static final String EXTRA_ACCOUNT = "com.android.email.intent.extra.ACCOUNT"; 85 private static final String EXTRA_ACCOUNT_INFO = "com.android.email.intent.extra.ACCOUNT_INFO"; 86 private static final String EXTRA_DEBUG_WATCHDOG = "com.android.email.intent.extra.WATCHDOG"; 87 88 private static final int WATCHDOG_DELAY = 10 * 60 * 1000; // 10 minutes 89 90 // Sentinel value asking to update mSyncReports if it's currently empty 91 /*package*/ static final int SYNC_REPORTS_ALL_ACCOUNTS_IF_EMPTY = -1; 92 // Sentinel value asking that mSyncReports be rebuilt 93 /*package*/ static final int SYNC_REPORTS_RESET = -2; 94 95 private static final String[] NEW_MESSAGE_COUNT_PROJECTION = 96 new String[] {AccountColumns.NEW_MESSAGE_COUNT}; 97 98 // Keep track of the number of times we're calling the reconciler 99 // If the count is > 1, don't try to run another one (it means that one is running and one is 100 // already queued). 101 private static AtomicInteger sReconcilerCount = new AtomicInteger(0); 102 private static MailService sMailService; 103 104 /*package*/ Controller mController; 105 private final Controller.Result mControllerCallback = new ControllerResults(); 106 private ContentResolver mContentResolver; 107 private Context mContext; 108 /*package*/ AccountsUpdatedListener mAccountsUpdatedListener; 109 private Handler mHandler = new Handler(); 110 111 private int mStartId; 112 113 /** 114 * Access must be synchronized, because there are accesses from the Controller callback 115 */ 116 /*package*/ static HashMap<Long,AccountSyncReport> mSyncReports = 117 new HashMap<Long,AccountSyncReport>(); 118 119 public static void actionReschedule(Context context) { 120 Intent i = new Intent(); 121 i.setClass(context, MailService.class); 122 i.setAction(MailService.ACTION_RESCHEDULE); 123 context.startService(i); 124 } 125 126 public static void actionCancel(Context context) { 127 Intent i = new Intent(); 128 i.setClass(context, MailService.class); 129 i.setAction(MailService.ACTION_CANCEL); 130 context.startService(i); 131 } 132 133 /** 134 * Entry point for AttachmentDownloadService to ask that pending mail be sent 135 * @param context the caller's context 136 * @param accountId the account whose pending mail should be sent 137 */ 138 public static void actionSendPendingMail(Context context, long accountId) { 139 Intent i = new Intent(); 140 i.setClass(context, MailService.class); 141 i.setAction(MailService.ACTION_SEND_PENDING_MAIL); 142 i.putExtra(MailService.EXTRA_ACCOUNT, accountId); 143 context.startService(i); 144 } 145 146 /** 147 * Reset new message counts for one or all accounts. This clears both our local copy and 148 * the values (if any) stored in the account records. 149 * 150 * @param accountId account to clear, or -1 for all accounts 151 */ 152 public static void resetNewMessageCount(final Context context, final long accountId) { 153 synchronized (mSyncReports) { 154 for (AccountSyncReport report : mSyncReports.values()) { 155 if (accountId == -1 || accountId == report.accountId) { 156 report.unseenMessageCount = 0; 157 report.lastUnseenMessageCount = 0; 158 } 159 } 160 } 161 // Clear notification 162 NotificationController.getInstance(context).cancelNewMessageNotification(accountId); 163 164 // now do the database - all accounts, or just one of them 165 Utility.runAsync(new Runnable() { 166 @Override 167 public void run() { 168 Uri uri = Account.RESET_NEW_MESSAGE_COUNT_URI; 169 if (accountId != -1) { 170 uri = ContentUris.withAppendedId(uri, accountId); 171 } 172 context.getContentResolver().update(uri, null, null, null); 173 } 174 }); 175 } 176 177 /** 178 * Entry point for asynchronous message services (e.g. push mode) to post notifications of new 179 * messages. This assumes that the push provider has already synced the messages into the 180 * appropriate database - this simply triggers the notification mechanism. 181 * 182 * @param context a context 183 * @param accountId the id of the account that is reporting new messages 184 * @param newCount the number of new messages 185 */ 186 public static void actionNotifyNewMessages(Context context, long accountId) { 187 Intent i = new Intent(ACTION_NOTIFY_MAIL); 188 i.setClass(context, MailService.class); 189 i.putExtra(EXTRA_ACCOUNT, accountId); 190 context.startService(i); 191 } 192 193 /*package*/ static MailService getMailServiceForTest() { 194 return sMailService; 195 } 196 197 @Override 198 public int onStartCommand(Intent intent, int flags, int startId) { 199 super.onStartCommand(intent, flags, startId); 200 201 // Save the service away (for unit tests) 202 sMailService = this; 203 204 // Restore accounts, if it has not happened already 205 AccountBackupRestore.restoreAccountsIfNeeded(this); 206 207 // Set up our observer for AccountManager 208 mAccountsUpdatedListener = new AccountsUpdatedListener(); 209 AccountManager.get(getApplication()).addOnAccountsUpdatedListener( 210 mAccountsUpdatedListener, mHandler, true); 211 // Run reconciliation to make sure we're up-to-date on account status 212 mAccountsUpdatedListener.onAccountsUpdated(null); 213 214 // TODO this needs to be passed through the controller and back to us 215 this.mStartId = startId; 216 String action = intent.getAction(); 217 218 mController = Controller.getInstance(this); 219 mController.addResultCallback(mControllerCallback); 220 mContentResolver = getContentResolver(); 221 mContext = this; 222 223 if (ACTION_CHECK_MAIL.equals(action)) { 224 // If we have the data, restore the last-sync-times for each account 225 // These are cached in the wakeup intent in case the process was killed. 226 restoreSyncReports(intent); 227 228 // Sync a specific account if given 229 AlarmManager alarmManager = (AlarmManager)getSystemService(Context.ALARM_SERVICE); 230 long checkAccountId = intent.getLongExtra(EXTRA_ACCOUNT, -1); 231 if (Config.LOGD && Email.DEBUG) { 232 Log.d(LOG_TAG, "action: check mail for id=" + checkAccountId); 233 } 234 if (checkAccountId >= 0) { 235 setWatchdog(checkAccountId, alarmManager); 236 } 237 238 // Start sync if account is given and background data is enabled and the account itself 239 // has sync enabled 240 boolean syncStarted = false; 241 if (checkAccountId != -1 && isBackgroundDataEnabled()) { 242 synchronized(mSyncReports) { 243 for (AccountSyncReport report: mSyncReports.values()) { 244 if (report.accountId == checkAccountId) { 245 if (report.syncEnabled) { 246 syncStarted = syncOneAccount(mController, checkAccountId, startId); 247 } 248 break; 249 } 250 } 251 } 252 } 253 254 // Reschedule if we didn't start sync. 255 if (!syncStarted) { 256 // Prevent runaway on the current account by pretending it updated 257 if (checkAccountId != -1) { 258 updateAccountReport(checkAccountId, 0); 259 } 260 // Find next account to sync, and reschedule 261 reschedule(alarmManager); 262 stopSelf(startId); 263 } 264 } 265 else if (ACTION_CANCEL.equals(action)) { 266 if (Config.LOGD && Email.DEBUG) { 267 Log.d(LOG_TAG, "action: cancel"); 268 } 269 cancel(); 270 stopSelf(startId); 271 } 272 else if (ACTION_SEND_PENDING_MAIL.equals(action)) { 273 if (Config.LOGD && Email.DEBUG) { 274 Log.d(LOG_TAG, "action: send pending mail"); 275 } 276 final long accountId = intent.getLongExtra(EXTRA_ACCOUNT, -1); 277 EmailContent.Account account = 278 EmailContent.Account.restoreAccountWithId(this, accountId); 279 if (account != null) { 280 Utility.runAsync(new Runnable() { 281 public void run() { 282 mController.sendPendingMessages(accountId); 283 } 284 }); 285 } 286 stopSelf(startId); 287 } 288 else if (ACTION_RESCHEDULE.equals(action)) { 289 if (Config.LOGD && Email.DEBUG) { 290 Log.d(LOG_TAG, "action: reschedule"); 291 } 292 // Clear all notifications, in case account list has changed. 293 // 294 // TODO Clear notifications for non-existing accounts. Now that we have separate 295 // notification for each account, NotificationController should be able to do that. 296 NotificationController.getInstance(this).cancelNewMessageNotification(-1); 297 298 // When called externally, we refresh the sync reports table to pick up 299 // any changes in the account list or account settings 300 refreshSyncReports(); 301 // Finally, scan for the next needing update, and set an alarm for it 302 AlarmManager alarmManager = (AlarmManager)getSystemService(Context.ALARM_SERVICE); 303 reschedule(alarmManager); 304 stopSelf(startId); 305 } else if (ACTION_NOTIFY_MAIL.equals(action)) { 306 long accountId = intent.getLongExtra(EXTRA_ACCOUNT, -1); 307 // Get the current new message count 308 Cursor c = mContentResolver.query( 309 ContentUris.withAppendedId(Account.CONTENT_URI, accountId), 310 NEW_MESSAGE_COUNT_PROJECTION, null, null, null); 311 int newMessageCount = 0; 312 try { 313 if (c.moveToFirst()) { 314 newMessageCount = c.getInt(0); 315 } else { 316 // If the account no longer exists, set to -1 (which is handled below) 317 accountId = -1; 318 } 319 } finally { 320 c.close(); 321 } 322 if (Config.LOGD && Email.DEBUG) { 323 Log.d(LOG_TAG, "notify accountId=" + Long.toString(accountId) 324 + " count=" + newMessageCount); 325 } 326 if (accountId != -1) { 327 updateAccountReport(accountId, newMessageCount); 328 notifyNewMessages(accountId); 329 } 330 stopSelf(startId); 331 } 332 333 // Returning START_NOT_STICKY means that if a mail check is killed (e.g. due to memory 334 // pressure, there will be no explicit restart. This is OK; Note that we set a watchdog 335 // alarm before each mailbox check. If the mailbox check never completes, the watchdog 336 // will fire and get things running again. 337 return START_NOT_STICKY; 338 } 339 340 @Override 341 public IBinder onBind(Intent intent) { 342 return null; 343 } 344 345 @Override 346 public void onDestroy() { 347 super.onDestroy(); 348 Controller.getInstance(getApplication()).removeResultCallback(mControllerCallback); 349 // Unregister our account listener 350 if (mAccountsUpdatedListener != null) { 351 AccountManager.get(this).removeOnAccountsUpdatedListener(mAccountsUpdatedListener); 352 } 353 } 354 355 private void cancel() { 356 AlarmManager alarmMgr = (AlarmManager)getSystemService(Context.ALARM_SERVICE); 357 PendingIntent pi = createAlarmIntent(-1, null, false); 358 alarmMgr.cancel(pi); 359 } 360 361 /** 362 * Refresh the sync reports, to pick up any changes in the account list or account settings. 363 */ 364 /*package*/ void refreshSyncReports() { 365 synchronized (mSyncReports) { 366 // Make shallow copy of sync reports so we can recover the prev sync times 367 HashMap<Long,AccountSyncReport> oldSyncReports = 368 new HashMap<Long,AccountSyncReport>(mSyncReports); 369 370 // Delete the sync reports to force a refresh from live account db data 371 setupSyncReportsLocked(SYNC_REPORTS_RESET, this); 372 373 // Restore prev-sync & next-sync times for any reports in the new list 374 for (AccountSyncReport newReport : mSyncReports.values()) { 375 AccountSyncReport oldReport = oldSyncReports.get(newReport.accountId); 376 if (oldReport != null) { 377 newReport.prevSyncTime = oldReport.prevSyncTime; 378 if (newReport.syncInterval > 0 && newReport.prevSyncTime != 0) { 379 newReport.nextSyncTime = 380 newReport.prevSyncTime + (newReport.syncInterval * 1000 * 60); 381 } 382 } 383 } 384 } 385 } 386 387 /** 388 * Create and send an alarm with the entire list. This also sends a list of known last-sync 389 * times with the alarm, so if we are killed between alarms, we don't lose this info. 390 * 391 * @param alarmMgr passed in so we can mock for testing. 392 */ 393 /* package */ void reschedule(AlarmManager alarmMgr) { 394 // restore the reports if lost 395 setupSyncReports(SYNC_REPORTS_ALL_ACCOUNTS_IF_EMPTY); 396 synchronized (mSyncReports) { 397 int numAccounts = mSyncReports.size(); 398 long[] accountInfo = new long[numAccounts * 2]; // pairs of { accountId, lastSync } 399 int accountInfoIndex = 0; 400 401 long nextCheckTime = Long.MAX_VALUE; 402 AccountSyncReport nextAccount = null; 403 long timeNow = SystemClock.elapsedRealtime(); 404 405 for (AccountSyncReport report : mSyncReports.values()) { 406 if (report.syncInterval <= 0) { // no timed checks - skip 407 continue; 408 } 409 long prevSyncTime = report.prevSyncTime; 410 long nextSyncTime = report.nextSyncTime; 411 412 // select next account to sync 413 if ((prevSyncTime == 0) || (nextSyncTime < timeNow)) { // never checked, or overdue 414 nextCheckTime = 0; 415 nextAccount = report; 416 } else if (nextSyncTime < nextCheckTime) { // next to be checked 417 nextCheckTime = nextSyncTime; 418 nextAccount = report; 419 } 420 // collect last-sync-times for all accounts 421 // this is using pairs of {long,long} to simplify passing in a bundle 422 accountInfo[accountInfoIndex++] = report.accountId; 423 accountInfo[accountInfoIndex++] = report.prevSyncTime; 424 } 425 426 // Clear out any unused elements in the array 427 while (accountInfoIndex < accountInfo.length) { 428 accountInfo[accountInfoIndex++] = -1; 429 } 430 431 // set/clear alarm as needed 432 long idToCheck = (nextAccount == null) ? -1 : nextAccount.accountId; 433 PendingIntent pi = createAlarmIntent(idToCheck, accountInfo, false); 434 435 if (nextAccount == null) { 436 alarmMgr.cancel(pi); 437 if (Config.LOGD && Email.DEBUG) { 438 Log.d(LOG_TAG, "reschedule: alarm cancel - no account to check"); 439 } 440 } else { 441 alarmMgr.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, nextCheckTime, pi); 442 if (Config.LOGD && Email.DEBUG) { 443 Log.d(LOG_TAG, "reschedule: alarm set at " + nextCheckTime 444 + " for " + nextAccount); 445 } 446 } 447 } 448 } 449 450 /** 451 * Create a watchdog alarm and set it. This is used in case a mail check fails (e.g. we are 452 * killed by the system due to memory pressure.) Normally, a mail check will complete and 453 * the watchdog will be replaced by the call to reschedule(). 454 * @param accountId the account we were trying to check 455 * @param alarmMgr system alarm manager 456 */ 457 private void setWatchdog(long accountId, AlarmManager alarmMgr) { 458 PendingIntent pi = createAlarmIntent(accountId, null, true); 459 long timeNow = SystemClock.elapsedRealtime(); 460 long nextCheckTime = timeNow + WATCHDOG_DELAY; 461 alarmMgr.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, nextCheckTime, pi); 462 } 463 464 /** 465 * Return a pending intent for use by this alarm. Most of the fields must be the same 466 * (in order for the intent to be recognized by the alarm manager) but the extras can 467 * be different, and are passed in here as parameters. 468 */ 469 /* package */ PendingIntent createAlarmIntent(long checkId, long[] accountInfo, 470 boolean isWatchdog) { 471 Intent i = new Intent(); 472 i.setClass(this, MailService.class); 473 i.setAction(ACTION_CHECK_MAIL); 474 i.putExtra(EXTRA_ACCOUNT, checkId); 475 i.putExtra(EXTRA_ACCOUNT_INFO, accountInfo); 476 if (isWatchdog) { 477 i.putExtra(EXTRA_DEBUG_WATCHDOG, true); 478 } 479 PendingIntent pi = PendingIntent.getService(this, 0, i, PendingIntent.FLAG_UPDATE_CURRENT); 480 return pi; 481 } 482 483 /** 484 * Start a controller sync for a specific account 485 * 486 * @param controller The controller to do the sync work 487 * @param checkAccountId the account Id to try and check 488 * @param startId the id of this service launch 489 * @return true if mail checking has started, false if it could not (e.g. bad account id) 490 */ 491 private boolean syncOneAccount(Controller controller, long checkAccountId, int startId) { 492 long inboxId = Mailbox.findMailboxOfType(this, checkAccountId, Mailbox.TYPE_INBOX); 493 if (inboxId == Mailbox.NO_MAILBOX) { 494 return false; 495 } else { 496 controller.serviceCheckMail(checkAccountId, inboxId, startId); 497 return true; 498 } 499 } 500 501 /** 502 * Note: Times are relative to SystemClock.elapsedRealtime() 503 * 504 * TODO: Look more closely at syncEnabled and see if we can simply coalesce it into 505 * syncInterval (e.g. if !syncEnabled, set syncInterval to -1). 506 */ 507 /*package*/ static class AccountSyncReport { 508 long accountId; 509 long prevSyncTime; // 0 == unknown 510 long nextSyncTime; // 0 == ASAP -1 == don't sync 511 512 /** # of "unseen" messages to show in notification */ 513 int unseenMessageCount; 514 515 /** 516 * # of unseen, the value shown on the last notification. Used to 517 * calculate "the number of messages that have just been fetched". 518 * 519 * TODO It's a sort of cheating. Should we use the "real" number? The only difference 520 * is the first notification after reboot / process restart. 521 */ 522 int lastUnseenMessageCount; 523 524 int syncInterval; 525 boolean notify; 526 527 boolean syncEnabled; // whether auto sync is enabled for this account 528 529 /** # of messages that have just been fetched */ 530 int getJustFetchedMessageCount() { 531 return unseenMessageCount - lastUnseenMessageCount; 532 } 533 534 @Override 535 public String toString() { 536 return "id=" + accountId 537 + " prevSync=" + prevSyncTime + " nextSync=" + nextSyncTime + " numUnseen=" 538 + unseenMessageCount; 539 } 540 } 541 542 /** 543 * scan accounts to create a list of { acct, prev sync, next sync, #new } 544 * use this to create a fresh copy. assumes all accounts need sync 545 * 546 * @param accountId -1 will rebuild the list if empty. other values will force loading 547 * of a single account (e.g if it was created after the original list population) 548 */ 549 /* package */ void setupSyncReports(long accountId) { 550 synchronized (mSyncReports) { 551 setupSyncReportsLocked(accountId, mContext); 552 } 553 } 554 555 /** 556 * Handle the work of setupSyncReports. Must be synchronized on mSyncReports. 557 */ 558 /*package*/ void setupSyncReportsLocked(long accountId, Context context) { 559 ContentResolver resolver = context.getContentResolver(); 560 if (accountId == SYNC_REPORTS_RESET) { 561 // For test purposes, force refresh of mSyncReports 562 mSyncReports.clear(); 563 accountId = SYNC_REPORTS_ALL_ACCOUNTS_IF_EMPTY; 564 } else if (accountId == SYNC_REPORTS_ALL_ACCOUNTS_IF_EMPTY) { 565 // -1 == reload the list if empty, otherwise exit immediately 566 if (mSyncReports.size() > 0) { 567 return; 568 } 569 } else { 570 // load a single account if it doesn't already have a sync record 571 if (mSyncReports.containsKey(accountId)) { 572 return; 573 } 574 } 575 576 // setup to add a single account or all accounts 577 Uri uri; 578 if (accountId == SYNC_REPORTS_ALL_ACCOUNTS_IF_EMPTY) { 579 uri = Account.CONTENT_URI; 580 } else { 581 uri = ContentUris.withAppendedId(Account.CONTENT_URI, accountId); 582 } 583 584 // We use a full projection here because we'll restore each account object from it 585 Cursor c = resolver.query(uri, Account.CONTENT_PROJECTION, null, null, null); 586 try { 587 while (c.moveToNext()) { 588 AccountSyncReport report = new AccountSyncReport(); 589 Account account = Account.getContent(c, Account.class); 590 int syncInterval = account.mSyncInterval; 591 592 // If we're not using MessagingController (EAS at this point), don't schedule syncs 593 if (!mController.isMessagingController(account.mId)) { 594 syncInterval = Account.CHECK_INTERVAL_NEVER; 595 } else if (DEBUG_FORCE_QUICK_REFRESH && syncInterval >= 0) { 596 syncInterval = 1; 597 } 598 599 report.accountId = account.mId; 600 report.prevSyncTime = 0; 601 report.nextSyncTime = (syncInterval > 0) ? 0 : -1; // 0 == ASAP -1 == no sync 602 report.unseenMessageCount = 0; 603 report.lastUnseenMessageCount = 0; 604 605 report.syncInterval = syncInterval; 606 report.notify = (account.mFlags & Account.FLAGS_NOTIFY_NEW_MAIL) != 0; 607 608 // See if the account is enabled for sync in AccountManager 609 android.accounts.Account accountManagerAccount = 610 new android.accounts.Account(account.mEmailAddress, 611 Email.POP_IMAP_ACCOUNT_MANAGER_TYPE); 612 report.syncEnabled = ContentResolver.getSyncAutomatically(accountManagerAccount, 613 EmailProvider.EMAIL_AUTHORITY); 614 615 // TODO lookup # new in inbox 616 mSyncReports.put(report.accountId, report); 617 } 618 } finally { 619 c.close(); 620 } 621 } 622 623 /** 624 * Update list with a single account's sync times and unread count 625 * 626 * @param accountId the account being updated 627 * @param newCount the number of new messages, or -1 if not being reported (don't update) 628 * @return the report for the updated account, or null if it doesn't exist (e.g. deleted) 629 */ 630 /* package */ AccountSyncReport updateAccountReport(long accountId, int newCount) { 631 // restore the reports if lost 632 setupSyncReports(accountId); 633 synchronized (mSyncReports) { 634 AccountSyncReport report = mSyncReports.get(accountId); 635 if (report == null) { 636 // discard result - there is no longer an account with this id 637 Log.d(LOG_TAG, "No account to update for id=" + Long.toString(accountId)); 638 return null; 639 } 640 641 // report found - update it (note - editing the report while in-place in the hashmap) 642 report.prevSyncTime = SystemClock.elapsedRealtime(); 643 if (report.syncInterval > 0) { 644 report.nextSyncTime = report.prevSyncTime + (report.syncInterval * 1000 * 60); 645 } 646 if (newCount != -1) { 647 report.unseenMessageCount = newCount; 648 } 649 if (Config.LOGD && Email.DEBUG) { 650 Log.d(LOG_TAG, "update account " + report.toString()); 651 } 652 return report; 653 } 654 } 655 656 /** 657 * when we receive an alarm, update the account sync reports list if necessary 658 * this will be the case when if we have restarted the process and lost the data 659 * in the global. 660 * 661 * @param restoreIntent the intent with the list 662 */ 663 /* package */ void restoreSyncReports(Intent restoreIntent) { 664 // restore the reports if lost 665 setupSyncReports(SYNC_REPORTS_ALL_ACCOUNTS_IF_EMPTY); 666 synchronized (mSyncReports) { 667 long[] accountInfo = restoreIntent.getLongArrayExtra(EXTRA_ACCOUNT_INFO); 668 if (accountInfo == null) { 669 Log.d(LOG_TAG, "no data in intent to restore"); 670 return; 671 } 672 int accountInfoIndex = 0; 673 int accountInfoLimit = accountInfo.length; 674 while (accountInfoIndex < accountInfoLimit) { 675 long accountId = accountInfo[accountInfoIndex++]; 676 long prevSync = accountInfo[accountInfoIndex++]; 677 AccountSyncReport report = mSyncReports.get(accountId); 678 if (report != null) { 679 if (report.prevSyncTime == 0) { 680 report.prevSyncTime = prevSync; 681 if (report.syncInterval > 0 && report.prevSyncTime != 0) { 682 report.nextSyncTime = 683 report.prevSyncTime + (report.syncInterval * 1000 * 60); 684 } 685 } 686 } 687 } 688 } 689 } 690 691 class ControllerResults extends Controller.Result { 692 @Override 693 public void updateMailboxCallback(MessagingException result, long accountId, 694 long mailboxId, int progress, int numNewMessages) { 695 // First, look for authentication failures and notify 696 //checkAuthenticationStatus(result, accountId); 697 if (result != null || progress == 100) { 698 // We only track the inbox here in the service - ignore other mailboxes 699 long inboxId = Mailbox.findMailboxOfType(MailService.this, 700 accountId, Mailbox.TYPE_INBOX); 701 if (mailboxId == inboxId) { 702 if (progress == 100) { 703 updateAccountReport(accountId, numNewMessages); 704 if (numNewMessages > 0) { 705 notifyNewMessages(accountId); 706 } 707 } else { 708 updateAccountReport(accountId, -1); 709 } 710 } 711 } 712 } 713 714 @Override 715 public void serviceCheckMailCallback(MessagingException result, long accountId, 716 long mailboxId, int progress, long tag) { 717 if (result != null || progress == 100) { 718 if (result != null) { 719 // the checkmail ended in an error. force an update of the refresh 720 // time, so we don't just spin on this account 721 updateAccountReport(accountId, -1); 722 } 723 AlarmManager alarmManager = (AlarmManager)getSystemService(Context.ALARM_SERVICE); 724 reschedule(alarmManager); 725 int serviceId = MailService.this.mStartId; 726 if (tag != 0) { 727 serviceId = (int) tag; 728 } 729 stopSelf(serviceId); 730 } 731 } 732 } 733 734 /** 735 * Show "new message" notification for an account. (Notification is shown per account.) 736 */ 737 private void notifyNewMessages(final long accountId) { 738 final int unseenMessageCount; 739 final int justFetchedCount; 740 synchronized (mSyncReports) { 741 AccountSyncReport report = mSyncReports.get(accountId); 742 if (report == null || report.unseenMessageCount == 0 || !report.notify) { 743 return; 744 } 745 unseenMessageCount = report.unseenMessageCount; 746 justFetchedCount = report.getJustFetchedMessageCount(); 747 report.lastUnseenMessageCount = report.unseenMessageCount; 748 } 749 750 NotificationController.getInstance(this).showNewMessageNotification(accountId, 751 unseenMessageCount, justFetchedCount); 752 } 753 754 /** 755 * @see ConnectivityManager#getBackgroundDataSetting() 756 */ 757 private boolean isBackgroundDataEnabled() { 758 ConnectivityManager cm = 759 (ConnectivityManager)getSystemService(Context.CONNECTIVITY_SERVICE); 760 return cm.getBackgroundDataSetting(); 761 } 762 763 public class EmailSyncStatusObserver implements SyncStatusObserver { 764 public void onStatusChanged(int which) { 765 // We ignore the argument (we can only get called in one case - when settings change) 766 } 767 } 768 769 public static ArrayList<Account> getPopImapAccountList(Context context) { 770 ArrayList<Account> providerAccounts = new ArrayList<Account>(); 771 Cursor c = context.getContentResolver().query(Account.CONTENT_URI, Account.ID_PROJECTION, 772 null, null, null); 773 try { 774 while (c.moveToNext()) { 775 long accountId = c.getLong(Account.CONTENT_ID_COLUMN); 776 String protocol = Account.getProtocol(context, accountId); 777 if ((protocol != null) && ("pop3".equals(protocol) || "imap".equals(protocol))) { 778 Account account = Account.restoreAccountWithId(context, accountId); 779 if (account != null) { 780 providerAccounts.add(account); 781 } 782 } 783 } 784 } finally { 785 c.close(); 786 } 787 return providerAccounts; 788 } 789 790 /** 791 * We synchronize this, since it can be called from multiple threads 792 */ 793 public static synchronized void reconcilePopImapAccounts(Context context) { 794 android.accounts.Account[] accountManagerAccounts = AccountManager.get(context) 795 .getAccountsByType(Email.POP_IMAP_ACCOUNT_MANAGER_TYPE); 796 ArrayList<Account> providerAccounts = getPopImapAccountList(context); 797 MailService.reconcileAccountsWithAccountManager(context, providerAccounts, 798 accountManagerAccounts, false, context.getContentResolver()); 799 800 } 801 802 /** 803 * Reconcile accounts when accounts are added/removed from AccountManager; note that the 804 * required argument is ignored (we request those of our specific type within the method) 805 */ 806 public class AccountsUpdatedListener implements OnAccountsUpdateListener { 807 808 public void onAccountsUpdated(android.accounts.Account[] accounts) { 809 // Only allow one to be queued at a time (and one running) 810 if (sReconcilerCount.getAndIncrement() > 1) { 811 sReconcilerCount.decrementAndGet(); 812 return; 813 } 814 new AccountReconcilerTask().execute(); 815 } 816 817 private class AccountReconcilerTask extends AsyncTask<Void, Void, Void> { 818 @Override 819 protected Void doInBackground(Void... params) { 820 try { 821 reconcilePopImapAccounts(MailService.this); 822 } finally { 823 // Belt & suspenders; most likely, any RuntimeException in this code would 824 // kill the Email process 825 sReconcilerCount.decrementAndGet(); 826 } 827 return null; 828 } 829 } 830 } 831 832 /** 833 * Compare our account list (obtained from EmailProvider) with the account list owned by 834 * AccountManager. If there are any orphans (an account in one list without a corresponding 835 * account in the other list), delete the orphan, as these must remain in sync. 836 * 837 * Note that the duplication of account information is caused by the Email application's 838 * incomplete integration with AccountManager. 839 * 840 * This function may not be called from the main/UI thread, because it makes blocking calls 841 * into the account manager. 842 * 843 * @param context The context in which to operate 844 * @param emailProviderAccounts the exchange provider accounts to work from 845 * @param accountManagerAccounts The account manager accounts to work from 846 * @param blockExternalChanges FOR TESTING ONLY - block backups, security changes, etc. 847 * @param resolver the content resolver for making provider updates (injected for testability) 848 */ 849 /* package */ public static void reconcileAccountsWithAccountManager(Context context, 850 List<Account> emailProviderAccounts, android.accounts.Account[] accountManagerAccounts, 851 boolean blockExternalChanges, ContentResolver resolver) { 852 // First, look through our EmailProvider accounts to make sure there's a corresponding 853 // AccountManager account 854 boolean accountsDeleted = false; 855 for (Account providerAccount: emailProviderAccounts) { 856 String providerAccountName = providerAccount.mEmailAddress; 857 boolean found = false; 858 for (android.accounts.Account accountManagerAccount: accountManagerAccounts) { 859 if (accountManagerAccount.name.equalsIgnoreCase(providerAccountName)) { 860 found = true; 861 break; 862 } 863 } 864 if (!found) { 865 if ((providerAccount.mFlags & Account.FLAGS_INCOMPLETE) != 0) { 866 if (Email.DEBUG) { 867 Log.d(LOG_TAG, "Account reconciler noticed incomplete account; ignoring"); 868 } 869 continue; 870 } 871 // This account has been deleted in the AccountManager! 872 Log.d(LOG_TAG, "Account deleted in AccountManager; deleting from provider: " + 873 providerAccountName); 874 // TODO This will orphan downloaded attachments; need to handle this 875 resolver.delete(ContentUris.withAppendedId(Account.CONTENT_URI, 876 providerAccount.mId), null, null); 877 accountsDeleted = true; 878 } 879 } 880 // Now, look through AccountManager accounts to make sure we have a corresponding cached EAS 881 // account from EmailProvider 882 for (android.accounts.Account accountManagerAccount: accountManagerAccounts) { 883 String accountManagerAccountName = accountManagerAccount.name; 884 boolean found = false; 885 for (Account cachedEasAccount: emailProviderAccounts) { 886 if (cachedEasAccount.mEmailAddress.equalsIgnoreCase(accountManagerAccountName)) { 887 found = true; 888 } 889 } 890 if (!found) { 891 // This account has been deleted from the EmailProvider database 892 Log.d(LOG_TAG, "Account deleted from provider; deleting from AccountManager: " + 893 accountManagerAccountName); 894 // Delete the account 895 AccountManagerFuture<Boolean> blockingResult = AccountManager.get(context) 896 .removeAccount(accountManagerAccount, null, null); 897 try { 898 // Note: All of the potential errors from removeAccount() are simply logged 899 // here, as there is nothing to actually do about them. 900 blockingResult.getResult(); 901 } catch (OperationCanceledException e) { 902 Log.w(Email.LOG_TAG, e.toString()); 903 } catch (AuthenticatorException e) { 904 Log.w(Email.LOG_TAG, e.toString()); 905 } catch (IOException e) { 906 Log.w(Email.LOG_TAG, e.toString()); 907 } 908 accountsDeleted = true; 909 } 910 } 911 // If we changed the list of accounts, refresh the backup & security settings 912 if (!blockExternalChanges && accountsDeleted) { 913 AccountBackupRestore.backupAccounts(context); 914 SecurityPolicy.getInstance(context).reducePolicies(); 915 Email.setNotifyUiAccountsChanged(true); 916 MailService.actionReschedule(context); 917 } 918 } 919 920 public static void setupAccountManagerAccount(Context context, EmailContent.Account account, 921 boolean email, boolean calendar, boolean contacts, 922 AccountManagerCallback<Bundle> callback) { 923 Bundle options = new Bundle(); 924 HostAuth hostAuthRecv = HostAuth.restoreHostAuthWithId(context, account.mHostAuthKeyRecv); 925 // Set up username/password 926 options.putString(EasAuthenticatorService.OPTIONS_USERNAME, account.mEmailAddress); 927 options.putString(EasAuthenticatorService.OPTIONS_PASSWORD, hostAuthRecv.mPassword); 928 options.putBoolean(EasAuthenticatorService.OPTIONS_CONTACTS_SYNC_ENABLED, contacts); 929 options.putBoolean(EasAuthenticatorService.OPTIONS_CALENDAR_SYNC_ENABLED, calendar); 930 options.putBoolean(EasAuthenticatorService.OPTIONS_EMAIL_SYNC_ENABLED, email); 931 String accountType = hostAuthRecv.mProtocol.equals("eas") ? 932 Email.EXCHANGE_ACCOUNT_MANAGER_TYPE : 933 Email.POP_IMAP_ACCOUNT_MANAGER_TYPE; 934 AccountManager.get(context).addAccount(accountType, null, null, options, null, callback, 935 null); 936 } 937} 938