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