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