/* * Copyright (C) 2020 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.deskclock.alarms import android.annotation.TargetApi import android.app.AlarmManager import android.app.AlarmManager.AlarmClockInfo import android.app.PendingIntent import android.content.BroadcastReceiver import android.content.ContentResolver import android.content.Context import android.content.Context.ALARM_SERVICE import android.content.Intent import android.net.Uri import android.os.Build import android.os.Handler import android.os.PowerManager import android.provider.Settings import android.provider.Settings.System.NEXT_ALARM_FORMATTED import android.text.format.DateFormat import android.widget.Toast import androidx.core.app.NotificationManagerCompat import com.android.deskclock.AlarmAlertWakeLock import com.android.deskclock.AlarmClockFragment import com.android.deskclock.AlarmUtils import com.android.deskclock.AsyncHandler import com.android.deskclock.DeskClock import com.android.deskclock.data.DataModel import com.android.deskclock.events.Events import com.android.deskclock.LogUtils import com.android.deskclock.provider.Alarm import com.android.deskclock.provider.AlarmInstance import com.android.deskclock.provider.ClockContract.InstancesColumns import com.android.deskclock.R import com.android.deskclock.Utils import java.util.Calendar /** * This class handles all the state changes for alarm instances. You need to * register all alarm instances with the state manager if you want them to * be activated. If a major time change has occurred (ie. TIMEZONE_CHANGE, TIMESET_CHANGE), * then you must also re-register instances to fix their states. * * Please see [) for special transitions when major time changes][.registerInstance] */ class AlarmStateManager : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { if (INDICATOR_ACTION == intent.getAction()) { return } val result: PendingResult = goAsync() val wl: PowerManager.WakeLock = AlarmAlertWakeLock.createPartialWakeLock(context) wl.acquire() AsyncHandler.post { handleIntent(context, intent) result.finish() wl.release() } } /** * Abstract away how the current time is computed. If no implementation of this interface is * given the default is to return [Calendar.getInstance]. Otherwise, the factory * instance is consulted for the current time. */ interface CurrentTimeFactory { val currentTime: Calendar } /** * Abstracts away how state changes are scheduled. The [AlarmManagerStateChangeScheduler] * implementation schedules callbacks within the system AlarmManager. Alternate * implementations, such as test case mocks can subvert this behavior. */ interface StateChangeScheduler { fun scheduleInstanceStateChange( context: Context, time: Calendar, instance: AlarmInstance, newState: Int ) fun cancelScheduledInstanceStateChange(context: Context, instance: AlarmInstance) } /** * Schedules state change callbacks within the AlarmManager. */ private class AlarmManagerStateChangeScheduler : StateChangeScheduler { override fun scheduleInstanceStateChange( context: Context, time: Calendar, instance: AlarmInstance, newState: Int ) { val timeInMillis = time.timeInMillis LogUtils.i("Scheduling state change %d to instance %d at %s (%d)", newState, instance.mId, AlarmUtils.getFormattedTime(context, time), timeInMillis) val stateChangeIntent: Intent = createStateChangeIntent(context, ALARM_MANAGER_TAG, instance, newState) // Treat alarm state change as high priority, use foreground broadcasts stateChangeIntent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND) val pendingIntent: PendingIntent = PendingIntent.getService(context, instance.hashCode(), stateChangeIntent, PendingIntent.FLAG_UPDATE_CURRENT) val am: AlarmManager = context.getSystemService(ALARM_SERVICE) as AlarmManager if (Utils.isMOrLater) { // Ensure the alarm fires even if the device is dozing. am.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, timeInMillis, pendingIntent) } else { am.setExact(AlarmManager.RTC_WAKEUP, timeInMillis, pendingIntent) } } override fun cancelScheduledInstanceStateChange(context: Context, instance: AlarmInstance) { LogUtils.v("Canceling instance " + instance.mId + " timers") // Create a PendingIntent that will match any one set for this instance val pendingIntent: PendingIntent? = PendingIntent.getService(context, instance.hashCode(), createStateChangeIntent(context, ALARM_MANAGER_TAG, instance, null), PendingIntent.FLAG_NO_CREATE) pendingIntent?.let { val am: AlarmManager = context.getSystemService(ALARM_SERVICE) as AlarmManager am.cancel(it) it.cancel() } } } companion object { // Intent action to trigger an instance state change. const val CHANGE_STATE_ACTION = "change_state" // Intent action to show the alarm and dismiss the instance const val SHOW_AND_DISMISS_ALARM_ACTION = "show_and_dismiss_alarm" // Intent action for an AlarmManager alarm serving only to set the next alarm indicators private const val INDICATOR_ACTION = "indicator" // System intent action to notify AppWidget that we changed the alarm text. const val ACTION_ALARM_CHANGED = "com.android.deskclock.ALARM_CHANGED" // Extra key to set the desired state change. const val ALARM_STATE_EXTRA = "intent.extra.alarm.state" // Extra key to indicate the state change was launched from a notification. const val FROM_NOTIFICATION_EXTRA = "intent.extra.from.notification" // Extra key to set the global broadcast id. private const val ALARM_GLOBAL_ID_EXTRA = "intent.extra.alarm.global.id" // Intent category tags used to dismiss, snooze or delete an alarm const val ALARM_DISMISS_TAG = "DISMISS_TAG" const val ALARM_SNOOZE_TAG = "SNOOZE_TAG" const val ALARM_DELETE_TAG = "DELETE_TAG" // Intent category tag used when schedule state change intents in alarm manager. private const val ALARM_MANAGER_TAG = "ALARM_MANAGER" // Buffer time in seconds to fire alarm instead of marking it missed. const val ALARM_FIRE_BUFFER = 15 // A factory for the current time; can be mocked for testing purposes. private var sCurrentTimeFactory: CurrentTimeFactory? = null // Schedules alarm state transitions; can be mocked for testing purposes. private var sStateChangeScheduler: StateChangeScheduler = AlarmManagerStateChangeScheduler() private val currentTime: Calendar get() = (if (sCurrentTimeFactory == null) { DataModel.dataModel.calendar } else { sCurrentTimeFactory!!.currentTime }) fun setCurrentTimeFactory(currentTimeFactory: CurrentTimeFactory?) { sCurrentTimeFactory = currentTimeFactory } fun setStateChangeScheduler(stateChangeScheduler: StateChangeScheduler?) { sStateChangeScheduler = stateChangeScheduler ?: AlarmManagerStateChangeScheduler() } /** * Update the next alarm stored in framework. This value is also displayed in digital * widgets and the clock tab in this app. */ private fun updateNextAlarm(context: Context) { val nextAlarm = getNextFiringAlarm(context) if (Utils.isPreL) { updateNextAlarmInSystemSettings(context, nextAlarm) } else { updateNextAlarmInAlarmManager(context, nextAlarm) } } /** * Returns an alarm instance of an alarm that's going to fire next. * * @param context application context * @return an alarm instance that will fire earliest relative to current time. */ @JvmStatic fun getNextFiringAlarm(context: Context): AlarmInstance? { val cr: ContentResolver = context.getContentResolver() val activeAlarmQuery: String = InstancesColumns.ALARM_STATE + "<" + InstancesColumns.FIRED_STATE val alarmInstances = AlarmInstance.getInstances(cr, activeAlarmQuery) var nextAlarm: AlarmInstance? = null for (instance in alarmInstances) { if (nextAlarm == null || instance.alarmTime.before(nextAlarm.alarmTime)) { nextAlarm = instance } } return nextAlarm } /** * Used in pre-L devices, where "next alarm" is stored in system settings. */ @TargetApi(Build.VERSION_CODES.KITKAT) private fun updateNextAlarmInSystemSettings(context: Context, nextAlarm: AlarmInstance?) { // Format the next alarm time if an alarm is scheduled. var time = "" if (nextAlarm != null) { time = AlarmUtils.getFormattedTime(context, nextAlarm.alarmTime) } try { // Write directly to NEXT_ALARM_FORMATTED in all pre-L versions Settings.System.putString(context.getContentResolver(), NEXT_ALARM_FORMATTED, time) LogUtils.i("Updated next alarm time to: '$time'") // Send broadcast message so pre-L AppWidgets will recognize an update. context.sendBroadcast(Intent(ACTION_ALARM_CHANGED)) } catch (se: SecurityException) { // The user has most likely revoked WRITE_SETTINGS. LogUtils.e("Unable to update next alarm to: '$time'", se) } } /** * Used in L and later devices where "next alarm" is stored in the Alarm Manager. */ @TargetApi(Build.VERSION_CODES.LOLLIPOP) private fun updateNextAlarmInAlarmManager(context: Context, nextAlarm: AlarmInstance?) { // Sets a surrogate alarm with alarm manager that provides the AlarmClockInfo for the // alarm that is going to fire next. The operation is constructed such that it is // ignored by AlarmStateManager. val alarmManager: AlarmManager = context.getSystemService(ALARM_SERVICE) as AlarmManager val flags = if (nextAlarm == null) PendingIntent.FLAG_NO_CREATE else 0 val operation: PendingIntent? = PendingIntent.getBroadcast(context, 0 /* requestCode */, createIndicatorIntent(context), flags) if (nextAlarm != null) { LogUtils.i("Setting upcoming AlarmClockInfo for alarm: " + nextAlarm.mId) val alarmTime: Long = nextAlarm.alarmTime.timeInMillis // Create an intent that can be used to show or edit details of the next alarm. val viewIntent: PendingIntent = PendingIntent.getActivity(context, nextAlarm.hashCode(), AlarmNotifications.createViewAlarmIntent(context, nextAlarm), PendingIntent.FLAG_UPDATE_CURRENT) val info = AlarmClockInfo(alarmTime, viewIntent) Utils.updateNextAlarm(alarmManager, info, operation) } else if (operation != null) { LogUtils.i("Canceling upcoming AlarmClockInfo") alarmManager.cancel(operation) } } /** * Used by dismissed and missed states, to update parent alarm. This will either * disable, delete or reschedule parent alarm. * * @param context application context * @param instance to update parent for */ private fun updateParentAlarm(context: Context, instance: AlarmInstance) { val cr: ContentResolver = context.getContentResolver() val alarm = Alarm.getAlarm(cr, instance.mAlarmId!!) if (alarm == null) { LogUtils.e("Parent has been deleted with instance: $instance") return } if (!alarm.daysOfWeek.isRepeating) { if (alarm.deleteAfterUse) { LogUtils.i("Deleting parent alarm: " + alarm.id) Alarm.deleteAlarm(cr, alarm.id) } else { LogUtils.i("Disabling parent alarm: " + alarm.id) alarm.enabled = false Alarm.updateAlarm(cr, alarm) } } else { // Schedule the next repeating instance which may be before the current instance if // a time jump has occurred. Otherwise, if the current instance is the next instance // and has already been fired, schedule the subsequent instance. var nextRepeatedInstance = alarm.createInstanceAfter(currentTime) if (instance.mAlarmState > InstancesColumns.FIRED_STATE && nextRepeatedInstance.alarmTime == instance.alarmTime) { nextRepeatedInstance = alarm.createInstanceAfter(instance.alarmTime) } LogUtils.i("Creating new instance for repeating alarm " + alarm.id + " at " + AlarmUtils.getFormattedTime(context, nextRepeatedInstance.alarmTime)) AlarmInstance.addInstance(cr, nextRepeatedInstance) registerInstance(context, nextRepeatedInstance, true) } } /** * Utility method to create a proper change state intent. * * @param context application context * @param tag used to make intent differ from other state change intents. * @param instance to change state to * @param state to change to. * @return intent that can be used to change an alarm instance state */ fun createStateChangeIntent( context: Context?, tag: String?, instance: AlarmInstance, state: Int? ): Intent { // This intent is directed to AlarmService, though the actual handling of it occurs here // in AlarmStateManager. The reason is that evidence exists showing the jump between the // broadcast receiver (AlarmStateManager) and service (AlarmService) can be thwarted by // the Out Of Memory killer. If clock is killed during that jump, firing an alarm can // fail to occur. To be safer, the call begins in AlarmService, which has the power to // display the firing alarm if needed, so no jump is needed. val intent: Intent = AlarmInstance.createIntent(context, AlarmService::class.java, instance.mId) intent.setAction(CHANGE_STATE_ACTION) intent.addCategory(tag) intent.putExtra(ALARM_GLOBAL_ID_EXTRA, DataModel.dataModel.globalIntentId) if (state != null) { intent.putExtra(ALARM_STATE_EXTRA, state.toInt()) } return intent } /** * Schedule alarm instance state changes with [AlarmManager]. * * @param ctx application context * @param time to trigger state change * @param instance to change state to * @param newState to change to */ private fun scheduleInstanceStateChange( ctx: Context, time: Calendar, instance: AlarmInstance, newState: Int ) { sStateChangeScheduler.scheduleInstanceStateChange(ctx, time, instance, newState) } /** * Cancel all [AlarmManager] timers for instance. * * @param ctx application context * @param instance to disable all [AlarmManager] timers */ private fun cancelScheduledInstanceStateChange(ctx: Context, instance: AlarmInstance) { sStateChangeScheduler.cancelScheduledInstanceStateChange(ctx, instance) } /** * This will set the alarm instance to the SILENT_STATE and update * the application notifications and schedule any state changes that need * to occur in the future. * * @param context application context * @param instance to set state to */ private fun setSilentState(context: Context, instance: AlarmInstance) { LogUtils.i("Setting silent state to instance " + instance.mId) // Update alarm in db val contentResolver: ContentResolver = context.getContentResolver() instance.mAlarmState = InstancesColumns.SILENT_STATE AlarmInstance.updateInstance(contentResolver, instance) // Setup instance notification and scheduling timers AlarmNotifications.clearNotification(context, instance) scheduleInstanceStateChange(context, instance.lowNotificationTime, instance, InstancesColumns.LOW_NOTIFICATION_STATE) } /** * This will set the alarm instance to the LOW_NOTIFICATION_STATE and update * the application notifications and schedule any state changes that need * to occur in the future. * * @param context application context * @param instance to set state to */ private fun setLowNotificationState(context: Context, instance: AlarmInstance) { LogUtils.i("Setting low notification state to instance " + instance.mId) // Update alarm state in db val contentResolver: ContentResolver = context.getContentResolver() instance.mAlarmState = InstancesColumns.LOW_NOTIFICATION_STATE AlarmInstance.updateInstance(contentResolver, instance) // Setup instance notification and scheduling timers AlarmNotifications.showLowPriorityNotification(context, instance) scheduleInstanceStateChange(context, instance.highNotificationTime, instance, InstancesColumns.HIGH_NOTIFICATION_STATE) } /** * This will set the alarm instance to the HIDE_NOTIFICATION_STATE and update * the application notifications and schedule any state changes that need * to occur in the future. * * @param context application context * @param instance to set state to */ private fun setHideNotificationState(context: Context, instance: AlarmInstance) { LogUtils.i("Setting hide notification state to instance " + instance.mId) // Update alarm state in db val contentResolver: ContentResolver = context.getContentResolver() instance.mAlarmState = InstancesColumns.HIDE_NOTIFICATION_STATE AlarmInstance.updateInstance(contentResolver, instance) // Setup instance notification and scheduling timers AlarmNotifications.clearNotification(context, instance) scheduleInstanceStateChange(context, instance.highNotificationTime, instance, InstancesColumns.HIGH_NOTIFICATION_STATE) } /** * This will set the alarm instance to the HIGH_NOTIFICATION_STATE and update * the application notifications and schedule any state changes that need * to occur in the future. * * @param context application context * @param instance to set state to */ private fun setHighNotificationState(context: Context, instance: AlarmInstance) { LogUtils.i("Setting high notification state to instance " + instance.mId) // Update alarm state in db val contentResolver: ContentResolver = context.getContentResolver() instance.mAlarmState = InstancesColumns.HIGH_NOTIFICATION_STATE AlarmInstance.updateInstance(contentResolver, instance) // Setup instance notification and scheduling timers AlarmNotifications.showHighPriorityNotification(context, instance) scheduleInstanceStateChange(context, instance.alarmTime, instance, InstancesColumns.FIRED_STATE) } /** * This will set the alarm instance to the FIRED_STATE and update * the application notifications and schedule any state changes that need * to occur in the future. * * @param context application context * @param instance to set state to */ private fun setFiredState(context: Context, instance: AlarmInstance) { LogUtils.i("Setting fire state to instance " + instance.mId) // Update alarm state in db val contentResolver: ContentResolver = context.getContentResolver() instance.mAlarmState = InstancesColumns.FIRED_STATE AlarmInstance.updateInstance(contentResolver, instance) instance.mAlarmId?.let { // if the time changed *backward* and pushed an instance from missed back to fired, // remove any other scheduled instances that may exist AlarmInstance.deleteOtherInstances(context, contentResolver, it, instance.mId) } Events.sendAlarmEvent(R.string.action_fire, 0) val timeout: Calendar? = instance.timeout timeout?.let { scheduleInstanceStateChange(context, it, instance, InstancesColumns.MISSED_STATE) } // Instance not valid anymore, so find next alarm that will fire and notify system updateNextAlarm(context) } /** * This will set the alarm instance to the SNOOZE_STATE and update * the application notifications and schedule any state changes that need * to occur in the future. * * @param context application context * @param instance to set state to */ @JvmStatic fun setSnoozeState( context: Context, instance: AlarmInstance, showToast: Boolean ) { // Stop alarm if this instance is firing it AlarmService.stopAlarm(context, instance) // Calculate the new snooze alarm time val snoozeMinutes = DataModel.dataModel.snoozeLength val newAlarmTime = Calendar.getInstance() newAlarmTime.add(Calendar.MINUTE, snoozeMinutes) // Update alarm state and new alarm time in db. LogUtils.i("Setting snoozed state to instance " + instance.mId + " for " + AlarmUtils.getFormattedTime(context, newAlarmTime)) instance.alarmTime = newAlarmTime instance.mAlarmState = InstancesColumns.SNOOZE_STATE AlarmInstance.updateInstance(context.getContentResolver(), instance) // Setup instance notification and scheduling timers AlarmNotifications.showSnoozeNotification(context, instance) scheduleInstanceStateChange(context, instance.alarmTime, instance, InstancesColumns.FIRED_STATE) // Display the snooze minutes in a toast. if (showToast) { val mainHandler = Handler(context.getMainLooper()) val myRunnable = Runnable { val displayTime = String.format( context .getResources() .getQuantityText(R.plurals.alarm_alert_snooze_set, snoozeMinutes) .toString(), snoozeMinutes) Toast.makeText(context, displayTime, Toast.LENGTH_LONG).show() } mainHandler.post(myRunnable) } // Instance time changed, so find next alarm that will fire and notify system updateNextAlarm(context) } /** * This will set the alarm instance to the MISSED_STATE and update * the application notifications and schedule any state changes that need * to occur in the future. * * @param context application context * @param instance to set state to */ fun setMissedState(context: Context, instance: AlarmInstance) { LogUtils.i("Setting missed state to instance " + instance.mId) // Stop alarm if this instance is firing it AlarmService.stopAlarm(context, instance) // Check parent if it needs to reschedule, disable or delete itself if (instance.mAlarmId != null) { updateParentAlarm(context, instance) } // Update alarm state val contentResolver: ContentResolver = context.getContentResolver() instance.mAlarmState = InstancesColumns.MISSED_STATE AlarmInstance.updateInstance(contentResolver, instance) // Setup instance notification and scheduling timers AlarmNotifications.showMissedNotification(context, instance) scheduleInstanceStateChange(context, instance.missedTimeToLive, instance, InstancesColumns.DISMISSED_STATE) // Instance is not valid anymore, so find next alarm that will fire and notify system updateNextAlarm(context) } /** * This will set the alarm instance to the PREDISMISSED_STATE and schedule an instance state * change to DISMISSED_STATE at the regularly scheduled firing time. * * @param context application context * @param instance to set state to */ @JvmStatic fun setPreDismissState(context: Context, instance: AlarmInstance) { LogUtils.i("Setting predismissed state to instance " + instance.mId) // Update alarm in db val contentResolver: ContentResolver = context.getContentResolver() instance.mAlarmState = InstancesColumns.PREDISMISSED_STATE AlarmInstance.updateInstance(contentResolver, instance) // Setup instance notification and scheduling timers AlarmNotifications.clearNotification(context, instance) scheduleInstanceStateChange(context, instance.alarmTime, instance, InstancesColumns.DISMISSED_STATE) // Check parent if it needs to reschedule, disable or delete itself if (instance.mAlarmId != null) { updateParentAlarm(context, instance) } updateNextAlarm(context) } /** * This just sets the alarm instance to DISMISSED_STATE. */ private fun setDismissState(context: Context, instance: AlarmInstance) { LogUtils.i("Setting dismissed state to instance " + instance.mId) instance.mAlarmState = InstancesColumns.DISMISSED_STATE val contentResolver: ContentResolver = context.getContentResolver() AlarmInstance.updateInstance(contentResolver, instance) } /** * This will delete the alarm instance, update the application notifications, and schedule * any state changes that need to occur in the future. * * @param context application context * @param instance to set state to */ @JvmStatic fun deleteInstanceAndUpdateParent(context: Context, instance: AlarmInstance) { LogUtils.i("Deleting instance " + instance.mId + " and updating parent alarm.") // Remove all other timers and notifications associated to it unregisterInstance(context, instance) // Check parent if it needs to reschedule, disable or delete itself if (instance.mAlarmId != null) { updateParentAlarm(context, instance) } // Delete instance as it is not needed anymore AlarmInstance.deleteInstance(context.getContentResolver(), instance.mId) // Instance is not valid anymore, so find next alarm that will fire and notify system updateNextAlarm(context) } /** * This will set the instance state to DISMISSED_STATE and remove its notifications and * alarm timers. * * @param context application context * @param instance to unregister */ fun unregisterInstance(context: Context, instance: AlarmInstance) { LogUtils.i("Unregistering instance " + instance.mId) // Stop alarm if this instance is firing it AlarmService.stopAlarm(context, instance) AlarmNotifications.clearNotification(context, instance) cancelScheduledInstanceStateChange(context, instance) setDismissState(context, instance) } /** * This registers the AlarmInstance to the state manager. This will look at the instance * and choose the most appropriate state to put it in. This is primarily used by new * alarms, but it can also be called when the system time changes. * * Most state changes are handled by the states themselves, but during major time changes we * have to correct the alarm instance state. This means we have to handle special cases as * describe below: * * * * Make sure all dismissed alarms are never re-activated * * Make sure pre-dismissed alarms stay predismissed * * Make sure firing alarms stayed fired unless they should be auto-silenced * * Missed instance that have parents should be re-enabled if we went back in time * * If alarm was SNOOZED, then show the notification but don't update time * * If low priority notification was hidden, then make sure it stays hidden * * * If none of these special case are found, then we just check the time and see what is the * proper state for the instance. * * @param context application context * @param instance to register */ @JvmStatic fun registerInstance( context: Context, instance: AlarmInstance, updateNextAlarm: Boolean ) { LogUtils.i("Registering instance: " + instance.mId) val cr: ContentResolver = context.getContentResolver() val alarm = Alarm.getAlarm(cr, instance.mAlarmId!!) val currentTime = currentTime val alarmTime: Calendar = instance.alarmTime val timeoutTime: Calendar? = instance.timeout val lowNotificationTime: Calendar = instance.lowNotificationTime val highNotificationTime: Calendar = instance.highNotificationTime val missedTTL: Calendar = instance.missedTimeToLive // Handle special use cases here if (instance.mAlarmState == InstancesColumns.DISMISSED_STATE) { // This should never happen, but add a quick check here LogUtils.e("Alarm Instance is dismissed, but never deleted") deleteInstanceAndUpdateParent(context, instance) return } else if (instance.mAlarmState == InstancesColumns.FIRED_STATE) { // Keep alarm firing, unless it should be timed out val hasTimeout = timeoutTime != null && currentTime.after(timeoutTime) if (!hasTimeout) { setFiredState(context, instance) return } } else if (instance.mAlarmState == InstancesColumns.MISSED_STATE) { if (currentTime.before(alarmTime)) { if (instance.mAlarmId == null) { LogUtils.i("Cannot restore missed instance for one-time alarm") // This instance parent got deleted (ie. deleteAfterUse), so // we should not re-activate it.- deleteInstanceAndUpdateParent(context, instance) return } // TODO: This will re-activate missed snoozed alarms, but will // use our normal notifications. This is not ideal, but very rare use-case. // We should look into fixing this in the future. // Make sure we re-enable the parent alarm of the instance // because it will get activated by by the below code alarm!!.enabled = true Alarm.updateAlarm(cr, alarm) } } else if (instance.mAlarmState == InstancesColumns.PREDISMISSED_STATE) { if (currentTime.before(alarmTime)) { setPreDismissState(context, instance) } else { deleteInstanceAndUpdateParent(context, instance) } return } // Fix states that are time sensitive if (currentTime.after(missedTTL)) { // Alarm is so old, just dismiss it deleteInstanceAndUpdateParent(context, instance) } else if (currentTime.after(alarmTime)) { // There is a chance that the TIME_SET occurred right when the alarm should go off, // so we need to add a check to see if we should fire the alarm instead of marking // it missed. val alarmBuffer = Calendar.getInstance() alarmBuffer.time = alarmTime.time alarmBuffer.add(Calendar.SECOND, ALARM_FIRE_BUFFER) if (currentTime.before(alarmBuffer)) { setFiredState(context, instance) } else { setMissedState(context, instance) } } else if (instance.mAlarmState == InstancesColumns.SNOOZE_STATE) { // We only want to display snooze notification and not update the time, // so handle showing the notification directly AlarmNotifications.showSnoozeNotification(context, instance) scheduleInstanceStateChange(context, instance.alarmTime, instance, InstancesColumns.FIRED_STATE) } else if (currentTime.after(highNotificationTime)) { setHighNotificationState(context, instance) } else if (currentTime.after(lowNotificationTime)) { // Only show low notification if it wasn't hidden in the past if (instance.mAlarmState == InstancesColumns.HIDE_NOTIFICATION_STATE) { setHideNotificationState(context, instance) } else { setLowNotificationState(context, instance) } } else { // Alarm is still active, so initialize as a silent alarm setSilentState(context, instance) } // The caller prefers to handle updateNextAlarm for optimization if (updateNextAlarm) { updateNextAlarm(context) } } /** * This will delete and unregister all instances associated with alarmId, without affect * the alarm itself. This should be used whenever modifying or deleting an alarm. * * @param context application context * @param alarmId to find instances to delete. */ @JvmStatic fun deleteAllInstances(context: Context, alarmId: Long) { LogUtils.i("Deleting all instances of alarm: $alarmId") val cr: ContentResolver = context.getContentResolver() val instances = AlarmInstance.getInstancesByAlarmId(cr, alarmId) for (instance in instances) { unregisterInstance(context, instance) AlarmInstance.deleteInstance(context.getContentResolver(), instance.mId) } updateNextAlarm(context) } /** * Delete and unregister all instances unless they are snoozed. This is used whenever an * alarm is modified superficially (label, vibrate, or ringtone change). */ fun deleteNonSnoozeInstances(context: Context, alarmId: Long) { LogUtils.i("Deleting all non-snooze instances of alarm: $alarmId") val cr: ContentResolver = context.getContentResolver() val instances = AlarmInstance.getInstancesByAlarmId(cr, alarmId) for (instance in instances) { if (instance.mAlarmState == InstancesColumns.SNOOZE_STATE) { continue } unregisterInstance(context, instance) AlarmInstance.deleteInstance(context.getContentResolver(), instance.mId) } updateNextAlarm(context) } /** * Fix and update all alarm instance when a time change event occurs. * * @param context application context */ @JvmStatic fun fixAlarmInstances(context: Context) { LogUtils.i("Fixing alarm instances") // Register all instances after major time changes or when phone restarts val contentResolver: ContentResolver = context.getContentResolver() val currentTime = currentTime // Sort the instances in reverse chronological order so that later instances are fixed // or deleted before re-scheduling prior instances (which may re-create or update the // later instances). val instances = AlarmInstance.getInstances( contentResolver, null /* selection */) instances.sortWith(Comparator { lhs, rhs -> rhs.alarmTime.compareTo(lhs.alarmTime) }) for (instance in instances) { val alarm = Alarm.getAlarm(contentResolver, instance.mAlarmId!!) if (alarm == null) { unregisterInstance(context, instance) AlarmInstance.deleteInstance(contentResolver, instance.mId) LogUtils.e("Found instance without matching alarm; deleting instance %s", instance) continue } val priorAlarmTime = alarm.getPreviousAlarmTime(instance.alarmTime) val missedTTLTime: Calendar = instance.missedTimeToLive if (currentTime.before(priorAlarmTime) || currentTime.after(missedTTLTime)) { val oldAlarmTime: Calendar = instance.alarmTime val newAlarmTime = alarm.getNextAlarmTime(currentTime) val oldTime: CharSequence = DateFormat.format("MM/dd/yyyy hh:mm a", oldAlarmTime) val newTime: CharSequence = DateFormat.format("MM/dd/yyyy hh:mm a", newAlarmTime) LogUtils.i("A time change has caused an existing alarm scheduled" + " to fire at %s to be replaced by a new alarm scheduled to fire at %s", oldTime, newTime) // The time change is so dramatic the AlarmInstance doesn't make any sense; // remove it and schedule the new appropriate instance. deleteInstanceAndUpdateParent(context, instance) } else { registerInstance(context, instance, false /* updateNextAlarm */) } } updateNextAlarm(context) } /** * Utility method to set alarm instance state via constants. * * @param context application context * @param instance to change state on * @param state to change to */ private fun setAlarmState(context: Context, instance: AlarmInstance?, state: Int) { if (instance == null) { LogUtils.e("Null alarm instance while setting state to %d", state) return } when (state) { InstancesColumns.SILENT_STATE -> setSilentState(context, instance) InstancesColumns.LOW_NOTIFICATION_STATE -> { setLowNotificationState(context, instance) } InstancesColumns.HIDE_NOTIFICATION_STATE -> { setHideNotificationState(context, instance) } InstancesColumns.HIGH_NOTIFICATION_STATE -> { setHighNotificationState(context, instance) } InstancesColumns.FIRED_STATE -> setFiredState(context, instance) InstancesColumns.SNOOZE_STATE -> { setSnoozeState(context, instance, true /* showToast */) } InstancesColumns.MISSED_STATE -> setMissedState(context, instance) InstancesColumns.PREDISMISSED_STATE -> setPreDismissState(context, instance) InstancesColumns.DISMISSED_STATE -> deleteInstanceAndUpdateParent(context, instance) else -> LogUtils.e("Trying to change to unknown alarm state: $state") } } fun handleIntent(context: Context, intent: Intent) { val action: String? = intent.getAction() LogUtils.v("AlarmStateManager received intent $intent") if (CHANGE_STATE_ACTION == action) { val uri: Uri = intent.getData()!! val instance: AlarmInstance? = AlarmInstance.getInstance(context.getContentResolver(), AlarmInstance.getId(uri)) if (instance == null) { LogUtils.e("Can not change state for unknown instance: $uri") return } val globalId = DataModel.dataModel.globalIntentId val intentId: Int = intent.getIntExtra(ALARM_GLOBAL_ID_EXTRA, -1) val alarmState: Int = intent.getIntExtra(ALARM_STATE_EXTRA, -1) if (intentId != globalId) { LogUtils.i("IntentId: " + intentId + " GlobalId: " + globalId + " AlarmState: " + alarmState) // Allows dismiss/snooze requests to go through if (!intent.hasCategory(ALARM_DISMISS_TAG) && !intent.hasCategory(ALARM_SNOOZE_TAG)) { LogUtils.i("Ignoring old Intent") return } } if (intent.getBooleanExtra(FROM_NOTIFICATION_EXTRA, false)) { if (intent.hasCategory(ALARM_DISMISS_TAG)) { Events.sendAlarmEvent(R.string.action_dismiss, R.string.label_notification) } else if (intent.hasCategory(ALARM_SNOOZE_TAG)) { Events.sendAlarmEvent(R.string.action_snooze, R.string.label_notification) } } if (alarmState >= 0) { setAlarmState(context, instance, alarmState) } else { registerInstance(context, instance, true) } } else if (SHOW_AND_DISMISS_ALARM_ACTION == action) { val uri: Uri = intent.getData()!! val instance: AlarmInstance? = AlarmInstance.getInstance(context.getContentResolver(), AlarmInstance.getId(uri)) if (instance == null) { LogUtils.e("Null alarminstance for SHOW_AND_DISMISS") // dismiss the notification val id: Int = intent.getIntExtra(AlarmNotifications.EXTRA_NOTIFICATION_ID, -1) if (id != -1) { NotificationManagerCompat.from(context).cancel(id) } return } val alarmId = instance.mAlarmId ?: Alarm.INVALID_ID val viewAlarmIntent: Intent = Alarm.createIntent(context, DeskClock::class.java, alarmId) .putExtra(AlarmClockFragment.SCROLL_TO_ALARM_INTENT_EXTRA, alarmId) .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) // Open DeskClock which is now positioned on the alarms tab. context.startActivity(viewAlarmIntent) deleteInstanceAndUpdateParent(context, instance) } } /** * Creates an intent that can be used to set an AlarmManager alarm to set the next alarm * indicators. */ private fun createIndicatorIntent(context: Context?): Intent { return Intent(context, AlarmStateManager::class.java).setAction(INDICATOR_ACTION) } } }