| /* |
| * 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.data |
| |
| import android.annotation.SuppressLint |
| import android.app.AlarmManager |
| import android.app.AlarmManager.ELAPSED_REALTIME_WAKEUP |
| import android.app.Notification |
| import android.app.PendingIntent |
| import android.app.Service |
| import android.content.BroadcastReceiver |
| import android.content.Context |
| import android.content.Intent |
| import android.content.IntentFilter |
| import android.content.SharedPreferences |
| import android.content.SharedPreferences.OnSharedPreferenceChangeListener |
| import android.net.Uri |
| import android.text.format.DateUtils.MINUTE_IN_MILLIS |
| import androidx.annotation.StringRes |
| import androidx.core.app.NotificationManagerCompat |
| |
| import com.android.deskclock.AlarmAlertWakeLock |
| import com.android.deskclock.LogUtils |
| import com.android.deskclock.R |
| import com.android.deskclock.Utils |
| import com.android.deskclock.events.Events |
| import com.android.deskclock.settings.SettingsActivity |
| import com.android.deskclock.timer.TimerKlaxon |
| import com.android.deskclock.timer.TimerService |
| |
| /** |
| * All [Timer] data is accessed via this model. |
| */ |
| internal class TimerModel( |
| private val mContext: Context, |
| private val mPrefs: SharedPreferences, |
| /** The model from which settings are fetched. */ |
| private val mSettingsModel: SettingsModel, |
| /** The model from which ringtone data are fetched. */ |
| private val mRingtoneModel: RingtoneModel, |
| /** The model from which notification data are fetched. */ |
| private val mNotificationModel: NotificationModel |
| ) { |
| /** The alarm manager system service that calls back when timers expire. */ |
| private val mAlarmManager = mContext.getSystemService(Context.ALARM_SERVICE) as AlarmManager |
| |
| /** Used to create and destroy system notifications related to timers. */ |
| private val mNotificationManager = NotificationManagerCompat.from(mContext) |
| |
| /** Update timer notification when locale changes. */ |
| private val mLocaleChangedReceiver: BroadcastReceiver = LocaleChangedReceiver() |
| |
| /** |
| * Retain a hard reference to the shared preference observer to prevent it from being garbage |
| * collected. See [SharedPreferences.registerOnSharedPreferenceChangeListener] for detail. |
| */ |
| private val mPreferenceListener: OnSharedPreferenceChangeListener = PreferenceListener() |
| |
| /** The listeners to notify when a timer is added, updated or removed. */ |
| private val mTimerListeners: MutableList<TimerListener> = mutableListOf() |
| |
| /** Delegate that builds platform-specific timer notifications. */ |
| private val mNotificationBuilder = TimerNotificationBuilder() |
| |
| /** |
| * The ids of expired timers for which the ringer is ringing. Not all expired timers have their |
| * ids in this collection. If a timer was already expired when the app was started its id will |
| * be absent from this collection. |
| */ |
| @SuppressLint("NewApi") |
| private val mRingingIds: MutableSet<Int> = mutableSetOf() |
| |
| /** The uri of the ringtone to play for timers. */ |
| private var mTimerRingtoneUri: Uri? = null |
| |
| /** The title of the ringtone to play for timers. */ |
| private var mTimerRingtoneTitle: String? = null |
| |
| /** A mutable copy of the timers. */ |
| private var mTimers: MutableList<Timer>? = null |
| |
| /** A mutable copy of the expired timers. */ |
| private var mExpiredTimers: MutableList<Timer>? = null |
| |
| /** A mutable copy of the missed timers. */ |
| private var mMissedTimers: MutableList<Timer>? = null |
| |
| /** |
| * The service that keeps this application in the foreground while a heads-up timer |
| * notification is displayed. Marking the service as foreground prevents the operating system |
| * from killing this application while expired timers are actively firing. |
| */ |
| private var mService: Service? = null |
| |
| init { |
| // Clear caches affected by preferences when preferences change. |
| mPrefs.registerOnSharedPreferenceChangeListener(mPreferenceListener) |
| |
| // Update timer notification when locale changes. |
| val localeBroadcastFilter = IntentFilter(Intent.ACTION_LOCALE_CHANGED) |
| mContext.registerReceiver(mLocaleChangedReceiver, localeBroadcastFilter) |
| } |
| |
| /** |
| * @param timerListener to be notified when timers are added, updated and removed |
| */ |
| fun addTimerListener(timerListener: TimerListener) { |
| mTimerListeners.add(timerListener) |
| } |
| |
| /** |
| * @param timerListener to no longer be notified when timers are added, updated and removed |
| */ |
| fun removeTimerListener(timerListener: TimerListener) { |
| mTimerListeners.remove(timerListener) |
| } |
| |
| /** |
| * @return all defined timers in their creation order |
| */ |
| val timers: List<Timer> |
| get() = mutableTimers |
| |
| /** |
| * @return all expired timers in their expiration order |
| */ |
| val expiredTimers: List<Timer> |
| get() = mutableExpiredTimers |
| |
| /** |
| * @return all missed timers in their expiration order |
| */ |
| private val missedTimers: List<Timer> |
| get() = mutableMissedTimers |
| |
| /** |
| * @param timerId identifies the timer to return |
| * @return the timer with the given `timerId` |
| */ |
| fun getTimer(timerId: Int): Timer? { |
| for (timer in mutableTimers) { |
| if (timer.id == timerId) { |
| return timer |
| } |
| } |
| |
| return null |
| } |
| |
| /** |
| * @return the timer that last expired and is still expired now; `null` if no timers are |
| * expired |
| */ |
| val mostRecentExpiredTimer: Timer? |
| get() { |
| val timers = mutableExpiredTimers |
| return if (timers.isEmpty()) null else timers[timers.size - 1] |
| } |
| |
| /** |
| * @param length the length of the timer in milliseconds |
| * @param label describes the purpose of the timer |
| * @param deleteAfterUse `true` indicates the timer should be deleted when it is reset |
| * @return the newly added timer |
| */ |
| fun addTimer(length: Long, label: String?, deleteAfterUse: Boolean): Timer { |
| // Create the timer instance. |
| var timer = |
| Timer(-1, Timer.State.RESET, length, length, Timer.UNUSED, Timer.UNUSED, length, |
| label, deleteAfterUse) |
| |
| // Add the timer to permanent storage. |
| timer = TimerDAO.addTimer(mPrefs, timer) |
| |
| // Add the timer to the cache. |
| mutableTimers.add(0, timer) |
| |
| // Update the timer notification. |
| updateNotification() |
| // Heads-Up notification is unaffected by this change |
| |
| // Notify listeners of the change. |
| for (timerListener in mTimerListeners) { |
| timerListener.timerAdded(timer) |
| } |
| |
| return timer |
| } |
| |
| /** |
| * @param service used to start foreground notifications related to expired timers |
| * @param timer the timer to be expired |
| */ |
| fun expireTimer(service: Service, timer: Timer) { |
| if (mService == null) { |
| // If this is the first expired timer, retain the service that will be used to start |
| // the heads-up notification in the foreground. |
| mService = service |
| } else if (mService != service) { |
| // If this is not the first expired timer, the service should match the one given when |
| // the first timer expired. |
| LogUtils.wtf("Expected TimerServices to be identical") |
| } |
| |
| updateTimer(timer.expire()) |
| } |
| |
| /** |
| * @param timer an updated timer to store |
| */ |
| fun updateTimer(timer: Timer) { |
| val before = doUpdateTimer(timer) |
| |
| // Update the notification after updating the timer data. |
| updateNotification() |
| |
| // If the timer started or stopped being expired, update the heads-up notification. |
| if (before.state != timer.state) { |
| if (before.isExpired || timer.isExpired) { |
| updateHeadsUpNotification() |
| } |
| } |
| } |
| |
| /** |
| * @param timer an existing timer to be removed |
| */ |
| fun removeTimer(timer: Timer) { |
| doRemoveTimer(timer) |
| |
| // Update the timer notifications after removing the timer data. |
| if (timer.isExpired) { |
| updateHeadsUpNotification() |
| } else { |
| updateNotification() |
| } |
| } |
| |
| /** |
| * If the given `timer` is expired and marked for deletion after use then this method |
| * removes the timer. The timer is otherwise transitioned to the reset state and continues |
| * to exist. |
| * |
| * @param timer the timer to be reset |
| * @param allowDelete `true` if the timer is allowed to be deleted instead of reset |
| * (e.g. one use timers) |
| * @param eventLabelId the label of the timer event to send; 0 if no event should be sent |
| * @return the reset `timer` or `null` if the timer was deleted |
| */ |
| fun resetTimer(timer: Timer, allowDelete: Boolean, @StringRes eventLabelId: Int): Timer? { |
| val result = doResetOrDeleteTimer(timer, allowDelete, eventLabelId) |
| |
| // Update the notification after updating the timer data. |
| when { |
| timer.isMissed -> updateMissedNotification() |
| timer.isExpired -> updateHeadsUpNotification() |
| else -> updateNotification() |
| } |
| |
| return result |
| } |
| |
| /** |
| * Update timers after system reboot. |
| */ |
| fun updateTimersAfterReboot() { |
| for (timer in timers) { |
| doUpdateAfterRebootTimer(timer) |
| } |
| |
| // Update the notifications once after all timers are updated. |
| updateNotification() |
| updateMissedNotification() |
| updateHeadsUpNotification() |
| } |
| |
| /** |
| * Update timers after time set. |
| */ |
| fun updateTimersAfterTimeSet() { |
| for (timer in timers) { |
| doUpdateAfterTimeSetTimer(timer) |
| } |
| |
| // Update the notifications once after all timers are updated. |
| updateNotification() |
| updateMissedNotification() |
| updateHeadsUpNotification() |
| } |
| |
| /** |
| * Reset all expired timers. Exactly one parameter should be filled, with preference given to |
| * eventLabelId. |
| * |
| * @param eventLabelId the label of the timer event to send; 0 if no event should be sent |
| */ |
| fun resetOrDeleteExpiredTimers(@StringRes eventLabelId: Int) { |
| for (timer in timers) { |
| if (timer.isExpired) { |
| doResetOrDeleteTimer(timer, true /* allowDelete */, eventLabelId) |
| } |
| } |
| |
| // Update the notifications once after all timers are updated. |
| updateHeadsUpNotification() |
| } |
| |
| /** |
| * Reset all missed timers. |
| * |
| * @param eventLabelId the label of the timer event to send; 0 if no event should be sent |
| */ |
| fun resetMissedTimers(@StringRes eventLabelId: Int) { |
| for (timer in timers) { |
| if (timer.isMissed) { |
| doResetOrDeleteTimer(timer, true /* allowDelete */, eventLabelId) |
| } |
| } |
| |
| // Update the notifications once after all timers are updated. |
| updateMissedNotification() |
| } |
| |
| /** |
| * Reset all unexpired timers. |
| * |
| * @param eventLabelId the label of the timer event to send; 0 if no event should be sent |
| */ |
| fun resetUnexpiredTimers(@StringRes eventLabelId: Int) { |
| for (timer in timers) { |
| if (timer.isRunning || timer.isPaused) { |
| doResetOrDeleteTimer(timer, true /* allowDelete */, eventLabelId) |
| } |
| } |
| |
| // Update the notification once after all timers are updated. |
| updateNotification() |
| // Heads-Up notification is unaffected by this change |
| } |
| |
| /** |
| * @return the uri of the default ringtone to play for all timers when no user selection exists |
| */ |
| val defaultTimerRingtoneUri: Uri |
| get() = mSettingsModel.defaultTimerRingtoneUri |
| |
| /** |
| * @return `true` iff the ringtone to play for all timers is the silent ringtone |
| */ |
| val isTimerRingtoneSilent: Boolean |
| get() = Uri.EMPTY.equals(timerRingtoneUri) |
| |
| var timerRingtoneUri: Uri |
| /** |
| * @return the uri of the ringtone to play for all timers |
| */ |
| get() { |
| if (mTimerRingtoneUri == null) { |
| mTimerRingtoneUri = mSettingsModel.timerRingtoneUri |
| } |
| |
| return mTimerRingtoneUri!! |
| } |
| /** |
| * @param uri the uri of the ringtone to play for all timers |
| */ |
| set(uri) { |
| mSettingsModel.timerRingtoneUri = uri |
| } |
| |
| /** |
| * @return the title of the ringtone that is played for all timers |
| */ |
| val timerRingtoneTitle: String? |
| get() { |
| if (mTimerRingtoneTitle == null) { |
| mTimerRingtoneTitle = if (isTimerRingtoneSilent) { |
| // Special case: no ringtone has a title of "Silent". |
| mContext.getString(R.string.silent_ringtone_title) |
| } else { |
| val defaultUri: Uri = defaultTimerRingtoneUri |
| val uri: Uri = timerRingtoneUri |
| if (defaultUri.equals(uri)) { |
| // Special case: default ringtone has a title of "Timer Expired". |
| mContext.getString(R.string.default_timer_ringtone_title) |
| } else { |
| mRingtoneModel.getRingtoneTitle(uri) |
| } |
| } |
| } |
| |
| return mTimerRingtoneTitle |
| } |
| |
| /** |
| * @return the duration, in milliseconds, of the crescendo to apply to timer ringtone playback; |
| * `0` implies no crescendo should be applied |
| */ |
| val timerCrescendoDuration: Long |
| get() = mSettingsModel.timerCrescendoDuration |
| |
| var timerVibrate: Boolean |
| /** |
| * @return `true` if the device vibrates when timers expire |
| */ |
| get() = mSettingsModel.timerVibrate |
| /** |
| * @param enabled `true` if the device should vibrate when timers expire |
| */ |
| set(enabled) { |
| mSettingsModel.timerVibrate = enabled |
| } |
| |
| private val mutableTimers: MutableList<Timer> |
| get() { |
| if (mTimers == null) { |
| mTimers = TimerDAO.getTimers(mPrefs) |
| mTimers!!.sortWith(Timer.ID_COMPARATOR) |
| } |
| |
| return mTimers!! |
| } |
| |
| private val mutableExpiredTimers: List<Timer> |
| get() { |
| if (mExpiredTimers == null) { |
| mExpiredTimers = mutableListOf() |
| for (timer in mutableTimers) { |
| if (timer.isExpired) { |
| mExpiredTimers!!.add(timer) |
| } |
| } |
| mExpiredTimers!!.sortWith(Timer.EXPIRY_COMPARATOR) |
| } |
| |
| return mExpiredTimers!! |
| } |
| |
| private val mutableMissedTimers: List<Timer> |
| get() { |
| if (mMissedTimers == null) { |
| mMissedTimers = mutableListOf() |
| for (timer in mutableTimers) { |
| if (timer.isMissed) { |
| mMissedTimers!!.add(timer) |
| } |
| } |
| mMissedTimers!!.sortWith(Timer.EXPIRY_COMPARATOR) |
| } |
| |
| return mMissedTimers!! |
| } |
| |
| /** |
| * This method updates timer data without updating notifications. This is useful in bulk-update |
| * scenarios so the notifications are only rebuilt once. |
| * |
| * @param timer an updated timer to store |
| * @return the state of the timer prior to the update |
| */ |
| private fun doUpdateTimer(timer: Timer): Timer { |
| // Retrieve the cached form of the timer. |
| val timers = mutableTimers |
| val index = timers.indexOf(timer) |
| val before = timers[index] |
| |
| // If no change occurred, ignore this update. |
| if (timer === before) { |
| return timer |
| } |
| |
| // Update the timer in permanent storage. |
| TimerDAO.updateTimer(mPrefs, timer) |
| |
| // Update the timer in the cache. |
| val oldTimer = timers.set(index, timer) |
| |
| // Clear the cache of expired timers if the timer changed to/from expired. |
| if (before.isExpired || timer.isExpired) { |
| mExpiredTimers = null |
| } |
| // Clear the cache of missed timers if the timer changed to/from missed. |
| if (before.isMissed || timer.isMissed) { |
| mMissedTimers = null |
| } |
| |
| // Update the timer expiration callback. |
| updateAlarmManager() |
| |
| // Update the timer ringer. |
| updateRinger(before, timer) |
| |
| // Notify listeners of the change. |
| for (timerListener in mTimerListeners) { |
| timerListener.timerUpdated(before, timer) |
| } |
| |
| return oldTimer |
| } |
| |
| /** |
| * This method removes timer data without updating notifications. This is useful in bulk-remove |
| * scenarios so the notifications are only rebuilt once. |
| * |
| * @param timer an existing timer to be removed |
| */ |
| private fun doRemoveTimer(timer: Timer) { |
| // Remove the timer from permanent storage. |
| var timerVar = timer |
| TimerDAO.removeTimer(mPrefs, timerVar) |
| |
| // Remove the timer from the cache. |
| val timers: MutableList<Timer> = mutableTimers |
| val index = timers.indexOf(timerVar) |
| |
| // If the timer cannot be located there is nothing to remove. |
| if (index == -1) { |
| return |
| } |
| timerVar = timers.removeAt(index) |
| |
| // Clear the cache of expired timers if a new expired timer was added. |
| if (timerVar.isExpired) { |
| mExpiredTimers = null |
| } |
| |
| // Clear the cache of missed timers if a new missed timer was added. |
| if (timerVar.isMissed) { |
| mMissedTimers = null |
| } |
| |
| // Update the timer expiration callback. |
| updateAlarmManager() |
| |
| // Update the timer ringer. |
| updateRinger(timerVar, null) |
| |
| // Notify listeners of the change. |
| for (timerListener in mTimerListeners) { |
| timerListener.timerRemoved(timerVar) |
| } |
| } |
| |
| /** |
| * This method updates/removes timer data without updating notifications. This is useful in |
| * bulk-update scenarios so the notifications are only rebuilt once. |
| * |
| * If the given `timer` is expired and marked for deletion after use then this method |
| * removes the timer. The timer is otherwise transitioned to the reset state and continues |
| * to exist. |
| * |
| * @param timer the timer to be reset |
| * @param allowDelete `true` if the timer is allowed to be deleted instead of reset |
| * (e.g. one use timers) |
| * @param eventLabelId the label of the timer event to send; 0 if no event should be sent |
| * @return the reset `timer` or `null` if the timer was deleted |
| */ |
| private fun doResetOrDeleteTimer( |
| timer: Timer, |
| allowDelete: Boolean, |
| @StringRes eventLabelId: Int |
| ): Timer? { |
| if (allowDelete && |
| (timer.isExpired || timer.isMissed) && |
| timer.deleteAfterUse) { |
| doRemoveTimer(timer) |
| if (eventLabelId != 0) { |
| Events.sendTimerEvent(R.string.action_delete, eventLabelId) |
| } |
| return null |
| } else if (!timer.isReset) { |
| val reset = timer.reset() |
| doUpdateTimer(reset) |
| if (eventLabelId != 0) { |
| Events.sendTimerEvent(R.string.action_reset, eventLabelId) |
| } |
| return reset |
| } |
| return timer |
| } |
| |
| /** |
| * This method updates/removes timer data after a reboot without updating notifications. |
| * |
| * @param timer the timer to be updated |
| */ |
| private fun doUpdateAfterRebootTimer(timer: Timer) { |
| var updated = timer.updateAfterReboot() |
| if (updated.remainingTime < MISSED_THRESHOLD && updated.isRunning) { |
| updated = updated.miss() |
| } |
| doUpdateTimer(updated) |
| } |
| |
| private fun doUpdateAfterTimeSetTimer(timer: Timer) { |
| val updated = timer.updateAfterTimeSet() |
| doUpdateTimer(updated) |
| } |
| |
| /** |
| * Updates the callback given to this application from the [AlarmManager] that signals the |
| * expiration of the next timer. If no timers are currently set to expire (i.e. no running |
| * timers exist) then this method clears the expiration callback from AlarmManager. |
| */ |
| private fun updateAlarmManager() { |
| // Locate the next firing timer if one exists. |
| var nextExpiringTimer: Timer? = null |
| for (timer in mutableTimers) { |
| if (timer.isRunning) { |
| if (nextExpiringTimer == null) { |
| nextExpiringTimer = timer |
| } else if (timer.expirationTime < nextExpiringTimer.expirationTime) { |
| nextExpiringTimer = timer |
| } |
| } |
| } |
| |
| // Build the intent that signals the timer expiration. |
| val intent: Intent = TimerService.createTimerExpiredIntent(mContext, nextExpiringTimer) |
| if (nextExpiringTimer == null) { |
| // Cancel the existing timer expiration callback. |
| val pi: PendingIntent? = PendingIntent.getService(mContext, |
| 0, intent, PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_NO_CREATE) |
| if (pi != null) { |
| mAlarmManager.cancel(pi) |
| pi.cancel() |
| } |
| } else { |
| // Update the existing timer expiration callback. |
| val pi: PendingIntent = PendingIntent.getService(mContext, |
| 0, intent, PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_UPDATE_CURRENT) |
| schedulePendingIntent(mAlarmManager, nextExpiringTimer.expirationTime, pi) |
| } |
| } |
| |
| /** |
| * Starts and stops the ringer for timers if the change to the timer demands it. |
| * |
| * @param before the state of the timer before the change; `null` indicates added |
| * @param after the state of the timer after the change; `null` indicates delete |
| */ |
| private fun updateRinger(before: Timer?, after: Timer?) { |
| // Retrieve the states before and after the change. |
| val beforeState = before?.state |
| val afterState = after?.state |
| |
| // If the timer state did not change, the ringer state is unchanged. |
| if (beforeState == afterState) { |
| return |
| } |
| |
| // If the timer is the first to expire, start ringing. |
| if (afterState == Timer.State.EXPIRED && mRingingIds.add(after.id) && |
| mRingingIds.size == 1) { |
| AlarmAlertWakeLock.acquireScreenCpuWakeLock(mContext) |
| TimerKlaxon.start(mContext) |
| } |
| |
| // If the expired timer was the last to reset, stop ringing. |
| if (beforeState == Timer.State.EXPIRED && mRingingIds.remove(before.id) && |
| mRingingIds.isEmpty()) { |
| TimerKlaxon.stop(mContext) |
| AlarmAlertWakeLock.releaseCpuLock() |
| } |
| } |
| |
| /** |
| * Updates the notification controlling unexpired timers. This notification is only displayed |
| * when the application is not open. |
| */ |
| fun updateNotification() { |
| // Notifications should be hidden if the app is open. |
| if (mNotificationModel.isApplicationInForeground) { |
| mNotificationManager.cancel(mNotificationModel.unexpiredTimerNotificationId) |
| return |
| } |
| |
| // Filter the timers to just include unexpired ones. |
| val unexpired: MutableList<Timer> = mutableListOf() |
| for (timer in mutableTimers) { |
| if (timer.isRunning || timer.isPaused) { |
| unexpired.add(timer) |
| } |
| } |
| |
| // If no unexpired timers exist, cancel the notification. |
| if (unexpired.isEmpty()) { |
| mNotificationManager.cancel(mNotificationModel.unexpiredTimerNotificationId) |
| return |
| } |
| |
| // Sort the unexpired timers to locate the next one scheduled to expire. |
| unexpired.sortWith(Timer.EXPIRY_COMPARATOR) |
| |
| // Otherwise build and post a notification reflecting the latest unexpired timers. |
| val notification: Notification = |
| mNotificationBuilder.build(mContext, mNotificationModel, unexpired) |
| val notificationId = mNotificationModel.unexpiredTimerNotificationId |
| mNotificationBuilder.buildChannel(mContext, mNotificationManager) |
| mNotificationManager.notify(notificationId, notification) |
| } |
| |
| /** |
| * Updates the notification controlling missed timers. This notification is only displayed when |
| * the application is not open. |
| */ |
| fun updateMissedNotification() { |
| // Notifications should be hidden if the app is open. |
| if (mNotificationModel.isApplicationInForeground) { |
| mNotificationManager.cancel(mNotificationModel.missedTimerNotificationId) |
| return |
| } |
| |
| val missed = missedTimers |
| |
| if (missed.isEmpty()) { |
| mNotificationManager.cancel(mNotificationModel.missedTimerNotificationId) |
| return |
| } |
| |
| val notification: Notification = mNotificationBuilder.buildMissed(mContext, |
| mNotificationModel, missed) |
| val notificationId = mNotificationModel.missedTimerNotificationId |
| mNotificationManager.notify(notificationId, notification) |
| } |
| |
| /** |
| * Updates the heads-up notification controlling expired timers. This heads-up notification is |
| * displayed whether the application is open or not. |
| */ |
| private fun updateHeadsUpNotification() { |
| // Nothing can be done with the heads-up notification without a valid service reference. |
| if (mService == null) { |
| return |
| } |
| |
| val expired = expiredTimers |
| |
| // If no expired timers exist, stop the service (which cancels the foreground notification). |
| if (expired.isEmpty()) { |
| mService!!.stopSelf() |
| mService = null |
| return |
| } |
| |
| // Otherwise build and post a foreground notification reflecting the latest expired timers. |
| val notification: Notification = mNotificationBuilder.buildHeadsUp(mContext, expired) |
| val notificationId = mNotificationModel.expiredTimerNotificationId |
| mService!!.startForeground(notificationId, notification) |
| } |
| |
| /** |
| * Update the timer notification in response to a locale change. |
| */ |
| private inner class LocaleChangedReceiver : BroadcastReceiver() { |
| override fun onReceive(context: Context?, intent: Intent?) { |
| mTimerRingtoneTitle = null |
| updateNotification() |
| updateMissedNotification() |
| updateHeadsUpNotification() |
| } |
| } |
| |
| /** |
| * This receiver is notified when shared preferences change. Cached information built on |
| * preferences must be cleared. |
| */ |
| private inner class PreferenceListener : OnSharedPreferenceChangeListener { |
| override fun onSharedPreferenceChanged(prefs: SharedPreferences?, key: String?) { |
| when (key) { |
| SettingsActivity.KEY_TIMER_RINGTONE -> { |
| mTimerRingtoneUri = null |
| mTimerRingtoneTitle = null |
| } |
| } |
| } |
| } |
| |
| companion object { |
| /** |
| * Running timers less than this threshold are left running/expired; greater than this |
| * threshold are considered missed. |
| */ |
| private val MISSED_THRESHOLD: Long = -MINUTE_IN_MILLIS |
| |
| fun schedulePendingIntent(am: AlarmManager, triggerTime: Long, pi: PendingIntent?) { |
| if (Utils.isMOrLater()) { |
| // Ensure the timer fires even if the device is dozing. |
| am.setExactAndAllowWhileIdle(ELAPSED_REALTIME_WAKEUP, triggerTime, pi) |
| } else { |
| am.setExact(ELAPSED_REALTIME_WAKEUP, triggerTime, pi) |
| } |
| } |
| } |
| } |