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