OSDN Git Service

b/2224837 Changed the plumbing of how alerts/notifications work.
authorMichael Chan <mchan@android.com>
Thu, 19 Nov 2009 02:37:09 +0000 (18:37 -0800)
committerMichael Chan <mchan@android.com>
Tue, 15 Dec 2009 00:16:35 +0000 (16:16 -0800)
Cut down on the number db queries when process alerts
Fixed a cursor leak
Flash the event name and location in the notification bar
b/2205255 Show the latest event in the notification. Prioritize based on your acceptance response.
b/1544909 Flash green LED for when there's a calendar notifications.
b/2224837 Cleanup alert/notification plumbing in Calendar
b/1735201 Calendar notifications are not updated when locale is changed

Change-Id: I86b6904607b0236fb04719f5782f43674ac6d2bc

AndroidManifest.xml
src/com/android/calendar/AlertActivity.java
src/com/android/calendar/AlertReceiver.java
src/com/android/calendar/AlertService.java
src/com/android/calendar/CalendarView.java

index 064f8ae..fc2fbd2 100644 (file)
@@ -4,16 +4,16 @@
 **
 ** Copyright 2006, 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 
+** 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 
+**     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 
+** 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.
 */
 -->
             android:theme="@style/CalendarTheme" />
         <activity android:name="DayActivity" android:label="@string/day_view"
             android:theme="@style/CalendarTheme" />
-        <activity android:name="AgendaActivity" android:label="@string/agenda_view" 
+        <activity android:name="AgendaActivity" android:label="@string/agenda_view"
             android:theme="@android:style/Theme.Light"
             android:exported="true" />
-        
+
         <activity android:name="EditEvent" android:label="@string/event_edit_title"
             android:theme="@android:style/Theme"
             android:configChanges="orientation|keyboardHidden">
-            
+
             <intent-filter>
                 <action android:name="android.intent.action.EDIT" />
                 <category android:name="android.intent.category.DEFAULT" />
                 <data android:mimeType="vnd.android.cursor.item/event" />
             </intent-filter>
         </activity>
-        
+
         <activity android:name="EventInfoActivity" android:label="@string/event_info_title"
             android:theme="@android:style/Theme.Light"
             android:configChanges="orientation|keyboardHidden">
-            
+
             <intent-filter>
                 <action android:name="android.intent.action.VIEW" />
                 <category android:name="android.intent.category.DEFAULT" />
         <receiver android:name="AlertReceiver">
             <intent-filter>
                 <action android:name="android.intent.action.EVENT_REMINDER" />
-                <data android:mimeType="vnd.android.cursor.item/calendar-alert" />
-            </intent-filter>
-            <intent-filter>
+                <action android:name="android.intent.action.LOCALE_CHANGED" />
                 <action android:name="android.intent.action.BOOT_COMPLETED" />
                 <action android:name="android.intent.action.TIME_SET" />
             </intent-filter>
index 2a041bf..e58c2cb 100644 (file)
@@ -22,7 +22,6 @@ import static android.provider.Calendar.EVENT_END_TIME;
 import android.app.Activity;
 import android.app.AlarmManager;
 import android.app.NotificationManager;
-import android.app.PendingIntent;
 import android.content.AsyncQueryHandler;
 import android.content.ContentResolver;
 import android.content.ContentUris;
@@ -33,7 +32,6 @@ import android.content.res.TypedArray;
 import android.database.Cursor;
 import android.net.Uri;
 import android.os.Bundle;
-import android.provider.Calendar;
 import android.provider.Calendar.CalendarAlerts;
 import android.provider.Calendar.CalendarAlertsColumns;
 import android.provider.Calendar.Events;
@@ -83,6 +81,11 @@ public class AlertActivity extends Activity {
     public static final int INDEX_STATE = 10;
     public static final int INDEX_ALARM_TIME = 11;
 
+    private static final String SELECTION = CalendarAlerts.STATE + "=?";
+    private static final String[] SELECTIONARG = new String[] {
+        Integer.toString(CalendarAlerts.FIRED)
+    };
+
     // We use one notification id for all events so that we don't clutter
     // the notification screen.  It doesn't matter what the id is, as long
     // as it is used consistently everywhere.
@@ -136,22 +139,14 @@ public class AlertActivity extends Activity {
         @Override
         protected void onInsertComplete(int token, Object cookie, Uri uri) {
             if (uri != null) {
-                ContentValues values = (ContentValues) cookie;
-
-                long begin = values.getAsLong(CalendarAlerts.BEGIN);
-                long end = values.getAsLong(CalendarAlerts.END);
-                long alarmTime = values.getAsLong(CalendarAlerts.ALARM_TIME);
-
-                // Set a new alarm to go off after the snooze delay.
-                Intent intent = new Intent(Calendar.EVENT_REMINDER_ACTION);
-                intent.setData(uri);
-                intent.putExtra(Calendar.EVENT_BEGIN_TIME, begin);
-                intent.putExtra(Calendar.EVENT_END_TIME, end);
-
-                PendingIntent sender = PendingIntent.getBroadcast(AlertActivity.this,
-                        0, intent, PendingIntent.FLAG_CANCEL_CURRENT);
-                AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
-                alarmManager.set(AlarmManager.RTC_WAKEUP, alarmTime, sender);
+                Long alarmTime = (Long) cookie;
+
+                if (alarmTime != 0) {
+                    // Set a new alarm to go off after the snooze delay.
+                    AlarmManager alarmManager =
+                            (AlarmManager) getSystemService(Context.ALARM_SERVICE);
+                    CalendarAlerts.scheduleAlarm(AlertActivity.this, alarmManager, alarmTime);
+                }
             }
         }
 
@@ -246,9 +241,8 @@ public class AlertActivity extends Activity {
         // If the cursor is null, start the async handler. If it is not null just requery.
         if (mCursor == null) {
             Uri uri = CalendarAlerts.CONTENT_URI_BY_INSTANCE;
-            String selection = CalendarAlerts.STATE + "=" + CalendarAlerts.FIRED;
-            mQueryHandler.startQuery(0, null, uri, PROJECTION, selection,
-                    null /* selection args */, CalendarAlerts.DEFAULT_SORT_ORDER);
+            mQueryHandler.startQuery(0, null, uri, PROJECTION, SELECTION,
+                    SELECTIONARG, CalendarAlerts.DEFAULT_SORT_ORDER);
         } else {
             mCursor.requery();
         }
@@ -257,7 +251,7 @@ public class AlertActivity extends Activity {
     @Override
     protected void onStop() {
         super.onStop();
-        AlertReceiver.updateAlertNotification(this);
+        AlertService.updateAlertNotification(this);
 
         if (mCursor != null) {
             mCursor.deactivate();
@@ -279,6 +273,8 @@ public class AlertActivity extends Activity {
             NotificationManager nm =
                 (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
             nm.cancel(NOTIFICATION_ID);
+
+            long scheduleAlarmTime = 0;
             mCursor.moveToPosition(-1);
             while (mCursor.moveToNext()) {
                 long eventId = mCursor.getLong(INDEX_EVENT_ID);
@@ -292,7 +288,10 @@ public class AlertActivity extends Activity {
                         makeContentValues(eventId, begin, end, alarmTime, 0 /* minutes */);
 
                 // Create a new alarm entry in the CalendarAlerts table
-                mQueryHandler.startInsert(0, values, CalendarAlerts.CONTENT_URI, values);
+                if (mCursor.isLast()) {
+                    scheduleAlarmTime = alarmTime;
+                }
+                mQueryHandler.startInsert(0, scheduleAlarmTime, CalendarAlerts.CONTENT_URI, values);
             }
 
             dismissFiredAlarms();
index 7943a9e..64d3595 100644 (file)
 package com.android.calendar;
 
 import android.app.Notification;
-import android.app.NotificationManager;
 import android.app.PendingIntent;
 import android.app.Service;
 import android.content.BroadcastReceiver;
-import android.content.ContentResolver;
-import android.content.ContentValues;
 import android.content.Context;
 import android.content.Intent;
-import android.content.SharedPreferences;
 import android.content.res.Resources;
-import android.database.Cursor;
 import android.net.Uri;
 import android.os.PowerManager;
-import android.preference.PreferenceManager;
-import android.provider.Calendar.CalendarAlerts;
+import android.util.Log;
 
 /**
  * Receives android.intent.action.EVENT_REMINDER intents and handles
- * event reminders.  The intent URI specifies an alert id in the 
+ * event reminders.  The intent URI specifies an alert id in the
  * CalendarAlerts database table.  This class also receives the
  * BOOT_COMPLETED intent so that it can add a status bar notification
  * if there are Calendar event alarms that have not been dismissed.
@@ -44,22 +38,22 @@ import android.provider.Calendar.CalendarAlerts;
  * the AlertService class.
  */
 public class AlertReceiver extends BroadcastReceiver {
-    private static final String[] ALERT_PROJECTION = new String[] { 
-        CalendarAlerts.TITLE,           // 0
-        CalendarAlerts.EVENT_LOCATION,  // 1
-    };
-    private static final int ALERT_INDEX_TITLE = 0;
-    private static final int ALERT_INDEX_EVENT_LOCATION = 1;
-    
+    private static final String TAG = "AlertReceiver";
+
     private static final String DELETE_ACTION = "delete";
-    
+
     static final Object mStartingServiceSync = new Object();
     static PowerManager.WakeLock mStartingService;
-    
+
     @Override
     public void onReceive(Context context, Intent intent) {
+        if (AlertService.DEBUG) {
+            Log.e(TAG, "==============================================================");
+            Log.d(TAG, "onReceive: a=" + intent.getAction() + " " + intent.toString());
+        }
+
         if (DELETE_ACTION.equals(intent.getAction())) {
-            
+
             /* The user has clicked the "Clear All Notifications"
              * buttons so dismiss all Calendar alerts.
              */
@@ -72,7 +66,7 @@ public class AlertReceiver extends BroadcastReceiver {
             i.putExtras(intent);
             i.putExtra("action", intent.getAction());
             Uri uri = intent.getData();
-            
+
             // This intent might be a BOOT_COMPLETED so it might not have a Uri.
             if (uri != null) {
                 i.putExtra("uri", uri.toString());
@@ -98,7 +92,7 @@ public class AlertReceiver extends BroadcastReceiver {
             context.startService(intent);
         }
     }
-    
+
     /**
      * Called back by the service when it has finished processing notifications,
      * releasing the wake lock if the service is now stopping.
@@ -112,70 +106,25 @@ public class AlertReceiver extends BroadcastReceiver {
             }
         }
     }
-    
-    public static void updateAlertNotification(Context context) {
-        // This can be called regularly to synchronize the alert notification
-        // with the contents of the CalendarAlerts table.
-        
-        ContentResolver cr = context.getContentResolver();
-        
-        if (cr == null) {
-            return;
-        }
-        
-        String selection = CalendarAlerts.STATE + "=" + CalendarAlerts.FIRED;
-        Cursor alertCursor = CalendarAlerts.query(cr, ALERT_PROJECTION, selection, null);
-        
-        NotificationManager nm = 
-                (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
-        
-        if (alertCursor == null) {
-            nm.cancel(AlertActivity.NOTIFICATION_ID);
-            return;
-        }
 
-        if (!alertCursor.moveToFirst()) {
-            alertCursor.close();
-            nm.cancel(AlertActivity.NOTIFICATION_ID);
-            return;
-        }
-
-        // Check the settings to see if alerts are disabled
-        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
-        String reminderType = prefs.getString(CalendarPreferenceActivity.KEY_ALERTS_TYPE,
-                CalendarPreferenceActivity.ALERT_TYPE_STATUS_BAR);
-        if (reminderType.equals(CalendarPreferenceActivity.ALERT_TYPE_OFF)) {
-            return;
-        }
-
-        String title = alertCursor.getString(ALERT_INDEX_TITLE);
-        String location = alertCursor.getString(ALERT_INDEX_EVENT_LOCATION);
-        
-        Notification notification = AlertReceiver.makeNewAlertNotification(context, title, 
-                location, alertCursor.getCount());
-        alertCursor.close();
-        
-        nm.notify(0, notification);
-    }
-    
-    public static Notification makeNewAlertNotification(Context context, String title, 
+    public static Notification makeNewAlertNotification(Context context, String title,
             String location, int numReminders) {
         Resources res = context.getResources();
-        
+
         // Create an intent triggered by clicking on the status icon.
         Intent clickIntent = new Intent();
         clickIntent.setClass(context, AlertActivity.class);
         clickIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
-        
+
         // Create an intent triggered by clicking on the "Clear All Notifications" button
         Intent deleteIntent = new Intent();
         deleteIntent.setClass(context, AlertReceiver.class);
         deleteIntent.setAction(DELETE_ACTION);
-        
+
         if (title == null || title.length() == 0) {
             title = res.getString(R.string.no_title_label);
         }
-        
+
         String helperString;
         if (numReminders > 1) {
             String format;
@@ -188,7 +137,7 @@ public class AlertReceiver extends BroadcastReceiver {
         } else {
             helperString = location;
         }
-        
+
         Notification notification = new Notification(
                 R.drawable.stat_notify_calendar,
                 null,
@@ -198,7 +147,7 @@ public class AlertReceiver extends BroadcastReceiver {
                 helperString,
                 PendingIntent.getActivity(context, 0, clickIntent, 0));
         notification.deleteIntent = PendingIntent.getBroadcast(context, 0, deleteIntent, 0);
-        
+
         return notification;
     }
 }
index 29170a9..f06ba50 100644 (file)
@@ -21,6 +21,7 @@ import android.app.Notification;
 import android.app.NotificationManager;
 import android.app.Service;
 import android.content.ContentResolver;
+import android.content.ContentUris;
 import android.content.ContentValues;
 import android.content.Context;
 import android.content.Intent;
@@ -35,21 +36,18 @@ 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;
+
+import java.util.HashMap;
 
 /**
  * This service is used to handle calendar event reminders.
  */
 public class AlertService extends Service {
+    static final boolean DEBUG = true;
     private static final String TAG = "AlertService";
 
     private volatile Looper mServiceLooper;
@@ -66,11 +64,7 @@ public class AlertService extends Service {
         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
+        CalendarAlerts.END,                     // 10
     };
 
     private static final int ALERT_INDEX_ID = 0;
@@ -83,31 +77,16 @@ public class AlertService extends Service {
     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 static final int ALERT_INDEX_END = 10;
 
-    private String[] INSTANCE_PROJECTION = { Instances.BEGIN, Instances.END };
-    private static final int INSTANCES_INDEX_BEGIN = 0;
-    private static final int INSTANCES_INDEX_END = 1;
+    private static final String ACTIVE_ALERTS_SELECTION = "(" + CalendarAlerts.STATE + "=? OR "
+            + CalendarAlerts.STATE + "=?) AND " + CalendarAlerts.ALARM_TIME + "<=";
 
-    // We just need a simple projection that returns any column
-    private static final String[] REMINDER_PROJECTION_SMALL = new String[] {
-        Reminders._ID,                     // 0
+    private static final String[] ACTIVE_ALERTS_SELECTION_ARGS = new String[] {
+            Integer.toString(CalendarAlerts.FIRED), Integer.toString(CalendarAlerts.SCHEDULED)
     };
 
-    private final boolean alarmsFiredRecently(ContentResolver cr) {
-        String selection = CalendarAlerts.RECEIVED_TIME + ">="
-                + (System.currentTimeMillis() - 10000);
-        String[] projection = new String[] { CalendarAlerts.ALARM_TIME };
-        Cursor cursor = cr.query(CalendarAlerts.CONTENT_URI, projection, selection, null, null);
-
-        boolean recentAlarms = false;
-        if (cursor != null) {
-            if (cursor.moveToFirst() && cursor.getCount() > 0) {
-                recentAlarms = true;
-            }
-            cursor.close();
-        }
-        return recentAlarms;
-    }
+    private static final String ACTIVE_ALERTS_SORT = "begin DESC, end DESC";
 
     @SuppressWarnings("deprecation")
     void processMessage(Message msg) {
@@ -116,316 +95,229 @@ public class AlertService extends Service {
         // On reboot, update the notification bar with the contents of the
         // CalendarAlerts table.
         String action = bundle.getString("action");
+        if (DEBUG) {
+            Log.d(TAG, "" + bundle.getLong(android.provider.Calendar.CalendarAlerts.ALARM_TIME)
+                    + " Action = " + 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 (!action.equals(android.provider.Calendar.EVENT_REMINDER_ACTION)
+                && !action.equals(Intent.ACTION_LOCALE_CHANGED)) {
+            Log.w(TAG, "Invalid action: " + action);
+            return;
         }
 
-        ContentResolver cr = getContentResolver();
-        boolean alarmsFiredRecently = alarmsFiredRecently(cr);
-
-        if (alertUri != null) {
-            if (!Calendar.AUTHORITY.equals(alertUri.getAuthority())) {
-                Log.w(TAG, "Invalid AUTHORITY uri: " + alertUri);
-                return;
-            }
+        updateAlertNotification(this);
+    }
 
-            // 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());
-            cr.update(alertUri, values, null /* where */, null /* args */);
-        }
+    static boolean updateAlertNotification(Context context) {
+        ContentResolver cr = context.getContentResolver();
+        final long currentTime = System.currentTimeMillis();
 
-        Cursor alertCursor = cr.query(alertUri, ALERT_PROJECTION,
-                null /* selection */, null, null /* sort order */);
+        Cursor alertCursor = CalendarAlerts.query(cr, ALERT_PROJECTION, ACTIVE_ALERTS_SELECTION
+                + currentTime, ACTIVE_ALERTS_SELECTION_ARGS, ACTIVE_ALERTS_SORT);
 
-        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.getCount() == 0) {
             if (alertCursor != null) {
                 alertCursor.close();
             }
+
+            if (DEBUG) Log.d(TAG, "No fired or scheduled alerts");
+            NotificationManager nm =
+                (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
+            nm.cancel(0);
+            return false;
         }
 
-        // 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;
+        if (DEBUG) {
+            Log.d(TAG, "alert count:" + alertCursor.getCount());
         }
 
-        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;
+        String notificationEventName = null;
+        String notificationEventLocation = null;
+        long notificationEventBegin = 0;
+        int notificationEventStatus = 0;
+        HashMap<Long, Long> eventIds = new HashMap<Long, Long>();
+        int numReminders = 0;
+        int numFired = 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");
+            while (alertCursor.moveToNext()) {
+                final long alertId = alertCursor.getLong(ALERT_INDEX_ID);
+                final long eventId = alertCursor.getLong(ALERT_INDEX_EVENT_ID);
+                final int minutes = alertCursor.getInt(ALERT_INDEX_MINUTES);
+                final String eventName = alertCursor.getString(ALERT_INDEX_TITLE);
+                final String location = alertCursor.getString(ALERT_INDEX_EVENT_LOCATION);
+                final boolean allDay = alertCursor.getInt(ALERT_INDEX_ALL_DAY) != 0;
+                final int status = alertCursor.getInt(ALERT_INDEX_SELF_ATTENDEE_STATUS);
+                final boolean declined = status == Attendees.ATTENDEE_STATUS_DECLINED;
+                final long beginTime = alertCursor.getLong(ALERT_INDEX_BEGIN);
+                final long endTime = alertCursor.getLong(ALERT_INDEX_END);
+                final Uri alertUri = ContentUris
+                        .withAppendedId(CalendarAlerts.CONTENT_URI, alertId);
+                final long alarmTime = alertCursor.getLong(ALERT_INDEX_ALARM_TIME);
+                int state = alertCursor.getInt(ALERT_INDEX_STATE);
+
+                if (DEBUG) {
+                    Log.d(TAG, "alarmTime:" + alarmTime + " alertId:" + alertId
+                            + " eventId:" + eventId + " state: " + state + " minutes:" + minutes
+                            + " declined:" + declined + " beginTime:" + beginTime
+                            + " endTime:" + endTime);
                 }
-                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");
+                ContentValues values = new ContentValues();
+                int newState = -1;
+
+                // Uncomment for the behavior of clearing out alerts after the
+                // events ended. b/1880369
+                //
+                // if (endTime < currentTime) {
+                //     newState = CalendarAlerts.DISMISSED;
+                // } else
+
+                // Remove declined events and duplicate alerts for the same event
+                if (!declined && eventIds.put(eventId, beginTime) == null) {
+                    numReminders++;
+                    if (state == CalendarAlerts.SCHEDULED) {
+                        newState = CalendarAlerts.FIRED;
+                        numFired++;
+
+                        // Record the received time in the CalendarAlerts table.
+                        // This is useful for finding bugs that cause alarms to be
+                        // missed or delayed.
+                        values.put(CalendarAlerts.RECEIVED_TIME, currentTime);
+                    }
+                } else {
+                    newState = CalendarAlerts.DISMISSED;
+                    if (DEBUG) {
+                        if (!declined) Log.d(TAG, "dropping dup alert for event " + eventId);
                     }
-                    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");
+                // Update row if state changed
+                if (newState != -1) {
+                    values.put(CalendarAlerts.STATE, newState);
+                    state = newState;
                 }
-                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();
+                if (state == CalendarAlerts.FIRED) {
+                    // Record the time posting to notification manager.
+                    // This is used for debugging missed alarms.
+                    values.put(CalendarAlerts.NOTIFY_TIME, currentTime);
                 }
-            }
-        }
 
-        // 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);
+                // Write row to if anything changed
+                if (values.size() > 0) cr.update(alertUri, values, null, null);
 
-        if (alertCursor == null || alertCursor.getCount() == 0) {
-            if (Log.isLoggable(TAG, Log.DEBUG)) {
-                Log.d(TAG, "no fired alarms found");
-            }
-            return;
-        }
+                if (state != CalendarAlerts.FIRED) {
+                    continue;
+                }
 
-        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;
+                // Pick an Event title for the notification panel by the latest
+                // alertTime and give prefer accepted events in case of ties.
+                int newStatus;
+                switch (status) {
+                    case Attendees.ATTENDEE_STATUS_ACCEPTED:
+                        newStatus = 2;
+                        break;
+                    case Attendees.ATTENDEE_STATUS_TENTATIVE:
+                        newStatus = 1;
+                        break;
+                    default:
+                        newStatus = 0;
+                }
+
+                // TODO Prioritize by "primary" calendar
+                // Assumes alerts are sorted by begin time in reverse
+                if (notificationEventName == null
+                        || (notificationEventBegin <= beginTime &&
+                                notificationEventStatus < newStatus)) {
+                    notificationEventName = eventName;
+                    notificationEventLocation = location;
+                    notificationEventBegin = beginTime;
+                    notificationEventStatus = newStatus;
                 }
             }
         } finally {
-            alertCursor.close();
-        }
-
-        if (Log.isLoggable(TAG, Log.DEBUG)) {
-            Log.d(TAG, "creating new alarm notification, numReminders: " + numReminders);
+            if (alertCursor != null) {
+                alertCursor.close();
+            }
         }
-        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);
+        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
         String reminderType = prefs.getString(CalendarPreferenceActivity.KEY_ALERTS_TYPE,
                 CalendarPreferenceActivity.ALERT_TYPE_STATUS_BAR);
 
+        // TODO check for this before adding stuff to the alerts table.
         if (reminderType.equals(CalendarPreferenceActivity.ALERT_TYPE_OFF)) {
-            if (Log.isLoggable(TAG, Log.DEBUG)) {
+            if (DEBUG) {
                 Log.d(TAG, "alert preference is OFF");
             }
-            return;
+            return true;
+        }
+
+        postNotification(context, prefs, notificationEventName, notificationEventLocation,
+                numReminders, numFired == 0 /* quiet update */);
+
+        if (numFired > 0 && reminderType.equals(CalendarPreferenceActivity.ALERT_TYPE_ALERTS)) {
+            Intent alertIntent = new Intent();
+            alertIntent.setClass(context, AlertActivity.class);
+            alertIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+            context.startActivity(alertIntent);
+        }
+
+        return true;
+    }
+
+    private static void postNotification(Context context, SharedPreferences prefs,
+            String eventName, String location, int numReminders, boolean quietUpdate) {
+        if (DEBUG) {
+            Log.d(TAG, "###### creating new alarm notification, numReminders: " + numReminders
+                    + (quietUpdate ? " QUIET" : " loud"));
         }
 
         NotificationManager nm =
-            (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
-        boolean reminderVibrate =
-                prefs.getBoolean(CalendarPreferenceActivity.KEY_ALERTS_VIBRATE, false);
+                (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
 
-        // Possibly generate a vibration
-        if (reminderVibrate) {
-            notification.defaults |= Notification.DEFAULT_VIBRATE;
+        if (numReminders == 0) {
+            nm.cancel(0);
+            return;
         }
 
-        // Temp fix. If we sounded an notification recently, be quiet so the
-        // audio won't overlap.
+        Notification notification = AlertReceiver.makeNewAlertNotification(context, eventName,
+                location, numReminders);
+        notification.defaults |= Notification.DEFAULT_LIGHTS;
+
+        // Quietly update notification bar. Nothing new. Maybe something just got deleted.
+        if (!quietUpdate) {
+            // Flash ticker in status bar
+            notification.tickerText = eventName;
+            if (!TextUtils.isEmpty(location)) {
+                notification.tickerText = eventName + " - " + location;
+            }
+
+            // 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.
+            boolean reminderVibrate =
+                    prefs.getBoolean(CalendarPreferenceActivity.KEY_ALERTS_VIBRATE, false);
+
+            // Possibly generate a vibration
+            if (reminderVibrate) {
+                notification.defaults |= Notification.DEFAULT_VIBRATE;
+            }
 
-        // TODO Long term fix: CalendarProvider currently setup an alarm with
-        // AlarmManager for each event notification. So AlertService can post
-        // multiple notifications back to back if there are multiple alarms that
-        // fire at the same time. Instead of doing that, CalendarProvider should
-        // setup one alarm for each wake up time. AlertService can query for
-        // alerts table and update notification manager only once.
-        if (!alarmsFiredRecently) {
             // Possibly generate a sound. If 'Silent' is chosen, the ringtone
             // string will be empty.
             String reminderRingtone = prefs.getString(
                     CalendarPreferenceActivity.KEY_ALERTS_RINGTONE, null);
             notification.sound = TextUtils.isEmpty(reminderRingtone) ? null : Uri
                     .parse(reminderRingtone);
-        } else {
-            notification.sound = null;
-        }
-
-        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);
@@ -436,7 +328,7 @@ public class AlertService extends Service {
         Object service = getSystemService(Context.ALARM_SERVICE);
         AlarmManager manager = (AlarmManager) service;
         CalendarAlerts.rescheduleMissedAlarms(cr, this, manager);
-        AlertReceiver.updateAlertNotification(this);
+        updateAlertNotification(this);
     }
 
     private final class ServiceHandler extends Handler {
@@ -451,7 +343,7 @@ public class AlertService extends Service {
             // make sure the wake lock acquired by AlertReceiver is released.
             AlertReceiver.finishStartingService(AlertService.this, msg.arg1);
         }
-    };
+    }
 
     @Override
     public void onCreate() {
index 6716edf..c8ece30 100644 (file)
@@ -115,7 +115,7 @@ public class CalendarView extends View
     }
 
     private DayHeader[] dayHeaders = new DayHeader[32];
-    
+
     // Make this visible within the package for more informative debugging
     Time mBaseDate;
 
@@ -186,10 +186,10 @@ public class CalendarView extends View
     private static int MAX_ALLDAY_HEIGHT = 72;
     private static int ALLDAY_TOP_MARGIN = 3;
     private static int MAX_ALLDAY_EVENT_HEIGHT = 18;
-    
+
     /* The extra space to leave above the text in all-day events */
     private static final int ALL_DAY_TEXT_TOP_MARGIN = 0;
-    
+
     /* The extra space to leave above the text in normal events */
     private static final int NORMAL_TEXT_TOP_MARGIN = 2;
 
@@ -329,7 +329,7 @@ public class CalendarView extends View
                 MIN_EVENT_HEIGHT *= mScale;
 
                 HORIZONTAL_SCROLL_THRESHOLD *= mScale;
+
                 SMALL_ROUND_RADIUS *= mScale;
             }
         }
@@ -765,7 +765,7 @@ public class CalendarView extends View
         view.mFirstHour = mFirstHour;
         view.mFirstHourOffset = mFirstHourOffset;
         view.remeasure(getWidth(), getHeight());
-        
+
         view.mSelectedEvent = null;
         view.mPrevSelectedEvent = null;
         view.mStartDay = mStartDay;
@@ -2118,7 +2118,7 @@ public class CalendarView extends View
     private RectF drawEventRect(Event event, Canvas canvas, Paint p, Paint eventTextPaint) {
 
         int color = event.color;
-        
+
         // Fade visible boxes if event was declined.
         boolean declined = (event.selfAttendeeStatus == Attendees.ATTENDEE_STATUS_DECLINED);
         if (declined) {
@@ -2130,7 +2130,7 @@ public class CalendarView extends View
             color = ((red >> 1) << 16) | ((green >> 1) << 8) | (blue >> 1);
             color += 0x7F7F7F + alpha;
         }
-        
+
         // If this event is selected, then use the selection color
         if (mSelectedEvent == event) {
             if (mSelectionMode == SELECTION_PRESSED) {
@@ -2177,7 +2177,7 @@ public class CalendarView extends View
 
         rf.left += 2;
         rf.right -= 2;
-        
+
         return rf;
     }
 
@@ -2427,7 +2427,7 @@ public class CalendarView extends View
                     mPreviousDirection = direction;
                 }
             }
-            
+
             // If we have moved at least the HORIZONTAL_SCROLL_THRESHOLD,
             // then change the title to the new day (or week), but only
             // if we haven't already changed the title.
@@ -2587,13 +2587,13 @@ public class CalendarView extends View
             invalidate();
         }
 
-        final long startMillis = getSelectedTimeInMillis();         
+        final long startMillis = getSelectedTimeInMillis();
         int flags = DateUtils.FORMAT_SHOW_TIME
                 | DateUtils.FORMAT_CAP_NOON_MIDNIGHT
                 | DateUtils.FORMAT_SHOW_WEEKDAY;
         final String title = DateUtils.formatDateTime(mParentActivity, startMillis, flags);
         menu.setHeaderTitle(title);
-        
+
         int numSelectedEvents = mSelectedEvents.size();
         if (mNumDays == 1) {
             // Day view.
@@ -2630,7 +2630,7 @@ public class CalendarView extends View
             }
         } else {
             // Week view.
-            
+
             // If there is a selected event, then allow it to be viewed and
             // edited.
             if (numSelectedEvents >= 1) {
@@ -2765,9 +2765,16 @@ public class CalendarView extends View
                 null /* selection */,
                 null /* selectionArgs */,
                 null /* sort */);
-        if ((cursor == null) || (cursor.getCount() == 0)) {
+
+        if (cursor == null) {
             return false;
         }
+
+        if (cursor.getCount() == 0) {
+            cursor.close();
+            return false;
+        }
+
         cursor.moveToFirst();
         long calId = cursor.getLong(0);
         cursor.deactivate();
@@ -2783,7 +2790,7 @@ public class CalendarView extends View
             calendarOwnerAccount = cursor.getString(CALENDARS_INDEX_OWNER_ACCOUNT);
             cursor.close();
         }
-        
+
         if (visibility < Calendars.CONTRIBUTOR_ACCESS) {
             return false;
         }
@@ -3060,7 +3067,7 @@ public class CalendarView extends View
         if (handler != null) {
             handler.removeCallbacks(mDismissPopup);
         }
-        
+
         // Turn off redraw
         mRemeasure = false;
         mRedrawScreen = false;