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 android.text.TextUtils;
20 
21 import java.util.Arrays;
22 import java.util.Comparator;
23 import java.util.List;
24 
25 import static android.text.format.DateUtils.HOUR_IN_MILLIS;
26 import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
27 import static android.text.format.DateUtils.SECOND_IN_MILLIS;
28 import static com.android.deskclock.Utils.now;
29 import static com.android.deskclock.Utils.wallClock;
30 import static com.android.deskclock.data.Timer.State.EXPIRED;
31 import static com.android.deskclock.data.Timer.State.MISSED;
32 import static com.android.deskclock.data.Timer.State.PAUSED;
33 import static com.android.deskclock.data.Timer.State.RESET;
34 import static com.android.deskclock.data.Timer.State.RUNNING;
35 
36 /**
37  * A read-only domain object representing a countdown timer.
38  */
39 public final class Timer {
40 
41     public enum State {
42         RUNNING(1), PAUSED(2), EXPIRED(3), RESET(4), MISSED(5);
43 
44         /** The value assigned to this State in prior releases. */
45         private final int mValue;
46 
State(int value)47         State(int value) {
48             mValue = value;
49         }
50 
51         /**
52          * @return the numeric value assigned to this state
53          */
getValue()54         public int getValue() {
55             return mValue;
56         }
57 
58         /**
59          * @return the state corresponding to the given {@code value}
60          */
fromValue(int value)61         public static State fromValue(int value) {
62             for (State state : values()) {
63                 if (state.getValue() == value) {
64                     return state;
65                 }
66             }
67 
68             return null;
69         }
70     }
71 
72     /** The minimum duration of a timer. */
73     public static final long MIN_LENGTH = SECOND_IN_MILLIS;
74 
75     /** The maximum duration of a new timer created via the user interface. */
76     static final long MAX_LENGTH =
77             99 * HOUR_IN_MILLIS + 99 * MINUTE_IN_MILLIS + 99 * SECOND_IN_MILLIS;
78 
79     static final long UNUSED = Long.MIN_VALUE;
80 
81     /** A unique identifier for the timer. */
82     private final int mId;
83 
84     /** The current state of the timer. */
85     private final State mState;
86 
87     /** The original length of the timer in milliseconds when it was created. */
88     private final long mLength;
89 
90     /** The length of the timer in milliseconds including additional time added by the user. */
91     private final long mTotalLength;
92 
93     /** The time at which the timer was last started; {@link #UNUSED} when not running. */
94     private final long mLastStartTime;
95 
96     /** The time since epoch at which the timer was last started. */
97     private final long mLastStartWallClockTime;
98 
99     /** The time at which the timer is scheduled to expire; negative if it is already expired. */
100     private final long mRemainingTime;
101 
102     /** A message describing the meaning of the timer. */
103     private final String mLabel;
104 
105     /** A flag indicating the timer should be deleted when it is reset. */
106     private final boolean mDeleteAfterUse;
107 
Timer(int id, State state, long length, long totalLength, long lastStartTime, long lastWallClockTime, long remainingTime, String label, boolean deleteAfterUse)108     Timer(int id, State state, long length, long totalLength, long lastStartTime,
109           long lastWallClockTime, long remainingTime, String label, boolean deleteAfterUse) {
110         mId = id;
111         mState = state;
112         mLength = length;
113         mTotalLength = totalLength;
114         mLastStartTime = lastStartTime;
115         mLastStartWallClockTime = lastWallClockTime;
116         mRemainingTime = remainingTime;
117         mLabel = label;
118         mDeleteAfterUse = deleteAfterUse;
119     }
120 
getId()121     public int getId() { return mId; }
getState()122     public State getState() { return mState; }
getLabel()123     public String getLabel() { return mLabel; }
getLength()124     public long getLength() { return mLength; }
getTotalLength()125     public long getTotalLength() { return mTotalLength; }
getDeleteAfterUse()126     public boolean getDeleteAfterUse() { return mDeleteAfterUse; }
isReset()127     public boolean isReset() { return mState == RESET; }
isRunning()128     public boolean isRunning() { return mState == RUNNING; }
isPaused()129     public boolean isPaused() { return mState == PAUSED; }
isExpired()130     public boolean isExpired() { return mState == EXPIRED; }
isMissed()131     public boolean isMissed() { return mState == MISSED; }
132 
133     /**
134      * @return the amount of remaining time when the timer was last started or paused.
135      */
getLastRemainingTime()136     public long getLastRemainingTime() {
137         return mRemainingTime;
138     }
139 
140     /**
141      * @return the total amount of time remaining up to this moment; expired and missed timers will
142      *      return a negative amount
143      */
getRemainingTime()144     public long getRemainingTime() {
145         if (mState == PAUSED || mState == RESET) {
146             return mRemainingTime;
147         }
148 
149         // In practice, "now" can be any value due to device reboots. When the real-time clock
150         // is reset, there is no more guarantee that "now" falls after the last start time. To
151         // ensure the timer is monotonically decreasing, normalize negative time segments to 0,
152         final long timeSinceStart = now() - mLastStartTime;
153         return mRemainingTime - Math.max(0, timeSinceStart);
154     }
155 
156     /**
157      * @return the elapsed realtime at which this timer will or did expire
158      */
getExpirationTime()159     public long getExpirationTime() {
160         if (mState != RUNNING && mState != EXPIRED && mState != MISSED) {
161             throw new IllegalStateException("cannot compute expiration time in state " + mState);
162         }
163 
164         return mLastStartTime + mRemainingTime;
165     }
166 
167     /**
168      * @return the wall clock time at which this timer will or did expire
169      */
getWallClockExpirationTime()170     public long getWallClockExpirationTime() {
171         if (mState != RUNNING && mState != EXPIRED && mState != MISSED) {
172             throw new IllegalStateException("cannot compute expiration time in state " + mState);
173         }
174 
175         return mLastStartWallClockTime + mRemainingTime;
176     }
177 
178     /**
179      *
180      * @return the total amount of time elapsed up to this moment; expired timers will report more
181      *      than the {@link #getTotalLength() total length}
182      */
getElapsedTime()183     public long getElapsedTime() {
184         return getTotalLength() - getRemainingTime();
185     }
186 
getLastStartTime()187     long getLastStartTime() { return mLastStartTime; }
getLastWallClockTime()188     long getLastWallClockTime() { return mLastStartWallClockTime; }
189 
190     /**
191      * @return a copy of this timer that is running, expired or missed
192      */
start()193     Timer start() {
194         if (mState == RUNNING || mState == EXPIRED || mState == MISSED) {
195             return this;
196         }
197 
198         return new Timer(mId, RUNNING, mLength, mTotalLength, now(), wallClock(), mRemainingTime,
199                 mLabel, mDeleteAfterUse);
200     }
201 
202     /**
203      * @return a copy of this timer that is paused or reset
204      */
pause()205     Timer pause() {
206         if (mState == PAUSED || mState == RESET) {
207             return this;
208         } else if (mState == EXPIRED || mState == MISSED) {
209             return reset();
210         }
211 
212         final long remainingTime = getRemainingTime();
213         return new Timer(mId, PAUSED, mLength, mTotalLength, UNUSED, UNUSED, remainingTime, mLabel,
214                 mDeleteAfterUse);
215     }
216 
217     /**
218      * @return a copy of this timer that is expired, missed or reset
219      */
expire()220     Timer expire() {
221         if (mState == EXPIRED || mState == RESET || mState == MISSED) {
222             return this;
223         }
224 
225         final long remainingTime = Math.min(0L, getRemainingTime());
226         return new Timer(mId, EXPIRED, mLength, 0L, now(), wallClock(), remainingTime, mLabel,
227                 mDeleteAfterUse);
228     }
229 
230     /**
231      * @return a copy of this timer that is missed or reset
232      */
miss()233     Timer miss() {
234         if (mState == RESET || mState == MISSED) {
235             return this;
236         }
237 
238         final long remainingTime = Math.min(0L, getRemainingTime());
239         return new Timer(mId, MISSED, mLength, 0L, now(), wallClock(), remainingTime, mLabel,
240                 mDeleteAfterUse);
241     }
242 
243     /**
244      * @return a copy of this timer that is reset
245      */
reset()246     Timer reset() {
247         if (mState == RESET) {
248             return this;
249         }
250 
251         return new Timer(mId, RESET, mLength, mLength, UNUSED, UNUSED, mLength, mLabel,
252                 mDeleteAfterUse);
253     }
254 
255     /**
256      * @return a copy of this timer that has its times adjusted after a reboot
257      */
updateAfterReboot()258     Timer updateAfterReboot() {
259         if (mState == RESET || mState == PAUSED) {
260             return this;
261         }
262 
263         final long timeSinceBoot = now();
264         final long wallClockTime = wallClock();
265         // Avoid negative time deltas. They can happen in practice, but they can't be used. Simply
266         // update the recorded times and proceed with no change in accumulated time.
267         final long delta = Math.max(0, wallClockTime - mLastStartWallClockTime);
268         final long remainingTime = mRemainingTime - delta;
269         return new Timer(mId, mState, mLength, mTotalLength, timeSinceBoot, wallClockTime,
270                 remainingTime, mLabel, mDeleteAfterUse);
271     }
272 
273     /**
274      * @return a copy of this timer that has its times adjusted after time has been set
275      */
updateAfterTimeSet()276     Timer updateAfterTimeSet() {
277         if (mState == RESET || mState == PAUSED) {
278             return this;
279         }
280 
281         final long timeSinceBoot = now();
282         final long wallClockTime = wallClock();
283         final long delta = timeSinceBoot - mLastStartTime;
284         final long remainingTime = mRemainingTime - delta;
285         if (delta < 0) {
286             // Avoid negative time deltas. They typically happen following reboots when TIME_SET is
287             // broadcast before BOOT_COMPLETED. Simply ignore the time update and hope
288             // updateAfterReboot() can successfully correct the data at a later time.
289             return this;
290         }
291         return new Timer(mId, mState, mLength, mTotalLength, timeSinceBoot, wallClockTime,
292                 remainingTime, mLabel, mDeleteAfterUse);
293     }
294 
295     /**
296      * @return a copy of this timer with the given {@code label}
297      */
setLabel(String label)298     Timer setLabel(String label) {
299         if (TextUtils.equals(mLabel, label)) {
300             return this;
301         }
302 
303         return new Timer(mId, mState, mLength, mTotalLength, mLastStartTime,
304                 mLastStartWallClockTime, mRemainingTime, label, mDeleteAfterUse);
305     }
306 
307     /**
308      * @return a copy of this timer with the given {@code length} or this timer if the length could
309      *      not be legally adjusted
310      */
setLength(long length)311     Timer setLength(long length) {
312         if (mLength == length || length <= Timer.MIN_LENGTH) {
313             return this;
314         }
315 
316         final long totalLength;
317         final long remainingTime;
318         if (mState == RESET) {
319             totalLength = length;
320             remainingTime = length;
321         } else {
322             totalLength = mTotalLength;
323             remainingTime = mRemainingTime;
324         }
325 
326         return new Timer(mId, mState, length, totalLength, mLastStartTime,
327                 mLastStartWallClockTime, remainingTime, mLabel, mDeleteAfterUse);
328     }
329 
330     /**
331      * @return a copy of this timer with the given {@code remainingTime} or this timer if the
332      *      remaining time could not be legally adjusted
333      */
setRemainingTime(long remainingTime)334     Timer setRemainingTime(long remainingTime) {
335         // Do not change the remaining time of a reset timer.
336         if (mRemainingTime == remainingTime || mState == RESET) {
337             return this;
338         }
339 
340         final long delta = remainingTime - mRemainingTime;
341         final long totalLength = mTotalLength + delta;
342 
343         final long lastStartTime;
344         final long lastWallClockTime;
345         final State state;
346         if (remainingTime > 0 && (mState == EXPIRED || mState == MISSED)) {
347             state = RUNNING;
348             lastStartTime = now();
349             lastWallClockTime = wallClock();
350         } else {
351             state = mState;
352             lastStartTime = mLastStartTime;
353             lastWallClockTime = mLastStartWallClockTime;
354         }
355 
356         return new Timer(mId, state, mLength, totalLength, lastStartTime,
357                 lastWallClockTime, remainingTime, mLabel, mDeleteAfterUse);
358     }
359 
360     /**
361      * @return a copy of this timer with an additional minute added to the remaining time and total
362      *      length, or this Timer if the minute could not be added
363      */
addMinute()364     Timer addMinute() {
365         // Expired and missed timers restart with 60 seconds of remaining time.
366         if (mState == EXPIRED || mState == MISSED) {
367             return setRemainingTime(MINUTE_IN_MILLIS);
368         }
369 
370         // Otherwise try to add a minute to the remaining time.
371         return setRemainingTime(mRemainingTime + MINUTE_IN_MILLIS);
372     }
373 
374     @Override
equals(Object o)375     public boolean equals(Object o) {
376         if (this == o) return true;
377         if (o == null || getClass() != o.getClass()) return false;
378 
379         final Timer timer = (Timer) o;
380 
381         return mId == timer.mId;
382     }
383 
384     @Override
hashCode()385     public int hashCode() {
386         return mId;
387     }
388 
389     /**
390      * Orders timers by their IDs. Oldest timers are at the bottom. Newest timers are at the top.
391      */
392     static Comparator<Timer> ID_COMPARATOR = new Comparator<Timer>() {
393         @Override
394         public int compare(Timer timer1, Timer timer2) {
395             return Integer.compare(timer2.getId(), timer1.getId());
396         }
397     };
398 
399     /**
400      * Orders timers by their expected/actual expiration time. The general order is:
401      *
402      * <ol>
403      *     <li>{@link State#MISSED MISSED} timers; ties broken by {@link #getRemainingTime()}</li>
404      *     <li>{@link State#EXPIRED EXPIRED} timers; ties broken by {@link #getRemainingTime()}</li>
405      *     <li>{@link State#RUNNING RUNNING} timers; ties broken by {@link #getRemainingTime()}</li>
406      *     <li>{@link State#PAUSED PAUSED} timers; ties broken by {@link #getRemainingTime()}</li>
407      *     <li>{@link State#RESET RESET} timers; ties broken by {@link #getLength()}</li>
408      * </ol>
409      */
410     static Comparator<Timer> EXPIRY_COMPARATOR = new Comparator<Timer>() {
411 
412         private final List<State> stateExpiryOrder = Arrays.asList(MISSED, EXPIRED, RUNNING, PAUSED,
413                 RESET);
414 
415         @Override
416         public int compare(Timer timer1, Timer timer2) {
417             final int stateIndex1 = stateExpiryOrder.indexOf(timer1.getState());
418             final int stateIndex2 = stateExpiryOrder.indexOf(timer2.getState());
419 
420             int order = Integer.compare(stateIndex1, stateIndex2);
421             if (order == 0) {
422                 final State state = timer1.getState();
423                 if (state == RESET) {
424                     order = Long.compare(timer1.getLength(), timer2.getLength());
425                 } else {
426                     order = Long.compare(timer1.getRemainingTime(), timer2.getRemainingTime());
427                 }
428             }
429 
430             return order;
431         }
432     };
433 }
434