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