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