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