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