AlertService.java revision 9881907c47b2658fa85954bfb339c4b1eab9fc8e
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.HashMap; 50 51/** 52 * This service is used to handle calendar event reminders. 53 */ 54public class AlertService extends Service { 55 static final boolean DEBUG = true; 56 private static final String TAG = "AlertService"; 57 58 private volatile Looper mServiceLooper; 59 private volatile ServiceHandler mServiceHandler; 60 61 private static final String[] ALERT_PROJECTION = new String[] { 62 CalendarAlerts._ID, // 0 63 CalendarAlerts.EVENT_ID, // 1 64 CalendarAlerts.STATE, // 2 65 CalendarAlerts.TITLE, // 3 66 CalendarAlerts.EVENT_LOCATION, // 4 67 CalendarAlerts.SELF_ATTENDEE_STATUS, // 5 68 CalendarAlerts.ALL_DAY, // 6 69 CalendarAlerts.ALARM_TIME, // 7 70 CalendarAlerts.MINUTES, // 8 71 CalendarAlerts.BEGIN, // 9 72 CalendarAlerts.END, // 10 73 }; 74 75 private static final int ALERT_INDEX_ID = 0; 76 private static final int ALERT_INDEX_EVENT_ID = 1; 77 private static final int ALERT_INDEX_STATE = 2; 78 private static final int ALERT_INDEX_TITLE = 3; 79 private static final int ALERT_INDEX_EVENT_LOCATION = 4; 80 private static final int ALERT_INDEX_SELF_ATTENDEE_STATUS = 5; 81 private static final int ALERT_INDEX_ALL_DAY = 6; 82 private static final int ALERT_INDEX_ALARM_TIME = 7; 83 private static final int ALERT_INDEX_MINUTES = 8; 84 private static final int ALERT_INDEX_BEGIN = 9; 85 private static final int ALERT_INDEX_END = 10; 86 87 private static final String ACTIVE_ALERTS_SELECTION = "(" + CalendarAlerts.STATE + "=? OR " 88 + CalendarAlerts.STATE + "=?) AND " + CalendarAlerts.ALARM_TIME + "<="; 89 90 private static final String[] ACTIVE_ALERTS_SELECTION_ARGS = new String[] { 91 Integer.toString(CalendarAlerts.STATE_FIRED), 92 Integer.toString(CalendarAlerts.STATE_SCHEDULED) 93 }; 94 95 private static final String ACTIVE_ALERTS_SORT = "begin DESC, end DESC"; 96 97 private static final String DISMISS_OLD_SELECTION = CalendarAlerts.END + "<? AND " 98 + CalendarAlerts.STATE + "=?"; 99 100 void processMessage(Message msg) { 101 Bundle bundle = (Bundle) msg.obj; 102 103 // On reboot, update the notification bar with the contents of the 104 // CalendarAlerts table. 105 String action = bundle.getString("action"); 106 if (DEBUG) { 107 Log.d(TAG, bundle.getLong(android.provider.CalendarContract.CalendarAlerts.ALARM_TIME) 108 + " Action = " + action); 109 } 110 111 if (action.equals(Intent.ACTION_BOOT_COMPLETED) 112 || action.equals(Intent.ACTION_TIME_CHANGED)) { 113 doTimeChanged(); 114 return; 115 } 116 117 if (!action.equals(android.provider.CalendarContract.ACTION_EVENT_REMINDER) 118 && !action.equals(Intent.ACTION_LOCALE_CHANGED) 119 && !action.equals(AlertReceiver.ACTION_DISMISS_OLD_REMINDERS)) { 120 Log.w(TAG, "Invalid action: " + action); 121 return; 122 } 123 if (action.equals(AlertReceiver.ACTION_DISMISS_OLD_REMINDERS)) { 124 dismissOldAlerts(this); 125 } 126 updateAlertNotification(this); 127 } 128 129 static void dismissOldAlerts(Context context) { 130 ContentResolver cr = context.getContentResolver(); 131 final long currentTime = System.currentTimeMillis(); 132 ContentValues vals = new ContentValues(); 133 vals.put(CalendarAlerts.STATE, CalendarAlerts.STATE_DISMISSED); 134 cr.update(CalendarAlerts.CONTENT_URI, vals, DISMISS_OLD_SELECTION, new String[] { 135 Long.toString(currentTime), Integer.toString(CalendarAlerts.STATE_SCHEDULED) 136 }); 137 } 138 139 static boolean updateAlertNotification(Context context) { 140 ContentResolver cr = context.getContentResolver(); 141 final long currentTime = System.currentTimeMillis(); 142 SharedPreferences prefs = GeneralPreferences.getSharedPreferences(context); 143 144 boolean doAlert = prefs.getBoolean(GeneralPreferences.KEY_ALERTS, true); 145 boolean doPopup = prefs.getBoolean(GeneralPreferences.KEY_ALERTS_POPUP, false); 146 147 if (!doAlert) { 148 if (DEBUG) { 149 Log.d(TAG, "alert preference is OFF"); 150 } 151 152 // If we shouldn't be showing notifications cancel any existing ones 153 // and return. 154 NotificationManager nm = (NotificationManager) context 155 .getSystemService(Context.NOTIFICATION_SERVICE); 156 nm.cancelAll(); 157 return true; 158 } 159 160 Cursor alertCursor = cr.query(CalendarAlerts.CONTENT_URI, ALERT_PROJECTION, 161 (ACTIVE_ALERTS_SELECTION + currentTime), ACTIVE_ALERTS_SELECTION_ARGS, 162 ACTIVE_ALERTS_SORT); 163 164 if (alertCursor == null || alertCursor.getCount() == 0) { 165 if (alertCursor != null) { 166 alertCursor.close(); 167 } 168 169 if (DEBUG) Log.d(TAG, "No fired or scheduled alerts"); 170 NotificationManager nm = 171 (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); 172 nm.cancel(AlertUtils.NOTIFICATION_ID); 173 return false; 174 } 175 176 if (DEBUG) { 177 Log.d(TAG, "alert count:" + alertCursor.getCount()); 178 } 179 180 String notificationEventName = null; 181 String notificationEventLocation = null; 182 long notificationEventBegin = 0; 183 long notificationEventEnd = 0; 184 long notificationEventId = -1; 185 int notificationEventStatus = 0; 186 boolean notificationEventAllDay = true; 187 HashMap<Long, Long> eventIds = new HashMap<Long, Long>(); 188 int numReminders = 0; 189 int numFired = 0; 190 try { 191 while (alertCursor.moveToNext()) { 192 final long alertId = alertCursor.getLong(ALERT_INDEX_ID); 193 final long eventId = alertCursor.getLong(ALERT_INDEX_EVENT_ID); 194 final int minutes = alertCursor.getInt(ALERT_INDEX_MINUTES); 195 final String eventName = alertCursor.getString(ALERT_INDEX_TITLE); 196 final String location = alertCursor.getString(ALERT_INDEX_EVENT_LOCATION); 197 final int status = alertCursor.getInt(ALERT_INDEX_SELF_ATTENDEE_STATUS); 198 final boolean declined = status == Attendees.ATTENDEE_STATUS_DECLINED; 199 final long beginTime = alertCursor.getLong(ALERT_INDEX_BEGIN); 200 final long endTime = alertCursor.getLong(ALERT_INDEX_END); 201 final Uri alertUri = ContentUris 202 .withAppendedId(CalendarAlerts.CONTENT_URI, alertId); 203 final long alarmTime = alertCursor.getLong(ALERT_INDEX_ALARM_TIME); 204 int state = alertCursor.getInt(ALERT_INDEX_STATE); 205 final boolean allDay = alertCursor.getInt(ALERT_INDEX_ALL_DAY) != 0; 206 207 if (DEBUG) { 208 Log.d(TAG, "alarmTime:" + alarmTime + " alertId:" + alertId 209 + " eventId:" + eventId + " state: " + state + " minutes:" + minutes 210 + " declined:" + declined + " beginTime:" + beginTime 211 + " endTime:" + endTime); 212 } 213 214 ContentValues values = new ContentValues(); 215 int newState = -1; 216 217 // Uncomment for the behavior of clearing out alerts after the 218 // events ended. b/1880369 219 // 220 // if (endTime < currentTime) { 221 // newState = CalendarAlerts.DISMISSED; 222 // } else 223 224 // Remove declined events 225 if (!declined) { 226 // Don't count duplicate alerts for the same event 227 if (eventIds.put(eventId, beginTime) == null) { 228 numReminders++; 229 } 230 231 if (state == CalendarAlerts.STATE_SCHEDULED) { 232 newState = CalendarAlerts.STATE_FIRED; 233 numFired++; 234 235 // Record the received time in the CalendarAlerts table. 236 // This is useful for finding bugs that cause alarms to be 237 // missed or delayed. 238 values.put(CalendarAlerts.RECEIVED_TIME, currentTime); 239 } 240 } else { 241 newState = CalendarAlerts.STATE_DISMISSED; 242 } 243 244 // Update row if state changed 245 if (newState != -1) { 246 values.put(CalendarAlerts.STATE, newState); 247 state = newState; 248 } 249 250 if (state == CalendarAlerts.STATE_FIRED) { 251 // Record the time posting to notification manager. 252 // This is used for debugging missed alarms. 253 values.put(CalendarAlerts.NOTIFY_TIME, currentTime); 254 } 255 256 // Write row to if anything changed 257 if (values.size() > 0) cr.update(alertUri, values, null, null); 258 259 if (state != CalendarAlerts.STATE_FIRED) { 260 continue; 261 } 262 263 // Pick an Event title for the notification panel by the latest 264 // alertTime and give prefer accepted events in case of ties. 265 int newStatus; 266 switch (status) { 267 case Attendees.ATTENDEE_STATUS_ACCEPTED: 268 newStatus = 2; 269 break; 270 case Attendees.ATTENDEE_STATUS_TENTATIVE: 271 newStatus = 1; 272 break; 273 default: 274 newStatus = 0; 275 } 276 277 // TODO Prioritize by "primary" calendar 278 // Assumes alerts are sorted by begin time in reverse 279 if (notificationEventName == null 280 || (notificationEventBegin <= beginTime && 281 notificationEventStatus < newStatus)) { 282 notificationEventLocation = location; 283 notificationEventBegin = beginTime; 284 notificationEventEnd = endTime; 285 notificationEventId = eventId; 286 notificationEventStatus = newStatus; 287 notificationEventAllDay = allDay; 288 } 289 if (numReminders == 1) { 290 notificationEventName = eventName; 291 } else { 292 notificationEventName = eventName + ", " + notificationEventName; 293 } 294 } 295 } finally { 296 if (alertCursor != null) { 297 alertCursor.close(); 298 } 299 } 300 301 boolean quietUpdate = numFired == 0; 302 boolean highPriority = numFired > 0 && doPopup; 303 postNotification(context, prefs, notificationEventName, notificationEventLocation, 304 numReminders, quietUpdate, highPriority, notificationEventBegin, 305 notificationEventEnd, notificationEventId, notificationEventAllDay); 306 307 return true; 308 } 309 310 private static void postNotification(Context context, SharedPreferences prefs, 311 String eventName, String location, int numReminders, boolean quietUpdate, 312 boolean highPriority, long startMillis, long endMillis, long id, boolean allDay) { 313 if (DEBUG) { 314 Log.d(TAG, "###### creating new alarm notification, numReminders: " + numReminders 315 + (quietUpdate ? " QUIET" : " loud") 316 + (highPriority ? " high-priority" : "")); 317 } 318 319 NotificationManager nm = 320 (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); 321 322 if (numReminders == 0) { 323 nm.cancel(AlertUtils.NOTIFICATION_ID); 324 return; 325 } 326 327 Notification notification = AlertReceiver.makeNewAlertNotification(context, eventName, 328 location, numReminders, highPriority, startMillis, endMillis, id, allDay); 329 notification.defaults |= Notification.DEFAULT_LIGHTS; 330 331 // Quietly update notification bar. Nothing new. Maybe something just got deleted. 332 if (!quietUpdate) { 333 // Flash ticker in status bar 334 notification.tickerText = eventName; 335 if (!TextUtils.isEmpty(location)) { 336 notification.tickerText = eventName + " - " + location; 337 } 338 339 // Generate either a pop-up dialog, status bar notification, or 340 // neither. Pop-up dialog and status bar notification may include a 341 // sound, an alert, or both. A status bar notification also includes 342 // a toast. 343 344 // Find out the circumstances under which to vibrate. 345 // Migrate from pre-Froyo boolean setting if necessary. 346 String vibrateWhen; // "always" or "silent" or "never" 347 if(prefs.contains(GeneralPreferences.KEY_ALERTS_VIBRATE_WHEN)) 348 { 349 // Look up Froyo setting 350 vibrateWhen = 351 prefs.getString(GeneralPreferences.KEY_ALERTS_VIBRATE_WHEN, null); 352 } else if(prefs.contains(GeneralPreferences.KEY_ALERTS_VIBRATE)) { 353 // No Froyo setting. Migrate pre-Froyo setting to new Froyo-defined value. 354 boolean vibrate = 355 prefs.getBoolean(GeneralPreferences.KEY_ALERTS_VIBRATE, false); 356 vibrateWhen = vibrate ? 357 context.getString(R.string.prefDefault_alerts_vibrate_true) : 358 context.getString(R.string.prefDefault_alerts_vibrate_false); 359 } else { 360 // No setting. Use Froyo-defined default. 361 vibrateWhen = context.getString(R.string.prefDefault_alerts_vibrateWhen); 362 } 363 boolean vibrateAlways = vibrateWhen.equals("always"); 364 boolean vibrateSilent = vibrateWhen.equals("silent"); 365 AudioManager audioManager = 366 (AudioManager)context.getSystemService(Context.AUDIO_SERVICE); 367 boolean nowSilent = 368 audioManager.getRingerMode() == AudioManager.RINGER_MODE_VIBRATE; 369 370 // Possibly generate a vibration 371 if (vibrateAlways || (vibrateSilent && nowSilent)) { 372 notification.defaults |= Notification.DEFAULT_VIBRATE; 373 } 374 375 // Possibly generate a sound. If 'Silent' is chosen, the ringtone 376 // string will be empty. 377 String reminderRingtone = prefs.getString( 378 GeneralPreferences.KEY_ALERTS_RINGTONE, null); 379 notification.sound = TextUtils.isEmpty(reminderRingtone) ? null : Uri 380 .parse(reminderRingtone); 381 } 382 383 nm.notify(AlertUtils.NOTIFICATION_ID, notification); 384 } 385 386 private void doTimeChanged() { 387 ContentResolver cr = getContentResolver(); 388 Object service = getSystemService(Context.ALARM_SERVICE); 389 AlarmManager manager = (AlarmManager) service; 390 // TODO Move this into Provider 391 rescheduleMissedAlarms(cr, this, manager); 392 updateAlertNotification(this); 393 } 394 395 private static final String SORT_ORDER_ALARMTIME_ASC = 396 CalendarContract.CalendarAlerts.ALARM_TIME + " ASC"; 397 398 private static final String WHERE_RESCHEDULE_MISSED_ALARMS = 399 CalendarContract.CalendarAlerts.STATE 400 + "=" 401 + CalendarContract.CalendarAlerts.STATE_SCHEDULED 402 + " AND " 403 + CalendarContract.CalendarAlerts.ALARM_TIME 404 + "<?" 405 + " AND " 406 + CalendarContract.CalendarAlerts.ALARM_TIME 407 + ">?" 408 + " AND " 409 + CalendarContract.CalendarAlerts.END + ">=?"; 410 411 /** 412 * Searches the CalendarAlerts table for alarms that should have fired but 413 * have not and then reschedules them. This method can be called at boot 414 * time to restore alarms that may have been lost due to a phone reboot. 415 * 416 * @param cr the ContentResolver 417 * @param context the Context 418 * @param manager the AlarmManager 419 */ 420 public static final void rescheduleMissedAlarms(ContentResolver cr, Context context, 421 AlarmManager manager) { 422 // Get all the alerts that have been scheduled but have not fired 423 // and should have fired by now and are not too old. 424 long now = System.currentTimeMillis(); 425 long ancient = now - DateUtils.DAY_IN_MILLIS; 426 String[] projection = new String[] { 427 CalendarContract.CalendarAlerts.ALARM_TIME, 428 }; 429 430 // TODO: construct an explicit SQL query so that we can add 431 // "GROUPBY" instead of doing a sort and de-dup 432 Cursor cursor = cr.query(CalendarAlerts.CONTENT_URI, projection, 433 WHERE_RESCHEDULE_MISSED_ALARMS, (new String[] { 434 Long.toString(now), Long.toString(ancient), Long.toString(now) 435 }), SORT_ORDER_ALARMTIME_ASC); 436 if (cursor == null) { 437 return; 438 } 439 440 if (DEBUG) { 441 Log.d(TAG, "missed alarms found: " + cursor.getCount()); 442 } 443 444 try { 445 long alarmTime = -1; 446 447 while (cursor.moveToNext()) { 448 long newAlarmTime = cursor.getLong(0); 449 if (alarmTime != newAlarmTime) { 450 if (DEBUG) { 451 Log.w(TAG, "rescheduling missed alarm. alarmTime: " + newAlarmTime); 452 } 453 AlertUtils.scheduleAlarm(context, manager, newAlarmTime); 454 alarmTime = newAlarmTime; 455 } 456 } 457 } finally { 458 cursor.close(); 459 } 460 } 461 462 private final class ServiceHandler extends Handler { 463 public ServiceHandler(Looper looper) { 464 super(looper); 465 } 466 467 @Override 468 public void handleMessage(Message msg) { 469 processMessage(msg); 470 // NOTE: We MUST not call stopSelf() directly, since we need to 471 // make sure the wake lock acquired by AlertReceiver is released. 472 AlertReceiver.finishStartingService(AlertService.this, msg.arg1); 473 } 474 } 475 476 @Override 477 public void onCreate() { 478 HandlerThread thread = new HandlerThread("AlertService", 479 Process.THREAD_PRIORITY_BACKGROUND); 480 thread.start(); 481 482 mServiceLooper = thread.getLooper(); 483 mServiceHandler = new ServiceHandler(mServiceLooper); 484 } 485 486 @Override 487 public int onStartCommand(Intent intent, int flags, int startId) { 488 if (intent != null) { 489 Message msg = mServiceHandler.obtainMessage(); 490 msg.arg1 = startId; 491 msg.obj = intent.getExtras(); 492 mServiceHandler.sendMessage(msg); 493 } 494 return START_REDELIVER_INTENT; 495 } 496 497 @Override 498 public void onDestroy() { 499 mServiceLooper.quit(); 500 } 501 502 @Override 503 public IBinder onBind(Intent intent) { 504 return null; 505 } 506} 507