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