OSDN Git Service

auto import from //branches/cupcake_rel/...@140373
[android-x86/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.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")
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     
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 {
406         public ServiceHandler(Looper looper) {
407             super(looper);
408         }
409         
410         @Override
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
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
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
438     public void onDestroy() {
439         mServiceLooper.quit();
440     }
441
442     @Override
443     public IBinder onBind(Intent intent) {
444         return null;
445     }
446 }