blob: f19b75a1962d77686c2e7c3d6a374742431f0519 [file] [log] [blame]
/*
* 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.text.TextUtils
import android.text.format.DateUtils.HOUR_IN_MILLIS
import android.text.format.DateUtils.MINUTE_IN_MILLIS
import android.text.format.DateUtils.SECOND_IN_MILLIS
import com.android.deskclock.Utils
import kotlin.math.max
import kotlin.math.min
/**
* A read-only domain object representing a countdown timer.
*/
class Timer internal constructor(
/** A unique identifier for the timer. */
val id: Int,
/** The current state of the timer. */
val state: State,
/** The original length of the timer in milliseconds when it was created. */
val length: Long,
/** The length of the timer in milliseconds including additional time added by the user. */
val totalLength: Long,
/** The time at which the timer was last started; [.UNUSED] when not running. */
val lastStartTime: Long,
/** The time since epoch at which the timer was last started. */
val lastWallClockTime: Long,
/** The time at which the timer is scheduled to expire; negative if it is already expired. */
val lastRemainingTime: Long,
/** A message describing the meaning of the timer. */
val label: String?,
/** A flag indicating the timer should be deleted when it is reset. */
val deleteAfterUse: Boolean
) {
enum class State(
/** The value assigned to this State in prior releases. */
val value: Int
) {
RUNNING(1), PAUSED(2), EXPIRED(3), RESET(4), MISSED(5);
companion object {
/**
* @return the state corresponding to the given `value`
*/
fun fromValue(value: Int): State? {
for (state in values()) {
if (state.value == value) {
return state
}
}
return null
}
}
}
val isReset: Boolean
get() = state == State.RESET
val isRunning: Boolean
get() = state == State.RUNNING
val isPaused: Boolean
get() = state == State.PAUSED
val isExpired: Boolean
get() = state == State.EXPIRED
val isMissed: Boolean
get() = state == State.MISSED
/**
* @return the total amount of time remaining up to this moment; expired and missed timers will
* return a negative amount
*/
val remainingTime: Long
get() {
if (state == State.PAUSED || state == State.RESET) {
return lastRemainingTime
}
// In practice, "now" can be any value due to device reboots. When the real-time clock
// is reset, there is no more guarantee that "now" falls after the last start time. To
// ensure the timer is monotonically decreasing, normalize negative time segments to 0,
val timeSinceStart = Utils.now() - lastStartTime
return lastRemainingTime - max(0, timeSinceStart)
}
/**
* @return the elapsed realtime at which this timer will or did expire
*/
val expirationTime: Long
get() {
check(!(state != State.RUNNING && state != State.EXPIRED && state != State.MISSED)) {
"cannot compute expiration time in state $state"
}
return lastStartTime + lastRemainingTime
}
/**
* @return the wall clock time at which this timer will or did expire
*/
val wallClockExpirationTime: Long
get() {
check(!(state != State.RUNNING && state != State.EXPIRED && state != State.MISSED)) {
"cannot compute expiration time in state $state"
}
return lastWallClockTime + lastRemainingTime
}
/**
*
* @return the total amount of time elapsed up to this moment; expired timers will report more
* than the [total length][.getTotalLength]
*/
val elapsedTime: Long
get() = totalLength - remainingTime
/**
* @return a copy of this timer that is running, expired or missed
*/
fun start(): Timer {
return if (state == State.RUNNING || state == State.EXPIRED || state == State.MISSED) {
this
} else {
Timer(id, State.RUNNING, length, totalLength,
Utils.now(), Utils.wallClock(), lastRemainingTime, label, deleteAfterUse)
}
}
/**
* @return a copy of this timer that is paused or reset
*/
fun pause(): Timer {
if (state == State.PAUSED || state == State.RESET) {
return this
} else if (state == State.EXPIRED || state == State.MISSED) {
return reset()
}
val remainingTime = this.remainingTime
return Timer(id, State.PAUSED, length, totalLength, UNUSED, UNUSED, remainingTime, label,
deleteAfterUse)
}
/**
* @return a copy of this timer that is expired, missed or reset
*/
fun expire(): Timer {
if (state == State.EXPIRED || state == State.RESET || state == State.MISSED) {
return this
}
val remainingTime = min(0L, lastRemainingTime)
return Timer(id, State.EXPIRED, length, 0L, Utils.now(),
Utils.wallClock(), remainingTime, label, deleteAfterUse)
}
/**
* @return a copy of this timer that is missed or reset
*/
fun miss(): Timer {
if (state == State.RESET || state == State.MISSED) {
return this
}
val remainingTime = min(0L, lastRemainingTime)
return Timer(id, State.MISSED, length, 0L, Utils.now(),
Utils.wallClock(), remainingTime, label, deleteAfterUse)
}
/**
* @return a copy of this timer that is reset
*/
fun reset(): Timer {
return if (state == State.RESET) {
this
} else {
Timer(id, State.RESET, length, length, UNUSED, UNUSED, length, label,
deleteAfterUse)
}
}
/**
* @return a copy of this timer that has its times adjusted after a reboot
*/
fun updateAfterReboot(): Timer {
if (state == State.RESET || state == State.PAUSED) {
return this
}
val timeSinceBoot = Utils.now()
val wallClockTime = Utils.wallClock()
// Avoid negative time deltas. They can happen in practice, but they can't be used. Simply
// update the recorded times and proceed with no change in accumulated time.
val delta = max(0, wallClockTime - lastWallClockTime)
val remainingTime = lastRemainingTime - delta
return Timer(id, state, length, totalLength, timeSinceBoot, wallClockTime,
remainingTime, label, deleteAfterUse)
}
/**
* @return a copy of this timer that has its times adjusted after time has been set
*/
fun updateAfterTimeSet(): Timer {
if (state == State.RESET || state == State.PAUSED) {
return this
}
val timeSinceBoot = Utils.now()
val wallClockTime = Utils.wallClock()
val delta = timeSinceBoot - lastStartTime
val remainingTime = lastRemainingTime - delta
return if (delta < 0) {
// Avoid negative time deltas. They typically happen following reboots when TIME_SET is
// broadcast before BOOT_COMPLETED. Simply ignore the time update and hope
// updateAfterReboot() can successfully correct the data at a later time.
this
} else {
Timer(id, state, length, totalLength, timeSinceBoot, wallClockTime,
remainingTime, label, deleteAfterUse)
}
}
/**
* @return a copy of this timer with the given `label`
*/
fun setLabel(label: String?): Timer {
return if (TextUtils.equals(this.label, label)) {
this
} else {
Timer(id, state, length, totalLength, lastStartTime,
lastWallClockTime, lastRemainingTime, label, deleteAfterUse)
}
}
/**
* @return a copy of this timer with the given `length` or this timer if the length could
* not be legally adjusted
*/
fun setLength(length: Long): Timer {
if (this.length == length || length <= MIN_LENGTH) {
return this
}
val totalLength: Long
val remainingTime: Long
if (state == State.RESET) {
totalLength = length
remainingTime = length
} else {
totalLength = this.totalLength
remainingTime = lastRemainingTime
}
return Timer(id, state, length, totalLength, lastStartTime,
lastWallClockTime, remainingTime, label, deleteAfterUse)
}
/**
* @return a copy of this timer with the given `remainingTime` or this timer if the
* remaining time could not be legally adjusted
*/
fun setRemainingTime(remainingTime: Long): Timer {
// Do not change the remaining time of a reset timer.
if (lastRemainingTime == remainingTime || state == State.RESET) {
return this
}
val delta = remainingTime - lastRemainingTime
val totalLength = totalLength + delta
val lastStartTime: Long
val lastWallClockTime: Long
val state: State?
if (remainingTime > 0 && (this.state == State.EXPIRED || this.state == State.MISSED)) {
state = State.RUNNING
lastStartTime = Utils.now()
lastWallClockTime = Utils.wallClock()
} else {
state = this.state
lastStartTime = this.lastStartTime
lastWallClockTime = this.lastWallClockTime
}
return Timer(id, state, length, totalLength, lastStartTime,
lastWallClockTime, remainingTime, label, deleteAfterUse)
}
/**
* @return a copy of this timer with an additional minute added to the remaining time and total
* length, or this Timer if the minute could not be added
*/
fun addMinute(): Timer {
return if (state == State.EXPIRED || state == State.MISSED) {
// Expired and missed timers restart with 60 seconds of remaining time.
setRemainingTime(MINUTE_IN_MILLIS)
} else {
// Otherwise try to add a minute to the remaining time.
setRemainingTime(lastRemainingTime + MINUTE_IN_MILLIS)
}
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || javaClass != other.javaClass) return false
val timer = other as Timer
return id == timer.id
}
override fun hashCode(): Int {
return id
}
companion object {
/** The minimum duration of a timer. */
@JvmField
val MIN_LENGTH: Long = SECOND_IN_MILLIS
/** The maximum duration of a new timer created via the user interface. */
val MAX_LENGTH: Long = 99 * HOUR_IN_MILLIS + 99 * MINUTE_IN_MILLIS + 99 * SECOND_IN_MILLIS
const val UNUSED = Long.MIN_VALUE
/**
* Orders timers by their IDs. Oldest timers are at the bottom. Newest timers are at the top
*/
@JvmField
var ID_COMPARATOR = Comparator<Timer> { timer1, timer2 -> timer2.id.compareTo(timer1.id) }
/**
* Orders timers by their expected/actual expiration time. The general order is:
*
* 1. [MISSED][State.MISSED] timers; ties broken by [.getRemainingTime]
* 2. [EXPIRED][State.EXPIRED] timers; ties broken by [.getRemainingTime]
* 3. [RUNNING][State.RUNNING] timers; ties broken by [.getRemainingTime]
* 4. [PAUSED][State.PAUSED] timers; ties broken by [.getRemainingTime]
* 5. [RESET][State.RESET] timers; ties broken by [.getLength]
*
*/
@JvmField
var EXPIRY_COMPARATOR: Comparator<Timer> = object : Comparator<Timer> {
private val stateExpiryOrder =
listOf(State.MISSED, State.EXPIRED, State.RUNNING, State.PAUSED, State.RESET)
override fun compare(timer1: Timer, timer2: Timer): Int {
val stateIndex1 = stateExpiryOrder.indexOf(timer1.state)
val stateIndex2 = stateExpiryOrder.indexOf(timer2.state)
var order = stateIndex1.compareTo(stateIndex2)
if (order == 0) {
val state = timer1.state
order = if (state == State.RESET) {
timer1.length.compareTo(timer2.length)
} else {
timer1.lastRemainingTime.compareTo(timer2.lastRemainingTime)
}
}
return order
}
}
}
}