Stopwatch.java

/*
 * Copyright 2022 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.appsearch.builtintypes;

import android.content.Context;
import android.os.Build;
import android.os.SystemClock;

import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
import androidx.appsearch.annotation.Document;
import androidx.appsearch.app.AppSearchSchema.StringPropertyConfig;
import androidx.appsearch.utils.BootCountUtil;
import androidx.core.util.Preconditions;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.Collections;
import java.util.List;

/**
 * An AppSearch document representing a {@link Stopwatch} entity.
 *
 * <p>A stopwatch is used to count time up, starting from 0, and can be paused and resumed at will.
 */
@Document(name = "builtin:Stopwatch")
public final class Stopwatch {
    /** @hide */
    @RestrictTo(RestrictTo.Scope.LIBRARY)
    @IntDef({STATUS_UNKNOWN, STATUS_RESET, STATUS_RUNNING, STATUS_PAUSED})
    @Retention(RetentionPolicy.SOURCE)
    public @interface Status {}

    /** The {@link Stopwatch} status is unknown */
    public static final int STATUS_UNKNOWN = 0;
    /** The {@link Stopwatch} is reset. */
    public static final int STATUS_RESET = 1;
    /** The {@link Stopwatch} is running. */
    public static final int STATUS_RUNNING = 2;
    /** The {@link Stopwatch} is paused. */
    public static final int STATUS_PAUSED = 3;

    @Document.Namespace
    private final String mNamespace;

    @Document.Id
    private final String mId;

    @Document.Score
    private final int mDocumentScore;

    @Document.CreationTimestampMillis
    private final long mCreationTimestampMillis;

    @Document.TtlMillis
    private final long mDocumentTtlMillis;

    @Document.StringProperty(indexingType = StringPropertyConfig.INDEXING_TYPE_PREFIXES)
    private final String mName;

    @Document.LongProperty
    private final long mBaseTimeMillis;

    @Document.LongProperty
    private final long mBaseTimeMillisInElapsedRealtime;

    @Document.LongProperty
    private final int mBootCount;

    @Document.LongProperty
    private final int mStatus;

    @Document.LongProperty
    private final long mAccumulatedDurationMillis;

    @Document.DocumentProperty
    private final List<StopwatchLap> mLaps;

    Stopwatch(@NonNull String namespace, @NonNull String id, int documentScore,
            long creationTimestampMillis,
            long documentTtlMillis, @Nullable String name, long baseTimeMillis,
            long baseTimeMillisInElapsedRealtime, int bootCount, int status,
            long accumulatedDurationMillis, @NonNull List<StopwatchLap> laps) {
        mNamespace = Preconditions.checkNotNull(namespace);
        mId = Preconditions.checkNotNull(id);
        mDocumentScore = documentScore;
        mCreationTimestampMillis = creationTimestampMillis;
        mDocumentTtlMillis = documentTtlMillis;
        mName = name;
        mBaseTimeMillis = baseTimeMillis;
        mBaseTimeMillisInElapsedRealtime = baseTimeMillisInElapsedRealtime;
        mBootCount = bootCount;
        mStatus = status;
        mAccumulatedDurationMillis = accumulatedDurationMillis;
        mLaps = Preconditions.checkNotNull(laps);
    }

    /** Returns the namespace. */
    @NonNull
    public String getNamespace() {
        return mNamespace;
    }

    /** Returns the unique identifier. */
    @NonNull
    public String getId() {
        return mId;
    }

    /**
     * Returns the user-provided opaque document score of the current AppSearch document, which can
     * be used for ranking using
     * {@link androidx.appsearch.app.SearchSpec.RankingStrategy#RANKING_STRATEGY_DOCUMENT_SCORE}.
     *
     * <p>See {@link Document.Score} for more information on score.
     */
    public int getDocumentScore() {
        return mDocumentScore;
    }

    /**
     * Returns the creation timestamp for the current AppSearch entity, in milliseconds using the
     * {@link System#currentTimeMillis()} time base.
     *
     * <p>This timestamp refers to the creation time of the AppSearch entity, not when the
     * document is written into AppSearch.
     *
     * <p>If not set, then the current timestamp will be used.
     *
     * <p>See {@link androidx.appsearch.annotation.Document.CreationTimestampMillis} for more
     * information on creation timestamp.
     */
    public long getCreationTimestampMillis() {
        return mCreationTimestampMillis;
    }

    /**
     * Returns the time-to-live (TTL) for the current AppSearch document as a duration in
     * milliseconds.
     *
     * <p>The document will be automatically deleted when the TTL expires.
     *
     * <p>See {@link Document.TtlMillis} for more information on TTL.
     */
    public long getDocumentTtlMillis() {
        return mDocumentTtlMillis;
    }

    /** Returns the name. */
    @Nullable
    public String getName() {
        return mName;
    }

    /**
     * Returns the point in time that the {@link Stopwatch} counts up from. In milliseconds using
     * the {@link System#currentTimeMillis()} time base.
     *
     * <p>Use {@link #calculateBaseTimeMillis(Context)} to get a more accurate base time that
     * accounts for the current boot count of the device.
     */
    public long getBaseTimeMillis() {
        return mBaseTimeMillis;
    }

    /**
     * Returns the point in time that the {@link Stopwatch} counts up from. In milliseconds using
     * the {@link android.os.SystemClock#elapsedRealtime()} time base.
     *
     * <p>ElapsedRealtime should only be used if the {@link #getBootCount()} matches the
     * bootCount of the current device.
     */
    public long getBaseTimeMillisInElapsedRealtime() {
        return mBaseTimeMillisInElapsedRealtime;
    }

    /**
     * Returns the boot count of the device when this document is last updated.
     *
     * <p>The boot count of the device can be accessed from Global Settings. See
     * {@link android.provider.Settings.Global#BOOT_COUNT}.
     *
     * <p>On older APIs where boot count is not available, this value should not be used.
     */
    public int getBootCount() {
        return mBootCount;
    }

    /**
     * Returns the current status.
     *
     * <p>Status can be {@link Stopwatch#STATUS_UNKNOWN}, {@link Stopwatch#STATUS_RESET},
     * {@link Stopwatch#STATUS_RUNNING}, or {@link Stopwatch#STATUS_PAUSED}.
     */
    @Status
    public int getStatus() {
        return mStatus;
    }

    /**
     * Returns the total duration in milliseconds accumulated by the {@link Stopwatch}.
     *
     * <p>Use this method to get the static accumulated time stored in the document. Use
     * {@link #calculateCurrentAccumulatedDurationMillis(Context)} to calculate the accumulated time
     * in real time.
     */
    public long getAccumulatedDurationMillis() {
        return mAccumulatedDurationMillis;
    }

    /** Returns all the {@link StopwatchLap} instances. */
    @NonNull
    public List<StopwatchLap> getLaps() {
        return mLaps;
    }

    /**
     * Calculates the base time in milliseconds using the {@link System#currentTimeMillis()} time
     * base.
     *
     * <p>If the boot count retrieved from the context matches {@link #getBootCount()}, then
     * {@link #getBaseTimeMillisInElapsedRealtime()} will be used to calculate the base time
     * in the {@link System#currentTimeMillis()} time base. Otherwise return
     * {@link #getBaseTimeMillis()}.
     */
    @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR1)
    public long calculateBaseTimeMillis(@NonNull Context context) {
        int currentBootCount = BootCountUtil.getCurrentBootCount(context);
        if (currentBootCount == -1 || currentBootCount != mBootCount) {
            // Boot count doesn't exist, or it doesn't match the current device boot count.
            // Therefore return the wall clock time since elapsed realtime is not valid.
            return mBaseTimeMillis;
        } else {
            // Boot count matches the current device boot count. Therefore calculate the wall
            // clock base time using elapsed realtime.
            long elapsedTime = SystemClock.elapsedRealtime() - mBaseTimeMillisInElapsedRealtime;
            return System.currentTimeMillis() - elapsedTime;
        }
    }

    /**
     * Calculates the current accumulated time in milliseconds.
     *
     * <p>Use this method to calculate the accumulated time in real time. Use
     * {@link #getAccumulatedDurationMillis()} to get the static accumulated time stored in the
     * document.
     */
    @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR1)
    public long calculateCurrentAccumulatedDurationMillis(@NonNull Context context) {
        if (mStatus == STATUS_PAUSED || mStatus == STATUS_RESET) {
            return mAccumulatedDurationMillis;
        }

        return System.currentTimeMillis() - calculateBaseTimeMillis(context)
                + mAccumulatedDurationMillis;
    }

    /** Builder for {@link Stopwatch}. */
    public static final class Builder extends BaseBuiltinTypeBuilder<Builder> {
        private String mName;
        private long mBaseTimeMillis;
        private long mBaseTimeMillisInElapsedRealtime;
        private int mBootCount;
        private int mStatus;
        private long mAccumulatedDurationMillis;
        private List<StopwatchLap> mLaps;

        /**
         * Constructor for {@link Stopwatch.Builder}.
         *
         * @param namespace Namespace for the Document. See {@link Document.Namespace}.
         * @param id Unique identifier for the Document. See {@link Document.Id}.
         */
        public Builder(@NonNull String namespace, @NonNull String id) {
            super(namespace, id);

            // Default empty laps
            mLaps = Collections.emptyList();
        }

        /**
         * Constructor for {@link Stopwatch.Builder} with all the existing values.
         */
        public Builder(@NonNull Stopwatch stopwatch) {
            this(stopwatch.getNamespace(), stopwatch.getId());

            mDocumentScore = stopwatch.getDocumentScore();
            mCreationTimestampMillis = stopwatch.getCreationTimestampMillis();
            mDocumentTtlMillis = stopwatch.getDocumentTtlMillis();
            mName = stopwatch.getName();
            mBaseTimeMillis = stopwatch.getBaseTimeMillis();
            mBaseTimeMillisInElapsedRealtime =
                    stopwatch.getBaseTimeMillisInElapsedRealtime();
            mBootCount = stopwatch.getBootCount();
            mStatus = stopwatch.getStatus();
            mAccumulatedDurationMillis = stopwatch.getAccumulatedDurationMillis();
            mLaps = stopwatch.getLaps();
        }

        /** Sets the name. */
        @NonNull
        public Builder setName(@Nullable String name) {
            mName = name;
            return this;
        }

        /**
         * Sets the point in time that the {@link Stopwatch} counts up from.
         *
         * <p>Base time should be sampled in both the {@link System#currentTimeMillis()} and
         * {@link android.os.SystemClock#elapsedRealtime()} time base. In addition, the boot
         * count of the device is needed to check if the
         * {@link android.os.SystemClock#elapsedRealtime()} time base is valid.
         *
         * @param baseTimeMillis The base time in milliseconds using the
         * {@link System#currentTimeMillis()} time base.
         * @param baseTimeMillisInElapsedRealtime The base time in milliseconds using the
         * {@link android.os.SystemClock#elapsedRealtime()} time base.
         * @param bootCount The current boot count of the device. See
         * {@link android.provider.Settings.Global#BOOT_COUNT}.
         */
        @NonNull
        public Builder setBaseTimeMillis(long baseTimeMillis,
                long baseTimeMillisInElapsedRealtime, int bootCount) {
            mBaseTimeMillis = baseTimeMillis;
            mBaseTimeMillisInElapsedRealtime = baseTimeMillisInElapsedRealtime;
            mBootCount = bootCount;
            return this;
        }

        /**
         * Sets the point in time that the {@link Stopwatch} counts up from.
         *
         * <p>See {@link #setBaseTimeMillis(long, long, int)}.
         *
         * @param context The app context used to fetch boot count.
         * @param baseTimeMillis The base time in milliseconds using the
         * {@link System#currentTimeMillis()} time base.
         * @param baseTimeMillisInElapsedRealtime The base time in milliseconds using the
         * {@link android.os.SystemClock#elapsedRealtime()} time base.
         */
        @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR1)
        @NonNull
        public Builder setBaseTimeMillis(@NonNull Context context, long baseTimeMillis,
                long baseTimeMillisInElapsedRealtime) {
            int bootCount = BootCountUtil.getCurrentBootCount(context);
            return setBaseTimeMillis(baseTimeMillis, baseTimeMillisInElapsedRealtime, bootCount);
        }

        /**
         * Sets the current status.
         *
         * <p>Status can be {@link Stopwatch#STATUS_UNKNOWN}, {@link Stopwatch#STATUS_RESET},
         * {@link Stopwatch#STATUS_RUNNING}, or {@link Stopwatch#STATUS_PAUSED}.
         */
        @NonNull
        public Builder setStatus(@Status int status) {
            mStatus = Preconditions.checkArgumentInRange(status, STATUS_UNKNOWN, STATUS_PAUSED,
                    "status");
            return this;
        }

        /**
         * Sets the total duration in milliseconds accumulated by the {@link Stopwatch}.
         */
        @NonNull
        public Builder setAccumulatedDurationMillis(long accumulatedDurationMillis) {
            mAccumulatedDurationMillis = accumulatedDurationMillis;
            return this;
        }

        /** Sets all the {@link StopwatchLap} instances. */
        @NonNull
        public Builder setLaps(@NonNull List<StopwatchLap> laps) {
            mLaps = Preconditions.checkNotNull(laps);
            return this;
        }

        /** Builds the {@link Stopwatch}. */
        @NonNull
        public Stopwatch build() {
            return new Stopwatch(mNamespace, mId, mDocumentScore,
                    mCreationTimestampMillis, mDocumentTtlMillis, mName, mBaseTimeMillis,
                    mBaseTimeMillisInElapsedRealtime, mBootCount, mStatus,
                    mAccumulatedDurationMillis, mLaps);
        }
    }
}