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