AlertService.java revision 8748724e382ca014067a3ceb5ff4eacbd9c4021a
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.calendar.alerts; 18 19import com.android.calendar.GeneralPreferences; 20import com.android.calendar.R; 21 22import android.app.AlarmManager; 23import android.app.Notification; 24import android.app.NotificationManager; 25import android.app.Service; 26import android.content.ContentResolver; 27import android.content.ContentUris; 28import android.content.ContentValues; 29import android.content.Context; 30import android.content.Intent; 31import android.content.SharedPreferences; 32import android.database.Cursor; 33import android.media.AudioManager; 34import android.net.Uri; 35import android.os.Bundle; 36import android.os.Handler; 37import android.os.HandlerThread; 38import android.os.IBinder; 39import android.os.Looper; 40import android.os.Message; 41import android.os.Process; 42import android.provider.CalendarContract; 43import android.provider.CalendarContract.Attendees; 44import android.provider.CalendarContract.CalendarAlerts; 45import android.text.TextUtils; 46import android.text.format.DateUtils; 47import android.util.Log; 48 49import java.util.ArrayList; 50import java.util.HashMap; 51 52/** 53 * This service is used to handle calendar event reminders. 54 */ 55public class AlertService extends Service { 56 static final boolean DEBUG = true; 57 private static final String TAG = "AlertService"; 58 59 private volatile Looper mServiceLooper; 60 private volatile ServiceHandler mServiceHandler; 61 62 private static final String[] ALERT_PROJECTION = new String[] { 63 CalendarAlerts._ID, // 0 64 CalendarAlerts.EVENT_ID, // 1 65 CalendarAlerts.STATE, // 2 66 CalendarAlerts.TITLE, // 3 67 CalendarAlerts.EVENT_LOCATION, // 4 68 CalendarAlerts.SELF_ATTENDEE_STATUS, // 5 69 CalendarAlerts.ALL_DAY, // 6 70 CalendarAlerts.ALARM_TIME, // 7 71 CalendarAlerts.MINUTES, // 8 72 CalendarAlerts.BEGIN, // 9 73 CalendarAlerts.END, // 10 74 CalendarAlerts.DESCRIPTION, // 11 75 }; 76 77 private static final int ALERT_INDEX_ID = 0; 78 private static final int ALERT_INDEX_EVENT_ID = 1; 79 private static final int ALERT_INDEX_STATE = 2; 80 private static final int ALERT_INDEX_TITLE = 3; 81 private static final int ALERT_INDEX_EVENT_LOCATION = 4; 82 private static final int ALERT_INDEX_SELF_ATTENDEE_STATUS = 5; 83 private static final int ALERT_INDEX_ALL_DAY = 6; 84 private static final int ALERT_INDEX_ALARM_TIME = 7; 85 private static final int ALERT_INDEX_MINUTES = 8; 86 private static final int ALERT_INDEX_BEGIN = 9; 87 private static final int ALERT_INDEX_END = 10; 88 private static final int ALERT_INDEX_DESCRIPTION = 11; 89 90 private static final String ACTIVE_ALERTS_SELECTION = "(" + CalendarAlerts.STATE + "=? OR " 91 + CalendarAlerts.STATE + "=?) AND " + CalendarAlerts.ALARM_TIME + "<="; 92 93 private static final String[] ACTIVE_ALERTS_SELECTION_ARGS = new String[] { 94 Integer.toString(CalendarAlerts.STATE_FIRED), 95 Integer.toString(CalendarAlerts.STATE_SCHEDULED) 96 }; 97 98 private static final String ACTIVE_ALERTS_SORT = "begin DESC, end DESC"; 99 100 private static final String DISMISS_OLD_SELECTION = CalendarAlerts.END + "<? AND " 101 + CalendarAlerts.STATE + "=?"; 102 103 // The grace period before changing a notification's priority bucket. 104 private static final int DEPRIORITIZE_GRACE_PERIOD_MS = 15 * 60 * 1000; 105 106 void processMessage(Message msg) { 107 Bundle bundle = (Bundle) msg.obj; 108 109 // On reboot, update the notification bar with the contents of the 110 // CalendarAlerts table. 111 String action = bundle.getString("action"); 112 if (DEBUG) { 113 Log.d(TAG, bundle.getLong(android.provider.CalendarContract.CalendarAlerts.ALARM_TIME) 114 + " Action = " + action); 115 } 116 117 if (action.equals(Intent.ACTION_BOOT_COMPLETED) 118 || action.equals(Intent.ACTION_TIME_CHANGED)) { 119 doTimeChanged(); 120 return; 121 } 122 123 if (!action.equals(android.provider.CalendarContract.ACTION_EVENT_REMINDER) 124 && !action.equals(Intent.ACTION_LOCALE_CHANGED) 125 && !action.equals(AlertReceiver.ACTION_DISMISS_OLD_REMINDERS)) { 126 Log.w(TAG, "Invalid action: " + action); 127 return; 128 } 129 if (action.equals(AlertReceiver.ACTION_DISMISS_OLD_REMINDERS)) { 130 dismissOldAlerts(this); 131 } 132 133 if (action.equals(android.provider.CalendarContract.ACTION_EVENT_REMINDER) && 134 bundle.getBoolean(AlertUtils.QUIET_UPDATE_KEY)) { 135 updateAlertNotification(this, true); 136 } else { 137 updateAlertNotification(this, false); 138 } 139 } 140 141 static void dismissOldAlerts(Context context) { 142 ContentResolver cr = context.getContentResolver(); 143 final long currentTime = System.currentTimeMillis(); 144 ContentValues vals = new ContentValues(); 145 vals.put(CalendarAlerts.STATE, CalendarAlerts.STATE_DISMISSED); 146 cr.update(CalendarAlerts.CONTENT_URI, vals, DISMISS_OLD_SELECTION, new String[] { 147 Long.toString(currentTime), Integer.toString(CalendarAlerts.STATE_SCHEDULED) 148 }); 149 } 150 151 static boolean updateAlertNotification(Context context, boolean quietUpdate) { 152 ContentResolver cr = context.getContentResolver(); 153 NotificationManager nm = 154 (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); 155 final long currentTime = System.currentTimeMillis(); 156 SharedPreferences prefs = GeneralPreferences.getSharedPreferences(context); 157 158 boolean doAlert = prefs.getBoolean(GeneralPreferences.KEY_ALERTS, true); 159 if (!doAlert) { 160 if (DEBUG) { 161 Log.d(TAG, "alert preference is OFF"); 162 } 163 164 // If we shouldn't be showing notifications cancel any existing ones 165 // and return. 166 nm.cancelAll(); 167 return true; 168 } 169 170 Cursor alertCursor = cr.query(CalendarAlerts.CONTENT_URI, ALERT_PROJECTION, 171 (ACTIVE_ALERTS_SELECTION + currentTime), ACTIVE_ALERTS_SELECTION_ARGS, 172 ACTIVE_ALERTS_SORT); 173 174 if (alertCursor == null || alertCursor.getCount() == 0) { 175 if (alertCursor != null) { 176 alertCursor.close(); 177 } 178 179 if (DEBUG) Log.d(TAG, "No fired or scheduled alerts"); 180 nm.cancelAll(); 181 return false; 182 } 183 184 if (DEBUG) { 185 Log.d(TAG, "alert count:" + alertCursor.getCount()); 186 } 187 188 // Process the query results and bucketize events. 189 ArrayList<NotificationInfo> currentEvents = new ArrayList<NotificationInfo>(); 190 ArrayList<NotificationInfo> futureEvents = new ArrayList<NotificationInfo>(); 191 ArrayList<NotificationInfo> expiredEvents = new ArrayList<NotificationInfo>(); 192 StringBuilder expiredDigestTitleBuilder = new StringBuilder(); 193 int numFired = processQuery(alertCursor, cr, currentTime, currentEvents, futureEvents, 194 expiredEvents, expiredDigestTitleBuilder); 195 String expiredDigestTitle = expiredDigestTitleBuilder.toString(); 196 197 if (currentEvents.size() + futureEvents.size() + expiredEvents.size() == 0) { 198 nm.cancelAll(); 199 return true; 200 } 201 202 quietUpdate = quietUpdate || (numFired == 0); 203 boolean doPopup = numFired > 0 && 204 prefs.getBoolean(GeneralPreferences.KEY_ALERTS_POPUP, false); 205 boolean defaultVibrate = shouldUseDefaultVibrate(context, prefs); 206 String ringtone = quietUpdate ? null : prefs.getString( 207 GeneralPreferences.KEY_ALERTS_RINGTONE, null); 208 long nextRefreshTime = Long.MAX_VALUE; 209 210 // Post the individual future events (higher priority). 211 for (NotificationInfo info : futureEvents) { 212 nextRefreshTime = Math.min(nextRefreshTime, info.startMillis); 213 String summaryText = AlertUtils.formatTimeLocation(context, info.startMillis, 214 info.allDay, info.location); 215 postNotification(info, summaryText, context, quietUpdate, doPopup, defaultVibrate, 216 ringtone, true, nm); 217 } 218 219 // Post the individual concurrent events (lower priority). 220 for (NotificationInfo info : currentEvents) { 221 // TODO: Change to a relative time description like: "Started 40 minutes ago". 222 // This requires constant refreshing to the message as time goes. 223 String summaryText = AlertUtils.formatTimeLocation(context, info.startMillis, 224 info.allDay, info.location); 225 226 // Keep concurrent events high priority (to appear higher in the notification list) 227 // until 15 minutes into the event. 228 boolean highPriority = false; 229 long gracePeriodEnd = info.startMillis + DEPRIORITIZE_GRACE_PERIOD_MS; 230 if (currentTime < gracePeriodEnd) { 231 highPriority = true; 232 nextRefreshTime = Math.min(nextRefreshTime, gracePeriodEnd); 233 } 234 nextRefreshTime = Math.min(nextRefreshTime, info.endMillis); 235 236 postNotification(info, summaryText, context, quietUpdate, (doPopup && highPriority), 237 defaultVibrate, ringtone, highPriority, nm); 238 } 239 240 // Post the expired events as 1 combined notification. 241 int numExpired = expiredEvents.size(); 242 if (numExpired > 0) { 243 Notification notification; 244 if (numExpired == 1) { 245 // If only 1 expired event, display an "old-style" basic alert. 246 NotificationInfo info = expiredEvents.get(0); 247 String summaryText = AlertUtils.formatTimeLocation(context, info.startMillis, 248 info.allDay, info.location); 249 notification = AlertReceiver.makeBasicNotification(context, info.eventName, 250 summaryText, info.startMillis, info.endMillis, info.eventId, 251 info.notificationId, false); 252 } else { 253 // Multiple expired events are listed in a digest. 254 notification = AlertReceiver.makeDigestNotification(context, 255 expiredEvents, expiredDigestTitle, false); 256 } 257 258 // Add options for a quiet update. 259 addNotificationOptions(notification, true, expiredDigestTitle, defaultVibrate, ringtone); 260 261 // Remove any individual expired notifications before posting. 262 for (NotificationInfo expiredInfo : expiredEvents) { 263 nm.cancel(expiredInfo.notificationId); 264 } 265 266 // Post the new notification for the group. 267 nm.notify(AlertUtils.EXPIRED_GROUP_NOTIFICATION_ID, notification); 268 269 if (DEBUG) { 270 Log.d(TAG, "Posting digest alarm notification, numEvents:" + expiredEvents.size() 271 + ", notificationId:" + AlertUtils.EXPIRED_GROUP_NOTIFICATION_ID 272 + (quietUpdate ? ", quiet" : ", loud")); 273 } 274 } else { 275 nm.cancel(AlertUtils.EXPIRED_GROUP_NOTIFICATION_ID); 276 } 277 278 // Schedule the next silent refresh time so notifications will change 279 // buckets (eg. drop into expired digest, etc). 280 AlertUtils.scheduleNextNotificationRefresh(context, null, nextRefreshTime); 281 282 return true; 283 } 284 285 /** 286 * Processes the query results and bucketizes the alerts. 287 * 288 * @param expiredDigestTitle Should pass in an empty StringBuilder; this will be 289 * modified to contain a title consolidating all expired event titles. 290 * @return Returns the number of new alerts to fire. If this is 0, it implies 291 * a quiet update. 292 */ 293 private static int processQuery(final Cursor alertCursor, final ContentResolver cr, 294 final long currentTime, ArrayList<NotificationInfo> currentEvents, 295 ArrayList<NotificationInfo> futureEvents, ArrayList<NotificationInfo> expiredEvents, 296 StringBuilder expiredDigestTitle) { 297 HashMap<Long, Long> eventIds = new HashMap<Long, Long>(); 298 int numFired = 0; 299 try { 300 while (alertCursor.moveToNext()) { 301 final long alertId = alertCursor.getLong(ALERT_INDEX_ID); 302 final long eventId = alertCursor.getLong(ALERT_INDEX_EVENT_ID); 303 final int minutes = alertCursor.getInt(ALERT_INDEX_MINUTES); 304 final String eventName = alertCursor.getString(ALERT_INDEX_TITLE); 305 final String description = alertCursor.getString(ALERT_INDEX_DESCRIPTION); 306 final String location = alertCursor.getString(ALERT_INDEX_EVENT_LOCATION); 307 final int status = alertCursor.getInt(ALERT_INDEX_SELF_ATTENDEE_STATUS); 308 final boolean declined = status == Attendees.ATTENDEE_STATUS_DECLINED; 309 final long beginTime = alertCursor.getLong(ALERT_INDEX_BEGIN); 310 final long endTime = alertCursor.getLong(ALERT_INDEX_END); 311 final Uri alertUri = ContentUris 312 .withAppendedId(CalendarAlerts.CONTENT_URI, alertId); 313 final long alarmTime = alertCursor.getLong(ALERT_INDEX_ALARM_TIME); 314 int state = alertCursor.getInt(ALERT_INDEX_STATE); 315 final boolean allDay = alertCursor.getInt(ALERT_INDEX_ALL_DAY) != 0; 316 317 if (DEBUG) { 318 Log.d(TAG, "alarmTime:" + alarmTime + " alertId:" + alertId 319 + " eventId:" + eventId + " state: " + state + " minutes:" + minutes 320 + " declined:" + declined + " beginTime:" + beginTime 321 + " endTime:" + endTime); 322 } 323 324 ContentValues values = new ContentValues(); 325 int newState = -1; 326 327 // Uncomment for the behavior of clearing out alerts after the 328 // events ended. b/1880369 329 // 330 // if (endTime < currentTime) { 331 // newState = CalendarAlerts.DISMISSED; 332 // } else 333 334 // Remove declined events 335 if (!declined) { 336 if (state == CalendarAlerts.STATE_SCHEDULED) { 337 newState = CalendarAlerts.STATE_FIRED; 338 numFired++; 339 340 // Record the received time in the CalendarAlerts table. 341 // This is useful for finding bugs that cause alarms to be 342 // missed or delayed. 343 values.put(CalendarAlerts.RECEIVED_TIME, currentTime); 344 } 345 } else { 346 newState = CalendarAlerts.STATE_DISMISSED; 347 } 348 349 // Update row if state changed 350 if (newState != -1) { 351 values.put(CalendarAlerts.STATE, newState); 352 state = newState; 353 } 354 355 if (state == CalendarAlerts.STATE_FIRED) { 356 // Record the time posting to notification manager. 357 // This is used for debugging missed alarms. 358 values.put(CalendarAlerts.NOTIFY_TIME, currentTime); 359 } 360 361 // Write row to if anything changed 362 if (values.size() > 0) cr.update(alertUri, values, null, null); 363 364 if (state != CalendarAlerts.STATE_FIRED) { 365 continue; 366 } 367 368 // Pick an Event title for the notification panel by the latest 369 // alertTime and give prefer accepted events in case of ties. 370 int newStatus; 371 switch (status) { 372 case Attendees.ATTENDEE_STATUS_ACCEPTED: 373 newStatus = 2; 374 break; 375 case Attendees.ATTENDEE_STATUS_TENTATIVE: 376 newStatus = 1; 377 break; 378 default: 379 newStatus = 0; 380 } 381 382 // Don't count duplicate alerts for the same event 383 if (eventIds.put(eventId, beginTime) == null) { 384 NotificationInfo notificationInfo = new NotificationInfo(eventName, location, 385 description, beginTime, endTime, eventId, allDay); 386 387 if ((beginTime <= currentTime) && (endTime >= currentTime)) { 388 currentEvents.add(notificationInfo); 389 } else if (beginTime > currentTime) { 390 futureEvents.add(notificationInfo); 391 } else { 392 // TODO: Prioritize by "primary" calendar 393 expiredEvents.add(notificationInfo); 394 if (!TextUtils.isEmpty(eventName)) { 395 if (expiredDigestTitle.length() > 0) { 396 expiredDigestTitle.append(", "); 397 } 398 expiredDigestTitle.append(eventName); 399 } 400 } 401 } 402 } 403 } finally { 404 if (alertCursor != null) { 405 alertCursor.close(); 406 } 407 } 408 return numFired; 409 } 410 411 private static void postNotification(NotificationInfo info, String summaryText, 412 Context context, boolean quietUpdate, boolean doPopup, boolean defaultVibrate, 413 String ringtone, boolean highPriority, NotificationManager notificationMgr) { 414 String tickerText = getTickerText(info.eventName, info.location); 415 Notification notification = AlertReceiver.makeExpandingNotification(context, 416 info.eventName, summaryText, info.description, info.startMillis, 417 info.endMillis, info.eventId, info.notificationId, doPopup, highPriority); 418 addNotificationOptions(notification, quietUpdate, tickerText, defaultVibrate, 419 ringtone); 420 notificationMgr.notify(info.notificationId, notification); 421 422 if (DEBUG) { 423 Log.d(TAG, "Posting individual alarm notification, eventId:" + info.eventId 424 + ", notificationId:" + info.notificationId 425 + (quietUpdate ? ", quiet" : ", loud") 426 + (highPriority ? ", high-priority" : "")); 427 } 428 } 429 430 private static String getTickerText(String eventName, String location) { 431 String tickerText = eventName; 432 if (!TextUtils.isEmpty(location)) { 433 tickerText = eventName + " - " + location; 434 } 435 return tickerText; 436 } 437 438 static class NotificationInfo { 439 String eventName; 440 String location; 441 String description; 442 long startMillis; 443 long endMillis; 444 long eventId; 445 int notificationId; 446 boolean allDay; 447 448 NotificationInfo(String eventName, String location, String description, long startMillis, 449 long endMillis, long eventId, boolean allDay) { 450 this.eventName = eventName; 451 this.location = location; 452 this.description = description; 453 this.startMillis = startMillis; 454 this.endMillis = endMillis; 455 this.eventId = eventId; 456 this.allDay = allDay; 457 this.notificationId = getNotificationId(eventId, startMillis); 458 } 459 460 /* 461 * Convert reminder into the ID for posting notifications. Use hash so we don't 462 * have to worry about any limits (but handle the case of a collision with the ID 463 * reserved for representing the expired notification digest). 464 */ 465 private static int getNotificationId(long eventId, long startMillis) { 466 long result = 17; 467 result = 37 * result + eventId; 468 result = 37 * result + startMillis; 469 int notificationId = Long.valueOf(result).hashCode(); 470 if (notificationId == AlertUtils.EXPIRED_GROUP_NOTIFICATION_ID) { 471 notificationId = Integer.MAX_VALUE; 472 } 473 return notificationId; 474 } 475 } 476 477 private static boolean shouldUseDefaultVibrate(Context context, SharedPreferences prefs) { 478 // Find out the circumstances under which to vibrate. 479 // Migrate from pre-Froyo boolean setting if necessary. 480 String vibrateWhen; // "always" or "silent" or "never" 481 if(prefs.contains(GeneralPreferences.KEY_ALERTS_VIBRATE_WHEN)) 482 { 483 // Look up Froyo setting 484 vibrateWhen = 485 prefs.getString(GeneralPreferences.KEY_ALERTS_VIBRATE_WHEN, null); 486 } else if(prefs.contains(GeneralPreferences.KEY_ALERTS_VIBRATE)) { 487 // No Froyo setting. Migrate pre-Froyo setting to new Froyo-defined value. 488 boolean vibrate = 489 prefs.getBoolean(GeneralPreferences.KEY_ALERTS_VIBRATE, false); 490 vibrateWhen = vibrate ? 491 context.getString(R.string.prefDefault_alerts_vibrate_true) : 492 context.getString(R.string.prefDefault_alerts_vibrate_false); 493 } else { 494 // No setting. Use Froyo-defined default. 495 vibrateWhen = context.getString(R.string.prefDefault_alerts_vibrateWhen); 496 } 497 498 if (vibrateWhen.equals("always")) { 499 return true; 500 } 501 if (!vibrateWhen.equals("silent")) { 502 return false; 503 } 504 505 // Settings are to vibrate when silent. Return true if it is now silent. 506 AudioManager audioManager = 507 (AudioManager)context.getSystemService(Context.AUDIO_SERVICE); 508 return audioManager.getRingerMode() == AudioManager.RINGER_MODE_VIBRATE; 509 } 510 511 private static void addNotificationOptions(Notification notification, boolean quietUpdate, 512 String tickerText, boolean defaultVibrate, String reminderRingtone) { 513 notification.defaults |= Notification.DEFAULT_LIGHTS; 514 515 // Quietly update notification bar. Nothing new. Maybe something just got deleted. 516 if (!quietUpdate) { 517 // Flash ticker in status bar 518 if (!TextUtils.isEmpty(tickerText)) { 519 notification.tickerText = tickerText; 520 } 521 522 // Generate either a pop-up dialog, status bar notification, or 523 // neither. Pop-up dialog and status bar notification may include a 524 // sound, an alert, or both. A status bar notification also includes 525 // a toast. 526 if (defaultVibrate) { 527 notification.defaults |= Notification.DEFAULT_VIBRATE; 528 } 529 530 // Possibly generate a sound. If 'Silent' is chosen, the ringtone 531 // string will be empty. 532 notification.sound = TextUtils.isEmpty(reminderRingtone) ? null : Uri 533 .parse(reminderRingtone); 534 } 535 } 536 537 private void doTimeChanged() { 538 ContentResolver cr = getContentResolver(); 539 Object service = getSystemService(Context.ALARM_SERVICE); 540 AlarmManager manager = (AlarmManager) service; 541 // TODO Move this into Provider 542 rescheduleMissedAlarms(cr, this, manager); 543 updateAlertNotification(this, false); 544 } 545 546 private static final String SORT_ORDER_ALARMTIME_ASC = 547 CalendarContract.CalendarAlerts.ALARM_TIME + " ASC"; 548 549 private static final String WHERE_RESCHEDULE_MISSED_ALARMS = 550 CalendarContract.CalendarAlerts.STATE 551 + "=" 552 + CalendarContract.CalendarAlerts.STATE_SCHEDULED 553 + " AND " 554 + CalendarContract.CalendarAlerts.ALARM_TIME 555 + "<?" 556 + " AND " 557 + CalendarContract.CalendarAlerts.ALARM_TIME 558 + ">?" 559 + " AND " 560 + CalendarContract.CalendarAlerts.END + ">=?"; 561 562 /** 563 * Searches the CalendarAlerts table for alarms that should have fired but 564 * have not and then reschedules them. This method can be called at boot 565 * time to restore alarms that may have been lost due to a phone reboot. 566 * 567 * @param cr the ContentResolver 568 * @param context the Context 569 * @param manager the AlarmManager 570 */ 571 public static final void rescheduleMissedAlarms(ContentResolver cr, Context context, 572 AlarmManager manager) { 573 // Get all the alerts that have been scheduled but have not fired 574 // and should have fired by now and are not too old. 575 long now = System.currentTimeMillis(); 576 long ancient = now - DateUtils.DAY_IN_MILLIS; 577 String[] projection = new String[] { 578 CalendarContract.CalendarAlerts.ALARM_TIME, 579 }; 580 581 // TODO: construct an explicit SQL query so that we can add 582 // "GROUPBY" instead of doing a sort and de-dup 583 Cursor cursor = cr.query(CalendarAlerts.CONTENT_URI, projection, 584 WHERE_RESCHEDULE_MISSED_ALARMS, (new String[] { 585 Long.toString(now), Long.toString(ancient), Long.toString(now) 586 }), SORT_ORDER_ALARMTIME_ASC); 587 if (cursor == null) { 588 return; 589 } 590 591 if (DEBUG) { 592 Log.d(TAG, "missed alarms found: " + cursor.getCount()); 593 } 594 595 try { 596 long alarmTime = -1; 597 598 while (cursor.moveToNext()) { 599 long newAlarmTime = cursor.getLong(0); 600 if (alarmTime != newAlarmTime) { 601 if (DEBUG) { 602 Log.w(TAG, "rescheduling missed alarm. alarmTime: " + newAlarmTime); 603 } 604 AlertUtils.scheduleAlarm(context, manager, newAlarmTime); 605 alarmTime = newAlarmTime; 606 } 607 } 608 } finally { 609 cursor.close(); 610 } 611 } 612 613 private final class ServiceHandler extends Handler { 614 public ServiceHandler(Looper looper) { 615 super(looper); 616 } 617 618 @Override 619 public void handleMessage(Message msg) { 620 processMessage(msg); 621 // NOTE: We MUST not call stopSelf() directly, since we need to 622 // make sure the wake lock acquired by AlertReceiver is released. 623 AlertReceiver.finishStartingService(AlertService.this, msg.arg1); 624 } 625 } 626 627 @Override 628 public void onCreate() { 629 HandlerThread thread = new HandlerThread("AlertService", 630 Process.THREAD_PRIORITY_BACKGROUND); 631 thread.start(); 632 633 mServiceLooper = thread.getLooper(); 634 mServiceHandler = new ServiceHandler(mServiceLooper); 635 } 636 637 @Override 638 public int onStartCommand(Intent intent, int flags, int startId) { 639 if (intent != null) { 640 Message msg = mServiceHandler.obtainMessage(); 641 msg.arg1 = startId; 642 msg.obj = intent.getExtras(); 643 mServiceHandler.sendMessage(msg); 644 } 645 return START_REDELIVER_INTENT; 646 } 647 648 @Override 649 public void onDestroy() { 650 mServiceLooper.quit(); 651 } 652 653 @Override 654 public IBinder onBind(Intent intent) { 655 return null; 656 } 657} 658