blob: 978282295794734fc75f7b1c93b543d165c21e9b [file] [log] [blame]
Sara Ting3a07a682012-10-31 13:19:38 -07001/*
2 * Copyright (C) 2012 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of 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,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.calendar.alerts;
18
19import android.app.AlarmManager;
20import android.app.PendingIntent;
21import android.content.ContentResolver;
22import android.content.ContentUris;
23import android.content.Context;
24import android.content.Intent;
25import android.database.Cursor;
26import android.net.Uri;
27import android.provider.CalendarContract;
28import android.provider.CalendarContract.Events;
29import android.provider.CalendarContract.Instances;
30import android.provider.CalendarContract.Reminders;
31import android.text.format.DateUtils;
32import android.text.format.Time;
33import android.util.Log;
34
35import com.android.calendar.Utils;
36
37import java.util.ArrayList;
38import java.util.HashMap;
39import java.util.List;
40import java.util.Map;
41
42/**
43 * Schedules the next EVENT_REMINDER_APP broadcast with AlarmManager, by querying the events
44 * and reminders tables for the next upcoming alert.
45 */
46public class AlarmScheduler {
47 private static final String TAG = "AlarmScheduler";
48
49 private static final String INSTANCES_WHERE = Events.VISIBLE + "=? AND "
50 + Instances.BEGIN + ">=? AND " + Instances.BEGIN + "<=? AND "
51 + Events.ALL_DAY + "=?";
52 static final String[] INSTANCES_PROJECTION = new String[] {
53 Instances.EVENT_ID,
54 Instances.BEGIN,
55 Instances.ALL_DAY,
56 };
57 private static final int INSTANCES_INDEX_EVENTID = 0;
58 private static final int INSTANCES_INDEX_BEGIN = 1;
59 private static final int INSTANCES_INDEX_ALL_DAY = 2;
60
61 private static final String REMINDERS_WHERE = Reminders.METHOD + "=1 AND "
62 + Reminders.EVENT_ID + " IN ";
63 static final String[] REMINDERS_PROJECTION = new String[] {
64 Reminders.EVENT_ID,
65 Reminders.MINUTES,
66 Reminders.METHOD,
67 };
68 private static final int REMINDERS_INDEX_EVENT_ID = 0;
69 private static final int REMINDERS_INDEX_MINUTES = 1;
70 private static final int REMINDERS_INDEX_METHOD = 2;
71
72 // Add a slight delay for the EVENT_REMINDER_APP broadcast for a couple reasons:
73 // (1) so that the concurrent reminder broadcast from the provider doesn't result
74 // in a double ring, and (2) some OEMs modified the provider to not add an alert to
75 // the CalendarAlerts table until the alert time, so for the unbundled app's
76 // notifications to work on these devices, a delay ensures that AlertService won't
77 // read from the CalendarAlerts table until the alert is present.
78 static final int ALARM_DELAY_MS = 1000;
79
80 // The reminders query looks like "SELECT ... AND eventId IN 101,102,202,...". This
81 // sets the max # of events in the query before batching into multiple queries, to
82 // limit the SQL query length.
83 private static final int REMINDER_QUERY_BATCH_SIZE = 50;
84
85 // We really need to query for reminder times that fall in some interval, but
86 // the Reminders table only stores the reminder interval (10min, 15min, etc), and
87 // we cannot do the join with the Events table to calculate the actual alert time
88 // from outside of the provider. So the best we can do for now consider events
89 // whose start times begin within some interval (ie. 1 week out). This means
90 // reminders which are configured for more than 1 week out won't fire on time. We
91 // can minimize this to being only 1 day late by putting a 1 day max on the alarm time.
92 private static final long EVENT_LOOKAHEAD_WINDOW_MS = DateUtils.WEEK_IN_MILLIS;
93 private static final long MAX_ALARM_ELAPSED_MS = DateUtils.DAY_IN_MILLIS;
94
95 /**
96 * Schedules the nearest upcoming alarm, to refresh notifications.
97 *
98 * This is historically done in the provider but we dupe this here so the unbundled
99 * app will work on devices that have modified this portion of the provider. This
100 * has the limitation of querying events within some interval from now (ie. looks at
101 * reminders for all events occurring in the next week). This means for example,
102 * a 2 week notification will not fire on time.
103 */
104 public static void scheduleNextAlarm(Context context) {
105 scheduleNextAlarm(context, AlertUtils.createAlarmManager(context),
106 REMINDER_QUERY_BATCH_SIZE, System.currentTimeMillis());
107 }
108
109 // VisibleForTesting
110 static void scheduleNextAlarm(Context context, AlarmManagerInterface alarmManager,
111 int batchSize, long currentMillis) {
112 Cursor instancesCursor = null;
113 try {
114 instancesCursor = queryUpcomingEvents(context, context.getContentResolver(),
115 currentMillis);
116 if (instancesCursor != null) {
117 queryNextReminderAndSchedule(instancesCursor, context,
118 context.getContentResolver(), alarmManager, batchSize, currentMillis);
119 }
120 } finally {
121 if (instancesCursor != null) {
122 instancesCursor.close();
123 }
124 }
125 }
126
127 /**
128 * Queries events starting within a fixed interval from now.
129 */
130 private static Cursor queryUpcomingEvents(Context context, ContentResolver contentResolver,
131 long currentMillis) {
132 Time time = new Time();
133 time.normalize(false);
134 long localOffset = time.gmtoff * 1000;
135 final long localStartMin = currentMillis;
136 final long localStartMax = localStartMin + EVENT_LOOKAHEAD_WINDOW_MS;
137 final long utcStartMin = localStartMin - localOffset;
138 final long utcStartMax = utcStartMin + EVENT_LOOKAHEAD_WINDOW_MS;
139
140 // Expand Instances table range by a day on either end to account for
141 // all-day events.
142 Uri.Builder uriBuilder = Instances.CONTENT_URI.buildUpon();
143 ContentUris.appendId(uriBuilder, localStartMin - DateUtils.DAY_IN_MILLIS);
144 ContentUris.appendId(uriBuilder, localStartMax + DateUtils.DAY_IN_MILLIS);
145
146 // Build query for all events starting within the fixed interval.
147 StringBuilder queryBuilder = new StringBuilder();
148 queryBuilder.append("(");
149 queryBuilder.append(INSTANCES_WHERE);
150 queryBuilder.append(") OR (");
151 queryBuilder.append(INSTANCES_WHERE);
152 queryBuilder.append(")");
153 String[] queryArgs = new String[] {
154 // allday selection
155 "1", /* visible = ? */
156 String.valueOf(utcStartMin), /* begin >= ? */
157 String.valueOf(utcStartMax), /* begin <= ? */
158 "1", /* allDay = ? */
159
160 // non-allday selection
161 "1", /* visible = ? */
162 String.valueOf(localStartMin), /* begin >= ? */
163 String.valueOf(localStartMax), /* begin <= ? */
164 "0" /* allDay = ? */
165 };
166
167 Cursor cursor = contentResolver.query(uriBuilder.build(), INSTANCES_PROJECTION,
168 queryBuilder.toString(), queryArgs, null);
169 return cursor;
170 }
171
172 /**
173 * Queries for all the reminders of the events in the instancesCursor, and schedules
174 * the alarm for the next upcoming reminder.
175 */
176 private static void queryNextReminderAndSchedule(Cursor instancesCursor, Context context,
177 ContentResolver contentResolver, AlarmManagerInterface alarmManager,
178 int batchSize, long currentMillis) {
179 if (AlertService.DEBUG) {
180 int eventCount = instancesCursor.getCount();
181 if (eventCount == 0) {
182 Log.d(TAG, "No events found starting within 1 week.");
183 } else {
184 Log.d(TAG, "Query result count for events starting within 1 week: " + eventCount);
185 }
186 }
187
188 // Put query results of all events starting within some interval into map of event ID to
189 // local start time.
190 Map<Integer, List<Long>> eventMap = new HashMap<Integer, List<Long>>();
191 Time timeObj = new Time();
192 long nextAlarmTime = Long.MAX_VALUE;
193 int nextAlarmEventId = 0;
194 instancesCursor.moveToPosition(-1);
195 while (!instancesCursor.isAfterLast()) {
196 int index = 0;
197 eventMap.clear();
198 StringBuilder eventIdsForQuery = new StringBuilder();
199 eventIdsForQuery.append('(');
200 while (index++ < batchSize && instancesCursor.moveToNext()) {
201 int eventId = instancesCursor.getInt(INSTANCES_INDEX_EVENTID);
202 long begin = instancesCursor.getLong(INSTANCES_INDEX_BEGIN);
203 boolean allday = instancesCursor.getInt(INSTANCES_INDEX_ALL_DAY) != 0;
204 long localStartTime;
205 if (allday) {
206 // Adjust allday to local time.
207 localStartTime = Utils.convertAlldayUtcToLocal(timeObj, begin,
208 Time.getCurrentTimezone());
209 } else {
210 localStartTime = begin;
211 }
212 List<Long> startTimes = eventMap.get(eventId);
213 if (startTimes == null) {
214 startTimes = new ArrayList<Long>();
215 eventMap.put(eventId, startTimes);
216 eventIdsForQuery.append(eventId);
217 eventIdsForQuery.append(",");
218 }
219 startTimes.add(localStartTime);
220
221 // Log for debugging.
222 if (Log.isLoggable(TAG, Log.DEBUG)) {
223 timeObj.set(localStartTime);
224 StringBuilder msg = new StringBuilder();
225 msg.append("Events cursor result -- eventId:").append(eventId);
226 msg.append(", allDay:").append(allday);
227 msg.append(", start:").append(localStartTime);
228 msg.append(" (").append(timeObj.format("%a, %b %d, %Y %I:%M%P")).append(")");
229 Log.d(TAG, msg.toString());
230 }
231 }
232 if (eventIdsForQuery.charAt(eventIdsForQuery.length() - 1) == ',') {
233 eventIdsForQuery.deleteCharAt(eventIdsForQuery.length() - 1);
234 }
235 eventIdsForQuery.append(')');
236
237 // Query the reminders table for the events found.
238 Cursor cursor = null;
239 try {
240 cursor = contentResolver.query(Reminders.CONTENT_URI, REMINDERS_PROJECTION,
241 REMINDERS_WHERE + eventIdsForQuery, null, null);
242
243 // Process the reminders query results to find the next reminder time.
244 cursor.moveToPosition(-1);
245 while (cursor.moveToNext()) {
246 int eventId = cursor.getInt(REMINDERS_INDEX_EVENT_ID);
247 int reminderMinutes = cursor.getInt(REMINDERS_INDEX_MINUTES);
248 List<Long> startTimes = eventMap.get(eventId);
249 if (startTimes != null) {
250 for (Long startTime : startTimes) {
251 long alarmTime = startTime -
252 reminderMinutes * DateUtils.MINUTE_IN_MILLIS;
253 if (alarmTime > currentMillis && alarmTime < nextAlarmTime) {
254 nextAlarmTime = alarmTime;
255 nextAlarmEventId = eventId;
256 }
257
258 if (Log.isLoggable(TAG, Log.DEBUG)) {
259 timeObj.set(alarmTime);
260 StringBuilder msg = new StringBuilder();
261 msg.append("Reminders cursor result -- eventId:").append(eventId);
262 msg.append(", startTime:").append(startTime);
263 msg.append(", minutes:").append(reminderMinutes);
264 msg.append(", alarmTime:").append(alarmTime);
265 msg.append(" (").append(timeObj.format("%a, %b %d, %Y %I:%M%P"))
266 .append(")");
267 Log.d(TAG, msg.toString());
268 }
269 }
270 }
271 }
272 } finally {
273 if (cursor != null) {
274 cursor.close();
275 }
276 }
277 }
278
279 // Schedule the alarm for the next reminder time.
280 if (nextAlarmTime < Long.MAX_VALUE) {
281 scheduleAlarm(context, nextAlarmEventId, nextAlarmTime, currentMillis, alarmManager);
282 }
283 }
284
285 /**
286 * Schedules an alarm for the EVENT_REMINDER_APP broadcast, for the specified
287 * alarm time with a slight delay (to account for the possible duplicate broadcast
288 * from the provider).
289 */
290 private static void scheduleAlarm(Context context, long eventId, long alarmTime,
291 long currentMillis, AlarmManagerInterface alarmManager) {
292 // Max out the alarm time to 1 day out, so an alert for an event far in the future
293 // (not present in our event query results for a limited range) can only be at
294 // most 1 day late.
295 long maxAlarmTime = currentMillis + MAX_ALARM_ELAPSED_MS;
296 if (alarmTime > maxAlarmTime) {
297 alarmTime = maxAlarmTime;
298 }
299
300 // Add a slight delay (see comments on the member var).
301 alarmTime += ALARM_DELAY_MS;
302
303 if (AlertService.DEBUG) {
304 Time time = new Time();
305 time.set(alarmTime);
306 String schedTime = time.format("%a, %b %d, %Y %I:%M%P");
307 Log.d(TAG, "Scheduling alarm for EVENT_REMINDER_APP broadcast for event " + eventId
308 + " at " + alarmTime + " (" + schedTime + ")");
309 }
310
311 // Schedule an EVENT_REMINDER_APP broadcast with AlarmManager. The extra is
312 // only used by AlertService for logging. It is ignored by Intent.filterEquals,
313 // so this scheduling will still overwrite the alarm that was previously pending.
314 // Note that the 'setClass' is required, because otherwise it seems the broadcast
315 // can be eaten by other apps and we somehow may never receive it.
316 Intent intent = new Intent(AlertReceiver.EVENT_REMINDER_APP_ACTION);
317 intent.setClass(context, AlertReceiver.class);
318 intent.putExtra(CalendarContract.CalendarAlerts.ALARM_TIME, alarmTime);
319 PendingIntent pi = PendingIntent.getBroadcast(context, 0, intent, 0);
320 alarmManager.set(AlarmManager.RTC_WAKEUP, alarmTime, pi);
321 }
322}