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 android.app.AlarmManager;
20import android.database.Cursor;
21import android.database.MatrixCursor;
22import android.net.Uri;
23import android.provider.CalendarContract;
24import android.provider.CalendarContract.Instances;
25import android.provider.CalendarContract.Reminders;
26import android.test.AndroidTestCase;
27import android.test.IsolatedContext;
28import android.test.mock.MockContentProvider;
29import android.test.mock.MockContentResolver;
30import android.test.suitebuilder.annotation.SmallTest;
31import android.text.format.DateUtils;
32import android.text.format.Time;
33import android.util.Log;
34
35import junit.framework.Assert;
36
37import java.util.ArrayList;
38import java.util.Arrays;
39import java.util.HashSet;
40
41@SmallTest
42public class AlarmSchedulerTest extends AndroidTestCase {
43    private static final int BATCH_SIZE = 50;
44    private MockProvider mMockProvider;
45    private MockAlarmManager mMockAlarmManager;
46    private IsolatedContext mIsolatedContext;
47
48    /**
49     * A helper class to mock query results from the test data.
50     */
51    private static class MockProvider extends MockContentProvider {
52        private ArrayList<EventInfo> mEvents = new ArrayList<EventInfo>();
53        private ArrayList<String> mExpectedRemindersQueries = new ArrayList<String>();
54        private int mCurrentReminderQueryIndex = 0;
55
56        /**
57         * Contains info for a test event and its reminder.
58         */
59        private static class EventInfo {
60            long mEventId;
61            long mBegin;
62            boolean mAllDay;
63            int mReminderMinutes;
64
65            public EventInfo(long eventId, boolean allDay, long begin, int reminderMinutes) {
66                mEventId = eventId;
67                mAllDay = allDay;
68                mBegin = begin;
69                mReminderMinutes = reminderMinutes;
70            }
71
72        }
73
74        /**
75         * Adds event/reminder data for testing.  These will always be returned in the mocked
76         * query result cursors.
77         */
78        void addEventInfo(long eventId, boolean allDay, long begin, int reminderMinutes) {
79            mEvents.add(new EventInfo(eventId, allDay, begin, reminderMinutes));
80        }
81
82        private MatrixCursor getInstancesCursor() {
83            MatrixCursor instancesCursor = new MatrixCursor(AlarmScheduler.INSTANCES_PROJECTION);
84            int i = 0;
85            HashSet<Long> eventIds = new HashSet<Long>();
86            for (EventInfo event : mEvents) {
87                if (!eventIds.contains(event.mEventId)) {
88                    Object[] ca = {
89                            event.mEventId,
90                            event.mBegin,
91                            event.mAllDay ? 1 : 0,
92                    };
93                    instancesCursor.addRow(ca);
94                    eventIds.add(event.mEventId);
95                }
96            }
97            return instancesCursor;
98        }
99
100        private MatrixCursor getRemindersCursor() {
101            MatrixCursor remindersCursor = new MatrixCursor(AlarmScheduler.REMINDERS_PROJECTION);
102            int i = 0;
103            for (EventInfo event : mEvents) {
104                Object[] ca = {
105                        event.mEventId,
106                        event.mReminderMinutes,
107                        1,
108                };
109                remindersCursor.addRow(ca);
110            }
111            return remindersCursor;
112        }
113
114        @Override
115        public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
116                String sortOrder) {
117            if (uri.toString().startsWith(Instances.CONTENT_URI.toString())) {
118                return getInstancesCursor();
119            } else if (Reminders.CONTENT_URI.equals(uri)) {
120                if (mExpectedRemindersQueries.size() > 0) {
121                    if (mExpectedRemindersQueries.size() <= mCurrentReminderQueryIndex ||
122                            !mExpectedRemindersQueries.get(mCurrentReminderQueryIndex).equals(
123                                    selection)) {
124                        String msg = "Reminders query not as expected.\n";
125                        msg += "  Expected:";
126                        msg += Arrays.deepToString(mExpectedRemindersQueries.toArray());
127                        msg += "\n  Got in position " + mCurrentReminderQueryIndex + ": ";
128                        msg += selection;
129                        fail(msg);
130                    }
131                    mCurrentReminderQueryIndex++;
132                }
133                return getRemindersCursor();
134            } else {
135                return super.query(uri, projection, selection, selectionArgs, sortOrder);
136            }
137        }
138
139        /**
140         * Optionally set up expectation for the reminders query selection.
141         */
142        public void addExpectedRemindersQuery(String expectedRemindersQuery) {
143            this.mExpectedRemindersQueries.add(expectedRemindersQuery);
144        }
145    }
146
147    /**
148     * Expect an alarm for the specified time.
149     */
150    private void expectAlarmAt(long millis) {
151        // AlarmScheduler adds a slight delay to the alarm so account for that here.
152        mMockAlarmManager.expectAlarmTime(AlarmManager.RTC_WAKEUP,
153                millis + AlarmScheduler.ALARM_DELAY_MS);
154    }
155
156    @Override
157    protected void setUp() throws Exception {
158        super.setUp();
159
160        mMockProvider = new MockProvider();
161        mMockAlarmManager = new MockAlarmManager(mContext);
162        MockContentResolver mockResolver = new MockContentResolver();
163        mockResolver.addProvider(CalendarContract.AUTHORITY, mMockProvider);
164        mIsolatedContext = new IsolatedContext(mockResolver, mContext);
165    }
166
167    public void testNoEvents() {
168        AlarmScheduler.scheduleNextAlarm(mIsolatedContext, mMockAlarmManager,
169                BATCH_SIZE, System.currentTimeMillis());
170        assertFalse(mMockAlarmManager.isAlarmSet());
171    }
172
173    public void testNonAllDayEvent() {
174        // Set up mock test data.
175        long currentMillis = System.currentTimeMillis();
176        long startMillis = currentMillis + DateUtils.HOUR_IN_MILLIS;
177        int reminderMin = 10;
178        mMockProvider.addEventInfo(1, false, startMillis, reminderMin);
179        expectAlarmAt(startMillis - reminderMin * DateUtils.MINUTE_IN_MILLIS);
180
181        // Invoke scheduleNextAlarm and verify alarm was set at the expected time.
182        AlarmScheduler.scheduleNextAlarm(mIsolatedContext, mMockAlarmManager, BATCH_SIZE,
183                currentMillis);
184        assertTrue(mMockAlarmManager.isAlarmSet());
185    }
186
187    public void testAllDayEvent() {
188        // Set up mock allday data.
189        long startMillisUtc = Utils.createTimeInMillis(0, 0, 0, 1, 5, 2012, Time.TIMEZONE_UTC);
190        long startMillisLocal = Utils.createTimeInMillis(0, 0, 0, 1, 5, 2012,
191                Time.getCurrentTimezone());
192        long currentMillis = startMillisLocal - DateUtils.DAY_IN_MILLIS;
193        int reminderMin = 15;
194        mMockProvider.addEventInfo(1, true, startMillisUtc, reminderMin);
195        expectAlarmAt(startMillisLocal - reminderMin * DateUtils.MINUTE_IN_MILLIS);
196
197        // Invoke scheduleNextAlarm and verify alarm was set at the expected time.
198        AlarmScheduler.scheduleNextAlarm(mIsolatedContext, mMockAlarmManager, BATCH_SIZE,
199                currentMillis);
200        assertTrue(mMockAlarmManager.isAlarmSet());
201    }
202
203    public void testAllDayAndNonAllDayEvents() {
204        // Set up mock test data.
205        long startMillisUtc = Utils.createTimeInMillis(0, 0, 0, 1, 5, 2012, Time.TIMEZONE_UTC);
206        long startMillisLocal = Utils.createTimeInMillis(0, 0, 0, 1, 5, 2012,
207                Time.getCurrentTimezone());
208        long currentMillis = startMillisLocal - DateUtils.DAY_IN_MILLIS;
209        mMockProvider.addEventInfo(1, true, startMillisUtc, 15);
210        mMockProvider.addEventInfo(1, false, startMillisLocal, 10);
211        expectAlarmAt(startMillisLocal - 15 * DateUtils.MINUTE_IN_MILLIS);
212
213        // Invoke scheduleNextAlarm and verify alarm was set at the expected time.
214        AlarmScheduler.scheduleNextAlarm(mIsolatedContext, mMockAlarmManager, BATCH_SIZE,
215                currentMillis);
216        assertTrue(mMockAlarmManager.isAlarmSet());
217    }
218
219    public void testExpiredReminder() {
220        // Set up mock test data.
221        long currentMillis = System.currentTimeMillis();
222        long startMillis = currentMillis + DateUtils.HOUR_IN_MILLIS;
223        int reminderMin = 61;
224        mMockProvider.addEventInfo(1, false, startMillis, reminderMin);
225
226        // Invoke scheduleNextAlarm and verify no alarm was set.
227        AlarmScheduler.scheduleNextAlarm(mIsolatedContext, mMockAlarmManager, BATCH_SIZE,
228                currentMillis);
229        assertFalse(mMockAlarmManager.isAlarmSet());
230    }
231
232    public void testAlarmMax() {
233        // Set up mock test data for a reminder greater than 1 day in the future.
234        // This will be maxed out to 1 day out.
235        long currentMillis = System.currentTimeMillis();
236        long startMillis = currentMillis + DateUtils.DAY_IN_MILLIS * 3;
237        int reminderMin = (int) DateUtils.DAY_IN_MILLIS / (1000 * 60);
238        mMockProvider.addEventInfo(1, false, startMillis, reminderMin);
239        expectAlarmAt(currentMillis + DateUtils.DAY_IN_MILLIS);
240
241        // Invoke scheduleNextAlarm and verify alarm was set at the expected time.
242        AlarmScheduler.scheduleNextAlarm(mIsolatedContext, mMockAlarmManager, BATCH_SIZE,
243                currentMillis);
244        assertTrue(mMockAlarmManager.isAlarmSet());
245    }
246
247    public void testMultipleEvents() {
248        // Set up multiple events where a later event time has an earlier reminder time.
249        long currentMillis = System.currentTimeMillis();
250        mMockProvider.addEventInfo(1, false, currentMillis + DateUtils.DAY_IN_MILLIS, 0);
251        mMockProvider.addEventInfo(2, false, currentMillis + DateUtils.MINUTE_IN_MILLIS * 60, 45);
252        mMockProvider.addEventInfo(3, false, currentMillis + DateUtils.MINUTE_IN_MILLIS * 30, 10);
253
254        // Expect event 2's reminder.
255        expectAlarmAt(currentMillis + DateUtils.MINUTE_IN_MILLIS * 15);
256
257        // Invoke scheduleNextAlarm and verify alarm was set at the expected time.
258        AlarmScheduler.scheduleNextAlarm(mIsolatedContext, mMockAlarmManager, BATCH_SIZE,
259                currentMillis);
260        assertTrue(mMockAlarmManager.isAlarmSet());
261    }
262
263    public void testRecurringEvents() {
264        long currentMillis = System.currentTimeMillis();
265
266        // Event in 3 days, with a 2 day reminder
267        mMockProvider.addEventInfo(1, false, currentMillis + DateUtils.DAY_IN_MILLIS * 3,
268                (int) DateUtils.DAY_IN_MILLIS * 2 / (1000 * 60) /* 2 day reminder */);
269        // Event for tomorrow, with a 2 day reminder
270        mMockProvider.addEventInfo(1, false, currentMillis + DateUtils.DAY_IN_MILLIS,
271                (int) DateUtils.DAY_IN_MILLIS * 2 / (1000 * 60) /* 2 day reminder */);
272
273        // Expect the reminder for the top event because the reminder time for the bottom
274        // one already passed.
275        expectAlarmAt(currentMillis + DateUtils.DAY_IN_MILLIS);
276
277        // Invoke scheduleNextAlarm and verify alarm was set at the expected time.
278        AlarmScheduler.scheduleNextAlarm(mIsolatedContext, mMockAlarmManager, BATCH_SIZE,
279                currentMillis);
280        assertTrue(mMockAlarmManager.isAlarmSet());
281    }
282
283    public void testMultipleRemindersForEvent() {
284        // Set up mock test data.
285        long currentMillis = System.currentTimeMillis();
286        mMockProvider.addEventInfo(1,  false, currentMillis + DateUtils.DAY_IN_MILLIS, 10);
287        mMockProvider.addEventInfo(1,  false, currentMillis + DateUtils.DAY_IN_MILLIS, 20);
288        mMockProvider.addEventInfo(1,  false, currentMillis + DateUtils.DAY_IN_MILLIS, 15);
289
290        // Expect earliest reminder.
291        expectAlarmAt(currentMillis + DateUtils.DAY_IN_MILLIS - DateUtils.MINUTE_IN_MILLIS * 20);
292
293        // Invoke scheduleNextAlarm and verify alarm was set at the expected time.
294        AlarmScheduler.scheduleNextAlarm(mIsolatedContext, mMockAlarmManager, BATCH_SIZE,
295                currentMillis);
296        assertTrue(mMockAlarmManager.isAlarmSet());
297    }
298
299    public void testLargeBatch() {
300        // Add enough events to require several batches.
301        long currentMillis = System.currentTimeMillis();
302        int batchSize = 5;
303        for (int i = 19; i > 0; i--) {
304            mMockProvider.addEventInfo(i, false, currentMillis + DateUtils.HOUR_IN_MILLIS * i,
305                    10);
306        }
307
308        // Set up expectations for the batch queries.
309        expectAlarmAt(currentMillis + DateUtils.MINUTE_IN_MILLIS * 50);
310        mMockProvider.addExpectedRemindersQuery("method=1 AND event_id IN (19,18,17,16,15)");
311        mMockProvider.addExpectedRemindersQuery("method=1 AND event_id IN (14,13,12,11,10)");
312        mMockProvider.addExpectedRemindersQuery("method=1 AND event_id IN (9,8,7,6,5)");
313        mMockProvider.addExpectedRemindersQuery("method=1 AND event_id IN (4,3,2,1)");
314
315        // Invoke scheduleNextAlarm and verify alarm and reminder query batches.
316        AlarmScheduler.scheduleNextAlarm(mIsolatedContext, mMockAlarmManager, batchSize,
317                currentMillis);
318    }
319}
320