1 /*
2  * Copyright (C) 2015 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 static com.android.deskclock.Utils.now;
20 import static com.android.deskclock.Utils.wallClock;
21 import static com.android.deskclock.data.Stopwatch.State.PAUSED;
22 import static com.android.deskclock.data.Stopwatch.State.RESET;
23 import static com.android.deskclock.data.Stopwatch.State.RUNNING;
24 
25 /**
26  * A read-only domain object representing a stopwatch.
27  */
28 public final class Stopwatch {
29 
30     public enum State { RESET, RUNNING, PAUSED }
31 
32     static final long UNUSED = Long.MIN_VALUE;
33 
34     /** The single, immutable instance of a reset stopwatch. */
35     private static final Stopwatch RESET_STOPWATCH = new Stopwatch(RESET, UNUSED, UNUSED, 0);
36 
37     /** Current state of this stopwatch. */
38     private final State mState;
39 
40     /** Elapsed time in ms the stopwatch was last started; {@link #UNUSED} if not running. */
41     private final long mLastStartTime;
42 
43     /** The time since epoch at which the stopwatch was last started. */
44     private final long mLastStartWallClockTime;
45 
46     /** Elapsed time in ms this stopwatch has accumulated while running. */
47     private final long mAccumulatedTime;
48 
Stopwatch(State state, long lastStartTime, long lastWallClockTime, long accumulatedTime)49     Stopwatch(State state, long lastStartTime, long lastWallClockTime, long accumulatedTime) {
50         mState = state;
51         mLastStartTime = lastStartTime;
52         mLastStartWallClockTime = lastWallClockTime;
53         mAccumulatedTime = accumulatedTime;
54     }
55 
getState()56     public State getState() { return mState; }
getLastStartTime()57     public long getLastStartTime() { return mLastStartTime; }
getLastWallClockTime()58     public long getLastWallClockTime() { return mLastStartWallClockTime; }
isReset()59     public boolean isReset() { return mState == RESET; }
isPaused()60     public boolean isPaused() { return mState == PAUSED; }
isRunning()61     public boolean isRunning() { return mState == RUNNING; }
62 
63     /**
64      * @return the total amount of time accumulated up to this moment
65      */
getTotalTime()66     public long getTotalTime() {
67         if (mState != RUNNING) {
68             return mAccumulatedTime;
69         }
70 
71         // In practice, "now" can be any value due to device reboots. When the real-time clock
72         // is reset, there is no more guarantee that "now" falls after the last start time. To
73         // ensure the stopwatch is monotonically increasing, normalize negative time segments to 0,
74         final long timeSinceStart = now() - mLastStartTime;
75         return mAccumulatedTime + Math.max(0, timeSinceStart);
76     }
77 
78     /**
79      * @return the amount of time accumulated up to the last time the stopwatch was started
80      */
getAccumulatedTime()81     public long getAccumulatedTime() {
82         return mAccumulatedTime;
83     }
84 
85     /**
86      * @return a copy of this stopwatch that is running
87      */
start()88     Stopwatch start() {
89         if (mState == RUNNING) {
90             return this;
91         }
92 
93         return new Stopwatch(RUNNING, now(), wallClock(), getTotalTime());
94     }
95 
96     /**
97      * @return a copy of this stopwatch that is paused
98      */
pause()99     Stopwatch pause() {
100         if (mState != RUNNING) {
101             return this;
102         }
103 
104         return new Stopwatch(PAUSED, UNUSED, UNUSED, getTotalTime());
105     }
106 
107     /**
108      * @return a copy of this stopwatch that is reset
109      */
reset()110     Stopwatch reset() {
111         return RESET_STOPWATCH;
112     }
113 
114     /**
115      * @return this Stopwatch if it is not running or an updated version based on wallclock time.
116      *      The internals of the stopwatch are updated using the wallclock time which is durable
117      *      across reboots.
118      */
updateAfterReboot()119     Stopwatch updateAfterReboot() {
120         if (mState != RUNNING) {
121             return this;
122         }
123         final long timeSinceBoot = now();
124         final long wallClockTime = wallClock();
125         // Avoid negative time deltas. They can happen in practice, but they can't be used. Simply
126         // update the recorded times and proceed with no change in accumulated time.
127         final long delta = Math.max(0, wallClockTime - mLastStartWallClockTime);
128         return new Stopwatch(mState, timeSinceBoot, wallClockTime, mAccumulatedTime + delta);
129     }
130 
131     /**
132      * @return this Stopwatch if it is not running or an updated version based on the realtime.
133      *      The internals of the stopwatch are updated using the realtime clock which is accurate
134      *      across wallclock time adjustments.
135      */
updateAfterTimeSet()136     Stopwatch updateAfterTimeSet() {
137         if (mState != RUNNING) {
138             return this;
139         }
140         final long timeSinceBoot = now();
141         final long wallClockTime = wallClock();
142         final long delta = timeSinceBoot - mLastStartTime;
143         if (delta < 0) {
144             // Avoid negative time deltas. They typically happen following reboots when TIME_SET is
145             // broadcast before BOOT_COMPLETED. Simply ignore the time update and hope
146             // updateAfterReboot() can successfully correct the data at a later time.
147             return this;
148         }
149         return new Stopwatch(mState, timeSinceBoot, wallClockTime, mAccumulatedTime + delta);
150     }
151 }