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