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