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 17 package com.android.email.service; 18 19 import com.android.email.AccountBackupRestore; 20 import com.android.email.Controller; 21 import com.android.email.Email; 22 import com.android.email.R; 23 import com.android.email.activity.MessageList; 24 import com.android.email.mail.MessagingException; 25 import com.android.email.provider.EmailContent.Account; 26 import com.android.email.provider.EmailContent.AccountColumns; 27 import com.android.email.provider.EmailContent.HostAuth; 28 import com.android.email.provider.EmailContent.Mailbox; 29 30 import android.app.AlarmManager; 31 import android.app.Notification; 32 import android.app.NotificationManager; 33 import android.app.PendingIntent; 34 import android.app.Service; 35 import android.content.ContentUris; 36 import android.content.ContentValues; 37 import android.content.Context; 38 import android.content.Intent; 39 import android.database.Cursor; 40 import android.media.AudioManager; 41 import android.net.Uri; 42 import android.os.IBinder; 43 import android.os.SystemClock; 44 import android.util.Config; 45 import android.util.Log; 46 47 import java.util.HashMap; 48 49 /** 50 * Background service for refreshing non-push email accounts. 51 */ 52 public class MailService extends Service { 53 /** DO NOT CHECK IN "TRUE" */ 54 private static final boolean DEBUG_FORCE_QUICK_REFRESH = false; // force 1-minute refresh 55 56 private static final String LOG_TAG = "Email-MailService"; 57 58 public static final int NOTIFICATION_ID_NEW_MESSAGES = 1; 59 public static final int NOTIFICATION_ID_SECURITY_NEEDED = 2; 60 public static final int NOTIFICATION_ID_EXCHANGE_CALENDAR_ADDED = 3; 61 62 private static final String ACTION_CHECK_MAIL = 63 "com.android.email.intent.action.MAIL_SERVICE_WAKEUP"; 64 private static final String ACTION_RESCHEDULE = 65 "com.android.email.intent.action.MAIL_SERVICE_RESCHEDULE"; 66 private static final String ACTION_CANCEL = 67 "com.android.email.intent.action.MAIL_SERVICE_CANCEL"; 68 private static final String ACTION_NOTIFY_MAIL = 69 "com.android.email.intent.action.MAIL_SERVICE_NOTIFY"; 70 71 private static final String EXTRA_CHECK_ACCOUNT = "com.android.email.intent.extra.ACCOUNT"; 72 private static final String EXTRA_ACCOUNT_INFO = "com.android.email.intent.extra.ACCOUNT_INFO"; 73 private static final String EXTRA_DEBUG_WATCHDOG = "com.android.email.intent.extra.WATCHDOG"; 74 75 private static final int WATCHDOG_DELAY = 10 * 60 * 1000; // 10 minutes 76 77 private static final String[] NEW_MESSAGE_COUNT_PROJECTION = 78 new String[] {AccountColumns.NEW_MESSAGE_COUNT}; 79 80 private final Controller.Result mControllerCallback = new ControllerResults(); 81 82 private int mStartId; 83 84 /** 85 * Access must be synchronized, because there are accesses from the Controller callback 86 */ 87 private static HashMap<Long,AccountSyncReport> mSyncReports = 88 new HashMap<Long,AccountSyncReport>(); 89 90 /** 91 * Simple template used for clearing new message count in accounts 92 */ 93 private static final ContentValues CLEAR_NEW_MESSAGES; 94 static { 95 CLEAR_NEW_MESSAGES = new ContentValues(); 96 CLEAR_NEW_MESSAGES.put(Account.NEW_MESSAGE_COUNT, 0); 97 } 98 99 public static void actionReschedule(Context context) { 100 Intent i = new Intent(); 101 i.setClass(context, MailService.class); 102 i.setAction(MailService.ACTION_RESCHEDULE); 103 context.startService(i); 104 } 105 106 public static void actionCancel(Context context) { 107 Intent i = new Intent(); 108 i.setClass(context, MailService.class); 109 i.setAction(MailService.ACTION_CANCEL); 110 context.startService(i); 111 } 112 113 /** 114 * Reset new message counts for one or all accounts. This clears both our local copy and 115 * the values (if any) stored in the account records. 116 * 117 * @param accountId account to clear, or -1 for all accounts 118 */ 119 public static void resetNewMessageCount(Context context, long accountId) { 120 synchronized (mSyncReports) { 121 for (AccountSyncReport report : mSyncReports.values()) { 122 if (accountId == -1 || accountId == report.accountId) { 123 report.numNewMessages = 0; 124 } 125 } 126 } 127 // now do the database - all accounts, or just one of them 128 Uri uri; 129 if (accountId == -1) { 130 uri = Account.CONTENT_URI; 131 } else { 132 uri = ContentUris.withAppendedId(Account.CONTENT_URI, accountId); 133 } 134 context.getContentResolver().update(uri, CLEAR_NEW_MESSAGES, null, null); 135 } 136 137 /** 138 * Entry point for asynchronous message services (e.g. push mode) to post notifications of new 139 * messages. This assumes that the push provider has already synced the messages into the 140 * appropriate database - this simply triggers the notification mechanism. 141 * 142 * @param context a context 143 * @param accountId the id of the account that is reporting new messages 144 * @param newCount the number of new messages 145 */ 146 public static void actionNotifyNewMessages(Context context, long accountId) { 147 Intent i = new Intent(ACTION_NOTIFY_MAIL); 148 i.setClass(context, MailService.class); 149 i.putExtra(EXTRA_CHECK_ACCOUNT, accountId); 150 context.startService(i); 151 } 152 153 @Override 154 public int onStartCommand(Intent intent, int flags, int startId) { 155 super.onStartCommand(intent, flags, startId); 156 157 // Restore accounts, if it has not happened already 158 AccountBackupRestore.restoreAccountsIfNeeded(this); 159 160 // TODO this needs to be passed through the controller and back to us 161 this.mStartId = startId; 162 String action = intent.getAction(); 163 164 Controller controller = Controller.getInstance(getApplication()); 165 controller.addResultCallback(mControllerCallback); 166 167 if (ACTION_CHECK_MAIL.equals(action)) { 168 // If we have the data, restore the last-sync-times for each account 169 // These are cached in the wakeup intent in case the process was killed. 170 restoreSyncReports(intent); 171 172 // Sync a specific account if given 173 AlarmManager alarmManager = (AlarmManager)getSystemService(Context.ALARM_SERVICE); 174 long checkAccountId = intent.getLongExtra(EXTRA_CHECK_ACCOUNT, -1); 175 if (Config.LOGD && Email.DEBUG) { 176 Log.d(LOG_TAG, "action: check mail for id=" + checkAccountId); 177 } 178 if (checkAccountId >= 0) { 179 setWatchdog(checkAccountId, alarmManager); 180 } 181 // if no account given, or the given account cannot be synced - reschedule 182 if (checkAccountId == -1 || !syncOneAccount(controller, checkAccountId, startId)) { 183 // Prevent runaway on the current account by pretending it updated 184 if (checkAccountId != -1) { 185 updateAccountReport(checkAccountId, 0); 186 } 187 // Find next account to sync, and reschedule 188 reschedule(alarmManager); 189 stopSelf(startId); 190 } 191 } 192 else if (ACTION_CANCEL.equals(action)) { 193 if (Config.LOGD && Email.DEBUG) { 194 Log.d(LOG_TAG, "action: cancel"); 195 } 196 cancel(); 197 stopSelf(startId); 198 } 199 else if (ACTION_RESCHEDULE.equals(action)) { 200 if (Config.LOGD && Email.DEBUG) { 201 Log.d(LOG_TAG, "action: reschedule"); 202 } 203 // As a precaution, clear any outstanding Email notifications 204 // We could be smarter and only do this when the list of accounts changes, 205 // but that's an edge condition and this is much safer. 206 NotificationManager notificationManager = 207 (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); 208 notificationManager.cancel(NOTIFICATION_ID_NEW_MESSAGES); 209 210 // When called externally, we refresh the sync reports table to pick up 211 // any changes in the account list or account settings 212 refreshSyncReports(); 213 // Finally, scan for the next needing update, and set an alarm for it 214 AlarmManager alarmManager = (AlarmManager)getSystemService(Context.ALARM_SERVICE); 215 reschedule(alarmManager); 216 stopSelf(startId); 217 } else if (ACTION_NOTIFY_MAIL.equals(action)) { 218 long accountId = intent.getLongExtra(EXTRA_CHECK_ACCOUNT, -1); 219 // Get the current new message count 220 Cursor c = getContentResolver().query( 221 ContentUris.withAppendedId(Account.CONTENT_URI, accountId), 222 NEW_MESSAGE_COUNT_PROJECTION, null, null, null); 223 int newMessageCount = 0; 224 try { 225 if (c.moveToFirst()) { 226 newMessageCount = c.getInt(0); 227 } else { 228 // If the account no longer exists, set to -1 (which is handled below) 229 accountId = -1; 230 } 231 } finally { 232 c.close(); 233 } 234 if (Config.LOGD && Email.DEBUG) { 235 Log.d(LOG_TAG, "notify accountId=" + Long.toString(accountId) 236 + " count=" + newMessageCount); 237 } 238 if (accountId != -1) { 239 updateAccountReport(accountId, newMessageCount); 240 notifyNewMessages(accountId); 241 } 242 stopSelf(startId); 243 } 244 245 // Returning START_NOT_STICKY means that if a mail check is killed (e.g. due to memory 246 // pressure, there will be no explicit restart. This is OK; Note that we set a watchdog 247 // alarm before each mailbox check. If the mailbox check never completes, the watchdog 248 // will fire and get things running again. 249 return START_NOT_STICKY; 250 } 251 252 @Override 253 public IBinder onBind(Intent intent) { 254 return null; 255 } 256 257 @Override 258 public void onDestroy() { 259 super.onDestroy(); 260 Controller.getInstance(getApplication()).removeResultCallback(mControllerCallback); 261 } 262 263 private void cancel() { 264 AlarmManager alarmMgr = (AlarmManager)getSystemService(Context.ALARM_SERVICE); 265 PendingIntent pi = createAlarmIntent(-1, null, false); 266 alarmMgr.cancel(pi); 267 } 268 269 /** 270 * Refresh the sync reports, to pick up any changes in the account list or account settings. 271 */ 272 private void refreshSyncReports() { 273 synchronized (mSyncReports) { 274 // Make shallow copy of sync reports so we can recover the prev sync times 275 HashMap<Long,AccountSyncReport> oldSyncReports = 276 new HashMap<Long,AccountSyncReport>(mSyncReports); 277 278 // Delete the sync reports to force a refresh from live account db data 279 mSyncReports.clear(); 280 setupSyncReportsLocked(-1); 281 282 // Restore prev-sync & next-sync times for any reports in the new list 283 for (AccountSyncReport newReport : mSyncReports.values()) { 284 AccountSyncReport oldReport = oldSyncReports.get(newReport.accountId); 285 if (oldReport != null) { 286 newReport.prevSyncTime = oldReport.prevSyncTime; 287 if (newReport.syncInterval > 0 && newReport.prevSyncTime != 0) { 288 newReport.nextSyncTime = 289 newReport.prevSyncTime + (newReport.syncInterval * 1000 * 60); 290 } 291 } 292 } 293 } 294 } 295 296 /** 297 * Create and send an alarm with the entire list. This also sends a list of known last-sync 298 * times with the alarm, so if we are killed between alarms, we don't lose this info. 299 * 300 * @param alarmMgr passed in so we can mock for testing. 301 */ 302 /* package */ void reschedule(AlarmManager alarmMgr) { 303 // restore the reports if lost 304 setupSyncReports(-1); 305 synchronized (mSyncReports) { 306 int numAccounts = mSyncReports.size(); 307 long[] accountInfo = new long[numAccounts * 2]; // pairs of { accountId, lastSync } 308 int accountInfoIndex = 0; 309 310 long nextCheckTime = Long.MAX_VALUE; 311 AccountSyncReport nextAccount = null; 312 long timeNow = SystemClock.elapsedRealtime(); 313 314 for (AccountSyncReport report : mSyncReports.values()) { 315 if (report.syncInterval <= 0) { // no timed checks - skip 316 continue; 317 } 318 if ("eas".equals(report.protocol)) { // no checks for eas accts 319 continue; 320 } 321 long prevSyncTime = report.prevSyncTime; 322 long nextSyncTime = report.nextSyncTime; 323 324 // select next account to sync 325 if ((prevSyncTime == 0) || (nextSyncTime < timeNow)) { // never checked, or overdue 326 nextCheckTime = 0; 327 nextAccount = report; 328 } else if (nextSyncTime < nextCheckTime) { // next to be checked 329 nextCheckTime = nextSyncTime; 330 nextAccount = report; 331 } 332 // collect last-sync-times for all accounts 333 // this is using pairs of {long,long} to simplify passing in a bundle 334 accountInfo[accountInfoIndex++] = report.accountId; 335 accountInfo[accountInfoIndex++] = report.prevSyncTime; 336 } 337 338 // Clear out any unused elements in the array 339 while (accountInfoIndex < accountInfo.length) { 340 accountInfo[accountInfoIndex++] = -1; 341 } 342 343 // set/clear alarm as needed 344 long idToCheck = (nextAccount == null) ? -1 : nextAccount.accountId; 345 PendingIntent pi = createAlarmIntent(idToCheck, accountInfo, false); 346 347 if (nextAccount == null) { 348 alarmMgr.cancel(pi); 349 if (Config.LOGD && Email.DEBUG) { 350 Log.d(LOG_TAG, "reschedule: alarm cancel - no account to check"); 351 } 352 } else { 353 alarmMgr.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, nextCheckTime, pi); 354 if (Config.LOGD && Email.DEBUG) { 355 Log.d(LOG_TAG, "reschedule: alarm set at " + nextCheckTime 356 + " for " + nextAccount); 357 } 358 } 359 } 360 } 361 362 /** 363 * Create a watchdog alarm and set it. This is used in case a mail check fails (e.g. we are 364 * killed by the system due to memory pressure.) Normally, a mail check will complete and 365 * the watchdog will be replaced by the call to reschedule(). 366 * @param accountId the account we were trying to check 367 * @param alarmMgr system alarm manager 368 */ 369 private void setWatchdog(long accountId, AlarmManager alarmMgr) { 370 PendingIntent pi = createAlarmIntent(accountId, null, true); 371 long timeNow = SystemClock.elapsedRealtime(); 372 long nextCheckTime = timeNow + WATCHDOG_DELAY; 373 alarmMgr.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, nextCheckTime, pi); 374 } 375 376 /** 377 * Return a pending intent for use by this alarm. Most of the fields must be the same 378 * (in order for the intent to be recognized by the alarm manager) but the extras can 379 * be different, and are passed in here as parameters. 380 */ 381 /* package */ PendingIntent createAlarmIntent(long checkId, long[] accountInfo, 382 boolean isWatchdog) { 383 Intent i = new Intent(); 384 i.setClass(this, MailService.class); 385 i.setAction(ACTION_CHECK_MAIL); 386 i.putExtra(EXTRA_CHECK_ACCOUNT, checkId); 387 i.putExtra(EXTRA_ACCOUNT_INFO, accountInfo); 388 if (isWatchdog) { 389 i.putExtra(EXTRA_DEBUG_WATCHDOG, true); 390 } 391 PendingIntent pi = PendingIntent.getService(this, 0, i, PendingIntent.FLAG_UPDATE_CURRENT); 392 return pi; 393 } 394 395 /** 396 * Start a controller sync for a specific account 397 * 398 * @param controller The controller to do the sync work 399 * @param checkAccountId the account Id to try and check 400 * @param startId the id of this service launch 401 * @return true if mail checking has started, false if it could not (e.g. bad account id) 402 */ 403 private boolean syncOneAccount(Controller controller, long checkAccountId, int startId) { 404 long inboxId = Mailbox.findMailboxOfType(this, checkAccountId, Mailbox.TYPE_INBOX); 405 if (inboxId == Mailbox.NO_MAILBOX) { 406 return false; 407 } else { 408 controller.serviceCheckMail(checkAccountId, inboxId, startId, mControllerCallback); 409 return true; 410 } 411 } 412 413 /** 414 * Note: Times are relative to SystemClock.elapsedRealtime() 415 */ 416 private static class AccountSyncReport { 417 long accountId; 418 String protocol; 419 long prevSyncTime; // 0 == unknown 420 long nextSyncTime; // 0 == ASAP -1 == don't sync 421 int numNewMessages; 422 423 int syncInterval; 424 boolean notify; 425 boolean vibrate; 426 boolean vibrateWhenSilent; 427 Uri ringtoneUri; 428 429 String displayName; // temporary, for debug logging 430 431 432 @Override 433 public String toString() { 434 return displayName + ": id=" + accountId + " prevSync=" + prevSyncTime 435 + " nextSync=" + nextSyncTime + " numNew=" + numNewMessages; 436 } 437 } 438 439 /** 440 * scan accounts to create a list of { acct, prev sync, next sync, #new } 441 * use this to create a fresh copy. assumes all accounts need sync 442 * 443 * @param accountId -1 will rebuild the list if empty. other values will force loading 444 * of a single account (e.g if it was created after the original list population) 445 */ 446 /* package */ void setupSyncReports(long accountId) { 447 synchronized (mSyncReports) { 448 setupSyncReportsLocked(accountId); 449 } 450 } 451 452 /** 453 * Handle the work of setupSyncReports. Must be synchronized on mSyncReports. 454 */ 455 private void setupSyncReportsLocked(long accountId) { 456 if (accountId == -1) { 457 // -1 == reload the list if empty, otherwise exit immediately 458 if (mSyncReports.size() > 0) { 459 return; 460 } 461 } else { 462 // load a single account if it doesn't already have a sync record 463 if (mSyncReports.containsKey(accountId)) { 464 return; 465 } 466 } 467 468 // setup to add a single account or all accounts 469 Uri uri; 470 if (accountId == -1) { 471 uri = Account.CONTENT_URI; 472 } else { 473 uri = ContentUris.withAppendedId(Account.CONTENT_URI, accountId); 474 } 475 476 // TODO use a narrower projection here 477 Cursor c = getContentResolver().query(uri, Account.CONTENT_PROJECTION, 478 null, null, null); 479 try { 480 while (c.moveToNext()) { 481 AccountSyncReport report = new AccountSyncReport(); 482 int syncInterval = c.getInt(Account.CONTENT_SYNC_INTERVAL_COLUMN); 483 int flags = c.getInt(Account.CONTENT_FLAGS_COLUMN); 484 String ringtoneString = c.getString(Account.CONTENT_RINGTONE_URI_COLUMN); 485 486 // For debugging only 487 if (DEBUG_FORCE_QUICK_REFRESH && syncInterval >= 0) { 488 syncInterval = 1; 489 } 490 491 long acctId = c.getLong(Account.CONTENT_ID_COLUMN); 492 Account account = Account.restoreAccountWithId(this, acctId); 493 if (account == null) continue; 494 HostAuth hostAuth = HostAuth.restoreHostAuthWithId(this, account.mHostAuthKeyRecv); 495 if (hostAuth == null) continue; 496 report.accountId = acctId; 497 report.protocol = hostAuth.mProtocol; 498 report.prevSyncTime = 0; 499 report.nextSyncTime = (syncInterval > 0) ? 0 : -1; // 0 == ASAP -1 == no sync 500 report.numNewMessages = 0; 501 502 report.syncInterval = syncInterval; 503 report.notify = (flags & Account.FLAGS_NOTIFY_NEW_MAIL) != 0; 504 report.vibrate = (flags & Account.FLAGS_VIBRATE_ALWAYS) != 0; 505 report.vibrateWhenSilent = (flags & Account.FLAGS_VIBRATE_WHEN_SILENT) != 0; 506 report.ringtoneUri = (ringtoneString == null) ? null 507 : Uri.parse(ringtoneString); 508 509 report.displayName = c.getString(Account.CONTENT_DISPLAY_NAME_COLUMN); 510 511 // TODO lookup # new in inbox 512 mSyncReports.put(report.accountId, report); 513 } 514 } finally { 515 c.close(); 516 } 517 } 518 519 /** 520 * Update list with a single account's sync times and unread count 521 * 522 * @param accountId the account being updated 523 * @param newCount the number of new messages, or -1 if not being reported (don't update) 524 * @return the report for the updated account, or null if it doesn't exist (e.g. deleted) 525 */ 526 /* package */ AccountSyncReport updateAccountReport(long accountId, int newCount) { 527 // restore the reports if lost 528 setupSyncReports(accountId); 529 synchronized (mSyncReports) { 530 AccountSyncReport report = mSyncReports.get(accountId); 531 if (report == null) { 532 // discard result - there is no longer an account with this id 533 Log.d(LOG_TAG, "No account to update for id=" + Long.toString(accountId)); 534 return null; 535 } 536 537 // report found - update it (note - editing the report while in-place in the hashmap) 538 report.prevSyncTime = SystemClock.elapsedRealtime(); 539 if (report.syncInterval > 0) { 540 report.nextSyncTime = report.prevSyncTime + (report.syncInterval * 1000 * 60); 541 } 542 if (newCount != -1) { 543 report.numNewMessages = newCount; 544 } 545 if (Config.LOGD && Email.DEBUG) { 546 Log.d(LOG_TAG, "update account " + report.toString()); 547 } 548 return report; 549 } 550 } 551 552 /** 553 * when we receive an alarm, update the account sync reports list if necessary 554 * this will be the case when if we have restarted the process and lost the data 555 * in the global. 556 * 557 * @param restoreIntent the intent with the list 558 */ 559 /* package */ void restoreSyncReports(Intent restoreIntent) { 560 // restore the reports if lost 561 setupSyncReports(-1); 562 synchronized (mSyncReports) { 563 long[] accountInfo = restoreIntent.getLongArrayExtra(EXTRA_ACCOUNT_INFO); 564 if (accountInfo == null) { 565 Log.d(LOG_TAG, "no data in intent to restore"); 566 return; 567 } 568 int accountInfoIndex = 0; 569 int accountInfoLimit = accountInfo.length; 570 while (accountInfoIndex < accountInfoLimit) { 571 long accountId = accountInfo[accountInfoIndex++]; 572 long prevSync = accountInfo[accountInfoIndex++]; 573 AccountSyncReport report = mSyncReports.get(accountId); 574 if (report != null) { 575 if (report.prevSyncTime == 0) { 576 report.prevSyncTime = prevSync; 577 if (report.syncInterval > 0 && report.prevSyncTime != 0) { 578 report.nextSyncTime = 579 report.prevSyncTime + (report.syncInterval * 1000 * 60); 580 } 581 } 582 } 583 } 584 } 585 } 586 587 class ControllerResults implements Controller.Result { 588 589 public void loadMessageForViewCallback(MessagingException result, long messageId, 590 int progress) { 591 } 592 593 public void loadAttachmentCallback(MessagingException result, long messageId, 594 long attachmentId, int progress) { 595 } 596 597 public void updateMailboxCallback(MessagingException result, long accountId, 598 long mailboxId, int progress, int numNewMessages) { 599 if (result != null || progress == 100) { 600 // We only track the inbox here in the service - ignore other mailboxes 601 long inboxId = Mailbox.findMailboxOfType(MailService.this, 602 accountId, Mailbox.TYPE_INBOX); 603 if (mailboxId == inboxId) { 604 if (progress == 100) { 605 updateAccountReport(accountId, numNewMessages); 606 if (numNewMessages > 0) { 607 notifyNewMessages(accountId); 608 } 609 } else { 610 updateAccountReport(accountId, -1); 611 } 612 } 613 // Call the global refresh tracker for all mailboxes 614 Email.updateMailboxRefreshTime(mailboxId); 615 } 616 } 617 618 public void updateMailboxListCallback(MessagingException result, long accountId, 619 int progress) { 620 } 621 622 public void serviceCheckMailCallback(MessagingException result, long accountId, 623 long mailboxId, int progress, long tag) { 624 if (result != null || progress == 100) { 625 if (result != null) { 626 // the checkmail ended in an error. force an update of the refresh 627 // time, so we don't just spin on this account 628 updateAccountReport(accountId, -1); 629 } 630 AlarmManager alarmManager = (AlarmManager)getSystemService(Context.ALARM_SERVICE); 631 reschedule(alarmManager); 632 int serviceId = MailService.this.mStartId; 633 if (tag != 0) { 634 serviceId = (int) tag; 635 } 636 stopSelf(serviceId); 637 } 638 } 639 640 public void sendMailCallback(MessagingException result, long accountId, long messageId, 641 int progress) { 642 } 643 } 644 645 /** 646 * Prepare notifications for a given new account having received mail 647 * The notification is organized around the account that has the new mail (e.g. selecting 648 * the alert preferences) but the notification will include a summary if other 649 * accounts also have new mail. 650 */ 651 private void notifyNewMessages(long accountId) { 652 boolean notify = false; 653 boolean vibrate = false; 654 boolean vibrateWhenSilent = false; 655 Uri ringtone = null; 656 int accountsWithNewMessages = 0; 657 int numNewMessages = 0; 658 String reportName = null; 659 synchronized (mSyncReports) { 660 for (AccountSyncReport report : mSyncReports.values()) { 661 if (report.numNewMessages == 0) { 662 continue; 663 } 664 numNewMessages += report.numNewMessages; 665 accountsWithNewMessages += 1; 666 if (report.accountId == accountId) { 667 notify = report.notify; 668 vibrate = report.vibrate; 669 vibrateWhenSilent = report.vibrateWhenSilent; 670 ringtone = report.ringtoneUri; 671 reportName = report.displayName; 672 } 673 } 674 } 675 if (!notify) { 676 return; 677 } 678 679 // set up to post a notification 680 Intent intent; 681 String reportString; 682 683 if (accountsWithNewMessages == 1) { 684 // Prepare a report for a single account 685 // "12 unread (gmail)" 686 reportString = getResources().getQuantityString( 687 R.plurals.notification_new_one_account_fmt, numNewMessages, 688 numNewMessages, reportName); 689 intent = MessageList.createIntent(this, accountId, -1, Mailbox.TYPE_INBOX); 690 } else { 691 // Prepare a report for multiple accounts 692 // "4 accounts" 693 reportString = getResources().getQuantityString( 694 R.plurals.notification_new_multi_account_fmt, accountsWithNewMessages, 695 accountsWithNewMessages); 696 intent = MessageList.createIntent(this, -1, Mailbox.QUERY_ALL_INBOXES, -1); 697 } 698 699 // prepare appropriate pending intent, set up notification, and send 700 PendingIntent pending = 701 PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); 702 703 Notification notification = new Notification( 704 R.drawable.stat_notify_email_generic, 705 getString(R.string.notification_new_title), 706 System.currentTimeMillis()); 707 notification.setLatestEventInfo(this, 708 getString(R.string.notification_new_title), 709 reportString, 710 pending); 711 712 notification.sound = ringtone; 713 AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE); 714 boolean nowSilent = audioManager.getRingerMode() != AudioManager.RINGER_MODE_NORMAL; 715 716 // Use same code here as in Gmail and GTalk for vibration 717 if (vibrate || (vibrateWhenSilent && nowSilent)) { 718 notification.defaults |= Notification.DEFAULT_VIBRATE; 719 } 720 721 // This code is identical to that used by Gmail and GTalk for notifications 722 notification.flags |= Notification.FLAG_SHOW_LIGHTS; 723 notification.defaults |= Notification.DEFAULT_LIGHTS; 724 725 NotificationManager notificationManager = 726 (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); 727 notificationManager.notify(NOTIFICATION_ID_NEW_MESSAGES, notification); 728 } 729 } 730