1 /* 2 * Copyright (C) 2020 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.deskclock.data 18 19 import com.android.deskclock.Utils 20 21 import kotlin.math.max 22 23 /** 24 * A read-only domain object representing a stopwatch. 25 */ 26 class Stopwatch internal constructor( 27 /** Current state of this stopwatch. */ 28 val state: State, 29 /** Elapsed time in ms the stopwatch was last started; [.UNUSED] if not running. */ 30 val lastStartTime: Long, 31 /** The time since epoch at which the stopwatch was last started. */ 32 val lastWallClockTime: Long, 33 /** Elapsed time in ms this stopwatch has accumulated while running. */ 34 val accumulatedTime: Long 35 ) { 36 37 enum class State { 38 RESET, RUNNING, PAUSED 39 } 40 41 val isReset: Boolean 42 get() = state == State.RESET 43 44 val isPaused: Boolean 45 get() = state == State.PAUSED 46 47 val isRunning: Boolean 48 get() = state == State.RUNNING 49 50 /** 51 * @return the total amount of time accumulated up to this moment 52 */ 53 val totalTime: Long 54 get() { 55 if (state != State.RUNNING) { 56 return accumulatedTime 57 } 58 59 // In practice, "now" can be any value due to device reboots. When the real-time clock 60 // is reset, there is no more guarantee that "now" falls after the last start time. To 61 // ensure the stopwatch is monotonically increasing, normalize negative time segments to 62 // 0 63 val timeSinceStart = Utils.now() - lastStartTime 64 return accumulatedTime + max(0, timeSinceStart) 65 } 66 67 /** 68 * @return a copy of this stopwatch that is running 69 */ startnull70 fun start(): Stopwatch { 71 return if (state == State.RUNNING) { 72 this 73 } else { 74 Stopwatch(State.RUNNING, Utils.now(), Utils.wallClock(), totalTime) 75 } 76 } 77 78 /** 79 * @return a copy of this stopwatch that is paused 80 */ pausenull81 fun pause(): Stopwatch { 82 return if (state != State.RUNNING) { 83 this 84 } else { 85 Stopwatch(State.PAUSED, UNUSED, UNUSED, totalTime) 86 } 87 } 88 89 /** 90 * @return a copy of this stopwatch that is reset 91 */ resetnull92 fun reset(): Stopwatch = RESET_STOPWATCH 93 94 /** 95 * @return this Stopwatch if it is not running or an updated version based on wallclock time. 96 * The internals of the stopwatch are updated using the wallclock time which is durable 97 * across reboots. 98 */ 99 fun updateAfterReboot(): Stopwatch { 100 if (state != State.RUNNING) { 101 return this 102 } 103 val timeSinceBoot = Utils.now() 104 val wallClockTime = Utils.wallClock() 105 // Avoid negative time deltas. They can happen in practice, but they can't be used. Simply 106 // update the recorded times and proceed with no change in accumulated time. 107 val delta = max(0, wallClockTime - lastWallClockTime) 108 return Stopwatch(state, timeSinceBoot, wallClockTime, accumulatedTime + delta) 109 } 110 111 /** 112 * @return this Stopwatch if it is not running or an updated version based on the realtime. 113 * The internals of the stopwatch are updated using the realtime clock which is accurate 114 * across wallclock time adjustments. 115 */ updateAfterTimeSetnull116 fun updateAfterTimeSet(): Stopwatch { 117 if (state != State.RUNNING) { 118 return this 119 } 120 val timeSinceBoot = Utils.now() 121 val wallClockTime = Utils.wallClock() 122 val delta = timeSinceBoot - lastStartTime 123 return if (delta < 0) { 124 // Avoid negative time deltas. They typically happen following reboots when TIME_SET is 125 // broadcast before BOOT_COMPLETED. Simply ignore the time update and hope 126 // updateAfterReboot() can successfully correct the data at a later time. 127 this 128 } else { 129 Stopwatch(state, timeSinceBoot, wallClockTime, accumulatedTime + delta) 130 } 131 } 132 133 companion object { 134 const val UNUSED = Long.MIN_VALUE 135 136 /** The single, immutable instance of a reset stopwatch. */ 137 private val RESET_STOPWATCH = Stopwatch(State.RESET, UNUSED, UNUSED, 0) 138 } 139 }