• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 }