/* * 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 { 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 = object : Comparator { 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 } } } }