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.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     /* 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     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     /** 24 hours - 15 minutes. */
110     static final long NEXT_ALARM_CHECK_TIME_MS = DateUtils.DAY_IN_MILLIS -
111             (15 * DateUtils.MINUTE_IN_MILLIS);
112 
113     /**
114      * Used for tracking if the next alarm is already scheduled
115      */
116     @VisibleForTesting
117     protected AtomicBoolean mNextAlarmCheckScheduled;
118     /**
119      * Used for synchronization
120      */
121     @VisibleForTesting
122     protected Object mAlarmLock;
123 
124     @VisibleForTesting
125     protected Context mContext;
126     private AlarmManager mAlarmManager;
127 
CalendarAlarmManager(Context context)128     public CalendarAlarmManager(Context context) {
129         initializeWithContext(context);
130     }
131 
initializeWithContext(Context context)132     protected void initializeWithContext(Context context) {
133         mContext = context;
134         mAlarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
135         mNextAlarmCheckScheduled = new AtomicBoolean(false);
136         mAlarmLock = new Object();
137     }
138 
139     @VisibleForTesting
getCheckNextAlarmIntent(Context context, boolean removeAlarms)140     static Intent getCheckNextAlarmIntent(Context context, boolean removeAlarms) {
141         Intent intent = new Intent(CalendarAlarmManager.ACTION_CHECK_NEXT_ALARM);
142         intent.setClass(context, CalendarProviderBroadcastReceiver.class);
143         if (removeAlarms) {
144             intent.putExtra(KEY_REMOVE_ALARMS, true);
145         }
146         return intent;
147     }
148 
getCheckNextAlarmIntentForBroadcast(Context context)149     public static Intent getCheckNextAlarmIntentForBroadcast(Context context) {
150         return getCheckNextAlarmIntent(context, false);
151     }
152 
153     /**
154      * Called by CalendarProvider to check the next alarm. A small delay is added before the real
155      * checking happens in order to batch the requests.
156      *
157      * @param removeAlarms Remove scheduled alarms or not. See @{link
158      *                     #removeScheduledAlarmsLocked} for details.
159      */
checkNextAlarm(boolean removeAlarms)160     void checkNextAlarm(boolean removeAlarms) {
161         // We must always run the following when 'removeAlarms' is true.  Previously it
162         // was possible to have a race condition on startup between TIME_CHANGED and
163         // BOOT_COMPLETED broadcast actions.  This resulted in alarms being
164         // missed (Bug 7221716) when the TIME_CHANGED broadcast ('removeAlarms' = false)
165         // happened right before the BOOT_COMPLETED ('removeAlarms' = true), and the
166         // BOOT_COMPLETED action was skipped since there was concurrent scheduling in progress.
167         if (!mNextAlarmCheckScheduled.getAndSet(true) || removeAlarms) {
168             if (Log.isLoggable(CalendarProvider2.TAG, Log.DEBUG)) {
169                 Log.d(CalendarProvider2.TAG, "Scheduling check of next Alarm");
170             }
171             Intent intent = getCheckNextAlarmIntent(mContext, removeAlarms);
172             PendingIntent pending = PendingIntent.getBroadcast(mContext, 0 /* ignored */, intent,
173                     PendingIntent.FLAG_NO_CREATE);
174             if (pending != null) {
175                 // Cancel any previous Alarm check requests
176                 cancel(pending);
177             }
178             pending = PendingIntent.getBroadcast(mContext, 0 /* ignored */, intent,
179                     PendingIntent.FLAG_CANCEL_CURRENT);
180 
181             // Trigger the check in 5s from now, so that we can have batch processing.
182             long triggerAtTime = SystemClock.elapsedRealtime() + ALARM_CHECK_DELAY_MILLIS;
183             // Given to the short delay, we just use setExact here.
184             setExactAndAllowWhileIdle(AlarmManager.ELAPSED_REALTIME_WAKEUP, triggerAtTime, pending);
185         }
186     }
187 
checkNextAlarmCheckRightNow(Context context)188     static void checkNextAlarmCheckRightNow(Context context) {
189         // We should probably call scheduleNextAlarmLocked() directly but we don't want
190         // to mix java synchronization and DB transactions that might cause deadlocks, so we
191         // just send a broadcast to serialize all the calls.
192         context.sendBroadcast(getCheckNextAlarmIntentForBroadcast(context));
193     }
194 
rescheduleMissedAlarms()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 removeAlarms
204      * @param cp2
205      */
runScheduleNextAlarm(boolean removeAlarms, CalendarProvider2 cp2)206     void runScheduleNextAlarm(boolean removeAlarms, CalendarProvider2 cp2) {
207         SQLiteDatabase db = cp2.getWritableDatabase();
208         if (db == null) {
209             Log.wtf(CalendarProvider2.TAG, "Unable to get the database.");
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     /**
228      * This method looks at the 24-hour window from now for any events that it
229      * needs to schedule. This method runs within a database transaction. It
230      * also runs in a background thread. The CalendarProvider2 keeps track of
231      * which alarms it has already scheduled to avoid scheduling them more than
232      * once and for debugging problems with alarms. It stores this knowledge in
233      * a database table called CalendarAlerts which persists across reboots. But
234      * the actual alarm list is in memory and disappears if the phone loses
235      * power. To avoid missing an alarm, we clear the entries in the
236      * CalendarAlerts table when we start up the CalendarProvider2. Scheduling
237      * an alarm multiple times is not tragic -- we filter out the extra ones
238      * when we receive them. But we still need to keep track of the scheduled
239      * alarms. The main reason is that we need to prevent multiple notifications
240      * for the same alarm (on the receive side) in case we accidentally schedule
241      * the same alarm multiple times. We don't have visibility into the system's
242      * alarm list so we can never know for sure if we have already scheduled an
243      * alarm and it's better to err on scheduling an alarm twice rather than
244      * missing an alarm. Another reason we keep track of scheduled alarms in a
245      * database table is that it makes it easy to run an SQL query to find the
246      * next reminder that we haven't scheduled.
247      *
248      * @param db the database
249      * @param cp2 TODO
250      */
scheduleNextAlarmLocked(SQLiteDatabase db, CalendarProvider2 cp2)251     private void scheduleNextAlarmLocked(SQLiteDatabase db, CalendarProvider2 cp2) {
252         cp2.mSanityChecker.updateLastCheckTime();
253 
254         Time time = new Time();
255 
256         final long currentMillis = System.currentTimeMillis();
257         final long start = currentMillis - SCHEDULE_ALARM_SLACK;
258         final long end = currentMillis + DateUtils.DAY_IN_MILLIS;
259 
260         boolean alarmScheduled = false;
261 
262         if (Log.isLoggable(CalendarProvider2.TAG, Log.DEBUG)) {
263             time.set(start);
264             String startTimeStr = time.format(" %a, %b %d, %Y %I:%M%P");
265             Log.d(CalendarProvider2.TAG, "runScheduleNextAlarm() start search: " + startTimeStr);
266         }
267 
268         // Delete rows in CalendarAlert where the corresponding Instance or
269         // Reminder no longer exist.
270         // Also clear old alarms but keep alarms around for a while to prevent
271         // multiple alerts for the same reminder. The "clearUpToTime'
272         // should be further in the past than the point in time where
273         // we start searching for events (the "start" variable defined above).
274         String selectArg[] = new String[] { Long.toString(
275                 currentMillis - CLEAR_OLD_ALARM_THRESHOLD) };
276 
277         int rowsDeleted = db.delete(
278                 CalendarAlerts.TABLE_NAME, INVALID_CALENDARALERTS_SELECTOR, selectArg);
279 
280         long nextAlarmTime = end;
281         final ContentResolver resolver = mContext.getContentResolver();
282         final long tmpAlarmTime = CalendarAlerts.findNextAlarmTime(resolver, currentMillis);
283         if (tmpAlarmTime != -1 && tmpAlarmTime < nextAlarmTime) {
284             nextAlarmTime = tmpAlarmTime;
285         }
286 
287         // Extract events from the database sorted by alarm time. The
288         // alarm times are computed from Instances.begin (whose units
289         // are milliseconds) and Reminders.minutes (whose units are
290         // minutes).
291         //
292         // Also, ignore events whose end time is already in the past.
293         // Also, ignore events alarms that we have already scheduled.
294         //
295         // Note 1: we can add support for the case where Reminders.minutes
296         // equals -1 to mean use Calendars.minutes by adding a UNION for
297         // that case where the two halves restrict the WHERE clause on
298         // Reminders.minutes != -1 and Reminders.minutes = 1, respectively.
299         //
300         // Note 2: we have to name "myAlarmTime" different from the
301         // "alarmTime" column in CalendarAlerts because otherwise the
302         // query won't find multiple alarms for the same event.
303         //
304         // The CAST is needed in the query because otherwise the expression
305         // will be untyped and sqlite3's manifest typing will not convert the
306         // string query parameter to an int in myAlarmtime>=?, so the comparison
307         // will fail. This could be simplified if bug 2464440 is resolved.
308 
309         time.setToNow();
310         time.normalize(false);
311         long localOffset = time.gmtoff * 1000;
312 
313         String allDayOffset = " -(" + localOffset + ") ";
314         String subQueryPrefix = "SELECT " + Instances.BEGIN;
315         String subQuerySuffix = " -(" + Reminders.MINUTES + "*" + +DateUtils.MINUTE_IN_MILLIS + ")"
316                 + " AS myAlarmTime" + "," + Tables.INSTANCES + "." + Instances.EVENT_ID
317                 + " AS eventId" + "," + Instances.BEGIN + "," + Instances.END + ","
318                 + Instances.TITLE + "," + Instances.ALL_DAY + "," + Reminders.METHOD + ","
319                 + Reminders.MINUTES + " FROM " + Tables.INSTANCES + " INNER JOIN " + Views.EVENTS
320                 + " ON (" + Views.EVENTS + "." + Events._ID + "=" + Tables.INSTANCES + "."
321                 + Instances.EVENT_ID + ")" + " INNER JOIN " + Tables.REMINDERS + " ON ("
322                 + Tables.INSTANCES + "." + Instances.EVENT_ID + "=" + Tables.REMINDERS + "."
323                 + Reminders.EVENT_ID + ")" + " WHERE " + Calendars.VISIBLE + "=1"
324                 + " AND myAlarmTime>=CAST(? AS INT)" + " AND myAlarmTime<=CAST(? AS INT)" + " AND "
325                 + Instances.END + ">=?" + " AND " + Reminders.METHOD + "=" + Reminders.METHOD_ALERT;
326 
327         // we query separately for all day events to convert to local time from
328         // UTC
329         // we need to /subtract/ the offset to get the correct resulting local
330         // time
331         String allDayQuery = subQueryPrefix + allDayOffset + subQuerySuffix + " AND "
332                 + Instances.ALL_DAY + "=1";
333         String nonAllDayQuery = subQueryPrefix + subQuerySuffix + " AND " + Instances.ALL_DAY
334                 + "=0";
335 
336         // we use UNION ALL because we are guaranteed to have no dupes between
337         // the two queries, and it is less expensive
338         String query = "SELECT *" + " FROM (" + allDayQuery + " UNION ALL " + nonAllDayQuery + ")"
339         // avoid rescheduling existing alarms
340                 + " WHERE 0=(SELECT count(*) FROM " + Tables.CALENDAR_ALERTS + " CA" + " WHERE CA."
341                 + CalendarAlerts.EVENT_ID + "=eventId" + " AND CA." + CalendarAlerts.BEGIN + "="
342                 + Instances.BEGIN + " AND CA." + CalendarAlerts.ALARM_TIME + "=myAlarmTime)"
343                 + " ORDER BY myAlarmTime," + Instances.BEGIN + "," + Instances.TITLE;
344 
345         String queryParams[] = new String[] { String.valueOf(start), String.valueOf(nextAlarmTime),
346                 String.valueOf(currentMillis), String.valueOf(start), String.valueOf(nextAlarmTime),
347                 String.valueOf(currentMillis) };
348 
349         String instancesTimezone = cp2.mCalendarCache.readTimezoneInstances();
350         final String timezoneType = cp2.mCalendarCache.readTimezoneType();
351         boolean isHomeTimezone = CalendarCache.TIMEZONE_TYPE_HOME.equals(timezoneType);
352         // expand this range by a day on either end to account for all day
353         // events
354         cp2.acquireInstanceRangeLocked(
355                 start - DateUtils.DAY_IN_MILLIS, end + DateUtils.DAY_IN_MILLIS, false /*
356                                                                                        * don't
357                                                                                        * use
358                                                                                        * minimum
359                                                                                        * expansion
360                                                                                        * windows
361                                                                                        */,
362                 false /* do not force Instances deletion and expansion */, instancesTimezone,
363                 isHomeTimezone);
364         Cursor cursor = null;
365         try {
366             cursor = db.rawQuery(query, queryParams);
367 
368             final int beginIndex = cursor.getColumnIndex(Instances.BEGIN);
369             final int endIndex = cursor.getColumnIndex(Instances.END);
370             final int eventIdIndex = cursor.getColumnIndex("eventId");
371             final int alarmTimeIndex = cursor.getColumnIndex("myAlarmTime");
372             final int minutesIndex = cursor.getColumnIndex(Reminders.MINUTES);
373 
374             if (Log.isLoggable(CalendarProvider2.TAG, Log.DEBUG)) {
375                 time.set(nextAlarmTime);
376                 String alarmTimeStr = time.format(" %a, %b %d, %Y %I:%M%P");
377                 Log.d(CalendarProvider2.TAG,
378                         "cursor results: " + cursor.getCount() + " nextAlarmTime: " + alarmTimeStr);
379             }
380 
381             while (cursor.moveToNext()) {
382                 // Schedule all alarms whose alarm time is as early as any
383                 // scheduled alarm. For example, if the earliest alarm is at
384                 // 1pm, then we will schedule all alarms that occur at 1pm
385                 // but no alarms that occur later than 1pm.
386                 // Actually, we allow alarms up to a minute later to also
387                 // be scheduled so that we don't have to check immediately
388                 // again after an event alarm goes off.
389                 final long alarmTime = cursor.getLong(alarmTimeIndex);
390                 final long eventId = cursor.getLong(eventIdIndex);
391                 final int minutes = cursor.getInt(minutesIndex);
392                 final long startTime = cursor.getLong(beginIndex);
393                 final long endTime = cursor.getLong(endIndex);
394 
395                 if (Log.isLoggable(CalendarProvider2.TAG, Log.DEBUG)) {
396                     time.set(alarmTime);
397                     String schedTime = time.format(" %a, %b %d, %Y %I:%M%P");
398                     time.set(startTime);
399                     String startTimeStr = time.format(" %a, %b %d, %Y %I:%M%P");
400 
401                     Log.d(CalendarProvider2.TAG,
402                             "  looking at id: " + eventId + " " + startTime + startTimeStr
403                                     + " alarm: " + alarmTime + schedTime);
404                 }
405 
406                 if (alarmTime < nextAlarmTime) {
407                     nextAlarmTime = alarmTime;
408                 } else if (alarmTime > nextAlarmTime + DateUtils.MINUTE_IN_MILLIS) {
409                     // This event alarm (and all later ones) will be scheduled
410                     // later.
411                     if (Log.isLoggable(CalendarProvider2.TAG, Log.DEBUG)) {
412                         Log.d(CalendarProvider2.TAG,
413                                 "This event alarm (and all later ones) will be scheduled later");
414                     }
415                     break;
416                 }
417 
418                 // Avoid an SQLiteContraintException by checking if this alarm
419                 // already exists in the table.
420                 if (CalendarAlerts.alarmExists(resolver, eventId, startTime, alarmTime)) {
421                     if (Log.isLoggable(CalendarProvider2.TAG, Log.DEBUG)) {
422                         int titleIndex = cursor.getColumnIndex(Events.TITLE);
423                         String title = cursor.getString(titleIndex);
424                         Log.d(CalendarProvider2.TAG,
425                                 "  alarm exists for id: " + eventId + " " + title);
426                     }
427                     continue;
428                 }
429 
430                 // Insert this alarm into the CalendarAlerts table
431                 Uri uri = CalendarAlerts.insert(
432                         resolver, eventId, startTime, endTime, alarmTime, minutes);
433                 if (uri == null) {
434                     if (Log.isLoggable(CalendarProvider2.TAG, Log.ERROR)) {
435                         Log.e(CalendarProvider2.TAG, "runScheduleNextAlarm() insert into "
436                                 + "CalendarAlerts table failed");
437                     }
438                     continue;
439                 }
440 
441                 scheduleAlarm(alarmTime);
442                 alarmScheduled = true;
443             }
444         } finally {
445             if (cursor != null) {
446                 cursor.close();
447             }
448         }
449 
450         // Refresh notification bar
451         if (rowsDeleted > 0) {
452             scheduleAlarm(currentMillis);
453             alarmScheduled = true;
454         }
455 
456         // No event alarm is scheduled, check again in 24 hours - 15
457         // minutes.
458         // We have a repeated alarm to check the next even every N hours, so nothing to do here.
459     }
460 
461     /**
462      * Removes the entries in the CalendarAlerts table for alarms that we have
463      * scheduled but that have not fired yet. We do this to ensure that we don't
464      * miss an alarm. The CalendarAlerts table keeps track of the alarms that we
465      * have scheduled but the actual alarm list is in memory and will be cleared
466      * if the phone reboots. We don't need to remove entries that have already
467      * fired, and in fact we should not remove them because we need to display
468      * the notifications until the user dismisses them. We could remove entries
469      * that have fired and been dismissed, but we leave them around for a while
470      * because it makes it easier to debug problems. Entries that are old enough
471      * will be cleaned up later when we schedule new alarms.
472      */
removeScheduledAlarmsLocked(SQLiteDatabase db)473     private static void removeScheduledAlarmsLocked(SQLiteDatabase db) {
474         if (Log.isLoggable(CalendarProvider2.TAG, Log.DEBUG)) {
475             Log.d(CalendarProvider2.TAG, "removing scheduled alarms");
476         }
477         db.delete(CalendarAlerts.TABLE_NAME, CalendarAlerts.STATE + "="
478                 + CalendarAlerts.STATE_SCHEDULED, null /* whereArgs */);
479     }
480 
set(int type, long triggerAtTime, PendingIntent operation)481     public void set(int type, long triggerAtTime, PendingIntent operation) {
482         if (mAlarmManager == null) return;
483         mAlarmManager.set(type, triggerAtTime, operation);
484     }
485 
setAndAllowWhileIdle(int type, long triggerAtTime, PendingIntent operation)486     public void setAndAllowWhileIdle(int type, long triggerAtTime, PendingIntent operation) {
487         if (mAlarmManager == null) return;
488         mAlarmManager.setAndAllowWhileIdle(type, triggerAtTime, operation);
489     }
490 
setExact(int type, long triggerAtTime, PendingIntent operation)491     public void setExact(int type, long triggerAtTime, PendingIntent operation) {
492         if (mAlarmManager == null) return;
493         mAlarmManager.setExact(type, triggerAtTime, operation);
494     }
495 
setExactAndAllowWhileIdle(int type, long triggerAtTime, PendingIntent operation)496     public void setExactAndAllowWhileIdle(int type, long triggerAtTime, PendingIntent operation) {
497         if (mAlarmManager == null) return;
498         mAlarmManager.setExactAndAllowWhileIdle(type, triggerAtTime, operation);
499     }
500 
cancel(PendingIntent operation)501     public void cancel(PendingIntent operation) {
502         if (mAlarmManager == null) return;
503         mAlarmManager.cancel(operation);
504     }
505 
506     /**
507      * Only run inside scheduleNextAlarmLocked, please!
508      * mAlarmScheduled is specific to that method, currently.
509      */
scheduleAlarm(long alarmTime)510     public void scheduleAlarm(long alarmTime) {
511         if (mAlarmManager == null) return;
512         // Debug log for investigating dozing related bugs, remove it once we confirm it is stable.
513         if (Build.IS_DEBUGGABLE) {
514             Log.d(TAG, "schedule reminder alarm fired at " + alarmTime);
515         }
516         CalendarContract.CalendarAlerts.scheduleAlarm(mContext, mAlarmManager, alarmTime);
517     }
518 
rescheduleMissedAlarms(ContentResolver cr)519     public void rescheduleMissedAlarms(ContentResolver cr) {
520         if (mAlarmManager == null) return;
521         CalendarContract.CalendarAlerts.rescheduleMissedAlarms(cr, mContext, mAlarmManager);
522     }
523 }
524