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