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