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