1 /* 2 * Copyright (C) 2013 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 package com.android.deskclock.alarms; 17 18 import android.annotation.TargetApi; 19 import android.app.AlarmManager; 20 import android.app.AlarmManager.AlarmClockInfo; 21 import android.app.PendingIntent; 22 import android.content.BroadcastReceiver; 23 import android.content.ContentResolver; 24 import android.content.Context; 25 import android.content.Intent; 26 import android.net.Uri; 27 import android.os.Build; 28 import android.os.Handler; 29 import android.os.PowerManager; 30 import android.provider.Settings; 31 import androidx.core.app.NotificationManagerCompat; 32 import android.text.format.DateFormat; 33 import android.widget.Toast; 34 35 import com.android.deskclock.AlarmAlertWakeLock; 36 import com.android.deskclock.AlarmClockFragment; 37 import com.android.deskclock.AlarmUtils; 38 import com.android.deskclock.AsyncHandler; 39 import com.android.deskclock.DeskClock; 40 import com.android.deskclock.LogUtils; 41 import com.android.deskclock.R; 42 import com.android.deskclock.Utils; 43 import com.android.deskclock.data.DataModel; 44 import com.android.deskclock.events.Events; 45 import com.android.deskclock.provider.Alarm; 46 import com.android.deskclock.provider.AlarmInstance; 47 48 import java.util.Calendar; 49 import java.util.Collections; 50 import java.util.Comparator; 51 import java.util.List; 52 53 import static android.content.Context.ALARM_SERVICE; 54 import static android.provider.Settings.System.NEXT_ALARM_FORMATTED; 55 56 /** 57 * This class handles all the state changes for alarm instances. You need to 58 * register all alarm instances with the state manager if you want them to 59 * be activated. If a major time change has occurred (ie. TIMEZONE_CHANGE, TIMESET_CHANGE), 60 * then you must also re-register instances to fix their states. 61 * 62 * Please see {@link #registerInstance) for special transitions when major time changes 63 * occur. 64 * 65 * Following states: 66 * 67 * SILENT_STATE: 68 * This state is used when the alarm is activated, but doesn't need to display anything. It 69 * is in charge of changing the alarm instance state to a LOW_NOTIFICATION_STATE. 70 * 71 * LOW_NOTIFICATION_STATE: 72 * This state is used to notify the user that the alarm will go off 73 * {@link AlarmInstance#LOW_NOTIFICATION_HOUR_OFFSET}. This 74 * state handles the state changes to HIGH_NOTIFICATION_STATE, HIDE_NOTIFICATION_STATE and 75 * DISMISS_STATE. 76 * 77 * HIDE_NOTIFICATION_STATE: 78 * This is a transient state of the LOW_NOTIFICATION_STATE, where the user wants to hide the 79 * notification. This will sit and wait until the HIGH_PRIORITY_NOTIFICATION should go off. 80 * 81 * HIGH_NOTIFICATION_STATE: 82 * This state behaves like the LOW_NOTIFICATION_STATE, but doesn't allow the user to hide it. 83 * This state is in charge of triggering a FIRED_STATE or DISMISS_STATE. 84 * 85 * SNOOZED_STATE: 86 * The SNOOZED_STATE behaves like a HIGH_NOTIFICATION_STATE, but with a different message. It 87 * also increments the alarm time in the instance to reflect the new snooze time. 88 * 89 * FIRED_STATE: 90 * The FIRED_STATE is used when the alarm is firing. It will start the AlarmService, and wait 91 * until the user interacts with the alarm via SNOOZED_STATE or DISMISS_STATE change. If the user 92 * doesn't then it might be change to MISSED_STATE if auto-silenced was enabled. 93 * 94 * MISSED_STATE: 95 * The MISSED_STATE is used when the alarm already fired, but the user could not interact with 96 * it. At this point the alarm instance is dead and we check the parent alarm to see if we need 97 * to disable or schedule a new alarm_instance. There is also a notification shown to the user 98 * that he/she missed the alarm and that stays for 99 * {@link AlarmInstance#MISSED_TIME_TO_LIVE_HOUR_OFFSET} or until the user acknownledges it. 100 * 101 * DISMISS_STATE: 102 * This is really a transient state that will properly delete the alarm instance. Use this state, 103 * whenever you want to get rid of the alarm instance. This state will also check the alarm 104 * parent to see if it should disable or schedule a new alarm instance. 105 */ 106 public final class AlarmStateManager extends BroadcastReceiver { 107 // Intent action to trigger an instance state change. 108 public static final String CHANGE_STATE_ACTION = "change_state"; 109 110 // Intent action to show the alarm and dismiss the instance 111 public static final String SHOW_AND_DISMISS_ALARM_ACTION = "show_and_dismiss_alarm"; 112 113 // Intent action for an AlarmManager alarm serving only to set the next alarm indicators 114 private static final String INDICATOR_ACTION = "indicator"; 115 116 // System intent action to notify AppWidget that we changed the alarm text. 117 public static final String ACTION_ALARM_CHANGED = "com.android.deskclock.ALARM_CHANGED"; 118 119 // Extra key to set the desired state change. 120 public static final String ALARM_STATE_EXTRA = "intent.extra.alarm.state"; 121 122 // Extra key to indicate the state change was launched from a notification. 123 public static final String FROM_NOTIFICATION_EXTRA = "intent.extra.from.notification"; 124 125 // Extra key to set the global broadcast id. 126 private static final String ALARM_GLOBAL_ID_EXTRA = "intent.extra.alarm.global.id"; 127 128 // Intent category tags used to dismiss, snooze or delete an alarm 129 public static final String ALARM_DISMISS_TAG = "DISMISS_TAG"; 130 public static final String ALARM_SNOOZE_TAG = "SNOOZE_TAG"; 131 public static final String ALARM_DELETE_TAG = "DELETE_TAG"; 132 133 // Intent category tag used when schedule state change intents in alarm manager. 134 private static final String ALARM_MANAGER_TAG = "ALARM_MANAGER"; 135 136 // Buffer time in seconds to fire alarm instead of marking it missed. 137 public static final int ALARM_FIRE_BUFFER = 15; 138 139 // A factory for the current time; can be mocked for testing purposes. 140 private static CurrentTimeFactory sCurrentTimeFactory; 141 142 // Schedules alarm state transitions; can be mocked for testing purposes. 143 private static StateChangeScheduler sStateChangeScheduler = 144 new AlarmManagerStateChangeScheduler(); 145 getCurrentTime()146 private static Calendar getCurrentTime() { 147 return sCurrentTimeFactory == null 148 ? DataModel.getDataModel().getCalendar() 149 : sCurrentTimeFactory.getCurrentTime(); 150 } 151 setCurrentTimeFactory(CurrentTimeFactory currentTimeFactory)152 static void setCurrentTimeFactory(CurrentTimeFactory currentTimeFactory) { 153 sCurrentTimeFactory = currentTimeFactory; 154 } 155 setStateChangeScheduler(StateChangeScheduler stateChangeScheduler)156 static void setStateChangeScheduler(StateChangeScheduler stateChangeScheduler) { 157 if (stateChangeScheduler == null) { 158 stateChangeScheduler = new AlarmManagerStateChangeScheduler(); 159 } 160 sStateChangeScheduler = stateChangeScheduler; 161 } 162 163 /** 164 * Update the next alarm stored in framework. This value is also displayed in digital widgets 165 * and the clock tab in this app. 166 */ updateNextAlarm(Context context)167 private static void updateNextAlarm(Context context) { 168 final AlarmInstance nextAlarm = getNextFiringAlarm(context); 169 170 if (Utils.isPreL()) { 171 updateNextAlarmInSystemSettings(context, nextAlarm); 172 } else { 173 updateNextAlarmInAlarmManager(context, nextAlarm); 174 } 175 } 176 177 /** 178 * Returns an alarm instance of an alarm that's going to fire next. 179 * 180 * @param context application context 181 * @return an alarm instance that will fire earliest relative to current time. 182 */ getNextFiringAlarm(Context context)183 public static AlarmInstance getNextFiringAlarm(Context context) { 184 final ContentResolver cr = context.getContentResolver(); 185 final String activeAlarmQuery = AlarmInstance.ALARM_STATE + "<" + AlarmInstance.FIRED_STATE; 186 final List<AlarmInstance> alarmInstances = AlarmInstance.getInstances(cr, activeAlarmQuery); 187 188 AlarmInstance nextAlarm = null; 189 for (AlarmInstance instance : alarmInstances) { 190 if (nextAlarm == null || instance.getAlarmTime().before(nextAlarm.getAlarmTime())) { 191 nextAlarm = instance; 192 } 193 } 194 return nextAlarm; 195 } 196 197 /** 198 * Used in pre-L devices, where "next alarm" is stored in system settings. 199 */ 200 @SuppressWarnings("deprecation") 201 @TargetApi(Build.VERSION_CODES.KITKAT) updateNextAlarmInSystemSettings(Context context, AlarmInstance nextAlarm)202 private static void updateNextAlarmInSystemSettings(Context context, AlarmInstance nextAlarm) { 203 // Format the next alarm time if an alarm is scheduled. 204 String time = ""; 205 if (nextAlarm != null) { 206 time = AlarmUtils.getFormattedTime(context, nextAlarm.getAlarmTime()); 207 } 208 209 try { 210 // Write directly to NEXT_ALARM_FORMATTED in all pre-L versions 211 Settings.System.putString(context.getContentResolver(), NEXT_ALARM_FORMATTED, time); 212 213 LogUtils.i("Updated next alarm time to: \'" + time + '\''); 214 215 // Send broadcast message so pre-L AppWidgets will recognize an update. 216 context.sendBroadcast(new Intent(ACTION_ALARM_CHANGED)); 217 } catch (SecurityException se) { 218 // The user has most likely revoked WRITE_SETTINGS. 219 LogUtils.e("Unable to update next alarm to: \'" + time + '\'', se); 220 } 221 } 222 223 /** 224 * Used in L and later devices where "next alarm" is stored in the Alarm Manager. 225 */ 226 @TargetApi(Build.VERSION_CODES.LOLLIPOP) updateNextAlarmInAlarmManager(Context context, AlarmInstance nextAlarm)227 private static void updateNextAlarmInAlarmManager(Context context, AlarmInstance nextAlarm) { 228 // Sets a surrogate alarm with alarm manager that provides the AlarmClockInfo for the 229 // alarm that is going to fire next. The operation is constructed such that it is ignored 230 // by AlarmStateManager. 231 232 final AlarmManager alarmManager = (AlarmManager) context.getSystemService(ALARM_SERVICE); 233 234 final int flags = nextAlarm == null ? PendingIntent.FLAG_NO_CREATE : 0; 235 final PendingIntent operation = PendingIntent.getBroadcast(context, 0 /* requestCode */, 236 AlarmStateManager.createIndicatorIntent(context), flags); 237 238 if (nextAlarm != null) { 239 LogUtils.i("Setting upcoming AlarmClockInfo for alarm: " + nextAlarm.mId); 240 long alarmTime = nextAlarm.getAlarmTime().getTimeInMillis(); 241 242 // Create an intent that can be used to show or edit details of the next alarm. 243 PendingIntent viewIntent = PendingIntent.getActivity(context, nextAlarm.hashCode(), 244 AlarmNotifications.createViewAlarmIntent(context, nextAlarm), 245 PendingIntent.FLAG_UPDATE_CURRENT); 246 247 final AlarmClockInfo info = new AlarmClockInfo(alarmTime, viewIntent); 248 Utils.updateNextAlarm(alarmManager, info, operation); 249 } else if (operation != null) { 250 LogUtils.i("Canceling upcoming AlarmClockInfo"); 251 alarmManager.cancel(operation); 252 } 253 } 254 255 /** 256 * Used by dismissed and missed states, to update parent alarm. This will either 257 * disable, delete or reschedule parent alarm. 258 * 259 * @param context application context 260 * @param instance to update parent for 261 */ updateParentAlarm(Context context, AlarmInstance instance)262 private static void updateParentAlarm(Context context, AlarmInstance instance) { 263 ContentResolver cr = context.getContentResolver(); 264 Alarm alarm = Alarm.getAlarm(cr, instance.mAlarmId); 265 if (alarm == null) { 266 LogUtils.e("Parent has been deleted with instance: " + instance.toString()); 267 return; 268 } 269 270 if (!alarm.daysOfWeek.isRepeating()) { 271 if (alarm.deleteAfterUse) { 272 LogUtils.i("Deleting parent alarm: " + alarm.id); 273 Alarm.deleteAlarm(cr, alarm.id); 274 } else { 275 LogUtils.i("Disabling parent alarm: " + alarm.id); 276 alarm.enabled = false; 277 Alarm.updateAlarm(cr, alarm); 278 } 279 } else { 280 // Schedule the next repeating instance which may be before the current instance if a 281 // time jump has occurred. Otherwise, if the current instance is the next instance 282 // and has already been fired, schedule the subsequent instance. 283 AlarmInstance nextRepeatedInstance = alarm.createInstanceAfter(getCurrentTime()); 284 if (instance.mAlarmState > AlarmInstance.FIRED_STATE 285 && nextRepeatedInstance.getAlarmTime().equals(instance.getAlarmTime())) { 286 nextRepeatedInstance = alarm.createInstanceAfter(instance.getAlarmTime()); 287 } 288 289 LogUtils.i("Creating new instance for repeating alarm " + alarm.id + " at " + 290 AlarmUtils.getFormattedTime(context, nextRepeatedInstance.getAlarmTime())); 291 AlarmInstance.addInstance(cr, nextRepeatedInstance); 292 registerInstance(context, nextRepeatedInstance, true); 293 } 294 } 295 296 /** 297 * Utility method to create a proper change state intent. 298 * 299 * @param context application context 300 * @param tag used to make intent differ from other state change intents. 301 * @param instance to change state to 302 * @param state to change to. 303 * @return intent that can be used to change an alarm instance state 304 */ createStateChangeIntent(Context context, String tag, AlarmInstance instance, Integer state)305 public static Intent createStateChangeIntent(Context context, String tag, 306 AlarmInstance instance, Integer state) { 307 // This intent is directed to AlarmService, though the actual handling of it occurs here 308 // in AlarmStateManager. The reason is that evidence exists showing the jump between the 309 // broadcast receiver (AlarmStateManager) and service (AlarmService) can be thwarted by the 310 // Out Of Memory killer. If clock is killed during that jump, firing an alarm can fail to 311 // occur. To be safer, the call begins in AlarmService, which has the power to display the 312 // firing alarm if needed, so no jump is needed. 313 Intent intent = AlarmInstance.createIntent(context, AlarmService.class, instance.mId); 314 intent.setAction(CHANGE_STATE_ACTION); 315 intent.addCategory(tag); 316 intent.putExtra(ALARM_GLOBAL_ID_EXTRA, DataModel.getDataModel().getGlobalIntentId()); 317 if (state != null) { 318 intent.putExtra(ALARM_STATE_EXTRA, state.intValue()); 319 } 320 return intent; 321 } 322 323 /** 324 * Schedule alarm instance state changes with {@link AlarmManager}. 325 * 326 * @param ctx application context 327 * @param time to trigger state change 328 * @param instance to change state to 329 * @param newState to change to 330 */ scheduleInstanceStateChange(Context ctx, Calendar time, AlarmInstance instance, int newState)331 private static void scheduleInstanceStateChange(Context ctx, Calendar time, 332 AlarmInstance instance, int newState) { 333 sStateChangeScheduler.scheduleInstanceStateChange(ctx, time, instance, newState); 334 } 335 336 /** 337 * Cancel all {@link AlarmManager} timers for instance. 338 * 339 * @param ctx application context 340 * @param instance to disable all {@link AlarmManager} timers 341 */ cancelScheduledInstanceStateChange(Context ctx, AlarmInstance instance)342 private static void cancelScheduledInstanceStateChange(Context ctx, AlarmInstance instance) { 343 sStateChangeScheduler.cancelScheduledInstanceStateChange(ctx, instance); 344 } 345 346 347 /** 348 * This will set the alarm instance to the SILENT_STATE and update 349 * the application notifications and schedule any state changes that need 350 * to occur in the future. 351 * 352 * @param context application context 353 * @param instance to set state to 354 */ setSilentState(Context context, AlarmInstance instance)355 public static void setSilentState(Context context, AlarmInstance instance) { 356 LogUtils.i("Setting silent state to instance " + instance.mId); 357 358 // Update alarm in db 359 ContentResolver contentResolver = context.getContentResolver(); 360 instance.mAlarmState = AlarmInstance.SILENT_STATE; 361 AlarmInstance.updateInstance(contentResolver, instance); 362 363 // Setup instance notification and scheduling timers 364 AlarmNotifications.clearNotification(context, instance); 365 scheduleInstanceStateChange(context, instance.getLowNotificationTime(), 366 instance, AlarmInstance.LOW_NOTIFICATION_STATE); 367 } 368 369 /** 370 * This will set the alarm instance to the LOW_NOTIFICATION_STATE and update 371 * the application notifications and schedule any state changes that need 372 * to occur in the future. 373 * 374 * @param context application context 375 * @param instance to set state to 376 */ setLowNotificationState(Context context, AlarmInstance instance)377 public static void setLowNotificationState(Context context, AlarmInstance instance) { 378 LogUtils.i("Setting low notification state to instance " + instance.mId); 379 380 // Update alarm state in db 381 ContentResolver contentResolver = context.getContentResolver(); 382 instance.mAlarmState = AlarmInstance.LOW_NOTIFICATION_STATE; 383 AlarmInstance.updateInstance(contentResolver, instance); 384 385 // Setup instance notification and scheduling timers 386 AlarmNotifications.showUpcomingNotification(context, instance, true); 387 scheduleInstanceStateChange(context, instance.getHighNotificationTime(), 388 instance, AlarmInstance.HIGH_NOTIFICATION_STATE); 389 } 390 391 /** 392 * This will set the alarm instance to the HIDE_NOTIFICATION_STATE and update 393 * the application notifications and schedule any state changes that need 394 * to occur in the future. 395 * 396 * @param context application context 397 * @param instance to set state to 398 */ setHideNotificationState(Context context, AlarmInstance instance)399 public static void setHideNotificationState(Context context, AlarmInstance instance) { 400 LogUtils.i("Setting hide notification state to instance " + instance.mId); 401 402 // Update alarm state in db 403 ContentResolver contentResolver = context.getContentResolver(); 404 instance.mAlarmState = AlarmInstance.HIDE_NOTIFICATION_STATE; 405 AlarmInstance.updateInstance(contentResolver, instance); 406 407 // Setup instance notification and scheduling timers 408 AlarmNotifications.clearNotification(context, instance); 409 scheduleInstanceStateChange(context, instance.getHighNotificationTime(), 410 instance, AlarmInstance.HIGH_NOTIFICATION_STATE); 411 } 412 413 /** 414 * This will set the alarm instance to the HIGH_NOTIFICATION_STATE and update 415 * the application notifications and schedule any state changes that need 416 * to occur in the future. 417 * 418 * @param context application context 419 * @param instance to set state to 420 */ setHighNotificationState(Context context, AlarmInstance instance)421 public static void setHighNotificationState(Context context, AlarmInstance instance) { 422 LogUtils.i("Setting high notification state to instance " + instance.mId); 423 424 // Update alarm state in db 425 ContentResolver contentResolver = context.getContentResolver(); 426 instance.mAlarmState = AlarmInstance.HIGH_NOTIFICATION_STATE; 427 AlarmInstance.updateInstance(contentResolver, instance); 428 429 // Setup instance notification and scheduling timers 430 AlarmNotifications.showUpcomingNotification(context, instance, false); 431 scheduleInstanceStateChange(context, instance.getAlarmTime(), 432 instance, AlarmInstance.FIRED_STATE); 433 } 434 435 /** 436 * This will set the alarm instance to the FIRED_STATE and update 437 * the application notifications and schedule any state changes that need 438 * to occur in the future. 439 * 440 * @param context application context 441 * @param instance to set state to 442 */ setFiredState(Context context, AlarmInstance instance)443 public static void setFiredState(Context context, AlarmInstance instance) { 444 LogUtils.i("Setting fire state to instance " + instance.mId); 445 446 // Update alarm state in db 447 ContentResolver contentResolver = context.getContentResolver(); 448 instance.mAlarmState = AlarmInstance.FIRED_STATE; 449 AlarmInstance.updateInstance(contentResolver, instance); 450 451 if (instance.mAlarmId != null) { 452 // if the time changed *backward* and pushed an instance from missed back to fired, 453 // remove any other scheduled instances that may exist 454 AlarmInstance.deleteOtherInstances(context, contentResolver, instance.mAlarmId, 455 instance.mId); 456 } 457 458 Events.sendAlarmEvent(R.string.action_fire, 0); 459 460 Calendar timeout = instance.getTimeout(); 461 if (timeout != null) { 462 scheduleInstanceStateChange(context, timeout, instance, AlarmInstance.MISSED_STATE); 463 } 464 465 // Instance not valid anymore, so find next alarm that will fire and notify system 466 updateNextAlarm(context); 467 } 468 469 /** 470 * This will set the alarm instance to the SNOOZE_STATE and update 471 * the application notifications and schedule any state changes that need 472 * to occur in the future. 473 * 474 * @param context application context 475 * @param instance to set state to 476 */ setSnoozeState(final Context context, AlarmInstance instance, boolean showToast)477 public static void setSnoozeState(final Context context, AlarmInstance instance, 478 boolean showToast) { 479 // Stop alarm if this instance is firing it 480 AlarmService.stopAlarm(context, instance); 481 482 // Calculate the new snooze alarm time 483 final int snoozeMinutes = DataModel.getDataModel().getSnoozeLength(); 484 Calendar newAlarmTime = Calendar.getInstance(); 485 newAlarmTime.add(Calendar.MINUTE, snoozeMinutes); 486 487 // Update alarm state and new alarm time in db. 488 LogUtils.i("Setting snoozed state to instance " + instance.mId + " for " 489 + AlarmUtils.getFormattedTime(context, newAlarmTime)); 490 instance.setAlarmTime(newAlarmTime); 491 instance.mAlarmState = AlarmInstance.SNOOZE_STATE; 492 AlarmInstance.updateInstance(context.getContentResolver(), instance); 493 494 // Setup instance notification and scheduling timers 495 AlarmNotifications.showSnoozeNotification(context, instance); 496 scheduleInstanceStateChange(context, instance.getAlarmTime(), 497 instance, AlarmInstance.FIRED_STATE); 498 499 // Display the snooze minutes in a toast. 500 if (showToast) { 501 final Handler mainHandler = new Handler(context.getMainLooper()); 502 final Runnable myRunnable = new Runnable() { 503 @Override 504 public void run() { 505 String displayTime = String.format(context.getResources().getQuantityText 506 (R.plurals.alarm_alert_snooze_set, snoozeMinutes).toString(), 507 snoozeMinutes); 508 Toast.makeText(context, displayTime, Toast.LENGTH_LONG).show(); 509 } 510 }; 511 mainHandler.post(myRunnable); 512 } 513 514 // Instance time changed, so find next alarm that will fire and notify system 515 updateNextAlarm(context); 516 } 517 518 /** 519 * This will set the alarm instance to the MISSED_STATE and update 520 * the application notifications and schedule any state changes that need 521 * to occur in the future. 522 * 523 * @param context application context 524 * @param instance to set state to 525 */ setMissedState(Context context, AlarmInstance instance)526 public static void setMissedState(Context context, AlarmInstance instance) { 527 LogUtils.i("Setting missed state to instance " + instance.mId); 528 // Stop alarm if this instance is firing it 529 AlarmService.stopAlarm(context, instance); 530 531 // Check parent if it needs to reschedule, disable or delete itself 532 if (instance.mAlarmId != null) { 533 updateParentAlarm(context, instance); 534 } 535 536 // Update alarm state 537 ContentResolver contentResolver = context.getContentResolver(); 538 instance.mAlarmState = AlarmInstance.MISSED_STATE; 539 AlarmInstance.updateInstance(contentResolver, instance); 540 541 // Setup instance notification and scheduling timers 542 AlarmNotifications.showMissedNotification(context, instance); 543 scheduleInstanceStateChange(context, instance.getMissedTimeToLive(), 544 instance, AlarmInstance.DISMISSED_STATE); 545 546 // Instance is not valid anymore, so find next alarm that will fire and notify system 547 updateNextAlarm(context); 548 } 549 550 /** 551 * This will set the alarm instance to the PREDISMISSED_STATE and schedule an instance state 552 * change to DISMISSED_STATE at the regularly scheduled firing time. 553 * 554 * @param context application context 555 * @param instance to set state to 556 */ setPreDismissState(Context context, AlarmInstance instance)557 public static void setPreDismissState(Context context, AlarmInstance instance) { 558 LogUtils.i("Setting predismissed state to instance " + instance.mId); 559 560 // Update alarm in db 561 final ContentResolver contentResolver = context.getContentResolver(); 562 instance.mAlarmState = AlarmInstance.PREDISMISSED_STATE; 563 AlarmInstance.updateInstance(contentResolver, instance); 564 565 // Setup instance notification and scheduling timers 566 AlarmNotifications.clearNotification(context, instance); 567 scheduleInstanceStateChange(context, instance.getAlarmTime(), instance, 568 AlarmInstance.DISMISSED_STATE); 569 570 // Check parent if it needs to reschedule, disable or delete itself 571 if (instance.mAlarmId != null) { 572 updateParentAlarm(context, instance); 573 } 574 575 updateNextAlarm(context); 576 } 577 578 /** 579 * This just sets the alarm instance to DISMISSED_STATE. 580 */ setDismissState(Context context, AlarmInstance instance)581 public static void setDismissState(Context context, AlarmInstance instance) { 582 LogUtils.i("Setting dismissed state to instance " + instance.mId); 583 instance.mAlarmState = AlarmInstance.DISMISSED_STATE; 584 final ContentResolver contentResolver = context.getContentResolver(); 585 AlarmInstance.updateInstance(contentResolver, instance); 586 } 587 588 /** 589 * This will delete the alarm instance, update the application notifications, and schedule 590 * any state changes that need to occur in the future. 591 * 592 * @param context application context 593 * @param instance to set state to 594 */ deleteInstanceAndUpdateParent(Context context, AlarmInstance instance)595 public static void deleteInstanceAndUpdateParent(Context context, AlarmInstance instance) { 596 LogUtils.i("Deleting instance " + instance.mId + " and updating parent alarm."); 597 598 // Remove all other timers and notifications associated to it 599 unregisterInstance(context, instance); 600 601 // Check parent if it needs to reschedule, disable or delete itself 602 if (instance.mAlarmId != null) { 603 updateParentAlarm(context, instance); 604 } 605 606 // Delete instance as it is not needed anymore 607 AlarmInstance.deleteInstance(context.getContentResolver(), instance.mId); 608 609 // Instance is not valid anymore, so find next alarm that will fire and notify system 610 updateNextAlarm(context); 611 } 612 613 /** 614 * This will set the instance state to DISMISSED_STATE and remove its notifications and 615 * alarm timers. 616 * 617 * @param context application context 618 * @param instance to unregister 619 */ unregisterInstance(Context context, AlarmInstance instance)620 public static void unregisterInstance(Context context, AlarmInstance instance) { 621 LogUtils.i("Unregistering instance " + instance.mId); 622 // Stop alarm if this instance is firing it 623 AlarmService.stopAlarm(context, instance); 624 AlarmNotifications.clearNotification(context, instance); 625 cancelScheduledInstanceStateChange(context, instance); 626 setDismissState(context, instance); 627 } 628 629 /** 630 * This registers the AlarmInstance to the state manager. This will look at the instance 631 * and choose the most appropriate state to put it in. This is primarily used by new 632 * alarms, but it can also be called when the system time changes. 633 * 634 * Most state changes are handled by the states themselves, but during major time changes we 635 * have to correct the alarm instance state. This means we have to handle special cases as 636 * describe below: 637 * 638 * <ul> 639 * <li>Make sure all dismissed alarms are never re-activated</li> 640 * <li>Make sure pre-dismissed alarms stay predismissed</li> 641 * <li>Make sure firing alarms stayed fired unless they should be auto-silenced</li> 642 * <li>Missed instance that have parents should be re-enabled if we went back in time</li> 643 * <li>If alarm was SNOOZED, then show the notification but don't update time</li> 644 * <li>If low priority notification was hidden, then make sure it stays hidden</li> 645 * </ul> 646 * 647 * If none of these special case are found, then we just check the time and see what is the 648 * proper state for the instance. 649 * 650 * @param context application context 651 * @param instance to register 652 */ registerInstance(Context context, AlarmInstance instance, boolean updateNextAlarm)653 public static void registerInstance(Context context, AlarmInstance instance, 654 boolean updateNextAlarm) { 655 LogUtils.i("Registering instance: " + instance.mId); 656 final ContentResolver cr = context.getContentResolver(); 657 final Alarm alarm = Alarm.getAlarm(cr, instance.mAlarmId); 658 final Calendar currentTime = getCurrentTime(); 659 final Calendar alarmTime = instance.getAlarmTime(); 660 final Calendar timeoutTime = instance.getTimeout(); 661 final Calendar lowNotificationTime = instance.getLowNotificationTime(); 662 final Calendar highNotificationTime = instance.getHighNotificationTime(); 663 final Calendar missedTTL = instance.getMissedTimeToLive(); 664 665 // Handle special use cases here 666 if (instance.mAlarmState == AlarmInstance.DISMISSED_STATE) { 667 // This should never happen, but add a quick check here 668 LogUtils.e("Alarm Instance is dismissed, but never deleted"); 669 deleteInstanceAndUpdateParent(context, instance); 670 return; 671 } else if (instance.mAlarmState == AlarmInstance.FIRED_STATE) { 672 // Keep alarm firing, unless it should be timed out 673 boolean hasTimeout = timeoutTime != null && currentTime.after(timeoutTime); 674 if (!hasTimeout) { 675 setFiredState(context, instance); 676 return; 677 } 678 } else if (instance.mAlarmState == AlarmInstance.MISSED_STATE) { 679 if (currentTime.before(alarmTime)) { 680 if (instance.mAlarmId == null) { 681 LogUtils.i("Cannot restore missed instance for one-time alarm"); 682 // This instance parent got deleted (ie. deleteAfterUse), so 683 // we should not re-activate it.- 684 deleteInstanceAndUpdateParent(context, instance); 685 return; 686 } 687 688 // TODO: This will re-activate missed snoozed alarms, but will 689 // use our normal notifications. This is not ideal, but very rare use-case. 690 // We should look into fixing this in the future. 691 692 // Make sure we re-enable the parent alarm of the instance 693 // because it will get activated by by the below code 694 alarm.enabled = true; 695 Alarm.updateAlarm(cr, alarm); 696 } 697 } else if (instance.mAlarmState == AlarmInstance.PREDISMISSED_STATE) { 698 if (currentTime.before(alarmTime)) { 699 setPreDismissState(context, instance); 700 } else { 701 deleteInstanceAndUpdateParent(context, instance); 702 } 703 return; 704 } 705 706 // Fix states that are time sensitive 707 if (currentTime.after(missedTTL)) { 708 // Alarm is so old, just dismiss it 709 deleteInstanceAndUpdateParent(context, instance); 710 } else if (currentTime.after(alarmTime)) { 711 // There is a chance that the TIME_SET occurred right when the alarm should go off, so 712 // we need to add a check to see if we should fire the alarm instead of marking it 713 // missed. 714 Calendar alarmBuffer = Calendar.getInstance(); 715 alarmBuffer.setTime(alarmTime.getTime()); 716 alarmBuffer.add(Calendar.SECOND, ALARM_FIRE_BUFFER); 717 if (currentTime.before(alarmBuffer)) { 718 setFiredState(context, instance); 719 } else { 720 setMissedState(context, instance); 721 } 722 } else if (instance.mAlarmState == AlarmInstance.SNOOZE_STATE) { 723 // We only want to display snooze notification and not update the time, 724 // so handle showing the notification directly 725 AlarmNotifications.showSnoozeNotification(context, instance); 726 scheduleInstanceStateChange(context, instance.getAlarmTime(), 727 instance, AlarmInstance.FIRED_STATE); 728 } else if (currentTime.after(highNotificationTime)) { 729 setHighNotificationState(context, instance); 730 } else if (currentTime.after(lowNotificationTime)) { 731 // Only show low notification if it wasn't hidden in the past 732 if (instance.mAlarmState == AlarmInstance.HIDE_NOTIFICATION_STATE) { 733 setHideNotificationState(context, instance); 734 } else { 735 setLowNotificationState(context, instance); 736 } 737 } else { 738 // Alarm is still active, so initialize as a silent alarm 739 setSilentState(context, instance); 740 } 741 742 // The caller prefers to handle updateNextAlarm for optimization 743 if (updateNextAlarm) { 744 updateNextAlarm(context); 745 } 746 } 747 748 /** 749 * This will delete and unregister all instances associated with alarmId, without affect 750 * the alarm itself. This should be used whenever modifying or deleting an alarm. 751 * 752 * @param context application context 753 * @param alarmId to find instances to delete. 754 */ deleteAllInstances(Context context, long alarmId)755 public static void deleteAllInstances(Context context, long alarmId) { 756 LogUtils.i("Deleting all instances of alarm: " + alarmId); 757 ContentResolver cr = context.getContentResolver(); 758 List<AlarmInstance> instances = AlarmInstance.getInstancesByAlarmId(cr, alarmId); 759 for (AlarmInstance instance : instances) { 760 unregisterInstance(context, instance); 761 AlarmInstance.deleteInstance(context.getContentResolver(), instance.mId); 762 } 763 updateNextAlarm(context); 764 } 765 766 /** 767 * Delete and unregister all instances unless they are snoozed. This is used whenever an alarm 768 * is modified superficially (label, vibrate, or ringtone change). 769 */ deleteNonSnoozeInstances(Context context, long alarmId)770 public static void deleteNonSnoozeInstances(Context context, long alarmId) { 771 LogUtils.i("Deleting all non-snooze instances of alarm: " + alarmId); 772 ContentResolver cr = context.getContentResolver(); 773 List<AlarmInstance> instances = AlarmInstance.getInstancesByAlarmId(cr, alarmId); 774 for (AlarmInstance instance : instances) { 775 if (instance.mAlarmState == AlarmInstance.SNOOZE_STATE) { 776 continue; 777 } 778 unregisterInstance(context, instance); 779 AlarmInstance.deleteInstance(context.getContentResolver(), instance.mId); 780 } 781 updateNextAlarm(context); 782 } 783 784 /** 785 * Fix and update all alarm instance when a time change event occurs. 786 * 787 * @param context application context 788 */ fixAlarmInstances(Context context)789 public static void fixAlarmInstances(Context context) { 790 LogUtils.i("Fixing alarm instances"); 791 // Register all instances after major time changes or when phone restarts 792 final ContentResolver contentResolver = context.getContentResolver(); 793 final Calendar currentTime = getCurrentTime(); 794 795 // Sort the instances in reverse chronological order so that later instances are fixed or 796 // deleted before re-scheduling prior instances (which may re-create or update the later 797 // instances). 798 final List<AlarmInstance> instances = AlarmInstance.getInstances( 799 contentResolver, null /* selection */); 800 Collections.sort(instances, new Comparator<AlarmInstance>() { 801 @Override 802 public int compare(AlarmInstance lhs, AlarmInstance rhs) { 803 return rhs.getAlarmTime().compareTo(lhs.getAlarmTime()); 804 } 805 }); 806 807 for (AlarmInstance instance : instances) { 808 final Alarm alarm = Alarm.getAlarm(contentResolver, instance.mAlarmId); 809 if (alarm == null) { 810 unregisterInstance(context, instance); 811 AlarmInstance.deleteInstance(contentResolver, instance.mId); 812 LogUtils.e("Found instance without matching alarm; deleting instance %s", instance); 813 continue; 814 } 815 final Calendar priorAlarmTime = alarm.getPreviousAlarmTime(instance.getAlarmTime()); 816 final Calendar missedTTLTime = instance.getMissedTimeToLive(); 817 if (currentTime.before(priorAlarmTime) || currentTime.after(missedTTLTime)) { 818 final Calendar oldAlarmTime = instance.getAlarmTime(); 819 final Calendar newAlarmTime = alarm.getNextAlarmTime(currentTime); 820 final CharSequence oldTime = DateFormat.format("MM/dd/yyyy hh:mm a", oldAlarmTime); 821 final CharSequence newTime = DateFormat.format("MM/dd/yyyy hh:mm a", newAlarmTime); 822 LogUtils.i("A time change has caused an existing alarm scheduled to fire at %s to" + 823 " be replaced by a new alarm scheduled to fire at %s", oldTime, newTime); 824 825 // The time change is so dramatic the AlarmInstance doesn't make any sense; 826 // remove it and schedule the new appropriate instance. 827 AlarmStateManager.deleteInstanceAndUpdateParent(context, instance); 828 } else { 829 registerInstance(context, instance, false /* updateNextAlarm */); 830 } 831 } 832 833 updateNextAlarm(context); 834 } 835 836 /** 837 * Utility method to set alarm instance state via constants. 838 * 839 * @param context application context 840 * @param instance to change state on 841 * @param state to change to 842 */ setAlarmState(Context context, AlarmInstance instance, int state)843 private static void setAlarmState(Context context, AlarmInstance instance, int state) { 844 if (instance == null) { 845 LogUtils.e("Null alarm instance while setting state to %d", state); 846 return; 847 } 848 switch (state) { 849 case AlarmInstance.SILENT_STATE: 850 setSilentState(context, instance); 851 break; 852 case AlarmInstance.LOW_NOTIFICATION_STATE: 853 setLowNotificationState(context, instance); 854 break; 855 case AlarmInstance.HIDE_NOTIFICATION_STATE: 856 setHideNotificationState(context, instance); 857 break; 858 case AlarmInstance.HIGH_NOTIFICATION_STATE: 859 setHighNotificationState(context, instance); 860 break; 861 case AlarmInstance.FIRED_STATE: 862 setFiredState(context, instance); 863 break; 864 case AlarmInstance.SNOOZE_STATE: 865 setSnoozeState(context, instance, true /* showToast */); 866 break; 867 case AlarmInstance.MISSED_STATE: 868 setMissedState(context, instance); 869 break; 870 case AlarmInstance.PREDISMISSED_STATE: 871 setPreDismissState(context, instance); 872 break; 873 case AlarmInstance.DISMISSED_STATE: 874 deleteInstanceAndUpdateParent(context, instance); 875 break; 876 default: 877 LogUtils.e("Trying to change to unknown alarm state: " + state); 878 } 879 } 880 881 @Override onReceive(final Context context, final Intent intent)882 public void onReceive(final Context context, final Intent intent) { 883 if (INDICATOR_ACTION.equals(intent.getAction())) { 884 return; 885 } 886 887 final PendingResult result = goAsync(); 888 final PowerManager.WakeLock wl = AlarmAlertWakeLock.createPartialWakeLock(context); 889 wl.acquire(); 890 AsyncHandler.post(new Runnable() { 891 @Override 892 public void run() { 893 handleIntent(context, intent); 894 result.finish(); 895 wl.release(); 896 } 897 }); 898 } 899 handleIntent(Context context, Intent intent)900 public static void handleIntent(Context context, Intent intent) { 901 final String action = intent.getAction(); 902 LogUtils.v("AlarmStateManager received intent " + intent); 903 if (CHANGE_STATE_ACTION.equals(action)) { 904 Uri uri = intent.getData(); 905 AlarmInstance instance = AlarmInstance.getInstance(context.getContentResolver(), 906 AlarmInstance.getId(uri)); 907 if (instance == null) { 908 LogUtils.e("Can not change state for unknown instance: " + uri); 909 return; 910 } 911 912 int globalId = DataModel.getDataModel().getGlobalIntentId(); 913 int intentId = intent.getIntExtra(ALARM_GLOBAL_ID_EXTRA, -1); 914 int alarmState = intent.getIntExtra(ALARM_STATE_EXTRA, -1); 915 if (intentId != globalId) { 916 LogUtils.i("IntentId: " + intentId + " GlobalId: " + globalId + " AlarmState: " + 917 alarmState); 918 // Allows dismiss/snooze requests to go through 919 if (!intent.hasCategory(ALARM_DISMISS_TAG) && 920 !intent.hasCategory(ALARM_SNOOZE_TAG)) { 921 LogUtils.i("Ignoring old Intent"); 922 return; 923 } 924 } 925 926 if (intent.getBooleanExtra(FROM_NOTIFICATION_EXTRA, false)) { 927 if (intent.hasCategory(ALARM_DISMISS_TAG)) { 928 Events.sendAlarmEvent(R.string.action_dismiss, R.string.label_notification); 929 } else if (intent.hasCategory(ALARM_SNOOZE_TAG)) { 930 Events.sendAlarmEvent(R.string.action_snooze, R.string.label_notification); 931 } 932 } 933 934 if (alarmState >= 0) { 935 setAlarmState(context, instance, alarmState); 936 } else { 937 registerInstance(context, instance, true); 938 } 939 } else if (SHOW_AND_DISMISS_ALARM_ACTION.equals(action)) { 940 Uri uri = intent.getData(); 941 AlarmInstance instance = AlarmInstance.getInstance(context.getContentResolver(), 942 AlarmInstance.getId(uri)); 943 944 if (instance == null) { 945 LogUtils.e("Null alarminstance for SHOW_AND_DISMISS"); 946 // dismiss the notification 947 final int id = intent.getIntExtra(AlarmNotifications.EXTRA_NOTIFICATION_ID, -1); 948 if (id != -1) { 949 NotificationManagerCompat.from(context).cancel(id); 950 } 951 return; 952 } 953 954 long alarmId = instance.mAlarmId == null ? Alarm.INVALID_ID : instance.mAlarmId; 955 final Intent viewAlarmIntent = Alarm.createIntent(context, DeskClock.class, alarmId) 956 .putExtra(AlarmClockFragment.SCROLL_TO_ALARM_INTENT_EXTRA, alarmId) 957 .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 958 959 // Open DeskClock which is now positioned on the alarms tab. 960 context.startActivity(viewAlarmIntent); 961 962 deleteInstanceAndUpdateParent(context, instance); 963 } 964 } 965 966 /** 967 * Creates an intent that can be used to set an AlarmManager alarm to set the next alarm 968 * indicators. 969 */ createIndicatorIntent(Context context)970 public static Intent createIndicatorIntent(Context context) { 971 return new Intent(context, AlarmStateManager.class).setAction(INDICATOR_ACTION); 972 } 973 974 /** 975 * Abstract away how the current time is computed. If no implementation of this interface is 976 * given the default is to return {@link Calendar#getInstance()}. Otherwise, the factory 977 * instance is consulted for the current time. 978 */ 979 interface CurrentTimeFactory { getCurrentTime()980 Calendar getCurrentTime(); 981 } 982 983 /** 984 * Abstracts away how state changes are scheduled. The {@link AlarmManagerStateChangeScheduler} 985 * implementation schedules callbacks within the system AlarmManager. Alternate 986 * implementations, such as test case mocks can subvert this behavior. 987 */ 988 interface StateChangeScheduler { scheduleInstanceStateChange(Context context, Calendar time, AlarmInstance instance, int newState)989 void scheduleInstanceStateChange(Context context, Calendar time, 990 AlarmInstance instance, int newState); 991 cancelScheduledInstanceStateChange(Context context, AlarmInstance instance)992 void cancelScheduledInstanceStateChange(Context context, AlarmInstance instance); 993 } 994 995 /** 996 * Schedules state change callbacks within the AlarmManager. 997 */ 998 private static class AlarmManagerStateChangeScheduler implements StateChangeScheduler { 999 @Override scheduleInstanceStateChange(Context context, Calendar time, AlarmInstance instance, int newState)1000 public void scheduleInstanceStateChange(Context context, Calendar time, 1001 AlarmInstance instance, int newState) { 1002 final long timeInMillis = time.getTimeInMillis(); 1003 LogUtils.i("Scheduling state change %d to instance %d at %s (%d)", newState, 1004 instance.mId, AlarmUtils.getFormattedTime(context, time), timeInMillis); 1005 final Intent stateChangeIntent = 1006 createStateChangeIntent(context, ALARM_MANAGER_TAG, instance, newState); 1007 // Treat alarm state change as high priority, use foreground broadcasts 1008 stateChangeIntent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND); 1009 PendingIntent pendingIntent = PendingIntent.getService(context, instance.hashCode(), 1010 stateChangeIntent, PendingIntent.FLAG_UPDATE_CURRENT); 1011 1012 final AlarmManager am = (AlarmManager) context.getSystemService(ALARM_SERVICE); 1013 if (Utils.isMOrLater()) { 1014 // Ensure the alarm fires even if the device is dozing. 1015 am.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, timeInMillis, pendingIntent); 1016 } else { 1017 am.setExact(AlarmManager.RTC_WAKEUP, timeInMillis, pendingIntent); 1018 } 1019 } 1020 1021 @Override cancelScheduledInstanceStateChange(Context context, AlarmInstance instance)1022 public void cancelScheduledInstanceStateChange(Context context, AlarmInstance instance) { 1023 LogUtils.v("Canceling instance " + instance.mId + " timers"); 1024 1025 // Create a PendingIntent that will match any one set for this instance 1026 PendingIntent pendingIntent = PendingIntent.getService(context, instance.hashCode(), 1027 createStateChangeIntent(context, ALARM_MANAGER_TAG, instance, null), 1028 PendingIntent.FLAG_NO_CREATE); 1029 1030 if (pendingIntent != null) { 1031 AlarmManager am = (AlarmManager) context.getSystemService(ALARM_SERVICE); 1032 am.cancel(pendingIntent); 1033 pendingIntent.cancel(); 1034 } 1035 } 1036 } 1037 } 1038