summaryrefslogtreecommitdiffstats
path: root/src/com/android/calendar/AlertService.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/com/android/calendar/AlertService.java')
-rw-r--r--src/com/android/calendar/AlertService.java391
1 files changed, 391 insertions, 0 deletions
diff --git a/src/com/android/calendar/AlertService.java b/src/com/android/calendar/AlertService.java
new file mode 100644
index 00000000..13f808bf
--- /dev/null
+++ b/src/com/android/calendar/AlertService.java
@@ -0,0 +1,391 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.calendar;
+
+import android.app.AlarmManager;
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.Service;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.Message;
+import android.os.Process;
+import android.pim.DateUtils;
+import android.preference.PreferenceManager;
+import android.provider.Calendar;
+import android.provider.Calendar.Attendees;
+import android.provider.Calendar.CalendarAlerts;
+import android.provider.Calendar.Instances;
+import android.provider.Calendar.Reminders;
+import android.text.TextUtils;
+import android.util.Config;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+
+/**
+ * This service is used to handle calendar event reminders.
+ */
+public class AlertService extends Service {
+ private static final String TAG = "AlertService";
+ private static final boolean localLOGV = false || Config.LOGV;
+
+ private volatile Looper mServiceLooper;
+ private volatile ServiceHandler mServiceHandler;
+
+ private static final String[] ALERT_PROJECTION = new String[] {
+ CalendarAlerts._ID, // 0
+ CalendarAlerts.EVENT_ID, // 1
+ CalendarAlerts.STATE, // 2
+ CalendarAlerts.TITLE, // 3
+ CalendarAlerts.EVENT_LOCATION, // 4
+ CalendarAlerts.SELF_ATTENDEE_STATUS, // 5
+ CalendarAlerts.ALL_DAY, // 6
+ CalendarAlerts.ALARM_TIME, // 7
+ CalendarAlerts.MINUTES, // 8
+ CalendarAlerts.BEGIN, // 9
+ };
+
+ // We just need a simple projection that returns any column
+ private static final String[] ALERT_PROJECTION_SMALL = new String[] {
+ CalendarAlerts._ID, // 0
+ };
+
+ private static final int ALERT_INDEX_ID = 0;
+ private static final int ALERT_INDEX_EVENT_ID = 1;
+ private static final int ALERT_INDEX_STATE = 2;
+ private static final int ALERT_INDEX_TITLE = 3;
+ private static final int ALERT_INDEX_EVENT_LOCATION = 4;
+ private static final int ALERT_INDEX_SELF_ATTENDEE_STATUS = 5;
+ private static final int ALERT_INDEX_ALL_DAY = 6;
+ private static final int ALERT_INDEX_ALARM_TIME = 7;
+ private static final int ALERT_INDEX_MINUTES = 8;
+ private static final int ALERT_INDEX_BEGIN = 9;
+
+ private String[] INSTANCE_PROJECTION = { Instances.BEGIN, Instances.END };
+ private static final int INSTANCES_INDEX_BEGIN = 0;
+ private static final int INSTANCES_INDEX_END = 1;
+
+ // We just need a simple projection that returns any column
+ private static final String[] REMINDER_PROJECTION_SMALL = new String[] {
+ Reminders._ID, // 0
+ };
+
+ private void processMessage(Message msg) {
+ Bundle bundle = (Bundle) msg.obj;
+
+ // On reboot, update the notification bar with the contents of the
+ // CalendarAlerts table.
+ String action = bundle.getString("action");
+ if (action.equals(Intent.ACTION_BOOT_COMPLETED)
+ || action.equals(Intent.ACTION_TIME_CHANGED)) {
+ doTimeChanged();
+ return;
+ }
+
+ // The Uri specifies an entry in the CalendarAlerts table
+ Uri alertUri = Uri.parse(bundle.getString("uri"));
+ if (localLOGV) Log.v(TAG, "uri: " + alertUri);
+
+ ContentResolver cr = getContentResolver();
+ Cursor alertCursor = cr.query(alertUri, ALERT_PROJECTION,
+ null /* selection */, null, null /* sort order */);
+
+ long alertId, eventId, instanceId, alarmTime;
+ int minutes;
+ String eventName;
+ String location;
+ boolean allDay;
+ boolean declined = false;
+ try {
+ if (alertCursor == null || !alertCursor.moveToFirst()) {
+ // This can happen if the event was deleted.
+ if (localLOGV) Log.v(TAG, "alert not found");
+ return;
+ }
+ alertId = alertCursor.getLong(ALERT_INDEX_ID);
+ eventId = alertCursor.getLong(ALERT_INDEX_EVENT_ID);
+ minutes = alertCursor.getInt(ALERT_INDEX_MINUTES);
+ eventName = alertCursor.getString(ALERT_INDEX_TITLE);
+ location = alertCursor.getString(ALERT_INDEX_EVENT_LOCATION);
+ allDay = alertCursor.getInt(ALERT_INDEX_ALL_DAY) != 0;
+ alarmTime = alertCursor.getLong(ALERT_INDEX_ALARM_TIME);
+ declined = alertCursor.getInt(ALERT_INDEX_SELF_ATTENDEE_STATUS) ==
+ Attendees.ATTENDEE_STATUS_DECLINED;
+
+ // If the event was declined, then mark the alarm DISMISSED,
+ // otherwise, mark the alarm FIRED.
+ int newState = CalendarAlerts.FIRED;
+ if (declined) {
+ newState = CalendarAlerts.DISMISSED;
+ }
+ alertCursor.updateInt(ALERT_INDEX_STATE, newState);
+ alertCursor.commitUpdates();
+ } finally {
+ if (alertCursor != null) {
+ alertCursor.close();
+ }
+ }
+
+ // Do not show an alert if the event was declined
+ if (declined) {
+ if (localLOGV) Log.v(TAG, "event declined, alert cancelled");
+ return;
+ }
+
+ long beginTime = bundle.getLong(Calendar.EVENT_BEGIN_TIME, 0);
+ long endTime = bundle.getLong(Calendar.EVENT_END_TIME, 0);
+
+ // Check if this alarm is still valid. The time of the event may
+ // have been changed, or the reminder may have been changed since
+ // this alarm was set. First, search for an instance in the Instances
+ // that has the same event id and the same begin and end time.
+ // Then check for a reminder in the Reminders table to ensure that
+ // the reminder minutes is consistent with this alarm.
+ String selection = Instances.EVENT_ID + "=" + eventId;
+ Cursor instanceCursor = Instances.query(cr, INSTANCE_PROJECTION,
+ beginTime, endTime, selection, Instances.DEFAULT_SORT_ORDER);
+ long instanceBegin = 0, instanceEnd = 0;
+ try {
+ if (instanceCursor == null || !instanceCursor.moveToFirst()) {
+ // Delete this alarm from the CalendarAlerts table
+ cr.delete(alertUri, null /* selection */, null /* selection args */);
+ if (localLOGV) Log.v(TAG, "instance not found, alert cancelled");
+ return;
+ }
+ instanceBegin = instanceCursor.getLong(INSTANCES_INDEX_BEGIN);
+ instanceEnd = instanceCursor.getLong(INSTANCES_INDEX_END);
+ } finally {
+ if (instanceCursor != null) {
+ instanceCursor.close();
+ }
+ }
+
+ // Check that a reminder for this event exists with the same number
+ // of minutes. But snoozed alarms have minutes = 0, so don't do this
+ // check for snoozed alarms.
+ if (minutes > 0) {
+ selection = Reminders.EVENT_ID + "=" + eventId
+ + " AND " + Reminders.MINUTES + "=" + minutes;
+ Cursor reminderCursor = cr.query(Reminders.CONTENT_URI, REMINDER_PROJECTION_SMALL,
+ selection, null /* selection args */, null /* sort order */);
+ try {
+ if (reminderCursor == null || reminderCursor.getCount() == 0) {
+ // Delete this alarm from the CalendarAlerts table
+ cr.delete(alertUri, null /* selection */, null /* selection args */);
+ if (localLOGV) Log.v(TAG, "reminder not found, alert cancelled");
+ return;
+ }
+ } finally {
+ if (reminderCursor != null) {
+ reminderCursor.close();
+ }
+ }
+ }
+
+ // If the event time was changed and the event has already ended,
+ // then don't sound the alarm.
+ if (alarmTime > instanceEnd) {
+ // Delete this alarm from the CalendarAlerts table
+ cr.delete(alertUri, null /* selection */, null /* selection args */);
+ if (localLOGV) Log.v(TAG, "event ended, alert cancelled");
+ return;
+ }
+
+ // If minutes > 0, then this is a normal alarm (not a snoozed alarm)
+ // so check for duplicate alarms. A duplicate alarm can occur when
+ // the start time of an event is changed to an earlier time. The
+ // later alarm (that was first scheduled for the later event time)
+ // should be discarded.
+ long computedAlarmTime = instanceBegin - minutes * DateUtils.MINUTE_IN_MILLIS;
+ if (minutes > 0 && computedAlarmTime != alarmTime) {
+ // If the event time was changed to a later time, then the computed
+ // alarm time is in the future and we shouldn't sound this alarm.
+ if (computedAlarmTime > alarmTime) {
+ // Delete this alarm from the CalendarAlerts table
+ cr.delete(alertUri, null /* selection */, null /* selection args */);
+ if (localLOGV) Log.v(TAG, "event postponed, alert cancelled");
+ return;
+ }
+
+ // Check for another alarm in the CalendarAlerts table that has the
+ // same event id and the same "minutes". This can occur
+ // if the event start time was changed to an earlier time and the
+ // alarm for the later time goes off. To avoid discarding alarms
+ // for repeating events (that have the same event id), we check
+ // that the other alarm fired recently (within an hour of this one).
+ long recently = alarmTime - 60 * DateUtils.MINUTE_IN_MILLIS;
+ selection = CalendarAlerts.EVENT_ID + "=" + eventId
+ + " AND " + CalendarAlerts.TABLE_NAME + "." + CalendarAlerts._ID
+ + "!=" + alertId
+ + " AND " + CalendarAlerts.MINUTES + "=" + minutes
+ + " AND " + CalendarAlerts.ALARM_TIME + ">" + recently
+ + " AND " + CalendarAlerts.ALARM_TIME + "<=" + alarmTime;
+ alertCursor = CalendarAlerts.query(cr, ALERT_PROJECTION_SMALL, selection, null);
+ if (alertCursor != null) {
+ try {
+ if (alertCursor.getCount() > 0) {
+ // Delete this alarm from the CalendarAlerts table
+ cr.delete(alertUri, null /* selection */, null /* selection args */);
+ if (localLOGV) Log.v(TAG, "duplicate alarm, alert cancelled");
+ return;
+ }
+ } finally {
+ alertCursor.close();
+ }
+ }
+ }
+
+ // Find all the alerts that have fired but have not been dismissed
+ selection = CalendarAlerts.STATE + "=" + CalendarAlerts.FIRED;
+ alertCursor = CalendarAlerts.query(cr, ALERT_PROJECTION, selection, null);
+
+ if (alertCursor == null || alertCursor.getCount() == 0) {
+ if (localLOGV) Log.v(TAG, "no fired alarms found");
+ return;
+ }
+
+ int numReminders = alertCursor.getCount();
+ try {
+ while (alertCursor.moveToNext()) {
+ long otherEventId = alertCursor.getLong(ALERT_INDEX_EVENT_ID);
+ long otherAlertId = alertCursor.getLong(ALERT_INDEX_ID);
+ int otherAlarmState = alertCursor.getInt(ALERT_INDEX_STATE);
+ long otherBeginTime = alertCursor.getLong(ALERT_INDEX_BEGIN);
+ if (otherEventId == eventId && otherAlertId != alertId
+ && otherAlarmState == CalendarAlerts.FIRED
+ && otherBeginTime == beginTime) {
+ // This event already has an alert that fired and has not
+ // been dismissed. This can happen if an event has
+ // multiple reminders. Do not count this as a separate
+ // reminder. But we do want to sound the alarm and vibrate
+ // the phone, if necessary.
+ if (localLOGV) Log.v(TAG, "multiple alarms for this event");
+ numReminders -= 1;
+ }
+ }
+ } finally {
+ alertCursor.close();
+ }
+
+ if (localLOGV) Log.v(TAG, "creating new alarm notification, numReminders: " + numReminders);
+ Notification notification = AlertReceiver.makeNewAlertNotification(this, eventName,
+ location, numReminders);
+
+ // Generate either a pop-up dialog, status bar notification, or
+ // neither. Pop-up dialog and status bar notification may include a
+ // sound, an alert, or both. A status bar notification also includes
+ // a toast.
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
+ String reminderType = prefs.getString(CalendarPreferenceActivity.KEY_ALERTS_TYPE,
+ CalendarPreferenceActivity.ALERT_TYPE_STATUS_BAR);
+
+ if (reminderType.equals(CalendarPreferenceActivity.ALERT_TYPE_OFF)) {
+ if (localLOGV) Log.v(TAG, "alert preference is OFF");
+ return;
+ }
+
+ NotificationManager nm =
+ (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
+ boolean reminderVibrate =
+ prefs.getBoolean(CalendarPreferenceActivity.KEY_ALERTS_VIBRATE, false);
+ String reminderRingtone =
+ prefs.getString(CalendarPreferenceActivity.KEY_ALERTS_RINGTONE, null);
+
+ // Possibly generate a vibration
+ if (reminderVibrate) {
+ notification.defaults |= Notification.DEFAULT_VIBRATE;
+ }
+
+ // Possibly generate a sound. If 'Silent' is chosen, the ringtone string will be empty.
+ notification.sound = TextUtils.isEmpty(reminderRingtone) ? null : Uri
+ .parse(reminderRingtone);
+
+ if (reminderType.equals(CalendarPreferenceActivity.ALERT_TYPE_ALERTS)) {
+ Intent alertIntent = new Intent();
+ alertIntent.setClass(this, AlertActivity.class);
+ alertIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ startActivity(alertIntent);
+ } else {
+ LayoutInflater inflater;
+ inflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ View view = inflater.inflate(R.layout.alert_toast, null);
+
+ AlertAdapter.updateView(this, view, eventName, location, beginTime, endTime, allDay);
+ }
+ nm.notify(0, notification);
+ }
+
+ private void doTimeChanged() {
+ ContentResolver cr = getContentResolver();
+ Object service = getSystemService(Context.ALARM_SERVICE);
+ AlarmManager manager = (AlarmManager) service;
+ CalendarAlerts.rescheduleMissedAlarms(cr, this, manager);
+ AlertReceiver.updateAlertNotification(this);
+ }
+
+ private final class ServiceHandler extends Handler {
+ public ServiceHandler(Looper looper) {
+ super(looper);
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ processMessage(msg);
+ // NOTE: We MUST not call stopSelf() directly, since we need to
+ // make sure the wake lock acquired by AlertReceiver is released.
+ AlertReceiver.finishStartingService(AlertService.this, msg.arg1);
+ }
+ };
+
+ @Override
+ public void onCreate() {
+ HandlerThread thread = new HandlerThread("AlertService",
+ Process.THREAD_PRIORITY_BACKGROUND);
+ thread.start();
+
+ mServiceLooper = thread.getLooper();
+ mServiceHandler = new ServiceHandler(mServiceLooper);
+ }
+
+ @Override
+ public void onStart(Intent intent, int startId) {
+ Message msg = mServiceHandler.obtainMessage();
+ msg.arg1 = startId;
+ msg.obj = intent.getExtras();
+ mServiceHandler.sendMessage(msg);
+ }
+
+ @Override
+ public void onDestroy() {
+ mServiceLooper.quit();
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return null;
+ }
+}