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 
17 package com.android.calendar.alerts;
18 
19 import android.app.AlarmManager;
20 import android.database.Cursor;
21 import android.database.MatrixCursor;
22 import android.net.Uri;
23 import android.provider.CalendarContract;
24 import android.provider.CalendarContract.Instances;
25 import android.provider.CalendarContract.Reminders;
26 import android.test.AndroidTestCase;
27 import android.test.IsolatedContext;
28 import android.test.mock.MockContentProvider;
29 import android.test.mock.MockContentResolver;
30 import android.test.suitebuilder.annotation.SmallTest;
31 import android.text.format.DateUtils;
32 import android.text.format.Time;
33 import android.util.Log;
34 
35 import junit.framework.Assert;
36 
37 import java.util.ArrayList;
38 import java.util.Arrays;
39 import java.util.HashSet;
40 
41 @SmallTest
42 public 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 
EventInfo(long eventId, boolean allDay, long begin, int reminderMinutes)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          */
addEventInfo(long eventId, boolean allDay, long begin, int reminderMinutes)78         void addEventInfo(long eventId, boolean allDay, long begin, int reminderMinutes) {
79             mEvents.add(new EventInfo(eventId, allDay, begin, reminderMinutes));
80         }
81 
getInstancesCursor()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 
getRemindersCursor()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
query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)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          */
addExpectedRemindersQuery(String expectedRemindersQuery)142         public void addExpectedRemindersQuery(String expectedRemindersQuery) {
143             this.mExpectedRemindersQueries.add(expectedRemindersQuery);
144         }
145     }
146 
147     /**
148      * Expect an alarm for the specified time.
149      */
expectAlarmAt(long millis)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
setUp()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 
testNoEvents()167     public void testNoEvents() {
168         AlarmScheduler.scheduleNextAlarm(mIsolatedContext, mMockAlarmManager,
169                 BATCH_SIZE, System.currentTimeMillis());
170         assertFalse(mMockAlarmManager.isAlarmSet());
171     }
172 
testNonAllDayEvent()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 
testAllDayEvent()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 
testAllDayAndNonAllDayEvents()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 
testExpiredReminder()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 
testAlarmMax()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 
testMultipleEvents()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 
testRecurringEvents()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 
testMultipleRemindersForEvent()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 
testLargeBatch()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