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