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.support.v4.app.NotificationManagerCompat; 33 import android.text.format.DateFormat; 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.Utils; 44 import com.android.deskclock.events.Events; 45 import com.android.deskclock.provider.Alarm; 46 import com.android.deskclock.provider.AlarmInstance; 47 import com.android.deskclock.settings.SettingsActivity; 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 getCurrentTime()145 private static Calendar getCurrentTime() { 146 return sCurrentTimeFactory == null ? 147 Calendar.getInstance() : sCurrentTimeFactory.getCurrentTime(); 148 } setCurrentTimeFactory(CurrentTimeFactory currentTimeFactory)149 static void setCurrentTimeFactory(CurrentTimeFactory currentTimeFactory) { 150 sCurrentTimeFactory = currentTimeFactory; 151 } 152 setStateChangeScheduler(StateChangeScheduler stateChangeScheduler)153 static void setStateChangeScheduler(StateChangeScheduler stateChangeScheduler) { 154 if (stateChangeScheduler == null) { 155 stateChangeScheduler = new AlarmManagerStateChangeScheduler(); 156 } 157 sStateChangeScheduler = stateChangeScheduler; 158 } 159 getGlobalIntentId(Context context)160 public static int getGlobalIntentId(Context context) { 161 SharedPreferences prefs = Utils.getDefaultSharedPreferences(context); 162 return prefs.getInt(ALARM_GLOBAL_ID_EXTRA, -1); 163 } 164 updateGlobalIntentId(Context context)165 public static void updateGlobalIntentId(Context context) { 166 SharedPreferences prefs = Utils.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 */ updateNextAlarm(Context context)177 public static void updateNextAlarm(Context context) { 178 final AlarmInstance nextAlarm = getNextFiringAlarm(context); 179 180 if (Utils.isPreL()) { 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 */ getNextFiringAlarm(Context context)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 */ updateNextAlarmInSystemSettings(Context context, AlarmInstance nextAlarm)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) updateNextAlarmInAlarmManager(Context context, AlarmInstance nextAlarm)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 */ updateParentAlarm(Context context, AlarmInstance instance)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 */ createStateChangeIntent(Context context, String tag, AlarmInstance instance, Integer state)304 public static Intent createStateChangeIntent(Context context, String tag, 305 AlarmInstance instance, Integer state) { 306 // This intent is directed to AlarmService, though the actual handling of it occurs here 307 // in AlarmStateManager. The reason is that evidence exists showing the jump between the 308 // broadcast receiver (AlarmStateManager) and service (AlarmService) can be thwarted by the 309 // Out Of Memory killer. If clock is killed during that jump, firing an alarm can fail to 310 // occur. To be safer, the call begins in AlarmService, which has the power to display the 311 // firing alarm if needed, so no jump is needed. 312 Intent intent = AlarmInstance.createIntent(context, AlarmService.class, instance.mId); 313 intent.setAction(CHANGE_STATE_ACTION); 314 intent.addCategory(tag); 315 intent.putExtra(ALARM_GLOBAL_ID_EXTRA, getGlobalIntentId(context)); 316 if (state != null) { 317 intent.putExtra(ALARM_STATE_EXTRA, state.intValue()); 318 } 319 return intent; 320 } 321 322 /** 323 * Schedule alarm instance state changes with {@link AlarmManager}. 324 * 325 * @param ctx application context 326 * @param time to trigger state change 327 * @param instance to change state to 328 * @param newState to change to 329 */ scheduleInstanceStateChange(Context ctx, Calendar time, AlarmInstance instance, int newState)330 private static void scheduleInstanceStateChange(Context ctx, Calendar time, 331 AlarmInstance instance, int newState) { 332 sStateChangeScheduler.scheduleInstanceStateChange(ctx, time, instance, newState); 333 } 334 335 /** 336 * Cancel all {@link AlarmManager} timers for instance. 337 * 338 * @param ctx application context 339 * @param instance to disable all {@link AlarmManager} timers 340 */ cancelScheduledInstanceStateChange(Context ctx, AlarmInstance instance)341 private static void cancelScheduledInstanceStateChange(Context ctx, AlarmInstance instance) { 342 sStateChangeScheduler.cancelScheduledInstanceStateChange(ctx, instance); 343 } 344 345 346 /** 347 * This will set the alarm instance to the SILENT_STATE and update 348 * the application notifications and schedule any state changes that need 349 * to occur in the future. 350 * 351 * @param context application context 352 * @param instance to set state to 353 */ setSilentState(Context context, AlarmInstance instance)354 public static void setSilentState(Context context, AlarmInstance instance) { 355 LogUtils.i("Setting silent state to instance " + instance.mId); 356 357 // Update alarm in db 358 ContentResolver contentResolver = context.getContentResolver(); 359 instance.mAlarmState = AlarmInstance.SILENT_STATE; 360 AlarmInstance.updateInstance(contentResolver, instance); 361 362 // Setup instance notification and scheduling timers 363 AlarmNotifications.clearNotification(context, instance); 364 scheduleInstanceStateChange(context, instance.getLowNotificationTime(), 365 instance, AlarmInstance.LOW_NOTIFICATION_STATE); 366 } 367 368 /** 369 * This will set the alarm instance to the LOW_NOTIFICATION_STATE and update 370 * the application notifications and schedule any state changes that need 371 * to occur in the future. 372 * 373 * @param context application context 374 * @param instance to set state to 375 */ setLowNotificationState(Context context, AlarmInstance instance)376 public static void setLowNotificationState(Context context, AlarmInstance instance) { 377 LogUtils.i("Setting low notification state to instance " + instance.mId); 378 379 // Update alarm state in db 380 ContentResolver contentResolver = context.getContentResolver(); 381 instance.mAlarmState = AlarmInstance.LOW_NOTIFICATION_STATE; 382 AlarmInstance.updateInstance(contentResolver, instance); 383 384 // Setup instance notification and scheduling timers 385 AlarmNotifications.showLowPriorityNotification(context, instance); 386 scheduleInstanceStateChange(context, instance.getHighNotificationTime(), 387 instance, AlarmInstance.HIGH_NOTIFICATION_STATE); 388 } 389 390 /** 391 * This will set the alarm instance to the HIDE_NOTIFICATION_STATE and update 392 * the application notifications and schedule any state changes that need 393 * to occur in the future. 394 * 395 * @param context application context 396 * @param instance to set state to 397 */ setHideNotificationState(Context context, AlarmInstance instance)398 public static void setHideNotificationState(Context context, AlarmInstance instance) { 399 LogUtils.i("Setting hide notification state to instance " + instance.mId); 400 401 // Update alarm state in db 402 ContentResolver contentResolver = context.getContentResolver(); 403 instance.mAlarmState = AlarmInstance.HIDE_NOTIFICATION_STATE; 404 AlarmInstance.updateInstance(contentResolver, instance); 405 406 // Setup instance notification and scheduling timers 407 AlarmNotifications.clearNotification(context, instance); 408 scheduleInstanceStateChange(context, instance.getHighNotificationTime(), 409 instance, AlarmInstance.HIGH_NOTIFICATION_STATE); 410 } 411 412 /** 413 * This will set the alarm instance to the HIGH_NOTIFICATION_STATE and update 414 * the application notifications and schedule any state changes that need 415 * to occur in the future. 416 * 417 * @param context application context 418 * @param instance to set state to 419 */ setHighNotificationState(Context context, AlarmInstance instance)420 public static void setHighNotificationState(Context context, AlarmInstance instance) { 421 LogUtils.i("Setting high notification state to instance " + instance.mId); 422 423 // Update alarm state in db 424 ContentResolver contentResolver = context.getContentResolver(); 425 instance.mAlarmState = AlarmInstance.HIGH_NOTIFICATION_STATE; 426 AlarmInstance.updateInstance(contentResolver, instance); 427 428 // Setup instance notification and scheduling timers 429 AlarmNotifications.showHighPriorityNotification(context, instance); 430 scheduleInstanceStateChange(context, instance.getAlarmTime(), 431 instance, AlarmInstance.FIRED_STATE); 432 } 433 434 /** 435 * This will set the alarm instance to the FIRED_STATE and update 436 * the application notifications and schedule any state changes that need 437 * to occur in the future. 438 * 439 * @param context application context 440 * @param instance to set state to 441 */ setFiredState(Context context, AlarmInstance instance)442 public static void setFiredState(Context context, AlarmInstance instance) { 443 LogUtils.i("Setting fire state to instance " + instance.mId); 444 445 // Update alarm state in db 446 ContentResolver contentResolver = context.getContentResolver(); 447 instance.mAlarmState = AlarmInstance.FIRED_STATE; 448 AlarmInstance.updateInstance(contentResolver, instance); 449 450 if (instance.mAlarmId != null) { 451 // if the time changed *backward* and pushed an instance from missed back to fired, 452 // remove any other scheduled instances that may exist 453 AlarmInstance.deleteOtherInstances(context, contentResolver, instance.mAlarmId, 454 instance.mId); 455 } 456 457 Events.sendAlarmEvent(R.string.action_fire, 0); 458 459 Calendar timeout = instance.getTimeout(context); 460 if (timeout != null) { 461 scheduleInstanceStateChange(context, timeout, instance, AlarmInstance.MISSED_STATE); 462 } 463 464 // Instance not valid anymore, so find next alarm that will fire and notify system 465 updateNextAlarm(context); 466 } 467 468 /** 469 * This will set the alarm instance to the SNOOZE_STATE and update 470 * the application notifications and schedule any state changes that need 471 * to occur in the future. 472 * 473 * @param context application context 474 * @param instance to set state to 475 * 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 String snoozeMinutesStr = Utils.getDefaultSharedPreferences(context) 484 .getString(SettingsActivity.KEY_ALARM_SNOOZE, DEFAULT_SNOOZE_MINUTES); 485 final int snoozeMinutes = Integer.parseInt(snoozeMinutesStr); 486 Calendar newAlarmTime = Calendar.getInstance(); 487 newAlarmTime.add(Calendar.MINUTE, snoozeMinutes); 488 489 // Update alarm state and new alarm time in db. 490 LogUtils.i("Setting snoozed state to instance " + instance.mId + " for " 491 + AlarmUtils.getFormattedTime(context, newAlarmTime)); 492 instance.setAlarmTime(newAlarmTime); 493 instance.mAlarmState = AlarmInstance.SNOOZE_STATE; 494 AlarmInstance.updateInstance(context.getContentResolver(), instance); 495 496 // Setup instance notification and scheduling timers 497 AlarmNotifications.showSnoozeNotification(context, instance); 498 scheduleInstanceStateChange(context, instance.getAlarmTime(), 499 instance, AlarmInstance.FIRED_STATE); 500 501 // Display the snooze minutes in a toast. 502 if (showToast) { 503 final Handler mainHandler = new Handler(context.getMainLooper()); 504 final Runnable myRunnable = new Runnable() { 505 @Override 506 public void run() { 507 String displayTime = String.format(context.getResources().getQuantityText 508 (R.plurals.alarm_alert_snooze_set, snoozeMinutes).toString(), 509 snoozeMinutes); 510 Toast.makeText(context, displayTime, Toast.LENGTH_LONG).show(); 511 } 512 }; 513 mainHandler.post(myRunnable); 514 } 515 516 // Instance time changed, so find next alarm that will fire and notify system 517 updateNextAlarm(context); 518 } 519 getSnoozedMinutes(Context context)520 public static int getSnoozedMinutes(Context context) { 521 final String snoozeMinutesStr = Utils.getDefaultSharedPreferences(context) 522 .getString(SettingsActivity.KEY_ALARM_SNOOZE, DEFAULT_SNOOZE_MINUTES); 523 return Integer.parseInt(snoozeMinutesStr); 524 } 525 526 /** 527 * This will set the alarm instance to the MISSED_STATE and update 528 * the application notifications and schedule any state changes that need 529 * to occur in the future. 530 * 531 * @param context application context 532 * @param instance to set state to 533 */ setMissedState(Context context, AlarmInstance instance)534 public static void setMissedState(Context context, AlarmInstance instance) { 535 LogUtils.i("Setting missed state to instance " + instance.mId); 536 // Stop alarm if this instance is firing it 537 AlarmService.stopAlarm(context, instance); 538 539 // Check parent if it needs to reschedule, disable or delete itself 540 if (instance.mAlarmId != null) { 541 updateParentAlarm(context, instance); 542 } 543 544 // Update alarm state 545 ContentResolver contentResolver = context.getContentResolver(); 546 instance.mAlarmState = AlarmInstance.MISSED_STATE; 547 AlarmInstance.updateInstance(contentResolver, instance); 548 549 // Setup instance notification and scheduling timers 550 AlarmNotifications.showMissedNotification(context, instance); 551 scheduleInstanceStateChange(context, instance.getMissedTimeToLive(), 552 instance, AlarmInstance.DISMISSED_STATE); 553 554 // Instance is not valid anymore, so find next alarm that will fire and notify system 555 updateNextAlarm(context); 556 } 557 558 /** 559 * This will set the alarm instance to the PREDISMISSED_STATE and schedule an instance state 560 * change to DISMISSED_STATE at the regularly scheduled firing time. 561 * @param context 562 * @param instance 563 */ setPreDismissState(Context context, AlarmInstance instance)564 public static void setPreDismissState(Context context, AlarmInstance instance) { 565 LogUtils.i("Setting predismissed state to instance " + instance.mId); 566 567 // Update alarm in db 568 final ContentResolver contentResolver = context.getContentResolver(); 569 instance.mAlarmState = AlarmInstance.PREDISMISSED_STATE; 570 AlarmInstance.updateInstance(contentResolver, instance); 571 572 // Setup instance notification and scheduling timers 573 AlarmNotifications.clearNotification(context, instance); 574 scheduleInstanceStateChange(context, instance.getAlarmTime(), instance, 575 AlarmInstance.DISMISSED_STATE); 576 577 final Alarm alarm = Alarm.getAlarm(contentResolver, instance.mAlarmId); 578 // if it's a one time alarm set the toggle to off 579 if (alarm != null && !alarm.daysOfWeek.isRepeating()) { 580 // Check parent if it needs to reschedule, disable or delete itself 581 if (instance.mAlarmId != null) { 582 updateParentAlarm(context, instance); 583 } 584 } 585 586 updateNextAlarm(context); 587 } 588 589 /** 590 * This just sets the alarm instance to DISMISSED_STATE. 591 */ setDismissState(Context context, AlarmInstance instance)592 public static void setDismissState(Context context, AlarmInstance instance) { 593 LogUtils.i("Setting dismissed state to instance " + instance.mId); 594 instance.mAlarmState = AlarmInstance.DISMISSED_STATE; 595 final ContentResolver contentResolver = context.getContentResolver(); 596 AlarmInstance.updateInstance(contentResolver, instance); 597 } 598 599 /** 600 * This will delete the alarm instance, update the application notifications, and schedule 601 * any state changes that need to occur in the future. 602 * 603 * @param context application context 604 * @param instance to set state to 605 */ deleteInstanceAndUpdateParent(Context context, AlarmInstance instance)606 public static void deleteInstanceAndUpdateParent(Context context, AlarmInstance instance) { 607 LogUtils.i("Deleting instance " + instance.mId + " and updating parent alarm."); 608 609 // Remove all other timers and notifications associated to it 610 unregisterInstance(context, instance); 611 612 // Check parent if it needs to reschedule, disable or delete itself 613 if (instance.mAlarmId != null) { 614 updateParentAlarm(context, instance); 615 } 616 617 // Delete instance as it is not needed anymore 618 AlarmInstance.deleteInstance(context.getContentResolver(), instance.mId); 619 620 // Instance is not valid anymore, so find next alarm that will fire and notify system 621 updateNextAlarm(context); 622 } 623 624 /** 625 * This will set the instance state to DISMISSED_STATE and remove its notifications and 626 * alarm timers. 627 * 628 * @param context application context 629 * @param instance to unregister 630 */ unregisterInstance(Context context, AlarmInstance instance)631 public static void unregisterInstance(Context context, AlarmInstance instance) { 632 LogUtils.i("Unregistering instance " + instance.mId); 633 // Stop alarm if this instance is firing it 634 AlarmService.stopAlarm(context, instance); 635 AlarmNotifications.clearNotification(context, instance); 636 cancelScheduledInstanceStateChange(context, instance); 637 setDismissState(context, instance); 638 } 639 640 /** 641 * This registers the AlarmInstance to the state manager. This will look at the instance 642 * and choose the most appropriate state to put it in. This is primarily used by new 643 * alarms, but it can also be called when the system time changes. 644 * 645 * Most state changes are handled by the states themselves, but during major time changes we 646 * have to correct the alarm instance state. This means we have to handle special cases as 647 * describe below: 648 * 649 * <ul> 650 * <li>Make sure all dismissed alarms are never re-activated</li> 651 * <li>Make sure pre-dismissed alarms stay predismissed</li> 652 * <li>Make sure firing alarms stayed fired unless they should be auto-silenced</li> 653 * <li>Missed instance that have parents should be re-enabled if we went back in time</li> 654 * <li>If alarm was SNOOZED, then show the notification but don't update time</li> 655 * <li>If low priority notification was hidden, then make sure it stays hidden</li> 656 * </ul> 657 * 658 * If none of these special case are found, then we just check the time and see what is the 659 * proper state for the instance. 660 * 661 * @param context application context 662 * @param instance to register 663 */ registerInstance(Context context, AlarmInstance instance, boolean updateNextAlarm)664 public static void registerInstance(Context context, AlarmInstance instance, 665 boolean updateNextAlarm) { 666 final ContentResolver cr = context.getContentResolver(); 667 final Alarm alarm = Alarm.getAlarm(cr, instance.mAlarmId); 668 final Calendar currentTime = getCurrentTime(); 669 final Calendar alarmTime = instance.getAlarmTime(); 670 final Calendar timeoutTime = instance.getTimeout(context); 671 final Calendar lowNotificationTime = instance.getLowNotificationTime(); 672 final Calendar highNotificationTime = instance.getHighNotificationTime(); 673 final Calendar missedTTL = instance.getMissedTimeToLive(); 674 675 // Handle special use cases here 676 if (instance.mAlarmState == AlarmInstance.DISMISSED_STATE) { 677 // This should never happen, but add a quick check here 678 LogUtils.e("Alarm Instance is dismissed, but never deleted"); 679 deleteInstanceAndUpdateParent(context, instance); 680 return; 681 } else if (instance.mAlarmState == AlarmInstance.FIRED_STATE) { 682 // Keep alarm firing, unless it should be timed out 683 boolean hasTimeout = timeoutTime != null && currentTime.after(timeoutTime); 684 if (!hasTimeout) { 685 setFiredState(context, instance); 686 return; 687 } 688 } else if (instance.mAlarmState == AlarmInstance.MISSED_STATE) { 689 if (currentTime.before(alarmTime)) { 690 if (instance.mAlarmId == null) { 691 LogUtils.i("Cannot restore missed instance for one-time alarm"); 692 // This instance parent got deleted (ie. deleteAfterUse), so 693 // we should not re-activate it.- 694 deleteInstanceAndUpdateParent(context, instance); 695 return; 696 } 697 698 // TODO: This will re-activate missed snoozed alarms, but will 699 // use our normal notifications. This is not ideal, but very rare use-case. 700 // We should look into fixing this in the future. 701 702 // Make sure we re-enable the parent alarm of the instance 703 // because it will get activated by by the below code 704 alarm.enabled = true; 705 Alarm.updateAlarm(cr, alarm); 706 } 707 } else if (instance.mAlarmState == AlarmInstance.PREDISMISSED_STATE) { 708 if (currentTime.before(alarmTime)) { 709 setPreDismissState(context, instance); 710 } else { 711 deleteInstanceAndUpdateParent(context, instance); 712 } 713 return; 714 } 715 716 // Fix states that are time sensitive 717 if (currentTime.after(missedTTL)) { 718 // Alarm is so old, just dismiss it 719 deleteInstanceAndUpdateParent(context, instance); 720 } else if (currentTime.after(alarmTime)) { 721 // There is a chance that the TIME_SET occurred right when the alarm should go off, so 722 // we need to add a check to see if we should fire the alarm instead of marking it 723 // missed. 724 Calendar alarmBuffer = Calendar.getInstance(); 725 alarmBuffer.setTime(alarmTime.getTime()); 726 alarmBuffer.add(Calendar.SECOND, ALARM_FIRE_BUFFER); 727 if (currentTime.before(alarmBuffer)) { 728 setFiredState(context, instance); 729 } else { 730 setMissedState(context, instance); 731 } 732 } else if (instance.mAlarmState == AlarmInstance.SNOOZE_STATE) { 733 // We only want to display snooze notification and not update the time, 734 // so handle showing the notification directly 735 AlarmNotifications.showSnoozeNotification(context, instance); 736 scheduleInstanceStateChange(context, instance.getAlarmTime(), 737 instance, AlarmInstance.FIRED_STATE); 738 } else if (currentTime.after(highNotificationTime)) { 739 setHighNotificationState(context, instance); 740 } else if (currentTime.after(lowNotificationTime)) { 741 // Only show low notification if it wasn't hidden in the past 742 if (instance.mAlarmState == AlarmInstance.HIDE_NOTIFICATION_STATE) { 743 setHideNotificationState(context, instance); 744 } else { 745 setLowNotificationState(context, instance); 746 } 747 } else { 748 // Alarm is still active, so initialize as a silent alarm 749 setSilentState(context, instance); 750 } 751 752 // The caller prefers to handle updateNextAlarm for optimization 753 if (updateNextAlarm) { 754 updateNextAlarm(context); 755 } 756 } 757 758 /** 759 * This will delete and unregister all instances associated with alarmId, without affect 760 * the alarm itself. This should be used whenever modifying or deleting an alarm. 761 * 762 * @param context application context 763 * @param alarmId to find instances to delete. 764 */ deleteAllInstances(Context context, long alarmId)765 public static void deleteAllInstances(Context context, long alarmId) { 766 ContentResolver cr = context.getContentResolver(); 767 List<AlarmInstance> instances = AlarmInstance.getInstancesByAlarmId(cr, alarmId); 768 for (AlarmInstance instance : instances) { 769 unregisterInstance(context, instance); 770 AlarmInstance.deleteInstance(context.getContentResolver(), instance.mId); 771 } 772 updateNextAlarm(context); 773 } 774 775 /** 776 * Delete and unregister all instances unless they are snoozed. This is used whenever an alarm 777 * is modified superficially (label, vibrate, or ringtone change). 778 */ deleteNonSnoozeInstances(Context context, long alarmId)779 public static void deleteNonSnoozeInstances(Context context, long alarmId) { 780 ContentResolver cr = context.getContentResolver(); 781 List<AlarmInstance> instances = AlarmInstance.getInstancesByAlarmId(cr, alarmId); 782 for (AlarmInstance instance : instances) { 783 if (instance.mAlarmState == AlarmInstance.SNOOZE_STATE) { 784 continue; 785 } 786 unregisterInstance(context, instance); 787 AlarmInstance.deleteInstance(context.getContentResolver(), instance.mId); 788 } 789 updateNextAlarm(context); 790 } 791 792 /** 793 * Fix and update all alarm instance when a time change event occurs. 794 * 795 * @param context application context 796 */ fixAlarmInstances(Context context)797 public static void fixAlarmInstances(Context context) { 798 // Register all instances after major time changes or when phone restarts 799 final ContentResolver contentResolver = context.getContentResolver(); 800 final Calendar currentTime = getCurrentTime(); 801 for (AlarmInstance instance : AlarmInstance.getInstances(contentResolver, null)) { 802 final Alarm alarm = Alarm.getAlarm(contentResolver, instance.mAlarmId); 803 if (alarm == null) { 804 unregisterInstance(context, instance); 805 AlarmInstance.deleteInstance(contentResolver, instance.mId); 806 LogUtils.e("Found instance without matching alarm; deleting instance %s", instance); 807 continue; 808 } 809 final Calendar priorAlarmTime = alarm.getPreviousAlarmTime(instance.getAlarmTime()); 810 final Calendar missedTTLTime = instance.getMissedTimeToLive(); 811 if (currentTime.before(priorAlarmTime) || currentTime.after(missedTTLTime)) { 812 final Calendar oldAlarmTime = instance.getAlarmTime(); 813 final Calendar newAlarmTime = alarm.getNextAlarmTime(currentTime); 814 final CharSequence oldTime = DateFormat.format("MM/dd/yyyy hh:mm a", oldAlarmTime); 815 final CharSequence newTime = DateFormat.format("MM/dd/yyyy hh:mm a", newAlarmTime); 816 LogUtils.i("A time change has caused an existing alarm scheduled to fire at %s to" + 817 " be replaced by a new alarm scheduled to fire at %s", oldTime, newTime); 818 819 // The time change is so dramatic the AlarmInstance doesn't make any sense; 820 // remove it and schedule the new appropriate instance. 821 AlarmStateManager.deleteInstanceAndUpdateParent(context, instance); 822 } else { 823 registerInstance(context, instance, false); 824 } 825 } 826 827 updateNextAlarm(context); 828 } 829 830 /** 831 * Utility method to set alarm instance state via constants. 832 * 833 * @param context application context 834 * @param instance to change state on 835 * @param state to change to 836 */ setAlarmState(Context context, AlarmInstance instance, int state)837 private static void setAlarmState(Context context, AlarmInstance instance, int state) { 838 if (instance == null) { 839 LogUtils.e("Null alarm instance while setting state to %d", state); 840 return; 841 } 842 switch(state) { 843 case AlarmInstance.SILENT_STATE: 844 setSilentState(context, instance); 845 break; 846 case AlarmInstance.LOW_NOTIFICATION_STATE: 847 setLowNotificationState(context, instance); 848 break; 849 case AlarmInstance.HIDE_NOTIFICATION_STATE: 850 setHideNotificationState(context, instance); 851 break; 852 case AlarmInstance.HIGH_NOTIFICATION_STATE: 853 setHighNotificationState(context, instance); 854 break; 855 case AlarmInstance.FIRED_STATE: 856 setFiredState(context, instance); 857 break; 858 case AlarmInstance.SNOOZE_STATE: 859 setSnoozeState(context, instance, true /* showToast */); 860 break; 861 case AlarmInstance.MISSED_STATE: 862 setMissedState(context, instance); 863 break; 864 case AlarmInstance.PREDISMISSED_STATE: 865 setPreDismissState(context, instance); 866 break; 867 case AlarmInstance.DISMISSED_STATE: 868 deleteInstanceAndUpdateParent(context, instance); 869 break; 870 default: 871 LogUtils.e("Trying to change to unknown alarm state: " + state); 872 } 873 } 874 875 @Override onReceive(final Context context, final Intent intent)876 public void onReceive(final Context context, final Intent intent) { 877 if (INDICATOR_ACTION.equals(intent.getAction())) { 878 return; 879 } 880 881 final PendingResult result = goAsync(); 882 final PowerManager.WakeLock wl = AlarmAlertWakeLock.createPartialWakeLock(context); 883 wl.acquire(); 884 AsyncHandler.post(new Runnable() { 885 @Override 886 public void run() { 887 handleIntent(context, intent); 888 result.finish(); 889 wl.release(); 890 } 891 }); 892 } 893 handleIntent(Context context, Intent intent)894 public static void handleIntent(Context context, Intent intent) { 895 final String action = intent.getAction(); 896 LogUtils.v("AlarmStateManager received intent " + intent); 897 if (CHANGE_STATE_ACTION.equals(action)) { 898 Uri uri = intent.getData(); 899 AlarmInstance instance = AlarmInstance.getInstance(context.getContentResolver(), 900 AlarmInstance.getId(uri)); 901 if (instance == null) { 902 LogUtils.e("Can not change state for unknown instance: " + uri); 903 return; 904 } 905 906 int globalId = getGlobalIntentId(context); 907 int intentId = intent.getIntExtra(ALARM_GLOBAL_ID_EXTRA, -1); 908 int alarmState = intent.getIntExtra(ALARM_STATE_EXTRA, -1); 909 if (intentId != globalId) { 910 LogUtils.i("IntentId: " + intentId + " GlobalId: " + globalId + " AlarmState: " + 911 alarmState); 912 // Allows dismiss/snooze requests to go through 913 if (!intent.hasCategory(ALARM_DISMISS_TAG) && 914 !intent.hasCategory(ALARM_SNOOZE_TAG)) { 915 LogUtils.i("Ignoring old Intent"); 916 return; 917 } 918 } 919 920 if (intent.getBooleanExtra(FROM_NOTIFICATION_EXTRA, false)) { 921 if (intent.hasCategory(ALARM_DISMISS_TAG)) { 922 Events.sendAlarmEvent(R.string.action_dismiss, R.string.label_notification); 923 } else if (intent.hasCategory(ALARM_SNOOZE_TAG)) { 924 Events.sendAlarmEvent(R.string.action_snooze, R.string.label_notification); 925 } 926 } 927 928 if (alarmState >= 0) { 929 setAlarmState(context, instance, alarmState); 930 } else { 931 registerInstance(context, instance, true); 932 } 933 } else if (SHOW_AND_DISMISS_ALARM_ACTION.equals(action)) { 934 Uri uri = intent.getData(); 935 AlarmInstance instance = AlarmInstance.getInstance(context.getContentResolver(), 936 AlarmInstance.getId(uri)); 937 938 if (instance == null) { 939 LogUtils.e("Null alarminstance for SHOW_AND_DISMISS"); 940 // dismiss the notification 941 final int id = intent.getIntExtra(AlarmNotifications.EXTRA_NOTIFICATION_ID, -1); 942 if (id != -1) { 943 NotificationManagerCompat.from(context).cancel(id); 944 } 945 return; 946 } 947 948 long alarmId = instance.mAlarmId == null ? Alarm.INVALID_ID : instance.mAlarmId; 949 Intent viewAlarmIntent = Alarm.createIntent(context, DeskClock.class, alarmId); 950 viewAlarmIntent.putExtra(DeskClock.SELECT_TAB_INTENT_EXTRA, DeskClock.ALARM_TAB_INDEX); 951 viewAlarmIntent.putExtra(AlarmClockFragment.SCROLL_TO_ALARM_INTENT_EXTRA, alarmId); 952 viewAlarmIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 953 context.startActivity(viewAlarmIntent); 954 deleteInstanceAndUpdateParent(context, instance); 955 } 956 } 957 958 /** 959 * Creates an intent that can be used to set an AlarmManager alarm to set the next alarm 960 * indicators. 961 */ createIndicatorIntent(Context context)962 public static Intent createIndicatorIntent(Context context) { 963 return new Intent(context, AlarmStateManager.class).setAction(INDICATOR_ACTION); 964 } 965 966 /** 967 * Abstract away how the current time is computed. If no implementation of this interface is 968 * given the default is to return {@link Calendar#getInstance()}. Otherwise, the factory 969 * instance is consulted for the current time. 970 */ 971 interface CurrentTimeFactory { getCurrentTime()972 Calendar getCurrentTime(); 973 } 974 975 /** 976 * Abstracts away how state changes are scheduled. The {@link AlarmManagerStateChangeScheduler} 977 * implementation schedules callbacks within the system AlarmManager. Alternate 978 * implementations, such as test case mocks can subvert this behavior. 979 */ 980 interface StateChangeScheduler { scheduleInstanceStateChange(Context context, Calendar time, AlarmInstance instance, int newState)981 void scheduleInstanceStateChange(Context context, Calendar time, 982 AlarmInstance instance, int newState); 983 cancelScheduledInstanceStateChange(Context context, AlarmInstance instance)984 void cancelScheduledInstanceStateChange(Context context, AlarmInstance instance); 985 } 986 987 /** 988 * Schedules state change callbacks within the AlarmManager. 989 */ 990 private static class AlarmManagerStateChangeScheduler implements StateChangeScheduler { 991 @Override scheduleInstanceStateChange(Context context, Calendar time, AlarmInstance instance, int newState)992 public void scheduleInstanceStateChange(Context context, Calendar time, 993 AlarmInstance instance, int newState) { 994 final long timeInMillis = time.getTimeInMillis(); 995 LogUtils.v("Scheduling state change %d to instance %d at %s (%d)", newState, 996 instance.mId, AlarmUtils.getFormattedTime(context, time), timeInMillis); 997 final Intent stateChangeIntent = 998 createStateChangeIntent(context, ALARM_MANAGER_TAG, instance, newState); 999 // Treat alarm state change as high priority, use foreground broadcasts 1000 stateChangeIntent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND); 1001 PendingIntent pendingIntent = PendingIntent.getService(context, instance.hashCode(), 1002 stateChangeIntent, PendingIntent.FLAG_UPDATE_CURRENT); 1003 1004 final AlarmManager am = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); 1005 am.setExact(AlarmManager.RTC_WAKEUP, timeInMillis, pendingIntent); 1006 } 1007 1008 @Override cancelScheduledInstanceStateChange(Context context, AlarmInstance instance)1009 public void cancelScheduledInstanceStateChange(Context context, AlarmInstance instance) { 1010 LogUtils.v("Canceling instance " + instance.mId + " timers"); 1011 1012 // Create a PendingIntent that will match any one set for this instance 1013 PendingIntent pendingIntent = PendingIntent.getService(context, instance.hashCode(), 1014 createStateChangeIntent(context, ALARM_MANAGER_TAG, instance, null), 1015 PendingIntent.FLAG_NO_CREATE); 1016 1017 if (pendingIntent != null) { 1018 AlarmManager am = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); 1019 am.cancel(pendingIntent); 1020 pendingIntent.cancel(); 1021 } 1022 } 1023 } 1024 } 1025