1 /*
2  * Copyright (C) 2010 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 
17 package com.android.providers.calendar;
18 
19 import com.android.providers.calendar.CalendarDatabaseHelper.Tables;
20 import com.android.providers.calendar.CalendarDatabaseHelper.Views;
21 import com.google.common.annotations.VisibleForTesting;
22 
23 import android.app.AlarmManager;
24 import android.app.PendingIntent;
25 import android.content.ContentResolver;
26 import android.content.Context;
27 import android.content.Intent;
28 import android.database.Cursor;
29 import android.database.sqlite.SQLiteDatabase;
30 import android.net.Uri;
31 import android.os.Build;
32 import android.os.PowerManager;
33 import android.os.PowerManager.WakeLock;
34 import android.os.SystemClock;
35 import android.provider.CalendarContract;
36 import android.provider.CalendarContract.CalendarAlerts;
37 import android.provider.CalendarContract.Calendars;
38 import android.provider.CalendarContract.Events;
39 import android.provider.CalendarContract.Instances;
40 import android.provider.CalendarContract.Reminders;
41 import android.text.format.DateUtils;
42 import android.text.format.Time;
43 import android.util.Log;
44 
45 import java.util.concurrent.atomic.AtomicBoolean;
46 
47 /**
48  * We are using the CalendarAlertManager to be able to mock the AlarmManager as the AlarmManager
49  * cannot be extended.
50  *
51  * CalendarAlertManager is delegating its calls to the real AlarmService.
52  */
53 public class CalendarAlarmManager {
54     protected static final String TAG = "CalendarAlarmManager";
55 
56     // SCHEDULE_ALARM_URI runs scheduleNextAlarm(false)
57     // SCHEDULE_ALARM_REMOVE_URI runs scheduleNextAlarm(true)
58     // TODO: use a service to schedule alarms rather than private URI
59     /* package */static final String SCHEDULE_ALARM_PATH = "schedule_alarms";
60     /* package */static final String SCHEDULE_ALARM_REMOVE_PATH = "schedule_alarms_remove";
61     /* package */static final String KEY_REMOVE_ALARMS = "removeAlarms";
62     /* package */static final Uri SCHEDULE_ALARM_REMOVE_URI = Uri.withAppendedPath(
63             CalendarContract.CONTENT_URI, SCHEDULE_ALARM_REMOVE_PATH);
64     /* package */static final Uri SCHEDULE_ALARM_URI = Uri.withAppendedPath(
65             CalendarContract.CONTENT_URI, SCHEDULE_ALARM_PATH);
66 
67     /**
68      * If no alarms are scheduled in the next 24h, check for future alarms again after this period
69      * has passed. Scheduling the check 15 minutes earlier than 24h to prevent the scheduler alarm
70      * from using up the alarms quota for reminders during dozing.
71      *
72      * @see AlarmManager#setExactAndAllowWhileIdle
73      */
74     private static final long ALARM_CHECK_WHEN_NO_ALARM_IS_SCHEDULED_INTERVAL_MILLIS =
75             DateUtils.DAY_IN_MILLIS - (15 * DateUtils.MINUTE_IN_MILLIS);
76 
77     static final String INVALID_CALENDARALERTS_SELECTOR =
78     "_id IN (SELECT ca." + CalendarAlerts._ID + " FROM "
79             + Tables.CALENDAR_ALERTS + " AS ca"
80             + " LEFT OUTER JOIN " + Tables.INSTANCES
81             + " USING (" + Instances.EVENT_ID + ","
82             + Instances.BEGIN + "," + Instances.END + ")"
83             + " LEFT OUTER JOIN " + Tables.REMINDERS + " AS r ON"
84             + " (ca." + CalendarAlerts.EVENT_ID + "=r." + Reminders.EVENT_ID
85             + " AND ca." + CalendarAlerts.MINUTES + "=r." + Reminders.MINUTES + ")"
86             + " LEFT OUTER JOIN " + Views.EVENTS + " AS e ON"
87             + " (ca." + CalendarAlerts.EVENT_ID + "=e." + Events._ID + ")"
88             + " WHERE " + Tables.INSTANCES + "." + Instances.BEGIN + " ISNULL"
89             + "   OR ca." + CalendarAlerts.ALARM_TIME + "<?"
90             + "   OR (r." + Reminders.MINUTES + " ISNULL"
91             + "       AND ca." + CalendarAlerts.MINUTES + "<>0)"
92             + "   OR e." + Calendars.VISIBLE + "=0)";
93 
94     /**
95      * We search backward in time for event reminders that we may have missed
96      * and schedule them if the event has not yet expired. The amount in the
97      * past to search backwards is controlled by this constant. It should be at
98      * least a few minutes to allow for an event that was recently created on
99      * the web to make its way to the phone. Two hours might seem like overkill,
100      * but it is useful in the case where the user just crossed into a new
101      * timezone and might have just missed an alarm.
102      */
103     private static final long SCHEDULE_ALARM_SLACK = 2 * DateUtils.HOUR_IN_MILLIS;
104     /**
105      * Alarms older than this threshold will be deleted from the CalendarAlerts
106      * table. This should be at least a day because if the timezone is wrong and
107      * the user corrects it we might delete good alarms that appear to be old
108      * because the device time was incorrectly in the future. This threshold
109      * must also be larger than SCHEDULE_ALARM_SLACK. We add the
110      * SCHEDULE_ALARM_SLACK to ensure this. To make it easier to find and debug
111      * problems with missed reminders, set this to something greater than a day.
112      */
113     private static final long CLEAR_OLD_ALARM_THRESHOLD = 7 * DateUtils.DAY_IN_MILLIS
114             + SCHEDULE_ALARM_SLACK;
115     private static final String SCHEDULE_NEXT_ALARM_WAKE_LOCK = "ScheduleNextAlarmWakeLock";
116     protected static final String ACTION_CHECK_NEXT_ALARM =
117             "com.android.providers.calendar.intent.CalendarProvider2";
118     static final int ALARM_CHECK_DELAY_MILLIS = 5000;
119 
120     /**
121      * Used for tracking if the next alarm is already scheduled
122      */
123     @VisibleForTesting
124     protected AtomicBoolean mNextAlarmCheckScheduled;
125     /**
126      * Used for synchronization
127      */
128     @VisibleForTesting
129     protected Object mAlarmLock;
130     /**
131      * Used to keep the process from getting killed while scheduling alarms
132      */
133     private final WakeLock mScheduleNextAlarmWakeLock;
134 
135     @VisibleForTesting
136     protected Context mContext;
137     private AlarmManager mAlarmManager;
138 
CalendarAlarmManager(Context context)139     public CalendarAlarmManager(Context context) {
140         initializeWithContext(context);
141 
142         PowerManager powerManager = (PowerManager) mContext.getSystemService(
143                 Context.POWER_SERVICE);
144         // Create a wake lock that will be used when we are actually
145         // scheduling the next alarm
146         mScheduleNextAlarmWakeLock = powerManager.newWakeLock(
147                 PowerManager.PARTIAL_WAKE_LOCK, SCHEDULE_NEXT_ALARM_WAKE_LOCK);
148         // We want the Wake Lock to be reference counted (so that we dont
149         // need to take care
150         // about its reference counting)
151         mScheduleNextAlarmWakeLock.setReferenceCounted(true);
152     }
153 
initializeWithContext(Context context)154     protected void initializeWithContext(Context context) {
155         mContext = context;
156         mAlarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
157         mNextAlarmCheckScheduled = new AtomicBoolean(false);
158         mAlarmLock = new Object();
159     }
160 
getCheckNextAlarmIntent(boolean removeAlarms)161     private Intent getCheckNextAlarmIntent(boolean removeAlarms) {
162         Intent intent = new Intent(CalendarAlarmManager.ACTION_CHECK_NEXT_ALARM);
163         intent.setClass(mContext, CalendarProviderBroadcastReceiver.class);
164         intent.putExtra(KEY_REMOVE_ALARMS, removeAlarms);
165         return intent;
166     }
167 
168     /**
169      * Called by CalendarProvider to check the next alarm. A small delay is added before the real
170      * checking happens in order to batch the requests.
171      *
172      * @param removeAlarms Remove scheduled alarms or not. See @{link
173      *                     #removeScheduledAlarmsLocked} for details.
174      */
checkNextAlarm(boolean removeAlarms)175     void checkNextAlarm(boolean removeAlarms) {
176         // We must always run the following when 'removeAlarms' is true.  Previously it
177         // was possible to have a race condition on startup between TIME_CHANGED and
178         // BOOT_COMPLETED broadcast actions.  This resulted in alarms being
179         // missed (Bug 7221716) when the TIME_CHANGED broadcast ('removeAlarms' = false)
180         // happened right before the BOOT_COMPLETED ('removeAlarms' = true), and the
181         // BOOT_COMPLETED action was skipped since there was concurrent scheduling in progress.
182         if (!mNextAlarmCheckScheduled.getAndSet(true) || removeAlarms) {
183             if (Log.isLoggable(CalendarProvider2.TAG, Log.DEBUG)) {
184                 Log.d(CalendarProvider2.TAG, "Scheduling check of next Alarm");
185             }
186             Intent intent = getCheckNextAlarmIntent(removeAlarms);
187             PendingIntent pending = PendingIntent.getBroadcast(mContext, 0 /* ignored */, intent,
188                     PendingIntent.FLAG_NO_CREATE);
189             if (pending != null) {
190                 // Cancel any previous Alarm check requests
191                 cancel(pending);
192             }
193             pending = PendingIntent.getBroadcast(mContext, 0 /* ignored */, intent,
194                     PendingIntent.FLAG_CANCEL_CURRENT);
195 
196             // Trigger the check in 5s from now, so that we can have batch processing.
197             long triggerAtTime = SystemClock.elapsedRealtime() + ALARM_CHECK_DELAY_MILLIS;
198             // Given to the short delay, we just use setExact here.
199             setExact(AlarmManager.ELAPSED_REALTIME_WAKEUP, triggerAtTime, pending);
200         }
201     }
202 
203     /**
204      * Similar to {@link #checkNextAlarm}, but schedule the checking at specific {@code
205      * triggerTime}. In general, we do not need an alarm for scheduling. Instead we set the next
206      * alarm check immediately when a reminder is shown. The only use case for this
207      * is to schedule the next alarm check when there is no reminder within 1 day.
208      *
209      * @param triggerTimeMillis Time to run the next alarm check, in milliseconds.
210      */
scheduleNextAlarmCheck(long triggerTimeMillis)211     void scheduleNextAlarmCheck(long triggerTimeMillis) {
212         Intent intent = getCheckNextAlarmIntent(false /* removeAlarms*/);
213         PendingIntent pending = PendingIntent.getBroadcast(
214                 mContext, 0, intent, PendingIntent.FLAG_NO_CREATE);
215         if (pending != null) {
216             // Cancel any previous alarms that do the same thing.
217             cancel(pending);
218         }
219         pending = PendingIntent.getBroadcast(
220                 mContext, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT);
221 
222         if (Log.isLoggable(CalendarProvider2.TAG, Log.DEBUG)) {
223             Time time = new Time();
224             time.set(triggerTimeMillis);
225             String timeStr = time.format(" %a, %b %d, %Y %I:%M%P");
226             Log.d(CalendarProvider2.TAG,
227                     "scheduleNextAlarmCheck at: " + triggerTimeMillis + timeStr);
228         }
229         setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, triggerTimeMillis, pending);
230     }
231 
getScheduleNextAlarmWakeLock()232     PowerManager.WakeLock getScheduleNextAlarmWakeLock() {
233         return mScheduleNextAlarmWakeLock;
234     }
235 
acquireScheduleNextAlarmWakeLock()236     void acquireScheduleNextAlarmWakeLock() {
237         getScheduleNextAlarmWakeLock().acquire();
238     }
239 
releaseScheduleNextAlarmWakeLock()240     void releaseScheduleNextAlarmWakeLock() {
241         try {
242             getScheduleNextAlarmWakeLock().release();
243         } catch (RuntimeException e) {
244             if (!e.getMessage().startsWith("WakeLock under-locked ")) {
245               throw e;
246             }
247             Log.w(TAG, "WakeLock under-locked ignored.");
248         }
249     }
250 
rescheduleMissedAlarms()251     void rescheduleMissedAlarms() {
252         rescheduleMissedAlarms(mContext.getContentResolver());
253     }
254 
255     /**
256      * This method runs in a background thread and schedules an alarm for the
257      * next calendar event, if necessary.
258      *
259      * @param removeAlarms
260      * @param cp2
261      */
runScheduleNextAlarm(boolean removeAlarms, CalendarProvider2 cp2)262     void runScheduleNextAlarm(boolean removeAlarms, CalendarProvider2 cp2) {
263         SQLiteDatabase db = cp2.mDb;
264         if (db == null) {
265             return;
266         }
267 
268         // Reset so that we can accept other schedules of next alarm
269         mNextAlarmCheckScheduled.set(false);
270         db.beginTransaction();
271         try {
272             if (removeAlarms) {
273                 removeScheduledAlarmsLocked(db);
274             }
275             scheduleNextAlarmLocked(db, cp2);
276             db.setTransactionSuccessful();
277         } finally {
278             db.endTransaction();
279         }
280     }
281 
282     /**
283      * This method looks at the 24-hour window from now for any events that it
284      * needs to schedule. This method runs within a database transaction. It
285      * also runs in a background thread. The CalendarProvider2 keeps track of
286      * which alarms it has already scheduled to avoid scheduling them more than
287      * once and for debugging problems with alarms. It stores this knowledge in
288      * a database table called CalendarAlerts which persists across reboots. But
289      * the actual alarm list is in memory and disappears if the phone loses
290      * power. To avoid missing an alarm, we clear the entries in the
291      * CalendarAlerts table when we start up the CalendarProvider2. Scheduling
292      * an alarm multiple times is not tragic -- we filter out the extra ones
293      * when we receive them. But we still need to keep track of the scheduled
294      * alarms. The main reason is that we need to prevent multiple notifications
295      * for the same alarm (on the receive side) in case we accidentally schedule
296      * the same alarm multiple times. We don't have visibility into the system's
297      * alarm list so we can never know for sure if we have already scheduled an
298      * alarm and it's better to err on scheduling an alarm twice rather than
299      * missing an alarm. Another reason we keep track of scheduled alarms in a
300      * database table is that it makes it easy to run an SQL query to find the
301      * next reminder that we haven't scheduled.
302      *
303      * @param db the database
304      * @param cp2 TODO
305      */
scheduleNextAlarmLocked(SQLiteDatabase db, CalendarProvider2 cp2)306     private void scheduleNextAlarmLocked(SQLiteDatabase db, CalendarProvider2 cp2) {
307         Time time = new Time();
308 
309         final long currentMillis = System.currentTimeMillis();
310         final long start = currentMillis - SCHEDULE_ALARM_SLACK;
311         final long end = start + (24 * 60 * 60 * 1000);
312         if (Log.isLoggable(CalendarProvider2.TAG, Log.DEBUG)) {
313             time.set(start);
314             String startTimeStr = time.format(" %a, %b %d, %Y %I:%M%P");
315             Log.d(CalendarProvider2.TAG, "runScheduleNextAlarm() start search: " + startTimeStr);
316         }
317 
318         // Delete rows in CalendarAlert where the corresponding Instance or
319         // Reminder no longer exist.
320         // Also clear old alarms but keep alarms around for a while to prevent
321         // multiple alerts for the same reminder. The "clearUpToTime'
322         // should be further in the past than the point in time where
323         // we start searching for events (the "start" variable defined above).
324         String selectArg[] = new String[] { Long.toString(
325                 currentMillis - CLEAR_OLD_ALARM_THRESHOLD) };
326 
327         int rowsDeleted = db.delete(
328                 CalendarAlerts.TABLE_NAME, INVALID_CALENDARALERTS_SELECTOR, selectArg);
329 
330         long nextAlarmTime = end;
331         final ContentResolver resolver = mContext.getContentResolver();
332         final long tmpAlarmTime = CalendarAlerts.findNextAlarmTime(resolver, currentMillis);
333         if (tmpAlarmTime != -1 && tmpAlarmTime < nextAlarmTime) {
334             nextAlarmTime = tmpAlarmTime;
335         }
336 
337         // Extract events from the database sorted by alarm time. The
338         // alarm times are computed from Instances.begin (whose units
339         // are milliseconds) and Reminders.minutes (whose units are
340         // minutes).
341         //
342         // Also, ignore events whose end time is already in the past.
343         // Also, ignore events alarms that we have already scheduled.
344         //
345         // Note 1: we can add support for the case where Reminders.minutes
346         // equals -1 to mean use Calendars.minutes by adding a UNION for
347         // that case where the two halves restrict the WHERE clause on
348         // Reminders.minutes != -1 and Reminders.minutes = 1, respectively.
349         //
350         // Note 2: we have to name "myAlarmTime" different from the
351         // "alarmTime" column in CalendarAlerts because otherwise the
352         // query won't find multiple alarms for the same event.
353         //
354         // The CAST is needed in the query because otherwise the expression
355         // will be untyped and sqlite3's manifest typing will not convert the
356         // string query parameter to an int in myAlarmtime>=?, so the comparison
357         // will fail. This could be simplified if bug 2464440 is resolved.
358 
359         time.setToNow();
360         time.normalize(false);
361         long localOffset = time.gmtoff * 1000;
362 
363         String allDayOffset = " -(" + localOffset + ") ";
364         String subQueryPrefix = "SELECT " + Instances.BEGIN;
365         String subQuerySuffix = " -(" + Reminders.MINUTES + "*" + +DateUtils.MINUTE_IN_MILLIS + ")"
366                 + " AS myAlarmTime" + "," + Tables.INSTANCES + "." + Instances.EVENT_ID
367                 + " AS eventId" + "," + Instances.BEGIN + "," + Instances.END + ","
368                 + Instances.TITLE + "," + Instances.ALL_DAY + "," + Reminders.METHOD + ","
369                 + Reminders.MINUTES + " FROM " + Tables.INSTANCES + " INNER JOIN " + Views.EVENTS
370                 + " ON (" + Views.EVENTS + "." + Events._ID + "=" + Tables.INSTANCES + "."
371                 + Instances.EVENT_ID + ")" + " INNER JOIN " + Tables.REMINDERS + " ON ("
372                 + Tables.INSTANCES + "." + Instances.EVENT_ID + "=" + Tables.REMINDERS + "."
373                 + Reminders.EVENT_ID + ")" + " WHERE " + Calendars.VISIBLE + "=1"
374                 + " AND myAlarmTime>=CAST(? AS INT)" + " AND myAlarmTime<=CAST(? AS INT)" + " AND "
375                 + Instances.END + ">=?" + " AND " + Reminders.METHOD + "=" + Reminders.METHOD_ALERT;
376 
377         // we query separately for all day events to convert to local time from
378         // UTC
379         // we need to /subtract/ the offset to get the correct resulting local
380         // time
381         String allDayQuery = subQueryPrefix + allDayOffset + subQuerySuffix + " AND "
382                 + Instances.ALL_DAY + "=1";
383         String nonAllDayQuery = subQueryPrefix + subQuerySuffix + " AND " + Instances.ALL_DAY
384                 + "=0";
385 
386         // we use UNION ALL because we are guaranteed to have no dupes between
387         // the two queries, and it is less expensive
388         String query = "SELECT *" + " FROM (" + allDayQuery + " UNION ALL " + nonAllDayQuery + ")"
389         // avoid rescheduling existing alarms
390                 + " WHERE 0=(SELECT count(*) FROM " + Tables.CALENDAR_ALERTS + " CA" + " WHERE CA."
391                 + CalendarAlerts.EVENT_ID + "=eventId" + " AND CA." + CalendarAlerts.BEGIN + "="
392                 + Instances.BEGIN + " AND CA." + CalendarAlerts.ALARM_TIME + "=myAlarmTime)"
393                 + " ORDER BY myAlarmTime," + Instances.BEGIN + "," + Instances.TITLE;
394 
395         String queryParams[] = new String[] { String.valueOf(start), String.valueOf(nextAlarmTime),
396                 String.valueOf(currentMillis), String.valueOf(start), String.valueOf(nextAlarmTime),
397                 String.valueOf(currentMillis) };
398 
399         String instancesTimezone = cp2.mCalendarCache.readTimezoneInstances();
400         final String timezoneType = cp2.mCalendarCache.readTimezoneType();
401         boolean isHomeTimezone = CalendarCache.TIMEZONE_TYPE_HOME.equals(timezoneType);
402         // expand this range by a day on either end to account for all day
403         // events
404         cp2.acquireInstanceRangeLocked(
405                 start - DateUtils.DAY_IN_MILLIS, end + DateUtils.DAY_IN_MILLIS, false /*
406                                                                                        * don't
407                                                                                        * use
408                                                                                        * minimum
409                                                                                        * expansion
410                                                                                        * windows
411                                                                                        */,
412                 false /* do not force Instances deletion and expansion */, instancesTimezone,
413                 isHomeTimezone);
414         Cursor cursor = null;
415         try {
416             cursor = db.rawQuery(query, queryParams);
417 
418             final int beginIndex = cursor.getColumnIndex(Instances.BEGIN);
419             final int endIndex = cursor.getColumnIndex(Instances.END);
420             final int eventIdIndex = cursor.getColumnIndex("eventId");
421             final int alarmTimeIndex = cursor.getColumnIndex("myAlarmTime");
422             final int minutesIndex = cursor.getColumnIndex(Reminders.MINUTES);
423 
424             if (Log.isLoggable(CalendarProvider2.TAG, Log.DEBUG)) {
425                 time.set(nextAlarmTime);
426                 String alarmTimeStr = time.format(" %a, %b %d, %Y %I:%M%P");
427                 Log.d(CalendarProvider2.TAG,
428                         "cursor results: " + cursor.getCount() + " nextAlarmTime: " + alarmTimeStr);
429             }
430 
431             while (cursor.moveToNext()) {
432                 // Schedule all alarms whose alarm time is as early as any
433                 // scheduled alarm. For example, if the earliest alarm is at
434                 // 1pm, then we will schedule all alarms that occur at 1pm
435                 // but no alarms that occur later than 1pm.
436                 // Actually, we allow alarms up to a minute later to also
437                 // be scheduled so that we don't have to check immediately
438                 // again after an event alarm goes off.
439                 final long alarmTime = cursor.getLong(alarmTimeIndex);
440                 final long eventId = cursor.getLong(eventIdIndex);
441                 final int minutes = cursor.getInt(minutesIndex);
442                 final long startTime = cursor.getLong(beginIndex);
443                 final long endTime = cursor.getLong(endIndex);
444 
445                 if (Log.isLoggable(CalendarProvider2.TAG, Log.DEBUG)) {
446                     time.set(alarmTime);
447                     String schedTime = time.format(" %a, %b %d, %Y %I:%M%P");
448                     time.set(startTime);
449                     String startTimeStr = time.format(" %a, %b %d, %Y %I:%M%P");
450 
451                     Log.d(CalendarProvider2.TAG,
452                             "  looking at id: " + eventId + " " + startTime + startTimeStr
453                                     + " alarm: " + alarmTime + schedTime);
454                 }
455 
456                 if (alarmTime < nextAlarmTime) {
457                     nextAlarmTime = alarmTime;
458                 } else if (alarmTime > nextAlarmTime + DateUtils.MINUTE_IN_MILLIS) {
459                     // This event alarm (and all later ones) will be scheduled
460                     // later.
461                     if (Log.isLoggable(CalendarProvider2.TAG, Log.DEBUG)) {
462                         Log.d(CalendarProvider2.TAG,
463                                 "This event alarm (and all later ones) will be scheduled later");
464                     }
465                     break;
466                 }
467 
468                 // Avoid an SQLiteContraintException by checking if this alarm
469                 // already exists in the table.
470                 if (CalendarAlerts.alarmExists(resolver, eventId, startTime, alarmTime)) {
471                     if (Log.isLoggable(CalendarProvider2.TAG, Log.DEBUG)) {
472                         int titleIndex = cursor.getColumnIndex(Events.TITLE);
473                         String title = cursor.getString(titleIndex);
474                         Log.d(CalendarProvider2.TAG,
475                                 "  alarm exists for id: " + eventId + " " + title);
476                     }
477                     continue;
478                 }
479 
480                 // Insert this alarm into the CalendarAlerts table
481                 Uri uri = CalendarAlerts.insert(
482                         resolver, eventId, startTime, endTime, alarmTime, minutes);
483                 if (uri == null) {
484                     if (Log.isLoggable(CalendarProvider2.TAG, Log.ERROR)) {
485                         Log.e(CalendarProvider2.TAG, "runScheduleNextAlarm() insert into "
486                                 + "CalendarAlerts table failed");
487                     }
488                     continue;
489                 }
490 
491                 scheduleAlarm(alarmTime);
492             }
493         } finally {
494             if (cursor != null) {
495                 cursor.close();
496             }
497         }
498 
499         // Refresh notification bar
500         if (rowsDeleted > 0) {
501             scheduleAlarm(currentMillis);
502         }
503 
504         // No event alarm is scheduled, check again in 24 hours. If a new
505         // event is inserted before the next alarm check, then this method
506         // will be run again when the new event is inserted.
507         if (nextAlarmTime == Long.MAX_VALUE) {
508             scheduleNextAlarmCheck(
509                     currentMillis + ALARM_CHECK_WHEN_NO_ALARM_IS_SCHEDULED_INTERVAL_MILLIS);
510         }
511     }
512 
513     /**
514      * Removes the entries in the CalendarAlerts table for alarms that we have
515      * scheduled but that have not fired yet. We do this to ensure that we don't
516      * miss an alarm. The CalendarAlerts table keeps track of the alarms that we
517      * have scheduled but the actual alarm list is in memory and will be cleared
518      * if the phone reboots. We don't need to remove entries that have already
519      * fired, and in fact we should not remove them because we need to display
520      * the notifications until the user dismisses them. We could remove entries
521      * that have fired and been dismissed, but we leave them around for a while
522      * because it makes it easier to debug problems. Entries that are old enough
523      * will be cleaned up later when we schedule new alarms.
524      */
removeScheduledAlarmsLocked(SQLiteDatabase db)525     private static void removeScheduledAlarmsLocked(SQLiteDatabase db) {
526         if (Log.isLoggable(CalendarProvider2.TAG, Log.DEBUG)) {
527             Log.d(CalendarProvider2.TAG, "removing scheduled alarms");
528         }
529         db.delete(CalendarAlerts.TABLE_NAME, CalendarAlerts.STATE + "="
530                 + CalendarAlerts.STATE_SCHEDULED, null /* whereArgs */);
531     }
532 
setExact(int type, long triggerAtTime, PendingIntent operation)533     public void setExact(int type, long triggerAtTime, PendingIntent operation) {
534         mAlarmManager.setExact(type, triggerAtTime, operation);
535     }
536 
setExactAndAllowWhileIdle(int type, long triggerAtTime, PendingIntent operation)537     public void setExactAndAllowWhileIdle(int type, long triggerAtTime, PendingIntent operation) {
538         mAlarmManager.setExactAndAllowWhileIdle(type, triggerAtTime, operation);
539     }
540 
cancel(PendingIntent operation)541     public void cancel(PendingIntent operation) {
542         mAlarmManager.cancel(operation);
543     }
544 
scheduleAlarm(long alarmTime)545     public void scheduleAlarm(long alarmTime) {
546         // Debug log for investigating dozing related bugs, remove it once we confirm it is stable.
547         if (Build.IS_DEBUGGABLE) {
548             Log.d(TAG, "schedule reminder alarm fired at " + alarmTime);
549         }
550         CalendarContract.CalendarAlerts.scheduleAlarm(mContext, mAlarmManager, alarmTime);
551     }
552 
rescheduleMissedAlarms(ContentResolver cr)553     public void rescheduleMissedAlarms(ContentResolver cr) {
554         CalendarContract.CalendarAlerts.rescheduleMissedAlarms(cr, mContext, mAlarmManager);
555     }
556 }
557