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