| /* |
| * Copyright (C) 2015 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.deskclock.data; |
| |
| import static android.text.format.DateUtils.MINUTE_IN_MILLIS; |
| import static android.text.format.DateUtils.SECOND_IN_MILLIS; |
| import static com.android.deskclock.Utils.now; |
| import static com.android.deskclock.Utils.wallClock; |
| import static com.android.deskclock.data.Timer.State.EXPIRED; |
| import static com.android.deskclock.data.Timer.State.MISSED; |
| import static com.android.deskclock.data.Timer.State.PAUSED; |
| import static com.android.deskclock.data.Timer.State.RESET; |
| import static com.android.deskclock.data.Timer.State.RUNNING; |
| |
| import android.text.TextUtils; |
| |
| import java.util.Arrays; |
| import java.util.Comparator; |
| import java.util.List; |
| |
| /** |
| * A read-only domain object representing a countdown timer. |
| */ |
| public final class Timer { |
| |
| public enum State { |
| RUNNING(1), PAUSED(2), EXPIRED(3), RESET(4), MISSED(5); |
| |
| /** The value assigned to this State in prior releases. */ |
| private final int mValue; |
| |
| State(int value) { |
| mValue = value; |
| } |
| |
| /** |
| * @return the numeric value assigned to this state |
| */ |
| public int getValue() { |
| return mValue; |
| } |
| |
| /** |
| * @return the state corresponding to the given {@code value} |
| */ |
| public static State fromValue(int value) { |
| for (State state : values()) { |
| if (state.getValue() == value) { |
| return state; |
| } |
| } |
| |
| return null; |
| } |
| } |
| |
| /** The minimum duration of a timer. */ |
| public static final long MIN_LENGTH = SECOND_IN_MILLIS; |
| |
| static final long UNUSED = Long.MIN_VALUE; |
| |
| /** A unique identifier for the timer. */ |
| private final int mId; |
| |
| /** The current state of the timer. */ |
| private final State mState; |
| |
| /** The original length of the timer in milliseconds when it was created. */ |
| private final long mLength; |
| |
| /** The length of the timer in milliseconds including additional time added by the user. */ |
| private final long mTotalLength; |
| |
| /** The time at which the timer was last started; {@link #UNUSED} when not running. */ |
| private final long mLastStartTime; |
| |
| /** The time since epoch at which the timer was last started. */ |
| private final long mLastStartWallClockTime; |
| |
| /** The time at which the timer is scheduled to expire; negative if it is already expired. */ |
| private final long mRemainingTime; |
| |
| /** A message describing the meaning of the timer. */ |
| private final String mLabel; |
| |
| /** A flag indicating the timer should be deleted when it is reset. */ |
| private final boolean mDeleteAfterUse; |
| |
| Timer(int id, State state, long length, long totalLength, long lastStartTime, |
| long lastWallClockTime, long remainingTime, String label, boolean deleteAfterUse) { |
| mId = id; |
| mState = state; |
| mLength = length; |
| mTotalLength = totalLength; |
| mLastStartTime = lastStartTime; |
| mLastStartWallClockTime = lastWallClockTime; |
| mRemainingTime = remainingTime; |
| mLabel = label; |
| mDeleteAfterUse = deleteAfterUse; |
| } |
| |
| public int getId() { return mId; } |
| public State getState() { return mState; } |
| public String getLabel() { return mLabel; } |
| public long getLength() { return mLength; } |
| public long getTotalLength() { return mTotalLength; } |
| public boolean getDeleteAfterUse() { return mDeleteAfterUse; } |
| public boolean isReset() { return mState == RESET; } |
| public boolean isRunning() { return mState == RUNNING; } |
| public boolean isPaused() { return mState == PAUSED; } |
| public boolean isExpired() { return mState == EXPIRED; } |
| public boolean isMissed() { return mState == MISSED; } |
| |
| /** |
| * @return the total amount of time remaining up to this moment; expired and missed timers will |
| * return a negative amount |
| */ |
| public long getRemainingTime() { |
| if (mState == PAUSED || mState == RESET) { |
| return mRemainingTime; |
| } |
| |
| // In practice, "now" can be any value due to device reboots. When the real-time clock |
| // is reset, there is no more guarantee that "now" falls after the last start time. To |
| // ensure the timer is monotonically decreasing, normalize negative time segments to 0, |
| final long timeSinceStart = now() - mLastStartTime; |
| return mRemainingTime - Math.max(0, timeSinceStart); |
| } |
| |
| /** |
| * @return the elapsed realtime at which this timer will or did expire |
| */ |
| public long getExpirationTime() { |
| if (mState != RUNNING && mState != EXPIRED && mState != MISSED) { |
| throw new IllegalStateException("cannot compute expiration time in state " + mState); |
| } |
| |
| return mLastStartTime + mRemainingTime; |
| } |
| |
| /** |
| * |
| * @return the total amount of time elapsed up to this moment; expired timers will report more |
| * than the {@link #getTotalLength() total length} |
| */ |
| public long getElapsedTime() { |
| return getTotalLength() - getRemainingTime(); |
| } |
| |
| long getLastStartTime() { return mLastStartTime; } |
| long getLastWallClockTime() { return mLastStartWallClockTime; } |
| |
| /** |
| * @return a copy of this timer that is running, expired or missed |
| */ |
| Timer start() { |
| if (mState == RUNNING || mState == EXPIRED || mState == MISSED) { |
| return this; |
| } |
| |
| return new Timer(mId, RUNNING, mLength, mTotalLength, now(), wallClock(), mRemainingTime, |
| mLabel, mDeleteAfterUse); |
| } |
| |
| /** |
| * @return a copy of this timer that is paused or reset |
| */ |
| Timer pause() { |
| if (mState == PAUSED || mState == RESET) { |
| return this; |
| } else if (mState == EXPIRED || mState == MISSED) { |
| return reset(); |
| } |
| |
| final long remainingTime = getRemainingTime(); |
| return new Timer(mId, PAUSED, mLength, mTotalLength, UNUSED, UNUSED, remainingTime, mLabel, |
| mDeleteAfterUse); |
| } |
| |
| /** |
| * @return a copy of this timer that is expired, missed or reset |
| */ |
| Timer expire() { |
| if (mState == EXPIRED || mState == RESET || mState == MISSED) { |
| return this; |
| } |
| |
| final long remainingTime = Math.min(0L, getRemainingTime()); |
| return new Timer(mId, EXPIRED, mLength, 0L, now(), wallClock(), remainingTime, mLabel, |
| mDeleteAfterUse); |
| } |
| |
| /** |
| * @return a copy of this timer that is missed or reset |
| */ |
| Timer miss() { |
| if (mState == RESET || mState == MISSED) { |
| return this; |
| } |
| |
| final long remainingTime = Math.min(0L, getRemainingTime()); |
| return new Timer(mId, MISSED, mLength, 0L, now(), wallClock(), remainingTime, mLabel, |
| mDeleteAfterUse); |
| } |
| |
| /** |
| * @return a copy of this timer that is reset |
| */ |
| Timer reset() { |
| if (mState == RESET) { |
| return this; |
| } |
| |
| return new Timer(mId, RESET, mLength, mLength, UNUSED, UNUSED, mLength, mLabel, |
| mDeleteAfterUse); |
| } |
| |
| /** |
| * @return a copy of this timer that has its times adjusted after a reboot |
| */ |
| Timer updateAfterReboot() { |
| if (mState == RESET || mState == PAUSED) { |
| return this; |
| } |
| |
| final long timeSinceBoot = now(); |
| final long wallClockTime = wallClock(); |
| // Avoid negative time deltas. They can happen in practice, but they can't be used. Simply |
| // update the recorded times and proceed with no change in accumulated time. |
| final long delta = Math.max(0, wallClockTime - mLastStartWallClockTime); |
| final long remainingTime = mRemainingTime - delta; |
| return new Timer(mId, mState, mLength, mTotalLength, timeSinceBoot, wallClockTime, |
| remainingTime, mLabel, mDeleteAfterUse); |
| } |
| |
| /** |
| * @return a copy of this timer that has its times adjusted after time has been set |
| */ |
| Timer updateAfterTimeSet() { |
| if (mState == RESET || mState == PAUSED) { |
| return this; |
| } |
| |
| final long timeSinceBoot = now(); |
| final long wallClockTime = wallClock(); |
| final long delta = timeSinceBoot - mLastStartTime; |
| final long remainingTime = mRemainingTime - delta; |
| if (delta < 0) { |
| // Avoid negative time deltas. They typically happen following reboots when TIME_SET is |
| // broadcast before BOOT_COMPLETED. Simply ignore the time update and hope |
| // updateAfterReboot() can successfully correct the data at a later time. |
| return this; |
| } |
| return new Timer(mId, mState, mLength, mTotalLength, timeSinceBoot, wallClockTime, |
| remainingTime, mLabel, mDeleteAfterUse); |
| } |
| |
| /** |
| * @return a copy of this timer with the given {@code label} |
| */ |
| Timer setLabel(String label) { |
| if (TextUtils.equals(mLabel, label)) { |
| return this; |
| } |
| |
| return new Timer(mId, mState, mLength, mTotalLength, mLastStartTime, |
| mLastStartWallClockTime, mRemainingTime, label, mDeleteAfterUse); |
| } |
| |
| /** |
| * @return a copy of this timer with the given {@code remainingTime} or this timer if the |
| * remaining time could not be legally adjusted |
| */ |
| Timer setRemainingTime(long remainingTime) { |
| // Do not change the remaining time of a reset timer. |
| if (mRemainingTime == remainingTime || mState == RESET) { |
| return this; |
| } |
| |
| final long delta = remainingTime - mRemainingTime; |
| final long totalLength = mTotalLength + delta; |
| |
| final long lastStartTime; |
| final long lastWallClockTime; |
| final State state; |
| if (remainingTime > 0 && (mState == EXPIRED || mState == MISSED)) { |
| state = RUNNING; |
| lastStartTime = now(); |
| lastWallClockTime = wallClock(); |
| } else { |
| state = mState; |
| lastStartTime = mLastStartTime; |
| lastWallClockTime = mLastStartWallClockTime; |
| } |
| |
| return new Timer(mId, state, mLength, totalLength, lastStartTime, |
| lastWallClockTime, remainingTime, mLabel, mDeleteAfterUse); |
| } |
| |
| /** |
| * @return a copy of this timer with an additional minute added to the remaining time and total |
| * length, or this Timer if the minute could not be added |
| */ |
| Timer addMinute() { |
| // Expired and missed timers restart with 60 seconds of remaining time. |
| if (mState == EXPIRED || mState == MISSED) { |
| return setRemainingTime(MINUTE_IN_MILLIS); |
| } |
| |
| // Otherwise try to add a minute to the remaining time. |
| return setRemainingTime(mRemainingTime + MINUTE_IN_MILLIS); |
| } |
| |
| @Override |
| public boolean equals(Object o) { |
| if (this == o) return true; |
| if (o == null || getClass() != o.getClass()) return false; |
| |
| final Timer timer = (Timer) o; |
| |
| return mId == timer.mId; |
| } |
| |
| @Override |
| public int hashCode() { |
| return mId; |
| } |
| |
| /** |
| * Orders timers by their IDs. Oldest timers are at the bottom. Newest timers are at the top. |
| */ |
| static final Comparator<Timer> ID_COMPARATOR = |
| (timer1, timer2) -> Integer.compare(timer2.getId(), timer1.getId()); |
| |
| /** |
| * Orders timers by their expected/actual expiration time. The general order is: |
| * |
| * <ol> |
| * <li>{@link State#MISSED MISSED} timers; ties broken by {@link #getRemainingTime()}</li> |
| * <li>{@link State#EXPIRED EXPIRED} timers; ties broken by {@link #getRemainingTime()}</li> |
| * <li>{@link State#RUNNING RUNNING} timers; ties broken by {@link #getRemainingTime()}</li> |
| * <li>{@link State#PAUSED PAUSED} timers; ties broken by {@link #getRemainingTime()}</li> |
| * <li>{@link State#RESET RESET} timers; ties broken by {@link #getLength()}</li> |
| * </ol> |
| */ |
| static final Comparator<Timer> EXPIRY_COMPARATOR = new Comparator<>() { |
| |
| private final List<State> stateExpiryOrder = Arrays.asList(MISSED, EXPIRED, RUNNING, PAUSED, |
| RESET); |
| |
| @Override |
| public int compare(Timer timer1, Timer timer2) { |
| final int stateIndex1 = stateExpiryOrder.indexOf(timer1.getState()); |
| final int stateIndex2 = stateExpiryOrder.indexOf(timer2.getState()); |
| |
| int order = Integer.compare(stateIndex1, stateIndex2); |
| if (order == 0) { |
| final State state = timer1.getState(); |
| if (state == RESET) { |
| order = Long.compare(timer1.getLength(), timer2.getLength()); |
| } else { |
| order = Long.compare(timer1.getRemainingTime(), timer2.getRemainingTime()); |
| } |
| } |
| |
| return order; |
| } |
| }; |
| } |