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 }