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.calendarcommon2.Time; 20 import com.android.providers.calendar.CalendarDatabaseHelper.Tables; 21 import com.android.providers.calendar.CalendarDatabaseHelper.Views; 22 import com.google.common.annotations.VisibleForTesting; 23 24 import android.app.AlarmManager; 25 import android.app.PendingIntent; 26 import android.content.ContentResolver; 27 import android.content.Context; 28 import android.content.Intent; 29 import android.database.Cursor; 30 import android.database.sqlite.SQLiteDatabase; 31 import android.net.Uri; 32 import android.os.Build; 33 import android.os.PowerManager; 34 import android.os.SystemClock; 35 import android.provider.CalendarContract; 36 import android.provider.CalendarContract.CalendarAlerts; 37 import android.provider.CalendarContract.Calendars; 38 import android.provider.CalendarContract.Events; 39 import android.provider.CalendarContract.Instances; 40 import android.provider.CalendarContract.Reminders; 41 import android.text.format.DateUtils; 42 import android.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 | PendingIntent.FLAG_IMMUTABLE); 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 | PendingIntent.FLAG_IMMUTABLE); 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.mConfidenceChecker.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(); 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.set(System.currentTimeMillis()); 310 time.normalize(); 311 long localOffset = time.getGmtOffset() * 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(); 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(); 398 time.set(startTime); 399 String startTimeStr = time.format(); 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