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