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