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