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 }