OSDN Git Service

Always use a consistent name for SharedPreferences.
[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.ContentUris;
25 import android.content.ContentValues;
26 import android.content.Context;
27 import android.content.Intent;
28 import android.content.SharedPreferences;
29 import android.database.Cursor;
30 import android.net.Uri;
31 import android.os.Bundle;
32 import android.os.Handler;
33 import android.os.HandlerThread;
34 import android.os.IBinder;
35 import android.os.Looper;
36 import android.os.Message;
37 import android.os.Process;
38 import android.preference.PreferenceManager;
39 import android.provider.Calendar.Attendees;
40 import android.provider.Calendar.CalendarAlerts;
41 import android.text.TextUtils;
42 import android.util.Log;
43
44 import java.util.HashMap;
45
46 /**
47  * This service is used to handle calendar event reminders.
48  */
49 public class AlertService extends Service {
50     static final boolean DEBUG = true;
51     private static final String TAG = "AlertService";
52
53     private volatile Looper mServiceLooper;
54     private volatile ServiceHandler mServiceHandler;
55
56     private static final String[] ALERT_PROJECTION = new String[] {
57         CalendarAlerts._ID,                     // 0
58         CalendarAlerts.EVENT_ID,                // 1
59         CalendarAlerts.STATE,                   // 2
60         CalendarAlerts.TITLE,                   // 3
61         CalendarAlerts.EVENT_LOCATION,          // 4
62         CalendarAlerts.SELF_ATTENDEE_STATUS,    // 5
63         CalendarAlerts.ALL_DAY,                 // 6
64         CalendarAlerts.ALARM_TIME,              // 7
65         CalendarAlerts.MINUTES,                 // 8
66         CalendarAlerts.BEGIN,                   // 9
67         CalendarAlerts.END,                     // 10
68     };
69
70     private static final int ALERT_INDEX_ID = 0;
71     private static final int ALERT_INDEX_EVENT_ID = 1;
72     private static final int ALERT_INDEX_STATE = 2;
73     private static final int ALERT_INDEX_TITLE = 3;
74     private static final int ALERT_INDEX_EVENT_LOCATION = 4;
75     private static final int ALERT_INDEX_SELF_ATTENDEE_STATUS = 5;
76     private static final int ALERT_INDEX_ALL_DAY = 6;
77     private static final int ALERT_INDEX_ALARM_TIME = 7;
78     private static final int ALERT_INDEX_MINUTES = 8;
79     private static final int ALERT_INDEX_BEGIN = 9;
80     private static final int ALERT_INDEX_END = 10;
81
82     private static final String ACTIVE_ALERTS_SELECTION = "(" + CalendarAlerts.STATE + "=? OR "
83             + CalendarAlerts.STATE + "=?) AND " + CalendarAlerts.ALARM_TIME + "<=";
84
85     private static final String[] ACTIVE_ALERTS_SELECTION_ARGS = new String[] {
86             Integer.toString(CalendarAlerts.FIRED), Integer.toString(CalendarAlerts.SCHEDULED)
87     };
88
89     private static final String ACTIVE_ALERTS_SORT = "begin DESC, end DESC";
90
91     @SuppressWarnings("deprecation")
92     void processMessage(Message msg) {
93         Bundle bundle = (Bundle) msg.obj;
94
95         // On reboot, update the notification bar with the contents of the
96         // CalendarAlerts table.
97         String action = bundle.getString("action");
98         if (DEBUG) {
99             Log.d(TAG, "" + bundle.getLong(android.provider.Calendar.CalendarAlerts.ALARM_TIME)
100                     + " Action = " + action);
101         }
102
103         if (action.equals(Intent.ACTION_BOOT_COMPLETED)
104                 || action.equals(Intent.ACTION_TIME_CHANGED)) {
105             doTimeChanged();
106             return;
107         }
108
109         if (!action.equals(android.provider.Calendar.EVENT_REMINDER_ACTION)
110                 && !action.equals(Intent.ACTION_LOCALE_CHANGED)) {
111             Log.w(TAG, "Invalid action: " + action);
112             return;
113         }
114
115         updateAlertNotification(this);
116     }
117
118     static boolean updateAlertNotification(Context context) {
119         ContentResolver cr = context.getContentResolver();
120         final long currentTime = System.currentTimeMillis();
121
122         Cursor alertCursor = CalendarAlerts.query(cr, ALERT_PROJECTION, ACTIVE_ALERTS_SELECTION
123                 + currentTime, ACTIVE_ALERTS_SELECTION_ARGS, ACTIVE_ALERTS_SORT);
124
125         if (alertCursor == null || alertCursor.getCount() == 0) {
126             if (alertCursor != null) {
127                 alertCursor.close();
128             }
129
130             if (DEBUG) Log.d(TAG, "No fired or scheduled alerts");
131             NotificationManager nm =
132                 (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
133             nm.cancel(0);
134             return false;
135         }
136
137         if (DEBUG) {
138             Log.d(TAG, "alert count:" + alertCursor.getCount());
139         }
140
141         String notificationEventName = null;
142         String notificationEventLocation = null;
143         long notificationEventBegin = 0;
144         int notificationEventStatus = 0;
145         HashMap<Long, Long> eventIds = new HashMap<Long, Long>();
146         int numReminders = 0;
147         int numFired = 0;
148         try {
149             while (alertCursor.moveToNext()) {
150                 final long alertId = alertCursor.getLong(ALERT_INDEX_ID);
151                 final long eventId = alertCursor.getLong(ALERT_INDEX_EVENT_ID);
152                 final int minutes = alertCursor.getInt(ALERT_INDEX_MINUTES);
153                 final String eventName = alertCursor.getString(ALERT_INDEX_TITLE);
154                 final String location = alertCursor.getString(ALERT_INDEX_EVENT_LOCATION);
155                 final boolean allDay = alertCursor.getInt(ALERT_INDEX_ALL_DAY) != 0;
156                 final int status = alertCursor.getInt(ALERT_INDEX_SELF_ATTENDEE_STATUS);
157                 final boolean declined = status == Attendees.ATTENDEE_STATUS_DECLINED;
158                 final long beginTime = alertCursor.getLong(ALERT_INDEX_BEGIN);
159                 final long endTime = alertCursor.getLong(ALERT_INDEX_END);
160                 final Uri alertUri = ContentUris
161                         .withAppendedId(CalendarAlerts.CONTENT_URI, alertId);
162                 final long alarmTime = alertCursor.getLong(ALERT_INDEX_ALARM_TIME);
163                 int state = alertCursor.getInt(ALERT_INDEX_STATE);
164
165                 if (DEBUG) {
166                     Log.d(TAG, "alarmTime:" + alarmTime + " alertId:" + alertId
167                             + " eventId:" + eventId + " state: " + state + " minutes:" + minutes
168                             + " declined:" + declined + " beginTime:" + beginTime
169                             + " endTime:" + endTime);
170                 }
171
172                 ContentValues values = new ContentValues();
173                 int newState = -1;
174
175                 // Uncomment for the behavior of clearing out alerts after the
176                 // events ended. b/1880369
177                 //
178                 // if (endTime < currentTime) {
179                 //     newState = CalendarAlerts.DISMISSED;
180                 // } else
181
182                 // Remove declined events and duplicate alerts for the same event
183                 if (!declined && eventIds.put(eventId, beginTime) == null) {
184                     numReminders++;
185                     if (state == CalendarAlerts.SCHEDULED) {
186                         newState = CalendarAlerts.FIRED;
187                         numFired++;
188
189                         // Record the received time in the CalendarAlerts table.
190                         // This is useful for finding bugs that cause alarms to be
191                         // missed or delayed.
192                         values.put(CalendarAlerts.RECEIVED_TIME, currentTime);
193                     }
194                 } else {
195                     newState = CalendarAlerts.DISMISSED;
196                     if (DEBUG) {
197                         if (!declined) Log.d(TAG, "dropping dup alert for event " + eventId);
198                     }
199                 }
200
201                 // Update row if state changed
202                 if (newState != -1) {
203                     values.put(CalendarAlerts.STATE, newState);
204                     state = newState;
205                 }
206
207                 if (state == CalendarAlerts.FIRED) {
208                     // Record the time posting to notification manager.
209                     // This is used for debugging missed alarms.
210                     values.put(CalendarAlerts.NOTIFY_TIME, currentTime);
211                 }
212
213                 // Write row to if anything changed
214                 if (values.size() > 0) cr.update(alertUri, values, null, null);
215
216                 if (state != CalendarAlerts.FIRED) {
217                     continue;
218                 }
219
220                 // Pick an Event title for the notification panel by the latest
221                 // alertTime and give prefer accepted events in case of ties.
222                 int newStatus;
223                 switch (status) {
224                     case Attendees.ATTENDEE_STATUS_ACCEPTED:
225                         newStatus = 2;
226                         break;
227                     case Attendees.ATTENDEE_STATUS_TENTATIVE:
228                         newStatus = 1;
229                         break;
230                     default:
231                         newStatus = 0;
232                 }
233
234                 // TODO Prioritize by "primary" calendar
235                 // Assumes alerts are sorted by begin time in reverse
236                 if (notificationEventName == null
237                         || (notificationEventBegin <= beginTime &&
238                                 notificationEventStatus < newStatus)) {
239                     notificationEventName = eventName;
240                     notificationEventLocation = location;
241                     notificationEventBegin = beginTime;
242                     notificationEventStatus = newStatus;
243                 }
244             }
245         } finally {
246             if (alertCursor != null) {
247                 alertCursor.close();
248             }
249         }
250
251         SharedPreferences prefs = CalendarPreferenceActivity.getSharedPreferences(context);
252         String reminderType = prefs.getString(CalendarPreferenceActivity.KEY_ALERTS_TYPE,
253                 CalendarPreferenceActivity.ALERT_TYPE_STATUS_BAR);
254
255         // TODO check for this before adding stuff to the alerts table.
256         if (reminderType.equals(CalendarPreferenceActivity.ALERT_TYPE_OFF)) {
257             if (DEBUG) {
258                 Log.d(TAG, "alert preference is OFF");
259             }
260             return true;
261         }
262
263         postNotification(context, prefs, notificationEventName, notificationEventLocation,
264                 numReminders, numFired == 0 /* quiet update */);
265
266         if (numFired > 0 && reminderType.equals(CalendarPreferenceActivity.ALERT_TYPE_ALERTS)) {
267             Intent alertIntent = new Intent();
268             alertIntent.setClass(context, AlertActivity.class);
269             alertIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
270             context.startActivity(alertIntent);
271         }
272
273         return true;
274     }
275
276     private static void postNotification(Context context, SharedPreferences prefs,
277             String eventName, String location, int numReminders, boolean quietUpdate) {
278         if (DEBUG) {
279             Log.d(TAG, "###### creating new alarm notification, numReminders: " + numReminders
280                     + (quietUpdate ? " QUIET" : " loud"));
281         }
282
283         NotificationManager nm =
284                 (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
285
286         if (numReminders == 0) {
287             nm.cancel(0);
288             return;
289         }
290
291         Notification notification = AlertReceiver.makeNewAlertNotification(context, eventName,
292                 location, numReminders);
293         notification.defaults |= Notification.DEFAULT_LIGHTS;
294
295         // Quietly update notification bar. Nothing new. Maybe something just got deleted.
296         if (!quietUpdate) {
297             // Flash ticker in status bar
298             notification.tickerText = eventName;
299             if (!TextUtils.isEmpty(location)) {
300                 notification.tickerText = eventName + " - " + location;
301             }
302
303             // Generate either a pop-up dialog, status bar notification, or
304             // neither. Pop-up dialog and status bar notification may include a
305             // sound, an alert, or both. A status bar notification also includes
306             // a toast.
307             boolean reminderVibrate =
308                     prefs.getBoolean(CalendarPreferenceActivity.KEY_ALERTS_VIBRATE, false);
309
310             // Possibly generate a vibration
311             if (reminderVibrate) {
312                 notification.defaults |= Notification.DEFAULT_VIBRATE;
313             }
314
315             // Possibly generate a sound. If 'Silent' is chosen, the ringtone
316             // string will be empty.
317             String reminderRingtone = prefs.getString(
318                     CalendarPreferenceActivity.KEY_ALERTS_RINGTONE, null);
319             notification.sound = TextUtils.isEmpty(reminderRingtone) ? null : Uri
320                     .parse(reminderRingtone);
321         }
322
323         nm.notify(0, notification);
324     }
325
326     private void doTimeChanged() {
327         ContentResolver cr = getContentResolver();
328         Object service = getSystemService(Context.ALARM_SERVICE);
329         AlarmManager manager = (AlarmManager) service;
330         CalendarAlerts.rescheduleMissedAlarms(cr, this, manager);
331         updateAlertNotification(this);
332     }
333
334     private final class ServiceHandler extends Handler {
335         public ServiceHandler(Looper looper) {
336             super(looper);
337         }
338
339         @Override
340         public void handleMessage(Message msg) {
341             processMessage(msg);
342             // NOTE: We MUST not call stopSelf() directly, since we need to
343             // make sure the wake lock acquired by AlertReceiver is released.
344             AlertReceiver.finishStartingService(AlertService.this, msg.arg1);
345         }
346     }
347
348     @Override
349     public void onCreate() {
350         HandlerThread thread = new HandlerThread("AlertService",
351                 Process.THREAD_PRIORITY_BACKGROUND);
352         thread.start();
353
354         mServiceLooper = thread.getLooper();
355         mServiceHandler = new ServiceHandler(mServiceLooper);
356     }
357
358     @Override
359     public int onStartCommand(Intent intent, int flags, int startId) {
360         if (intent != null) {
361             Message msg = mServiceHandler.obtainMessage();
362             msg.arg1 = startId;
363             msg.obj = intent.getExtras();
364             mServiceHandler.sendMessage(msg);
365         }
366         return START_REDELIVER_INTENT;
367     }
368
369     @Override
370     public void onDestroy() {
371         mServiceLooper.quit();
372     }
373
374     @Override
375     public IBinder onBind(Intent intent) {
376         return null;
377     }
378 }