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