/* * 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 com.android.deskclock.Utils import kotlin.math.max /** * A read-only domain object representing a stopwatch. */ class Stopwatch internal constructor( /** Current state of this stopwatch. */ val state: State, /** Elapsed time in ms the stopwatch was last started; [.UNUSED] if not running. */ val lastStartTime: Long, /** The time since epoch at which the stopwatch was last started. */ val lastWallClockTime: Long, /** Elapsed time in ms this stopwatch has accumulated while running. */ val accumulatedTime: Long ) { enum class State { RESET, RUNNING, PAUSED } val isReset: Boolean get() = state == State.RESET val isPaused: Boolean get() = state == State.PAUSED val isRunning: Boolean get() = state == State.RUNNING /** * @return the total amount of time accumulated up to this moment */ val totalTime: Long get() { if (state != State.RUNNING) { return accumulatedTime } // 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 stopwatch is monotonically increasing, normalize negative time segments to // 0 val timeSinceStart = Utils.now() - lastStartTime return accumulatedTime + max(0, timeSinceStart) } /** * @return a copy of this stopwatch that is running */ fun start(): Stopwatch { return if (state == State.RUNNING) { this } else { Stopwatch(State.RUNNING, Utils.now(), Utils.wallClock(), totalTime) } } /** * @return a copy of this stopwatch that is paused */ fun pause(): Stopwatch { return if (state != State.RUNNING) { this } else { Stopwatch(State.PAUSED, UNUSED, UNUSED, totalTime) } } /** * @return a copy of this stopwatch that is reset */ fun reset(): Stopwatch = RESET_STOPWATCH /** * @return this Stopwatch if it is not running or an updated version based on wallclock time. * The internals of the stopwatch are updated using the wallclock time which is durable * across reboots. */ fun updateAfterReboot(): Stopwatch { if (state != State.RUNNING) { 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) return Stopwatch(state, timeSinceBoot, wallClockTime, accumulatedTime + delta) } /** * @return this Stopwatch if it is not running or an updated version based on the realtime. * The internals of the stopwatch are updated using the realtime clock which is accurate * across wallclock time adjustments. */ fun updateAfterTimeSet(): Stopwatch { if (state != State.RUNNING) { return this } val timeSinceBoot = Utils.now() val wallClockTime = Utils.wallClock() val delta = timeSinceBoot - lastStartTime 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 { Stopwatch(state, timeSinceBoot, wallClockTime, accumulatedTime + delta) } } companion object { const val UNUSED = Long.MIN_VALUE /** The single, immutable instance of a reset stopwatch. */ private val RESET_STOPWATCH = Stopwatch(State.RESET, UNUSED, UNUSED, 0) } }