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