1/*
2 * Copyright (C) 2012 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
5 * use this file except in compliance with the License. You may obtain a copy of
6 * the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13 * License for the specific language governing permissions and limitations under
14 * the License.
15 */
16
17package com.android.calendar.alerts;
18
19import static android.app.Notification.PRIORITY_DEFAULT;
20import static android.app.Notification.PRIORITY_HIGH;
21import static android.app.Notification.PRIORITY_MIN;
22
23import android.app.AlarmManager;
24import android.content.SharedPreferences;
25import android.database.MatrixCursor;
26import android.provider.CalendarContract.Attendees;
27import android.provider.CalendarContract.CalendarAlerts;
28import android.test.AndroidTestCase;
29import android.test.suitebuilder.annotation.SmallTest;
30import android.test.suitebuilder.annotation.Smoke;
31import android.text.format.DateUtils;
32import android.text.format.Time;
33
34import com.android.calendar.GeneralPreferences;
35import com.android.calendar.alerts.AlertService.NotificationInfo;
36import com.android.calendar.alerts.AlertService.NotificationWrapper;
37
38import junit.framework.Assert;
39
40import java.util.ArrayList;
41import java.util.Arrays;
42import java.util.Map;
43import java.util.Set;
44
45public class AlertServiceTest extends AndroidTestCase {
46
47    class MockSharedPreferences implements SharedPreferences {
48
49        private Boolean mVibrate;
50        private String mRingtone;
51        private Boolean mPopup;
52
53        // Strict mode will fail if a preference key is queried more than once.
54        private boolean mStrict = false;
55
56        MockSharedPreferences() {
57            this(false);
58        }
59
60        MockSharedPreferences(boolean strict) {
61            super();
62            init();
63            this.mStrict = strict;
64        }
65
66        void init() {
67            mVibrate = true;
68            mRingtone = "/some/cool/ringtone";
69            mPopup = true;
70        }
71
72        @Override
73        public boolean contains(String key) {
74            if (GeneralPreferences.KEY_ALERTS_VIBRATE.equals(key)) {
75                return true;
76            }
77            return false;
78        }
79
80        @Override
81        public boolean getBoolean(String key, boolean defValue) {
82            if (GeneralPreferences.KEY_ALERTS_VIBRATE.equals(key)) {
83                if (mVibrate == null) {
84                    Assert.fail(GeneralPreferences.KEY_ALERTS_VIBRATE
85                            + " fetched more than once.");
86                }
87                boolean val = mVibrate;
88                if (mStrict) {
89                    mVibrate = null;
90                }
91                return val;
92            }
93            if (GeneralPreferences.KEY_ALERTS_POPUP.equals(key)) {
94                if (mPopup == null) {
95                    Assert.fail(GeneralPreferences.KEY_ALERTS_POPUP + " fetched more than once.");
96                }
97                boolean val = mPopup;
98                if (mStrict) {
99                    mPopup = null;
100                }
101                return val;
102            }
103            throw new IllegalArgumentException();
104        }
105
106        @Override
107        public String getString(String key, String defValue) {
108            if (GeneralPreferences.KEY_ALERTS_RINGTONE.equals(key)) {
109                if (mRingtone == null) {
110                    Assert.fail(GeneralPreferences.KEY_ALERTS_RINGTONE
111                            + " fetched more than once.");
112                }
113                String val = mRingtone;
114                if (mStrict) {
115                    mRingtone = null;
116                }
117                return val;
118            }
119            throw new IllegalArgumentException();
120        }
121
122        @Override
123        public Map<String, ?> getAll() {
124            throw new IllegalArgumentException();
125        }
126
127        @Override
128        public Set<String> getStringSet(String key, Set<String> defValues) {
129            throw new IllegalArgumentException();
130        }
131
132        @Override
133        public int getInt(String key, int defValue) {
134            throw new IllegalArgumentException();
135        }
136
137        @Override
138        public long getLong(String key, long defValue) {
139            throw new IllegalArgumentException();
140        }
141
142        @Override
143        public float getFloat(String key, float defValue) {
144            throw new IllegalArgumentException();
145        }
146
147        @Override
148        public Editor edit() {
149            throw new IllegalArgumentException();
150        }
151
152        @Override
153        public void registerOnSharedPreferenceChangeListener(
154                OnSharedPreferenceChangeListener listener) {
155            throw new IllegalArgumentException();
156        }
157
158        @Override
159        public void unregisterOnSharedPreferenceChangeListener(
160                OnSharedPreferenceChangeListener listener) {
161            throw new IllegalArgumentException();
162        }
163
164    }
165
166    // Created these constants so the test cases are shorter
167    public static final int SCHEDULED = CalendarAlerts.STATE_SCHEDULED;
168    public static final int FIRED = CalendarAlerts.STATE_FIRED;
169    public static final int DISMISSED = CalendarAlerts.STATE_DISMISSED;
170
171    public static final int ACCEPTED = Attendees.ATTENDEE_STATUS_ACCEPTED;
172    public static final int DECLINED = Attendees.ATTENDEE_STATUS_DECLINED;
173    public static final int INVITED = Attendees.ATTENDEE_STATUS_INVITED;
174    public static final int TENTATIVE = Attendees.ATTENDEE_STATUS_TENTATIVE;
175
176    class NotificationInstance {
177        int mAlertId;
178        int[] mAlertIdsInDigest;
179        int mPriority;
180
181        public NotificationInstance(int alertId, int priority) {
182            mAlertId = alertId;
183            mPriority = priority;
184        }
185
186        public NotificationInstance(int[] alertIdsInDigest, int priority) {
187            mAlertIdsInDigest = alertIdsInDigest;
188            mPriority = priority;
189        }
190    }
191
192    class Alert {
193        long mEventId;
194        int mAlertStatus;
195        int mResponseStatus;
196        int mAllDay;
197        long mBegin;
198        long mEnd;
199        int mMinute;
200        long mAlarmTime;
201
202        public Alert(long eventId, int alertStatus, int responseStatus, int allDay, long begin,
203                long end, int minute, long alarmTime) {
204            mEventId = eventId;
205            mAlertStatus = alertStatus;
206            mResponseStatus = responseStatus;
207            mAllDay = allDay;
208            mBegin = begin;
209            mEnd = end;
210            mMinute = minute;
211            mAlarmTime = alarmTime;
212        }
213
214    }
215
216    class AlertsTable {
217
218        ArrayList<Alert> mAlerts = new ArrayList<Alert>();
219
220        int addAlertRow(long eventId, int alertStatus, int responseStatus, int allDay, long begin,
221                long end, long alarmTime) {
222            Alert a = new Alert(eventId, alertStatus, responseStatus, allDay, begin, end,
223                    5 /* minute */, alarmTime);
224            int id = mAlerts.size();
225            mAlerts.add(a);
226            return id;
227        }
228
229        public MatrixCursor getAlertCursor() {
230            MatrixCursor alertCursor = new MatrixCursor(AlertService.ALERT_PROJECTION);
231
232            int i = 0;
233            for (Alert a : mAlerts) {
234                Object[] ca = {
235                        i++,
236                        a.mEventId,
237                        a.mAlertStatus,
238                        "Title" + a.mEventId + " " + a.mMinute,
239                        "Loc" + a.mEventId,
240                        a.mResponseStatus,
241                        a.mAllDay,
242                        a.mAlarmTime > 0 ? a.mAlarmTime : a.mBegin - a.mMinute * 60 * 1000,
243                        a.mMinute,
244                        a.mBegin,
245                        a.mEnd,
246                        "Desc: " + a.mAlarmTime
247                };
248                alertCursor.addRow(ca);
249            }
250            return alertCursor;
251        }
252
253    }
254
255    class NotificationTestManager extends NotificationMgr {
256        // Expected notifications
257        NotificationInstance[] mExpectedNotifications;
258        NotificationWrapper[] mActualNotifications;
259        boolean[] mCancelled;
260
261        // CalendarAlerts table
262        private ArrayList<Alert> mAlerts;
263
264        public NotificationTestManager(ArrayList<Alert> alerts, int maxNotifications) {
265            assertEquals(0, AlertUtils.EXPIRED_GROUP_NOTIFICATION_ID);
266            mAlerts = alerts;
267            mExpectedNotifications = new NotificationInstance[maxNotifications + 1];
268            mActualNotifications = new NotificationWrapper[mExpectedNotifications.length];
269            mCancelled = new boolean[mExpectedNotifications.length];
270        }
271
272        public void expectTestNotification(int notificationId, int alertId, int highPriority) {
273            mExpectedNotifications[notificationId] = new NotificationInstance(alertId,
274                    highPriority);
275        }
276
277        public void expectTestNotification(int notificationId, int[] alertIds, int priority) {
278            mExpectedNotifications[notificationId] = new NotificationInstance(alertIds, priority);
279        }
280
281        private <T> boolean nullContents(T[] array) {
282            for (T item : array) {
283                if (item != null) {
284                    return false;
285                }
286            }
287            return true;
288        }
289
290        public void validateNotificationsAndReset() {
291            if (nullContents(mExpectedNotifications)) {
292                return;
293            }
294
295            String debugStr = printActualNotifications();
296            for (int id = 0; id < mActualNotifications.length; id++) {
297                NotificationInstance expected = mExpectedNotifications[id];
298                NotificationWrapper actual = mActualNotifications[id];
299                if (expected == null) {
300                    assertNull("Received unexpected notificationId " + id + debugStr, actual);
301                    assertTrue("NotificationId " + id + " should have been cancelled." + debugStr,
302                            mCancelled[id]);
303                } else {
304                    assertNotNull("Expected notificationId " + id + " but it was not posted."
305                            + debugStr, actual);
306                    assertFalse("NotificationId " + id + " should not have been cancelled."
307                            + debugStr, mCancelled[id]);
308                    assertEquals("Priority not as expected for notification " + id + debugStr,
309                            expected.mPriority, actual.mNotification.priority);
310                    if (expected.mAlertIdsInDigest == null) {
311                        Alert a = mAlerts.get(expected.mAlertId);
312                        assertEquals("Event ID not expected for notification " + id + debugStr,
313                                a.mEventId, actual.mEventId);
314                        assertEquals("Begin time not expected for notification " + id + debugStr,
315                                a.mBegin, actual.mBegin);
316                        assertEquals("End time not expected for notification " + id + debugStr,
317                                a.mEnd, actual.mEnd);
318                    } else {
319                        // Notification should be a digest.
320                        assertNotNull("Posted notification not a digest as expected." + debugStr,
321                                actual.mNw);
322                        assertEquals("Number of notifications in digest not as expected."
323                                + debugStr, expected.mAlertIdsInDigest.length, actual.mNw.size());
324                        for (int i = 0; i < actual.mNw.size(); i++) {
325                            Alert a = mAlerts.get(expected.mAlertIdsInDigest[i]);
326                            assertEquals("Digest item " + i + ": Event ID not as expected"
327                                    + debugStr, a.mEventId, actual.mNw.get(i).mEventId);
328                            assertEquals("Digest item " + i + ": Begin time in digest not expected"
329                                    + debugStr, a.mBegin, actual.mNw.get(i).mBegin);
330                            assertEquals("Digest item " + i + ": End time in digest not expected"
331                                    + debugStr, a.mEnd, actual.mNw.get(i).mEnd);
332                        }
333                    }
334                }
335            }
336
337            Arrays.fill(mCancelled, false);
338            Arrays.fill(mExpectedNotifications, null);
339            Arrays.fill(mActualNotifications, null);
340        }
341
342        private String printActualNotifications() {
343            StringBuilder s = new StringBuilder();
344            s.append("\n\nNotifications actually posted:\n");
345            for (int i = mActualNotifications.length - 1; i >= 0; i--) {
346                NotificationWrapper actual = mActualNotifications[i];
347                if (actual == null) {
348                    continue;
349                }
350                s.append("Notification " + i + " -- ");
351                s.append("priority:" + actual.mNotification.priority);
352                if (actual.mNw == null) {
353                    s.append(", eventId:" +  actual.mEventId);
354                } else {
355                    s.append(", eventIds:{");
356                    for (int digestIndex = 0; digestIndex < actual.mNw.size(); digestIndex++) {
357                        s.append(actual.mNw.get(digestIndex).mEventId + ",");
358                    }
359                    s.append("}");
360                }
361                s.append("\n");
362            }
363            return s.toString();
364        }
365
366        ///////////////////////////////
367        // NotificationMgr methods
368        @Override
369        public void cancel(int id) {
370            assertTrue("id out of bound: " + id, 0 <= id);
371            assertTrue("id out of bound: " + id, id < mCancelled.length);
372            assertNull("id already used", mActualNotifications[id]);
373            assertFalse("id already used", mCancelled[id]);
374            mCancelled[id] = true;
375            assertNull("Unexpected cancel for id " + id, mExpectedNotifications[id]);
376        }
377
378        @Override
379        public void notify(int id, NotificationWrapper nw) {
380            assertTrue("id out of bound: " + id, 0 <= id);
381            assertTrue("id out of bound: " + id, id < mExpectedNotifications.length);
382            assertNull("id already used: " + id, mActualNotifications[id]);
383            mActualNotifications[id] = nw;
384        }
385    }
386
387    // TODO
388    // Catch updates of new state, notify time, and received time
389    // Test ringer, vibrate,
390    // Test intents, action email
391
392    @Smoke
393    @SmallTest
394    public void testGenerateAlerts_none() {
395        MockSharedPreferences prefs = new MockSharedPreferences();
396        AlertsTable at = new AlertsTable();
397        NotificationTestManager ntm = new NotificationTestManager(at.mAlerts,
398                AlertService.MAX_NOTIFICATIONS);
399
400        // Test no alert
401        long currentTime = 1000000;
402        AlertService.generateAlerts(mContext, ntm, new MockAlarmManager(mContext), prefs,
403                at.getAlertCursor(), currentTime, AlertService.MAX_NOTIFICATIONS);
404        ntm.validateNotificationsAndReset();
405    }
406
407    @Smoke
408    @SmallTest
409    public void testGenerateAlerts_single() {
410        MockSharedPreferences prefs = new MockSharedPreferences();
411        MockAlarmManager alarmMgr = new MockAlarmManager(mContext);
412        AlertsTable at = new AlertsTable();
413        NotificationTestManager ntm = new NotificationTestManager(at.mAlerts,
414                AlertService.MAX_NOTIFICATIONS);
415
416        int id = at.addAlertRow(100, SCHEDULED, ACCEPTED, 0 /* all day */, 1300000, 2300000, 0);
417
418        // Test one up coming alert
419        long currentTime = 1000000;
420        ntm.expectTestNotification(1, id, PRIORITY_HIGH);
421
422        AlertService.generateAlerts(mContext, ntm, alarmMgr, prefs, at.getAlertCursor(), currentTime,
423                AlertService.MAX_NOTIFICATIONS);
424        ntm.validateNotificationsAndReset(); // This wipes out notification
425                                             // tests added so far
426
427        // Test half way into an event
428        currentTime = 2300000;
429        ntm.expectTestNotification(AlertUtils.EXPIRED_GROUP_NOTIFICATION_ID, id, PRIORITY_MIN);
430
431        AlertService.generateAlerts(mContext, ntm, alarmMgr, prefs, at.getAlertCursor(), currentTime,
432                AlertService.MAX_NOTIFICATIONS);
433        ntm.validateNotificationsAndReset();
434
435        // Test event ended
436        currentTime = 4300000;
437        ntm.expectTestNotification(AlertUtils.EXPIRED_GROUP_NOTIFICATION_ID, id, PRIORITY_MIN);
438
439        AlertService.generateAlerts(mContext, ntm, alarmMgr, prefs, at.getAlertCursor(), currentTime,
440                AlertService.MAX_NOTIFICATIONS);
441        ntm.validateNotificationsAndReset();
442    }
443
444    @SmallTest
445    public void testGenerateAlerts_multiple() {
446        int maxNotifications = 10;
447        MockSharedPreferences prefs = new MockSharedPreferences();
448        MockAlarmManager alarmMgr = new MockAlarmManager(mContext);
449        AlertsTable at = new AlertsTable();
450        NotificationTestManager ntm = new NotificationTestManager(at.mAlerts, maxNotifications);
451
452        // Current time - 5:00
453        long currentTime = createTimeInMillis(5, 0);
454
455        // Set up future alerts.  The real query implementation sorts by descending start
456        // time so simulate that here with our order of adds to AlertsTable.
457        int id9 = at.addAlertRow(9, SCHEDULED, ACCEPTED, 0, createTimeInMillis(9, 0),
458                createTimeInMillis(10, 0), 0);
459        int id8 = at.addAlertRow(8, SCHEDULED, ACCEPTED, 0, createTimeInMillis(8, 0),
460                createTimeInMillis(9, 0), 0);
461        int id7 = at.addAlertRow(7, SCHEDULED, ACCEPTED, 0, createTimeInMillis(7, 0),
462                createTimeInMillis(8, 0), 0);
463
464        // Set up concurrent alerts (that started recently).
465        int id6 = at.addAlertRow(6, SCHEDULED, ACCEPTED, 0, createTimeInMillis(5, 0),
466                createTimeInMillis(5, 40), 0);
467        int id5 = at.addAlertRow(5, SCHEDULED, ACCEPTED, 0, createTimeInMillis(4, 55),
468                createTimeInMillis(7, 30), 0);
469        int id4 = at.addAlertRow(4, SCHEDULED, ACCEPTED, 0, createTimeInMillis(4, 50),
470                createTimeInMillis(4, 50), 0);
471
472        // Set up past alerts.
473        int id3 = at.addAlertRow(3, SCHEDULED, ACCEPTED, 0, createTimeInMillis(3, 0),
474                createTimeInMillis(4, 0), 0);
475        int id2 = at.addAlertRow(2, SCHEDULED, ACCEPTED, 0, createTimeInMillis(2, 0),
476                createTimeInMillis(3, 0), 0);
477        int id1 = at.addAlertRow(1, SCHEDULED, ACCEPTED, 0, createTimeInMillis(1, 0),
478                createTimeInMillis(2, 0), 0);
479
480        // Check posted notifications.  The order listed here is the order simulates the
481        // order in the real notification bar (last one posted appears on top), so these
482        // should be lowest start time on top.
483        ntm.expectTestNotification(6, id4, PRIORITY_HIGH); // concurrent
484        ntm.expectTestNotification(5, id5, PRIORITY_HIGH); // concurrent
485        ntm.expectTestNotification(4, id6, PRIORITY_HIGH); // concurrent
486        ntm.expectTestNotification(3, id7, PRIORITY_HIGH); // future
487        ntm.expectTestNotification(2, id8, PRIORITY_HIGH); // future
488        ntm.expectTestNotification(1, id9, PRIORITY_HIGH); // future
489        ntm.expectTestNotification(AlertUtils.EXPIRED_GROUP_NOTIFICATION_ID,
490                new int[] {id3, id2, id1}, PRIORITY_MIN);
491        AlertService.generateAlerts(mContext, ntm, alarmMgr, prefs, at.getAlertCursor(),
492                currentTime, maxNotifications);
493        ntm.validateNotificationsAndReset();
494
495        // Increase time by 15 minutes to check that some concurrent events dropped
496        // to the low priority bucket.
497        currentTime = createTimeInMillis(5, 15);
498        ntm.expectTestNotification(4, id5, PRIORITY_HIGH); // concurrent
499        ntm.expectTestNotification(3, id7, PRIORITY_HIGH); // future
500        ntm.expectTestNotification(2, id8, PRIORITY_HIGH); // future
501        ntm.expectTestNotification(1, id9, PRIORITY_HIGH); // future
502        ntm.expectTestNotification(AlertUtils.EXPIRED_GROUP_NOTIFICATION_ID,
503                new int[] {id6, id4, id3, id2, id1}, PRIORITY_MIN);
504        AlertService.generateAlerts(mContext, ntm, alarmMgr, prefs, at.getAlertCursor(),
505                currentTime, maxNotifications);
506        ntm.validateNotificationsAndReset();
507
508        // Increase time so some of the previously future ones change state.
509        currentTime = createTimeInMillis(8, 15);
510        ntm.expectTestNotification(1, id9, PRIORITY_HIGH); // future
511        ntm.expectTestNotification(AlertUtils.EXPIRED_GROUP_NOTIFICATION_ID,
512                new int[] {id8, id7, id6, id5, id4, id3, id2, id1}, PRIORITY_MIN);
513        AlertService.generateAlerts(mContext, ntm, alarmMgr, prefs, at.getAlertCursor(),
514                currentTime, maxNotifications);
515        ntm.validateNotificationsAndReset();
516    }
517
518    @SmallTest
519    public void testGenerateAlerts_maxAlerts() {
520        MockSharedPreferences prefs = new MockSharedPreferences();
521        MockAlarmManager alarmMgr = new MockAlarmManager(mContext);
522        AlertsTable at = new AlertsTable();
523
524        // Current time - 5:00
525        long currentTime = createTimeInMillis(5, 0);
526
527        // Set up future alerts.  The real query implementation sorts by descending start
528        // time so simulate that here with our order of adds to AlertsTable.
529        int id9 = at.addAlertRow(9, SCHEDULED, ACCEPTED, 0, createTimeInMillis(9, 0),
530                createTimeInMillis(10, 0), 0);
531        int id8 = at.addAlertRow(8, SCHEDULED, ACCEPTED, 0, createTimeInMillis(8, 0),
532                createTimeInMillis(9, 0), 0);
533        int id7 = at.addAlertRow(7, SCHEDULED, ACCEPTED, 0, createTimeInMillis(7, 0),
534                createTimeInMillis(8, 0), 0);
535
536        // Set up concurrent alerts (that started recently).
537        int id6 = at.addAlertRow(6, SCHEDULED, ACCEPTED, 0, createTimeInMillis(5, 0),
538                createTimeInMillis(5, 40), 0);
539        int id5 = at.addAlertRow(5, SCHEDULED, ACCEPTED, 0, createTimeInMillis(4, 55),
540                createTimeInMillis(7, 30), 0);
541        int id4 = at.addAlertRow(4, SCHEDULED, ACCEPTED, 0, createTimeInMillis(4, 50),
542                createTimeInMillis(4, 50), 0);
543
544        // Set up past alerts.
545        int id3 = at.addAlertRow(3, SCHEDULED, ACCEPTED, 0, createTimeInMillis(3, 0),
546                createTimeInMillis(4, 0), 0);
547        int id2 = at.addAlertRow(2, SCHEDULED, ACCEPTED, 0, createTimeInMillis(2, 0),
548                createTimeInMillis(3, 0), 0);
549        int id1 = at.addAlertRow(1, SCHEDULED, ACCEPTED, 0, createTimeInMillis(1, 0),
550                createTimeInMillis(2, 0), 0);
551
552        // Test when # alerts = max.
553        int maxNotifications = 6;
554        NotificationTestManager ntm = new NotificationTestManager(at.mAlerts, maxNotifications);
555        ntm.expectTestNotification(6, id4, PRIORITY_HIGH); // concurrent
556        ntm.expectTestNotification(5, id5, PRIORITY_HIGH); // concurrent
557        ntm.expectTestNotification(4, id6, PRIORITY_HIGH); // concurrent
558        ntm.expectTestNotification(3, id7, PRIORITY_HIGH); // future
559        ntm.expectTestNotification(2, id8, PRIORITY_HIGH); // future
560        ntm.expectTestNotification(1, id9, PRIORITY_HIGH); // future
561        ntm.expectTestNotification(AlertUtils.EXPIRED_GROUP_NOTIFICATION_ID,
562                new int[] {id3, id2, id1}, PRIORITY_MIN);
563        AlertService.generateAlerts(mContext, ntm, alarmMgr, prefs, at.getAlertCursor(),
564                currentTime, maxNotifications);
565        ntm.validateNotificationsAndReset();
566
567        // Test when # alerts > max.
568        maxNotifications = 4;
569        ntm = new NotificationTestManager(at.mAlerts, maxNotifications);
570        ntm.expectTestNotification(4, id4, PRIORITY_HIGH); // concurrent
571        ntm.expectTestNotification(3, id5, PRIORITY_HIGH); // concurrent
572        ntm.expectTestNotification(2, id6, PRIORITY_HIGH); // concurrent
573        ntm.expectTestNotification(1, id7, PRIORITY_HIGH); // future
574        ntm.expectTestNotification(AlertUtils.EXPIRED_GROUP_NOTIFICATION_ID,
575                new int[] {id9, id8, id3, id2, id1}, PRIORITY_MIN);
576        AlertService.generateAlerts(mContext, ntm, alarmMgr, prefs, at.getAlertCursor(),
577                currentTime, maxNotifications);
578        ntm.validateNotificationsAndReset();
579    }
580
581    /**
582     * Test that the SharedPreferences are only fetched once for each setting.
583     */
584    @SmallTest
585    public void testGenerateAlerts_sharedPreferences() {
586        MockSharedPreferences prefs = new MockSharedPreferences(true /* strict mode */);
587        AlertsTable at = new AlertsTable();
588        NotificationTestManager ntm = new NotificationTestManager(at.mAlerts,
589                AlertService.MAX_NOTIFICATIONS);
590
591        // Current time - 5:00
592        long currentTime = createTimeInMillis(5, 0);
593
594        // Set up future alerts.  The real query implementation sorts by descending start
595        // time so simulate that here with our order of adds to AlertsTable.
596        at.addAlertRow(3, SCHEDULED, ACCEPTED, 0, createTimeInMillis(9, 0),
597                createTimeInMillis(10, 0), 0);
598        at.addAlertRow(2, SCHEDULED, ACCEPTED, 0, createTimeInMillis(8, 0),
599                createTimeInMillis(9, 0), 0);
600        at.addAlertRow(1, SCHEDULED, ACCEPTED, 0, createTimeInMillis(7, 0),
601                createTimeInMillis(8, 0), 0);
602
603        // If this does not result in a failure (MockSharedPreferences fails for duplicate
604        // queries), then test passes.
605        AlertService.generateAlerts(mContext, ntm, new MockAlarmManager(mContext), prefs,
606                at.getAlertCursor(), currentTime, AlertService.MAX_NOTIFICATIONS);
607    }
608
609    public void testGenerateAlerts_refreshTime() {
610        AlertsTable at = new AlertsTable();
611        MockSharedPreferences prefs = new MockSharedPreferences();
612        MockAlarmManager alarmMgr = new MockAlarmManager(mContext);
613        NotificationTestManager ntm = new NotificationTestManager(at.mAlerts,
614                AlertService.MAX_NOTIFICATIONS);
615
616        // Since AlertService.processQuery uses DateUtils.isToday instead of checking against
617        // the passed in currentTime (not worth allocating the extra Time objects to do so), use
618        // today's date for this test.
619        Time now = new Time();
620        now.setToNow();
621        int day = now.monthDay;
622        int month = now.month;
623        int year = now.year;
624        Time yesterday = new Time();
625        yesterday.set(System.currentTimeMillis() - DateUtils.DAY_IN_MILLIS);
626        Time tomorrow = new Time();
627        tomorrow.set(System.currentTimeMillis() + DateUtils.DAY_IN_MILLIS);
628        long allDayStart = Utils.createTimeInMillis(0, 0, 0, day, month, year, Time.TIMEZONE_UTC);
629
630        /* today 10am - 10:30am */
631        int id4 = at.addAlertRow(4, SCHEDULED, ACCEPTED, 0,
632                Utils.createTimeInMillis(0, 0, 10, day, month, year, Time.getCurrentTimezone()),
633                Utils.createTimeInMillis(0, 30, 10, day, month, year, Time.getCurrentTimezone()),
634                        0);
635        /* today 6am - 6am (0 duration event) */
636        int id3 = at.addAlertRow(3, SCHEDULED, ACCEPTED, 0,
637                Utils.createTimeInMillis(0, 0, 6, day, month, year, Time.getCurrentTimezone()),
638                Utils.createTimeInMillis(0, 0, 6, day, month, year, Time.getCurrentTimezone()), 0);
639        /* today allDay */
640        int id2 = at.addAlertRow(2, SCHEDULED, ACCEPTED, 1, allDayStart,
641                allDayStart + DateUtils.HOUR_IN_MILLIS * 24, 0);
642        /* yesterday 11pm - today 7am (multiday event) */
643        int id1 = at.addAlertRow(1, SCHEDULED, ACCEPTED, 0,
644                Utils.createTimeInMillis(0, 0, 23, yesterday.monthDay, yesterday.month,
645                        yesterday.year, Time.getCurrentTimezone()),
646                Utils.createTimeInMillis(0, 0, 7, day, month, year, Time.getCurrentTimezone()), 0);
647
648        // Test at midnight - next refresh should be 15 min later (15 min into the all
649        // day event).
650        long currentTime = Utils.createTimeInMillis(0, 0, 0, day, month, year,
651                Time.getCurrentTimezone());
652        alarmMgr.expectAlarmTime(AlarmManager.RTC, currentTime + 15 * DateUtils.MINUTE_IN_MILLIS);
653        ntm.expectTestNotification(4, id1, PRIORITY_HIGH);
654        ntm.expectTestNotification(3, id2, PRIORITY_HIGH);
655        ntm.expectTestNotification(2, id3, PRIORITY_HIGH);
656        ntm.expectTestNotification(1, id4, PRIORITY_HIGH);
657        AlertService.generateAlerts(mContext, ntm, alarmMgr, prefs, at.getAlertCursor(),
658                currentTime, AlertService.MAX_NOTIFICATIONS);
659        ntm.validateNotificationsAndReset();
660
661        // Test at 12:30am - next refresh should be 30 min later (1/4 into event 'id1').
662        currentTime = Utils.createTimeInMillis(0, 30, 0, day, month, year,
663                Time.getCurrentTimezone());
664        alarmMgr.expectAlarmTime(AlarmManager.RTC, currentTime + 30 * DateUtils.MINUTE_IN_MILLIS);
665        ntm.expectTestNotification(3, id1, PRIORITY_HIGH);
666        ntm.expectTestNotification(2, id3, PRIORITY_HIGH);
667        ntm.expectTestNotification(1, id4, PRIORITY_HIGH);
668        ntm.expectTestNotification(4, id2, PRIORITY_DEFAULT);
669        AlertService.generateAlerts(mContext, ntm, alarmMgr, prefs, at.getAlertCursor(),
670                currentTime, AlertService.MAX_NOTIFICATIONS);
671        ntm.validateNotificationsAndReset();
672
673        // Test at 5:55am - next refresh should be 20 min later (15 min after 'id3').
674        currentTime = Utils.createTimeInMillis(0, 55, 5, day, month, year,
675                Time.getCurrentTimezone());
676        alarmMgr.expectAlarmTime(AlarmManager.RTC, currentTime + 20 * DateUtils.MINUTE_IN_MILLIS);
677        ntm.expectTestNotification(2, id3, PRIORITY_HIGH);
678        ntm.expectTestNotification(1, id4, PRIORITY_HIGH);
679        ntm.expectTestNotification(3, id2, PRIORITY_DEFAULT);
680        ntm.expectTestNotification(AlertUtils.EXPIRED_GROUP_NOTIFICATION_ID, id1, PRIORITY_MIN);
681        AlertService.generateAlerts(mContext, ntm, alarmMgr, prefs, at.getAlertCursor(),
682                currentTime, AlertService.MAX_NOTIFICATIONS);
683        ntm.validateNotificationsAndReset();
684
685        // Test at 10:14am - next refresh should be 1 min later (15 min into event 'id4').
686        currentTime = Utils.createTimeInMillis(0, 14, 10, day, month, year,
687                Time.getCurrentTimezone());
688        alarmMgr.expectAlarmTime(AlarmManager.RTC, currentTime + 1 * DateUtils.MINUTE_IN_MILLIS);
689        ntm.expectTestNotification(1, id4, PRIORITY_HIGH);
690        ntm.expectTestNotification(2, id2, PRIORITY_DEFAULT);
691        ntm.expectTestNotification(AlertUtils.EXPIRED_GROUP_NOTIFICATION_ID, new int[] {id3, id1},
692                PRIORITY_MIN);
693        AlertService.generateAlerts(mContext, ntm, alarmMgr, prefs, at.getAlertCursor(),
694                currentTime, AlertService.MAX_NOTIFICATIONS);
695        ntm.validateNotificationsAndReset();
696
697        // Test at 10:15am - next refresh should be tomorrow midnight (end of all day event 'id2').
698        currentTime = Utils.createTimeInMillis(0, 15, 10, day, month, year,
699                Time.getCurrentTimezone());
700        alarmMgr.expectAlarmTime(AlarmManager.RTC, Utils.createTimeInMillis(0, 0, 23,
701                tomorrow.monthDay, tomorrow.month, tomorrow.year, Time.getCurrentTimezone()));
702        ntm.expectTestNotification(1, id2, PRIORITY_DEFAULT);
703        ntm.expectTestNotification(AlertUtils.EXPIRED_GROUP_NOTIFICATION_ID,
704                new int[] {id4, id3, id1}, PRIORITY_MIN);
705        AlertService.generateAlerts(mContext, ntm, alarmMgr, prefs, at.getAlertCursor(),
706                currentTime, AlertService.MAX_NOTIFICATIONS);
707        ntm.validateNotificationsAndReset();
708    }
709
710    private NotificationInfo createNotificationInfo(long eventId) {
711        return new NotificationInfo("eventName", "location", "description", 100L, 200L, eventId,
712                false, false);
713    }
714
715    private static long createTimeInMillis(int hour, int minute) {
716        return Utils.createTimeInMillis(0 /* second */, minute, hour, 1 /* day */, 1 /* month */,
717                2012 /* year */, Time.getCurrentTimezone());
718    }
719
720    @SmallTest
721    public void testProcessQuery_skipDeclinedDismissed() {
722        int declinedEventId = 1;
723        int dismissedEventId = 2;
724        int acceptedEventId = 3;
725        long acceptedStartTime = createTimeInMillis(10, 0);
726        long acceptedEndTime = createTimeInMillis(10, 30);
727
728        AlertsTable at = new AlertsTable();
729        at.addAlertRow(declinedEventId, SCHEDULED, DECLINED, 0, createTimeInMillis(9, 0),
730                createTimeInMillis(10, 0), 0);
731        at.addAlertRow(dismissedEventId, SCHEDULED, DISMISSED, 0, createTimeInMillis(9, 30),
732                createTimeInMillis(11, 0), 0);
733        at.addAlertRow(acceptedEventId, SCHEDULED, ACCEPTED, 1, acceptedStartTime, acceptedEndTime,
734                0);
735
736        ArrayList<NotificationInfo> highPriority = new ArrayList<NotificationInfo>();
737        ArrayList<NotificationInfo> mediumPriority = new ArrayList<NotificationInfo>();
738        ArrayList<NotificationInfo> lowPriority = new ArrayList<NotificationInfo>();
739        long currentTime = createTimeInMillis(5, 0);
740        AlertService.processQuery(at.getAlertCursor(), mContext, currentTime, highPriority,
741                mediumPriority, lowPriority);
742
743        assertEquals(0, lowPriority.size());
744        assertEquals(0, mediumPriority.size());
745        assertEquals(1, highPriority.size());
746        assertEquals(acceptedEventId, highPriority.get(0).eventId);
747        assertEquals(acceptedStartTime, highPriority.get(0).startMillis);
748        assertEquals(acceptedEndTime, highPriority.get(0).endMillis);
749        assertTrue(highPriority.get(0).allDay);
750    }
751
752    @SmallTest
753    public void testProcessQuery_newAlert() {
754        int scheduledAlertEventId = 1;
755        int firedAlertEventId = 2;
756
757        AlertsTable at = new AlertsTable();
758        at.addAlertRow(scheduledAlertEventId, SCHEDULED, ACCEPTED, 0, createTimeInMillis(9, 0),
759                createTimeInMillis(10, 0), 0);
760        at.addAlertRow(firedAlertEventId, FIRED, ACCEPTED, 0, createTimeInMillis(4, 0),
761                createTimeInMillis(10, 30), 0);
762
763        ArrayList<NotificationInfo> highPriority = new ArrayList<NotificationInfo>();
764        ArrayList<NotificationInfo> mediumPriority = new ArrayList<NotificationInfo>();
765        ArrayList<NotificationInfo> lowPriority = new ArrayList<NotificationInfo>();
766        long currentTime = createTimeInMillis(5, 0);
767        AlertService.processQuery(at.getAlertCursor(), mContext, currentTime, highPriority,
768                mediumPriority, lowPriority);
769
770        assertEquals(0, lowPriority.size());
771        assertEquals(0, mediumPriority.size());
772        assertEquals(2, highPriority.size());
773        assertEquals(scheduledAlertEventId, highPriority.get(0).eventId);
774        assertTrue("newAlert should be ON for scheduled alerts", highPriority.get(0).newAlert);
775        assertEquals(firedAlertEventId, highPriority.get(1).eventId);
776        assertFalse("newAlert should be OFF for fired alerts", highPriority.get(1).newAlert);
777    }
778
779    @SmallTest
780    public void testProcessQuery_recurringEvent() {
781        int eventId = 1;
782        long earlierStartTime = createTimeInMillis(10, 0);
783        long laterStartTime = createTimeInMillis(11, 0);
784
785        ArrayList<NotificationInfo> highPriority = new ArrayList<NotificationInfo>();
786        ArrayList<NotificationInfo> mediumPriority = new ArrayList<NotificationInfo>();
787        ArrayList<NotificationInfo> lowPriority = new ArrayList<NotificationInfo>();
788
789        AlertsTable at = new AlertsTable();
790        at.addAlertRow(eventId, SCHEDULED, ACCEPTED, 0, laterStartTime,
791                laterStartTime + DateUtils.HOUR_IN_MILLIS, 0);
792        at.addAlertRow(eventId, FIRED, ACCEPTED, 0, earlierStartTime,
793                earlierStartTime + DateUtils.HOUR_IN_MILLIS, 0);
794
795        // Both events in the future: the earliest one should be chosen.
796        long currentTime = earlierStartTime - DateUtils.DAY_IN_MILLIS * 5;
797        AlertService.processQuery(at.getAlertCursor(), mContext, currentTime, highPriority,
798                mediumPriority, lowPriority);
799        assertEquals(0, lowPriority.size());
800        assertEquals(0, mediumPriority.size());
801        assertEquals(1, highPriority.size());
802        assertEquals("Recurring event with earlier start time expected", earlierStartTime,
803                highPriority.get(0).startMillis);
804
805        // Increment time just past the earlier event: the earlier one should be chosen.
806        highPriority.clear();
807        currentTime = earlierStartTime + DateUtils.MINUTE_IN_MILLIS * 10;
808        AlertService.processQuery(at.getAlertCursor(), mContext, currentTime, highPriority,
809                mediumPriority, lowPriority);
810        assertEquals(0, lowPriority.size());
811        assertEquals(0, mediumPriority.size());
812        assertEquals(1, highPriority.size());
813        assertEquals("Recurring event with earlier start time expected", earlierStartTime,
814                highPriority.get(0).startMillis);
815
816        // Increment time to 15 min past the earlier event: the later one should be chosen.
817        highPriority.clear();
818        currentTime = earlierStartTime + DateUtils.MINUTE_IN_MILLIS * 15;
819        AlertService.processQuery(at.getAlertCursor(), mContext, currentTime, highPriority,
820                mediumPriority, lowPriority);
821        assertEquals(0, lowPriority.size());
822        assertEquals(0, mediumPriority.size());
823        assertEquals(1, highPriority.size());
824        assertEquals("Recurring event with later start time expected", laterStartTime,
825                highPriority.get(0).startMillis);
826
827        // Both events in the past: the later one should be chosen (in the low priority bucket).
828        highPriority.clear();
829        currentTime = laterStartTime + DateUtils.DAY_IN_MILLIS * 5;
830        AlertService.processQuery(at.getAlertCursor(), mContext, currentTime, highPriority,
831                mediumPriority, lowPriority);
832        assertEquals(0, highPriority.size());
833        assertEquals(0, mediumPriority.size());
834        assertEquals(1, lowPriority.size());
835        assertEquals("Recurring event with later start time expected", laterStartTime,
836                lowPriority.get(0).startMillis);
837    }
838
839    @SmallTest
840    public void testProcessQuery_recurringAllDayEvent() {
841        int eventId = 1;
842        long day1 = Utils.createTimeInMillis(0, 0, 0, 1, 5, 2012, Time.TIMEZONE_UTC);
843        long day2 = Utils.createTimeInMillis(0, 0, 0, 2, 5, 2012, Time.TIMEZONE_UTC);
844
845        ArrayList<NotificationInfo> highPriority = new ArrayList<NotificationInfo>();
846        ArrayList<NotificationInfo> mediumPriority = new ArrayList<NotificationInfo>();
847        ArrayList<NotificationInfo> lowPriority = new ArrayList<NotificationInfo>();
848
849        AlertsTable at = new AlertsTable();
850        at.addAlertRow(eventId, SCHEDULED, ACCEPTED, 1, day2, day2 + DateUtils.HOUR_IN_MILLIS * 24,
851                0);
852        at.addAlertRow(eventId, SCHEDULED, ACCEPTED, 1, day1, day1 + DateUtils.HOUR_IN_MILLIS * 24,
853                0);
854
855        // Both events in the future: the earliest one should be chosen.
856        long currentTime = day1 - DateUtils.DAY_IN_MILLIS * 3;
857        AlertService.processQuery(at.getAlertCursor(), mContext, currentTime, highPriority,
858                mediumPriority, lowPriority);
859        assertEquals(0, lowPriority.size());
860        assertEquals(0, mediumPriority.size());
861        assertEquals(1, highPriority.size());
862        assertEquals("Recurring event with earlier start time expected", day1,
863                highPriority.get(0).startMillis);
864
865        // Increment time just past the earlier event (to 12:10am).  The earlier one should
866        // be chosen.
867        highPriority.clear();
868        currentTime = Utils.createTimeInMillis(0, 10, 0, 1, 5, 2012, Time.getCurrentTimezone());
869        AlertService.processQuery(at.getAlertCursor(), mContext, currentTime, highPriority,
870                mediumPriority, lowPriority);
871        assertEquals(0, lowPriority.size());
872        assertEquals(0, mediumPriority.size());
873        assertEquals(1, highPriority.size());
874        assertEquals("Recurring event with earlier start time expected", day1,
875                highPriority.get(0).startMillis);
876
877        // Increment time to 15 min past the earlier event: the later one should be chosen.
878        highPriority.clear();
879        currentTime = Utils.createTimeInMillis(0, 15, 0, 1, 5, 2012, Time.getCurrentTimezone());
880        AlertService.processQuery(at.getAlertCursor(), mContext, currentTime, highPriority,
881                mediumPriority, lowPriority);
882        assertEquals(0, lowPriority.size());
883        assertEquals(0, mediumPriority.size());
884        assertEquals(1, highPriority.size());
885        assertEquals("Recurring event with earlier start time expected", day2,
886                highPriority.get(0).startMillis);
887
888        // Both events in the past: the later one should be chosen (in the low priority bucket).
889        highPriority.clear();
890        currentTime = day2 + DateUtils.DAY_IN_MILLIS * 1;
891        AlertService.processQuery(at.getAlertCursor(), mContext, currentTime, highPriority,
892                mediumPriority, lowPriority);
893        assertEquals(0, highPriority.size());
894        assertEquals(0, mediumPriority.size());
895        assertEquals(1, lowPriority.size());
896        assertEquals("Recurring event with later start time expected", day2,
897                lowPriority.get(0).startMillis);
898    }
899
900    @SmallTest
901    public void testRedistributeBuckets_withinLimits() throws Exception {
902        int maxNotifications = 3;
903        ArrayList<NotificationInfo> threeItemList = new ArrayList<NotificationInfo>();
904        threeItemList.add(createNotificationInfo(5));
905        threeItemList.add(createNotificationInfo(4));
906        threeItemList.add(createNotificationInfo(3));
907
908        // Test when max notifications at high priority.
909        ArrayList<NotificationInfo> high = threeItemList;
910        ArrayList<NotificationInfo> medium = new ArrayList<NotificationInfo>();
911        ArrayList<NotificationInfo> low = new ArrayList<NotificationInfo>();
912        AlertService.redistributeBuckets(high, medium, low, maxNotifications);
913        assertEquals(3, high.size());
914        assertEquals(0, medium.size());
915        assertEquals(0, low.size());
916
917        // Test when max notifications at medium priority.
918        high = new ArrayList<NotificationInfo>();
919        medium = threeItemList;
920        low = new ArrayList<NotificationInfo>();
921        AlertService.redistributeBuckets(high, medium, low, maxNotifications);
922        assertEquals(0, high.size());
923        assertEquals(3, medium.size());
924        assertEquals(0, low.size());
925
926        // Test when max notifications at high and medium priority
927        high = new ArrayList<NotificationInfo>(threeItemList);
928        medium = new ArrayList<NotificationInfo>();
929        medium.add(high.remove(1));
930        low = new ArrayList<NotificationInfo>();
931        AlertService.redistributeBuckets(high, medium, low, maxNotifications);
932        assertEquals(2, high.size());
933        assertEquals(1, medium.size());
934        assertEquals(0, low.size());
935    }
936
937    @SmallTest
938    public void testRedistributeBuckets_tooManyHighPriority() throws Exception {
939        ArrayList<NotificationInfo> high = new ArrayList<NotificationInfo>();
940        ArrayList<NotificationInfo> medium = new ArrayList<NotificationInfo>();
941        ArrayList<NotificationInfo> low = new ArrayList<NotificationInfo>();
942        high.add(createNotificationInfo(5));
943        high.add(createNotificationInfo(4));
944        high.add(createNotificationInfo(3));
945        high.add(createNotificationInfo(2));
946        high.add(createNotificationInfo(1));
947
948        // Invoke the method under test.
949        int maxNotifications = 3;
950        AlertService.redistributeBuckets(high, medium, low, maxNotifications);
951
952        // Verify some high priority were kicked out.
953        assertEquals(3, high.size());
954        assertEquals(3, high.get(0).eventId);
955        assertEquals(2, high.get(1).eventId);
956        assertEquals(1, high.get(2).eventId);
957
958        // Verify medium priority untouched.
959        assertEquals(0, medium.size());
960
961        // Verify the extras went to low priority.
962        assertEquals(2, low.size());
963        assertEquals(5, low.get(0).eventId);
964        assertEquals(4, low.get(1).eventId);
965    }
966
967    @SmallTest
968    public void testRedistributeBuckets_tooManyMediumPriority() throws Exception {
969        ArrayList<NotificationInfo> high = new ArrayList<NotificationInfo>();
970        ArrayList<NotificationInfo> medium = new ArrayList<NotificationInfo>();
971        ArrayList<NotificationInfo> low = new ArrayList<NotificationInfo>();
972        high.add(createNotificationInfo(5));
973        high.add(createNotificationInfo(4));
974        medium.add(createNotificationInfo(3));
975        medium.add(createNotificationInfo(2));
976        medium.add(createNotificationInfo(1));
977
978        // Invoke the method under test.
979        int maxNotifications = 3;
980        AlertService.redistributeBuckets(high, medium, low, maxNotifications);
981
982        // Verify high priority untouched.
983        assertEquals(2, high.size());
984        assertEquals(5, high.get(0).eventId);
985        assertEquals(4, high.get(1).eventId);
986
987        // Verify some medium priority were kicked out (the ones near the end of the
988        // list).
989        assertEquals(1, medium.size());
990        assertEquals(3, medium.get(0).eventId);
991
992        // Verify the extras went to low priority.
993        assertEquals(2, low.size());
994        assertEquals(2, low.get(0).eventId);
995        assertEquals(1, low.get(1).eventId);
996    }
997
998    @SmallTest
999    public void testRedistributeBuckets_tooManyHighMediumPriority() throws Exception {
1000        ArrayList<NotificationInfo> high = new ArrayList<NotificationInfo>();
1001        ArrayList<NotificationInfo> medium = new ArrayList<NotificationInfo>();
1002        ArrayList<NotificationInfo> low = new ArrayList<NotificationInfo>();
1003        high.add(createNotificationInfo(8));
1004        high.add(createNotificationInfo(7));
1005        high.add(createNotificationInfo(6));
1006        high.add(createNotificationInfo(5));
1007        high.add(createNotificationInfo(4));
1008        medium.add(createNotificationInfo(3));
1009        medium.add(createNotificationInfo(2));
1010        medium.add(createNotificationInfo(1));
1011
1012        // Invoke the method under test.
1013        int maxNotifications = 3;
1014        AlertService.redistributeBuckets(high, medium, low, maxNotifications);
1015
1016        // Verify high priority.
1017        assertEquals(3, high.size());
1018        assertEquals(6, high.get(0).eventId);
1019        assertEquals(5, high.get(1).eventId);
1020        assertEquals(4, high.get(2).eventId);
1021
1022        // Verify some medium priority.
1023        assertEquals(0, medium.size());
1024
1025        // Verify low priority.
1026        assertEquals(5, low.size());
1027        assertEquals(8, low.get(0).eventId);
1028        assertEquals(7, low.get(1).eventId);
1029        assertEquals(3, low.get(2).eventId);
1030        assertEquals(2, low.get(3).eventId);
1031        assertEquals(1, low.get(4).eventId);
1032    }
1033}
1034