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 }