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 }