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