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 17 package com.android.calendar; 18 19 import android.app.AlarmManager; 20 import android.app.Notification; 21 import android.app.NotificationManager; 22 import android.app.Service; 23 import android.content.ContentResolver; 24 import android.content.ContentValues; 25 import android.content.Context; 26 import android.content.Intent; 27 import android.content.SharedPreferences; 28 import android.database.Cursor; 29 import android.net.Uri; 30 import android.os.Bundle; 31 import android.os.Handler; 32 import android.os.HandlerThread; 33 import android.os.IBinder; 34 import android.os.Looper; 35 import android.os.Message; 36 import android.os.Process; 37 import android.preference.PreferenceManager; 38 import android.provider.Calendar; 39 import android.provider.Calendar.Attendees; 40 import android.provider.Calendar.CalendarAlerts; 41 import android.provider.Calendar.Instances; 42 import android.provider.Calendar.Reminders; 43 import android.text.TextUtils; 44 import android.text.format.DateUtils; 45 import android.util.Log; 46 import android.view.LayoutInflater; 47 import android.view.View; 48 49 /** 50 * This service is used to handle calendar event reminders. 51 */ 52 public class AlertService extends Service { 53 private static final String TAG = "AlertService"; 54 55 private volatile Looper mServiceLooper; 56 private volatile ServiceHandler mServiceHandler; 57 58 private static final String[] ALERT_PROJECTION = new String[] { 59 CalendarAlerts._ID, // 0 60 CalendarAlerts.EVENT_ID, // 1 61 CalendarAlerts.STATE, // 2 62 CalendarAlerts.TITLE, // 3 63 CalendarAlerts.EVENT_LOCATION, // 4 64 CalendarAlerts.SELF_ATTENDEE_STATUS, // 5 65 CalendarAlerts.ALL_DAY, // 6 66 CalendarAlerts.ALARM_TIME, // 7 67 CalendarAlerts.MINUTES, // 8 68 CalendarAlerts.BEGIN, // 9 69 }; 70 71 // We just need a simple projection that returns any column 72 private static final String[] ALERT_PROJECTION_SMALL = new String[] { 73 CalendarAlerts._ID, // 0 74 }; 75 76 private static final int ALERT_INDEX_ID = 0; 77 private static final int ALERT_INDEX_EVENT_ID = 1; 78 private static final int ALERT_INDEX_STATE = 2; 79 private static final int ALERT_INDEX_TITLE = 3; 80 private static final int ALERT_INDEX_EVENT_LOCATION = 4; 81 private static final int ALERT_INDEX_SELF_ATTENDEE_STATUS = 5; 82 private static final int ALERT_INDEX_ALL_DAY = 6; 83 private static final int ALERT_INDEX_ALARM_TIME = 7; 84 private static final int ALERT_INDEX_MINUTES = 8; 85 private static final int ALERT_INDEX_BEGIN = 9; 86 87 private String[] INSTANCE_PROJECTION = { Instances.BEGIN, Instances.END }; 88 private static final int INSTANCES_INDEX_BEGIN = 0; 89 private static final int INSTANCES_INDEX_END = 1; 90 91 // We just need a simple projection that returns any column 92 private static final String[] REMINDER_PROJECTION_SMALL = new String[] { 93 Reminders._ID, // 0 94 }; 95 alarmsFiredRecently(ContentResolver cr)96 private final boolean alarmsFiredRecently(ContentResolver cr) { 97 String selection = CalendarAlerts.RECEIVED_TIME + ">=" 98 + (System.currentTimeMillis() - 10000); 99 String[] projection = new String[] { CalendarAlerts.ALARM_TIME }; 100 Cursor cursor = cr.query(CalendarAlerts.CONTENT_URI, projection, selection, null, null); 101 102 boolean recentAlarms = false; 103 if (cursor != null) { 104 if (cursor.moveToFirst() && cursor.getCount() > 0) { 105 recentAlarms = true; 106 } 107 cursor.close(); 108 } 109 return recentAlarms; 110 } 111 112 @SuppressWarnings("deprecation") processMessage(Message msg)113 void processMessage(Message msg) { 114 Bundle bundle = (Bundle) msg.obj; 115 116 // On reboot, update the notification bar with the contents of the 117 // CalendarAlerts table. 118 String action = bundle.getString("action"); 119 if (action.equals(Intent.ACTION_BOOT_COMPLETED) 120 || action.equals(Intent.ACTION_TIME_CHANGED)) { 121 doTimeChanged(); 122 return; 123 } 124 125 // The Uri specifies an entry in the CalendarAlerts table 126 Uri alertUri = Uri.parse(bundle.getString("uri")); 127 if (Log.isLoggable(TAG, Log.DEBUG)) { 128 Log.d(TAG, "uri: " + alertUri); 129 } 130 131 ContentResolver cr = getContentResolver(); 132 boolean alarmsFiredRecently = alarmsFiredRecently(cr); 133 134 if (alertUri != null) { 135 if (!Calendar.AUTHORITY.equals(alertUri.getAuthority())) { 136 Log.w(TAG, "Invalid AUTHORITY uri: " + alertUri); 137 return; 138 } 139 140 // Record the received time in the CalendarAlerts table. 141 // This is useful for finding bugs that cause alarms to be 142 // missed or delayed. 143 ContentValues values = new ContentValues(); 144 values.put(CalendarAlerts.RECEIVED_TIME, System.currentTimeMillis()); 145 cr.update(alertUri, values, null /* where */, null /* args */); 146 } 147 148 Cursor alertCursor = cr.query(alertUri, ALERT_PROJECTION, 149 null /* selection */, null, null /* sort order */); 150 151 long alertId, eventId, alarmTime; 152 int minutes; 153 String eventName; 154 String location; 155 boolean allDay; 156 boolean declined = false; 157 try { 158 if (alertCursor == null || !alertCursor.moveToFirst()) { 159 // This can happen if the event was deleted. 160 if (Log.isLoggable(TAG, Log.DEBUG)) { 161 Log.d(TAG, "alert not found"); 162 } 163 return; 164 } 165 alertId = alertCursor.getLong(ALERT_INDEX_ID); 166 eventId = alertCursor.getLong(ALERT_INDEX_EVENT_ID); 167 minutes = alertCursor.getInt(ALERT_INDEX_MINUTES); 168 eventName = alertCursor.getString(ALERT_INDEX_TITLE); 169 location = alertCursor.getString(ALERT_INDEX_EVENT_LOCATION); 170 allDay = alertCursor.getInt(ALERT_INDEX_ALL_DAY) != 0; 171 alarmTime = alertCursor.getLong(ALERT_INDEX_ALARM_TIME); 172 declined = alertCursor.getInt(ALERT_INDEX_SELF_ATTENDEE_STATUS) == 173 Attendees.ATTENDEE_STATUS_DECLINED; 174 175 // If the event was declined, then mark the alarm DISMISSED, 176 // otherwise, mark the alarm FIRED. 177 int newState = CalendarAlerts.FIRED; 178 if (declined) { 179 newState = CalendarAlerts.DISMISSED; 180 } 181 alertCursor.updateInt(ALERT_INDEX_STATE, newState); 182 alertCursor.commitUpdates(); 183 } finally { 184 if (alertCursor != null) { 185 alertCursor.close(); 186 } 187 } 188 189 // Do not show an alert if the event was declined 190 if (declined) { 191 if (Log.isLoggable(TAG, Log.DEBUG)) { 192 Log.d(TAG, "event declined, alert cancelled"); 193 } 194 return; 195 } 196 197 long beginTime = bundle.getLong(Calendar.EVENT_BEGIN_TIME, 0); 198 long endTime = bundle.getLong(Calendar.EVENT_END_TIME, 0); 199 200 // Check if this alarm is still valid. The time of the event may 201 // have been changed, or the reminder may have been changed since 202 // this alarm was set. First, search for an instance in the Instances 203 // that has the same event id and the same begin and end time. 204 // Then check for a reminder in the Reminders table to ensure that 205 // the reminder minutes is consistent with this alarm. 206 String selection = Instances.EVENT_ID + "=" + eventId; 207 Cursor instanceCursor = Instances.query(cr, INSTANCE_PROJECTION, 208 beginTime, endTime, selection, Instances.DEFAULT_SORT_ORDER); 209 long instanceBegin = 0, instanceEnd = 0; 210 try { 211 if (instanceCursor == null || !instanceCursor.moveToFirst()) { 212 // Delete this alarm from the CalendarAlerts table 213 cr.delete(alertUri, null /* selection */, null /* selection args */); 214 if (Log.isLoggable(TAG, Log.DEBUG)) { 215 Log.d(TAG, "instance not found, alert cancelled"); 216 } 217 return; 218 } 219 instanceBegin = instanceCursor.getLong(INSTANCES_INDEX_BEGIN); 220 instanceEnd = instanceCursor.getLong(INSTANCES_INDEX_END); 221 } finally { 222 if (instanceCursor != null) { 223 instanceCursor.close(); 224 } 225 } 226 227 // Check that a reminder for this event exists with the same number 228 // of minutes. But snoozed alarms have minutes = 0, so don't do this 229 // check for snoozed alarms. 230 if (minutes > 0) { 231 selection = Reminders.EVENT_ID + "=" + eventId 232 + " AND " + Reminders.MINUTES + "=" + minutes; 233 Cursor reminderCursor = cr.query(Reminders.CONTENT_URI, REMINDER_PROJECTION_SMALL, 234 selection, null /* selection args */, null /* sort order */); 235 try { 236 if (reminderCursor == null || reminderCursor.getCount() == 0) { 237 // Delete this alarm from the CalendarAlerts table 238 cr.delete(alertUri, null /* selection */, null /* selection args */); 239 if (Log.isLoggable(TAG, Log.DEBUG)) { 240 Log.d(TAG, "reminder not found, alert cancelled"); 241 } 242 return; 243 } 244 } finally { 245 if (reminderCursor != null) { 246 reminderCursor.close(); 247 } 248 } 249 } 250 251 // If the event time was changed and the event has already ended, 252 // then don't sound the alarm. 253 if (alarmTime > instanceEnd) { 254 // Delete this alarm from the CalendarAlerts table 255 cr.delete(alertUri, null /* selection */, null /* selection args */); 256 if (Log.isLoggable(TAG, Log.DEBUG)) { 257 Log.d(TAG, "event ended, alert cancelled"); 258 } 259 return; 260 } 261 262 // If minutes > 0, then this is a normal alarm (not a snoozed alarm) 263 // so check for duplicate alarms. A duplicate alarm can occur when 264 // the start time of an event is changed to an earlier time. The 265 // later alarm (that was first scheduled for the later event time) 266 // should be discarded. 267 long computedAlarmTime = instanceBegin - minutes * DateUtils.MINUTE_IN_MILLIS; 268 if (minutes > 0 && computedAlarmTime != alarmTime) { 269 // If the event time was changed to a later time, then the computed 270 // alarm time is in the future and we shouldn't sound this alarm. 271 if (computedAlarmTime > alarmTime) { 272 // Delete this alarm from the CalendarAlerts table 273 cr.delete(alertUri, null /* selection */, null /* selection args */); 274 if (Log.isLoggable(TAG, Log.DEBUG)) { 275 Log.d(TAG, "event postponed, alert cancelled"); 276 } 277 return; 278 } 279 280 // Check for another alarm in the CalendarAlerts table that has the 281 // same event id and the same "minutes". This can occur 282 // if the event start time was changed to an earlier time and the 283 // alarm for the later time goes off. To avoid discarding alarms 284 // for repeating events (that have the same event id), we check 285 // that the other alarm fired recently (within an hour of this one). 286 long recently = alarmTime - 60 * DateUtils.MINUTE_IN_MILLIS; 287 selection = CalendarAlerts.EVENT_ID + "=" + eventId 288 + " AND " + CalendarAlerts.TABLE_NAME + "." + CalendarAlerts._ID 289 + "!=" + alertId 290 + " AND " + CalendarAlerts.MINUTES + "=" + minutes 291 + " AND " + CalendarAlerts.ALARM_TIME + ">" + recently 292 + " AND " + CalendarAlerts.ALARM_TIME + "<=" + alarmTime; 293 alertCursor = CalendarAlerts.query(cr, ALERT_PROJECTION_SMALL, selection, null); 294 if (alertCursor != null) { 295 try { 296 if (alertCursor.getCount() > 0) { 297 // Delete this alarm from the CalendarAlerts table 298 cr.delete(alertUri, null /* selection */, null /* selection args */); 299 if (Log.isLoggable(TAG, Log.DEBUG)) { 300 Log.d(TAG, "duplicate alarm, alert cancelled"); 301 } 302 return; 303 } 304 } finally { 305 alertCursor.close(); 306 } 307 } 308 } 309 310 // Find all the alerts that have fired but have not been dismissed 311 selection = CalendarAlerts.STATE + "=" + CalendarAlerts.FIRED; 312 alertCursor = CalendarAlerts.query(cr, ALERT_PROJECTION, selection, null); 313 314 if (alertCursor == null || alertCursor.getCount() == 0) { 315 if (Log.isLoggable(TAG, Log.DEBUG)) { 316 Log.d(TAG, "no fired alarms found"); 317 } 318 return; 319 } 320 321 int numReminders = alertCursor.getCount(); 322 try { 323 while (alertCursor.moveToNext()) { 324 long otherEventId = alertCursor.getLong(ALERT_INDEX_EVENT_ID); 325 long otherAlertId = alertCursor.getLong(ALERT_INDEX_ID); 326 int otherAlarmState = alertCursor.getInt(ALERT_INDEX_STATE); 327 long otherBeginTime = alertCursor.getLong(ALERT_INDEX_BEGIN); 328 if (otherEventId == eventId && otherAlertId != alertId 329 && otherAlarmState == CalendarAlerts.FIRED 330 && otherBeginTime == beginTime) { 331 // This event already has an alert that fired and has not 332 // been dismissed. This can happen if an event has 333 // multiple reminders. Do not count this as a separate 334 // reminder. But we do want to sound the alarm and vibrate 335 // the phone, if necessary. 336 if (Log.isLoggable(TAG, Log.DEBUG)) { 337 Log.d(TAG, "multiple alarms for this event"); 338 } 339 numReminders -= 1; 340 } 341 } 342 } finally { 343 alertCursor.close(); 344 } 345 346 if (Log.isLoggable(TAG, Log.DEBUG)) { 347 Log.d(TAG, "creating new alarm notification, numReminders: " + numReminders); 348 } 349 Notification notification = AlertReceiver.makeNewAlertNotification(this, eventName, 350 location, numReminders); 351 352 // Generate either a pop-up dialog, status bar notification, or 353 // neither. Pop-up dialog and status bar notification may include a 354 // sound, an alert, or both. A status bar notification also includes 355 // a toast. 356 SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); 357 String reminderType = prefs.getString(CalendarPreferenceActivity.KEY_ALERTS_TYPE, 358 CalendarPreferenceActivity.ALERT_TYPE_STATUS_BAR); 359 360 if (reminderType.equals(CalendarPreferenceActivity.ALERT_TYPE_OFF)) { 361 if (Log.isLoggable(TAG, Log.DEBUG)) { 362 Log.d(TAG, "alert preference is OFF"); 363 } 364 return; 365 } 366 367 NotificationManager nm = 368 (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); 369 boolean reminderVibrate = 370 prefs.getBoolean(CalendarPreferenceActivity.KEY_ALERTS_VIBRATE, false); 371 372 // Possibly generate a vibration 373 if (reminderVibrate) { 374 notification.defaults |= Notification.DEFAULT_VIBRATE; 375 } 376 377 // Temp fix. If we sounded an notification recently, be quiet so the 378 // audio won't overlap. 379 380 // TODO Long term fix: CalendarProvider currently setup an alarm with 381 // AlarmManager for each event notification. So AlertService can post 382 // multiple notifications back to back if there are multiple alarms that 383 // fire at the same time. Instead of doing that, CalendarProvider should 384 // setup one alarm for each wake up time. AlertService can query for 385 // alerts table and update notification manager only once. 386 if (!alarmsFiredRecently) { 387 // Possibly generate a sound. If 'Silent' is chosen, the ringtone 388 // string will be empty. 389 String reminderRingtone = prefs.getString( 390 CalendarPreferenceActivity.KEY_ALERTS_RINGTONE, null); 391 notification.sound = TextUtils.isEmpty(reminderRingtone) ? null : Uri 392 .parse(reminderRingtone); 393 } else { 394 notification.sound = null; 395 } 396 397 if (reminderType.equals(CalendarPreferenceActivity.ALERT_TYPE_ALERTS)) { 398 Intent alertIntent = new Intent(); 399 alertIntent.setClass(this, AlertActivity.class); 400 alertIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 401 startActivity(alertIntent); 402 } else { 403 LayoutInflater inflater; 404 inflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE); 405 View view = inflater.inflate(R.layout.alert_toast, null); 406 407 AlertAdapter.updateView(this, view, eventName, location, beginTime, endTime, allDay); 408 } 409 410 // Record the notify time in the CalendarAlerts table. 411 // This is used for debugging missed alarms. 412 ContentValues values = new ContentValues(); 413 long currentTime = System.currentTimeMillis(); 414 values.put(CalendarAlerts.NOTIFY_TIME, currentTime); 415 cr.update(alertUri, values, null /* where */, null /* args */); 416 417 // The notification time should be pretty close to the reminder time 418 // that the user set for this event. If the notification is late, then 419 // that's a bug and we should log an error. 420 if (currentTime > alarmTime + DateUtils.MINUTE_IN_MILLIS) { 421 long minutesLate = (currentTime - alarmTime) / DateUtils.MINUTE_IN_MILLIS; 422 int flags = DateUtils.FORMAT_SHOW_YEAR | DateUtils.FORMAT_SHOW_TIME; 423 String alarmTimeStr = DateUtils.formatDateTime(this, alarmTime, flags); 424 String currentTimeStr = DateUtils.formatDateTime(this, currentTime, flags); 425 Log.w(TAG, "Calendar reminder alarm for event id " + eventId 426 + " is " + minutesLate + " minute(s) late;" 427 + " expected alarm at: " + alarmTimeStr 428 + " but got it at: " + currentTimeStr); 429 } 430 431 nm.notify(0, notification); 432 } 433 doTimeChanged()434 private void doTimeChanged() { 435 ContentResolver cr = getContentResolver(); 436 Object service = getSystemService(Context.ALARM_SERVICE); 437 AlarmManager manager = (AlarmManager) service; 438 CalendarAlerts.rescheduleMissedAlarms(cr, this, manager); 439 AlertReceiver.updateAlertNotification(this); 440 } 441 442 private final class ServiceHandler extends Handler { ServiceHandler(Looper looper)443 public ServiceHandler(Looper looper) { 444 super(looper); 445 } 446 447 @Override handleMessage(Message msg)448 public void handleMessage(Message msg) { 449 processMessage(msg); 450 // NOTE: We MUST not call stopSelf() directly, since we need to 451 // make sure the wake lock acquired by AlertReceiver is released. 452 AlertReceiver.finishStartingService(AlertService.this, msg.arg1); 453 } 454 }; 455 456 @Override onCreate()457 public void onCreate() { 458 HandlerThread thread = new HandlerThread("AlertService", 459 Process.THREAD_PRIORITY_BACKGROUND); 460 thread.start(); 461 462 mServiceLooper = thread.getLooper(); 463 mServiceHandler = new ServiceHandler(mServiceLooper); 464 } 465 466 @Override onStartCommand(Intent intent, int flags, int startId)467 public int onStartCommand(Intent intent, int flags, int startId) { 468 if (intent != null) { 469 Message msg = mServiceHandler.obtainMessage(); 470 msg.arg1 = startId; 471 msg.obj = intent.getExtras(); 472 mServiceHandler.sendMessage(msg); 473 } 474 return START_REDELIVER_INTENT; 475 } 476 477 @Override onDestroy()478 public void onDestroy() { 479 mServiceLooper.quit(); 480 } 481 482 @Override onBind(Intent intent)483 public IBinder onBind(Intent intent) { 484 return null; 485 } 486 } 487