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