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.os.SystemClock;
20 import android.text.TextUtils;
21 
22 import java.util.Arrays;
23 import java.util.Comparator;
24 import java.util.List;
25 
26 import static android.text.format.DateUtils.HOUR_IN_MILLIS;
27 import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
28 import static android.text.format.DateUtils.SECOND_IN_MILLIS;
29 import static com.android.deskclock.data.Timer.State.EXPIRED;
30 import static com.android.deskclock.data.Timer.State.PAUSED;
31 import static com.android.deskclock.data.Timer.State.RESET;
32 import static com.android.deskclock.data.Timer.State.RUNNING;
33 
34 /**
35  * A read-only domain object representing a countdown timer.
36  */
37 public final class Timer {
38 
39     public enum State {
40         RUNNING(1), PAUSED(2), EXPIRED(3), RESET(4);
41 
42         /** The value assigned to this State in prior releases. */
43         private final int mValue;
44 
State(int value)45         State(int value) {
46             mValue = value;
47         }
48 
49         /**
50          * @return the numeric value assigned to this state
51          */
getValue()52         public int getValue() {
53             return mValue;
54         }
55 
56         /**
57          * @return the state corresponding to the given {@code value}
58          */
fromValue(int value)59         public static State fromValue(int value) {
60             for (State state : values()) {
61                 if (state.getValue() == value) {
62                     return state;
63                 }
64             }
65 
66             return null;
67         }
68     }
69 
70     /** The minimum duration of a timer. */
71     public static final long MIN_LENGTH = SECOND_IN_MILLIS;
72 
73     /** The maximum duration of a timer. */
74     public static final long MAX_LENGTH =
75             99 * HOUR_IN_MILLIS + 99 * MINUTE_IN_MILLIS + 99 * SECOND_IN_MILLIS;
76 
77     /** A unique identifier for the city. */
78     private final int mId;
79 
80     /** The current state of the timer. */
81     private final State mState;
82 
83     /** The original length of the timer in milliseconds when it was created. */
84     private final long mLength;
85 
86     /** The length of the timer in milliseconds including additional time added by the user. */
87     private final long mTotalLength;
88 
89     /** The time at which the timer was last started; {@link Long#MIN_VALUE} when not running. */
90     private final long mLastStartTime;
91 
92     /** The time at which the timer is scheduled to expire; negative if it is already expired. */
93     private final long mRemainingTime;
94 
95     /** A message describing the meaning of the timer. */
96     private final String mLabel;
97 
98     /** A flag indicating the timer should be deleted when it is reset. */
99     private final boolean mDeleteAfterUse;
100 
Timer(int id, State state, long length, long totalLength, long lastStartTime, long remainingTime, String label, boolean deleteAfterUse)101     Timer(int id, State state, long length, long totalLength, long lastStartTime,
102             long remainingTime, String label, boolean deleteAfterUse) {
103         mId = id;
104         mState = state;
105         mLength = length;
106         mTotalLength = totalLength;
107         mLastStartTime = lastStartTime;
108         mRemainingTime = remainingTime;
109         mLabel = label;
110         mDeleteAfterUse = deleteAfterUse;
111     }
112 
getId()113     public int getId() { return mId; }
getState()114     public State getState() { return mState; }
getLabel()115     public String getLabel() { return mLabel; }
getLength()116     public long getLength() { return mLength; }
getTotalLength()117     public long getTotalLength() { return mTotalLength; }
getDeleteAfterUse()118     public boolean getDeleteAfterUse() { return mDeleteAfterUse; }
isReset()119     public boolean isReset() { return mState == RESET; }
isRunning()120     public boolean isRunning() { return mState == RUNNING; }
isPaused()121     public boolean isPaused() { return mState == PAUSED; }
isExpired()122     public boolean isExpired() { return mState == EXPIRED; }
123 
124     /**
125      * @return the total amount of time remaining up to this moment; expired timers will return a
126      *      negative amount
127      */
getRemainingTime()128     public long getRemainingTime() {
129         if (mState == RUNNING || mState == EXPIRED) {
130             return mRemainingTime - (now() - mLastStartTime);
131         }
132 
133         return mRemainingTime;
134     }
135 
136     /**
137      * @return the time at which this timer will or did expire
138      */
getExpirationTime()139     public long getExpirationTime() {
140         if (mState != RUNNING && mState != EXPIRED) {
141             throw new IllegalStateException("cannot compute expiration time in state " + mState);
142         }
143 
144         return mLastStartTime + mRemainingTime;
145     }
146 
147     /**
148      *
149      * @return the total amount of time elapsed up to this moment; expired timers will report more
150      *      than the {@link #getTotalLength() total length}
151      */
getElapsedTime()152     public long getElapsedTime() {
153         return getTotalLength() - getRemainingTime();
154     }
155 
getLastStartTime()156     long getLastStartTime() { return mLastStartTime; }
157 
158     /**
159      * @return a copy of this timer that is running or expired
160      */
start()161     Timer start() {
162         if (mState == RUNNING || mState == EXPIRED) {
163             return this;
164         }
165 
166         return new Timer(mId, RUNNING, mLength, mTotalLength, now(), mRemainingTime, mLabel,
167                 mDeleteAfterUse);
168     }
169 
170     /**
171      * @return a copy of this timer that is paused or reset
172      */
pause()173     Timer pause() {
174         if (mState == PAUSED || mState == RESET) {
175             return this;
176         } else if (mState == EXPIRED) {
177             return reset();
178         }
179 
180         final long remainingTime = getRemainingTime();
181         return new Timer(mId, PAUSED, mLength, mTotalLength, Long.MIN_VALUE, remainingTime, mLabel,
182                 mDeleteAfterUse);
183     }
184 
185     /**
186      * @return a copy of this timer that is expired or reset
187      */
expire()188     Timer expire() {
189         if (mState == EXPIRED || mState == RESET) {
190             return this;
191         }
192 
193         return new Timer(mId, EXPIRED, mLength, mTotalLength, mLastStartTime, mRemainingTime,
194                 mLabel, mDeleteAfterUse);
195     }
196 
197     /**
198      * @return a copy of this timer that is reset
199      */
reset()200     Timer reset() {
201         if (mState == RESET) {
202             return this;
203         }
204 
205         return new Timer(mId, RESET, mLength, mLength, Long.MIN_VALUE, mLength, mLabel,
206                 mDeleteAfterUse);
207     }
208 
209     /**
210      * @return a copy of this timer with the given {@code label}
211      */
setLabel(String label)212     Timer setLabel(String label) {
213         if (TextUtils.equals(mLabel, label)) {
214             return this;
215         }
216 
217         return new Timer(mId, mState, mLength, mTotalLength, mLastStartTime, mRemainingTime, label,
218                 mDeleteAfterUse);
219     }
220 
221     /**
222      * @return a copy of this timer with an additional minute added to the remaining time and total
223      *      length, or this Timer if adding a minute would exceed the maximum timer duration
224      */
addMinute()225     Timer addMinute() {
226         final long lastStartTime;
227         final long remainingTime;
228         final long totalLength;
229         final State state;
230         if (mState == EXPIRED) {
231             state = RUNNING;
232             lastStartTime = now();
233             totalLength = MINUTE_IN_MILLIS;
234             remainingTime = MINUTE_IN_MILLIS;
235         } else {
236             state = mState;
237             lastStartTime = mLastStartTime;
238             totalLength = mRemainingTime + MINUTE_IN_MILLIS;
239             remainingTime = mRemainingTime + MINUTE_IN_MILLIS;
240         }
241 
242         // Do not allow the remaining time to exceed the maximum.
243         if (remainingTime > MAX_LENGTH) {
244             return this;
245         }
246 
247         return new Timer(mId, state, mLength, totalLength, lastStartTime, remainingTime, mLabel,
248                 mDeleteAfterUse);
249     }
250 
251     @Override
equals(Object o)252     public boolean equals(Object o) {
253         if (this == o) return true;
254         if (o == null || getClass() != o.getClass()) return false;
255 
256         final Timer timer = (Timer) o;
257 
258         return mId == timer.mId;
259 
260     }
261 
262     @Override
hashCode()263     public int hashCode() {
264         return mId;
265     }
266 
now()267     private static long now() {
268         return SystemClock.elapsedRealtime();
269     }
270 
271     /**
272      * Orders timers by their IDs. Oldest timers are at the bottom. Newest timers are at the top.
273      */
274     public static Comparator<Timer> ID_COMPARATOR = new Comparator<Timer>() {
275         @Override
276         public int compare(Timer timer1, Timer timer2) {
277             return Integer.compare(timer2.getId(), timer1.getId());
278         }
279     };
280 
281     /**
282      * Orders timers by their expected/actual expiration time. The general order is:
283      *
284      * <ol>
285      *     <li>{@link State#EXPIRED EXPIRED} timers; ties broken by {@link #getRemainingTime()}</li>
286      *     <li>{@link State#RUNNING RUNNING} timers; ties broken by {@link #getRemainingTime()}</li>
287      *     <li>{@link State#PAUSED PAUSED} timers; ties broken by {@link #getRemainingTime()}</li>
288      *     <li>{@link State#RESET RESET} timers; ties broken by {@link #getLength()}</li>
289      * </ol>
290      */
291     public static Comparator<Timer> EXPIRY_COMPARATOR = new Comparator<Timer>() {
292 
293         private final List<State> stateExpiryOrder = Arrays.asList(EXPIRED, RUNNING, PAUSED, RESET);
294 
295         @Override
296         public int compare(Timer timer1, Timer timer2) {
297             final int stateIndex1 = stateExpiryOrder.indexOf(timer1.getState());
298             final int stateIndex2 = stateExpiryOrder.indexOf(timer2.getState());
299 
300             int order = Integer.compare(stateIndex1, stateIndex2);
301             if (order == 0) {
302                 final State state = timer1.getState();
303                 if (state == RESET) {
304                     order = Long.compare(timer1.getLength(), timer2.getLength());
305                 } else {
306                     order = Long.compare(timer1.getRemainingTime(), timer2.getRemainingTime());
307                 }
308             }
309 
310             return order;
311         }
312     };
313 }