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