OSDN Git Service

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