TimerStatusPart.java

/*
 * Copyright 2020 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 androidx.wear.ongoing;

import android.content.Context;
import android.text.format.DateUtils;

import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
import androidx.versionedparcelable.NonParcelField;
import androidx.versionedparcelable.ParcelField;
import androidx.versionedparcelable.VersionedParcelize;

import java.util.Objects;

/**
 * Implementation and internal representation of {@link Status.TimerPart} and
 * {@link Status.StopwatchPart}
 * <p>
 * Available since wear-ongoing:1.0.0
 * @hide
 */
@RestrictTo(RestrictTo.Scope.LIBRARY)
@VersionedParcelize
class TimerStatusPart extends StatusPart {
    @ParcelField(value = 1, defaultValue = "0")
    long mTimeZeroMillis;

    @ParcelField(value = 2, defaultValue = "false")
    boolean mCountDown = false;

    @ParcelField(value = 3, defaultValue = "-1")
    long mPausedAtMillis = LONG_DEFAULT;

    @ParcelField(value = 4, defaultValue = "-1")
    long mTotalDurationMillis = LONG_DEFAULT;

    @NonParcelField
    private final StringBuilder mStringBuilder = new StringBuilder(8);

    private static final String NEGATIVE_DURATION_PREFIX = "-";

    // Required by VersionedParcelable
    TimerStatusPart() {
    }

    /**
     * Create a Status representing a timer or stopwatch.
     *
     * @param timeZeroMillis      timestamp of the time at which this Timer should display 0,
     *                            will be in the
     *                            past for a stopwatch and usually in the future for timers.
     * @param countDown           indicates if this is a stopwatch (when {@code false} or timer
     *                            (when {@code true}).
     * @param pausedAtMillis      timestamp of the time when this timer was paused. Or
     *                            {@code -1L} if this
     *                            timer is running.
     * @param totalDurationMillis total duration of this timer/stopwatch, useful to display as a
     *                            progress bar or similar.
     */
    TimerStatusPart(long timeZeroMillis, boolean countDown, long pausedAtMillis,
            long totalDurationMillis) {
        this.mTimeZeroMillis = timeZeroMillis;
        this.mCountDown = countDown;
        this.mPausedAtMillis = pausedAtMillis;
        this.mTotalDurationMillis = totalDurationMillis;
    }

    /**
     * See {@link TimeDependentText#getText(Context, long)}]
     */
    @NonNull
    @Override
    public CharSequence getText(@NonNull Context context, long timeNowMillis) {
        long timeMillis = isPaused() ? mPausedAtMillis : timeNowMillis;
        long milliSeconds = timeMillis - mTimeZeroMillis;
        long seconds = milliSeconds >= 0 ? milliSeconds / 1000
                // Always round down (instead of the default round to 0) so all values are displayed
                // for 1 second.
                : (milliSeconds - 999) / 1000;

        if (mCountDown) {
            seconds = -seconds;
        }

        String prefix = "";
        if (seconds < 0) {
            seconds = -seconds;
            prefix = NEGATIVE_DURATION_PREFIX;
        }

        return prefix + DateUtils.formatElapsedTime(mStringBuilder, seconds);
    }

    /**
     * See {@link TimeDependentText#getNextChangeTimeMillis(long)}
     */
    @Override
    public long getNextChangeTimeMillis(long fromTimeMillis) {
        return isPaused() ? Long.MAX_VALUE :
                // We always want to return a value:
                //    * Strictly greater than fromTimeMillis.
                //    * Has the same millis as timeZero.
                //    * It's as small as possible.
                fromTimeMillis + ((mTimeZeroMillis - fromTimeMillis) % 1000 + 1999) % 1000 + 1;
    }

    public boolean isPaused() {
        return mPausedAtMillis >= 0L;
    }

    @Override
    public boolean equals(Object o) {
        if (!(o instanceof TimerStatusPart)) return false;
        TimerStatusPart that = (TimerStatusPart) o;
        return mTimeZeroMillis == that.mTimeZeroMillis
                && mCountDown == that.mCountDown
                && mPausedAtMillis == that.mPausedAtMillis
                && mTotalDurationMillis == that.mTotalDurationMillis;
    }

    @Override
    public int hashCode() {
        return Objects.hash(mTimeZeroMillis, mCountDown, mPausedAtMillis, mTotalDurationMillis);
    }

    // Invalid value to use for paused_at and duration, as suggested by api guidelines 5.15
    static final long LONG_DEFAULT = -1L;
}