diff options
Diffstat (limited to 'src/com/android/calendar/AlertService.java')
-rw-r--r-- | src/com/android/calendar/AlertService.java | 391 |
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; + } +} |