/* * 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.ContentValues; 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.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.text.format.DateUtils; 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 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 }; @SuppressWarnings("deprecation") 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 (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "uri: " + alertUri); } if (alertUri != null) { // Record the received time in the CalendarAlerts table. // This is useful for finding bugs that cause alarms to be // missed or delayed. ContentValues values = new ContentValues(); values.put(CalendarAlerts.RECEIVED_TIME, System.currentTimeMillis()); getContentResolver().update(alertUri, values, null /* where */, null /* args */); } ContentResolver cr = getContentResolver(); Cursor alertCursor = cr.query(alertUri, ALERT_PROJECTION, null /* selection */, null, null /* sort order */); long alertId, eventId, 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 (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(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 (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(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 (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(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 (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(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 (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(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 (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(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 (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(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 (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(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 (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "multiple alarms for this event"); } numReminders -= 1; } } } finally { alertCursor.close(); } if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(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 (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "alert preference is OFF"); } return; } NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); //Gill MARKED //boolean reminderVibrate = // prefs.getBoolean(CalendarPreferenceActivity.KEY_ALERTS_VIBRATE, false); boolean reminderVibrate = 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); } // Record the notify time in the CalendarAlerts table. // This is used for debugging missed alarms. ContentValues values = new ContentValues(); long currentTime = System.currentTimeMillis(); values.put(CalendarAlerts.NOTIFY_TIME, currentTime); cr.update(alertUri, values, null /* where */, null /* args */); // The notification time should be pretty close to the reminder time // that the user set for this event. If the notification is late, then // that's a bug and we should log an error. if (currentTime > alarmTime + DateUtils.MINUTE_IN_MILLIS) { long minutesLate = (currentTime - alarmTime) / DateUtils.MINUTE_IN_MILLIS; int flags = DateUtils.FORMAT_SHOW_YEAR | DateUtils.FORMAT_SHOW_TIME; String alarmTimeStr = DateUtils.formatDateTime(this, alarmTime, flags); String currentTimeStr = DateUtils.formatDateTime(this, currentTime, flags); Log.w(TAG, "Calendar reminder alarm for event id " + eventId + " is " + minutesLate + " minute(s) late;" + " expected alarm at: " + alarmTimeStr + " but got it at: " + currentTimeStr); } 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; } }