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