chromium/components/background_task_scheduler/android/java/src/org/chromium/components/background_task_scheduler/TaskInfo.java

// Copyright 2017 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.components.background_task_scheduler;

import android.content.Context;
import android.os.PersistableBundle;

import androidx.annotation.IntDef;
import androidx.annotation.NonNull;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

/**
 * TaskInfo represents a request to run a specific {@link BackgroundTask} given the required
 * parameters, such as whether a special type of network is available.
 */
public class TaskInfo {
    public static final String SERIALIZED_TASK_EXTRAS = "serialized_task_extras";

    /** Common interface for all types of task information. */
    public interface TimingInfo {
        /**
         * Receives a {@link TimingInfoVisitor}, which will perform actions on this object.
         * @param visitor object that will perform actions on this instance.
         */
        void accept(TimingInfoVisitor visitor);
    }

    /**
     * Common interface for actions over TimingInfo implementations.
     *
     * This implements the Visitor design pattern over {@link TimingInfo} objects.
     * For a guide on how to use it, see the `Performing actions over TimingInfo objects` section
     * in //components/background_task_scheduler/README.md.
     */
    public interface TimingInfoVisitor {
        /**
         * Applies actions on a given {@link OneOffInfo}. This affects information regarding
         * timing for a one-off task.
         * @param oneOffInfo object to act on.
         */
        void visit(OneOffInfo oneOffInfo);

        /**
         * Applies actions on a given {@link PeriodicInfo}. This affects information regarding
         * timing for a periodic task.
         * @param periodicInfo object to act on.
         */
        void visit(PeriodicInfo periodicInfo);
    }

    /** Specifies information regarding one-off tasks. */
    public static class OneOffInfo implements TimingInfo {
        private final long mWindowStartTimeMs;
        private final long mWindowEndTimeMs;
        private final boolean mHasWindowStartTimeConstraint;
        private final boolean mHasWindowEndTimeConstraint;
        private final boolean mExpiresAfterWindowEndTime;

        private OneOffInfo(Builder builder) {
            mWindowStartTimeMs = builder.mWindowStartTimeMs;
            mWindowEndTimeMs = builder.mWindowEndTimeMs;
            mHasWindowStartTimeConstraint = builder.mHasWindowStartTimeConstraint;
            mHasWindowEndTimeConstraint = builder.mHasWindowEndTimeConstraint;
            mExpiresAfterWindowEndTime = builder.mExpiresAfterWindowEndTime;
        }

        /**
         * @return the start of the window that the task can begin executing as a delta in
         * milliseconds from now.
         */
        public long getWindowStartTimeMs() {
            return mWindowStartTimeMs;
        }

        /**
         * @return the end of the window that the task can begin executing as a delta in
         * milliseconds from now.
         */
        public long getWindowEndTimeMs() {
            return mWindowEndTimeMs;
        }

        /** @return whether this one-off task has a window start time constraint. */
        public boolean hasWindowStartTimeConstraint() {
            return mHasWindowStartTimeConstraint;
        }

        /** @return whether this one-off task has a window end time constraint. */
        public boolean hasWindowEndTimeConstraint() {
            return mHasWindowEndTimeConstraint;
        }

        /**
         * @return whether this one-off task expires after {@link #getWindowEndTimeMs()}
         * False by default.
         */
        public boolean expiresAfterWindowEndTime() {
            return mExpiresAfterWindowEndTime;
        }

        /**
         * Checks if a one-off task expired.
         * @param scheduleTimeMs the time at which the task was scheduled.
         * @param endTimeMs the time at which the task was set to expire.
         * @param currentTimeMs the current time to check for expiration.
         * @return true if the task expired and false otherwise.
         */
        public static boolean getExpirationStatus(
                long scheduleTimeMs, long endTimeMs, long currentTimeMs) {
            return currentTimeMs >= scheduleTimeMs + endTimeMs;
        }

        @Override
        public void accept(TimingInfoVisitor visitor) {
            visitor.visit(this);
        }

        @Override
        public String toString() {
            StringBuilder sb = new StringBuilder("{");
            if (hasWindowStartTimeConstraint()) {
                sb.append("windowStartTimeMs: ").append(mWindowStartTimeMs).append(", ");
            }
            if (hasWindowEndTimeConstraint()) {
                sb.append("windowEndTimeMs: ").append(mWindowEndTimeMs).append(", ");
            }
            sb.append("expiresAfterWindowEndTime (+flex): ").append(mExpiresAfterWindowEndTime);
            sb.append("}");
            return sb.toString();
        }

        /** @return a new {@link Builder} object to set the values of the one-off task. */
        public static Builder create() {
            return new Builder();
        }

        /**
         * A helper builder to provide a way to build {@link OneOffInfo}.
         *
         * @see #create()
         */
        public static final class Builder {
            private long mWindowStartTimeMs;
            private long mWindowEndTimeMs;
            // By default, a {@link OneOffInfo} doesn't have a set start time. The start time is
            // considered the time of scheduling the task.
            private boolean mHasWindowStartTimeConstraint;
            // User initiated tasks aren't allowed to have a end time constraint.
            private boolean mHasWindowEndTimeConstraint;
            // By default, a {@link OneOffInfo} doesn't have the expiration feature activated.
            private boolean mExpiresAfterWindowEndTime;

            public Builder setWindowStartTimeMs(long windowStartTimeMs) {
                mWindowStartTimeMs = windowStartTimeMs;
                mHasWindowStartTimeConstraint = true;
                return this;
            }

            public Builder setWindowEndTimeMs(long windowEndTimeMs) {
                mWindowEndTimeMs = windowEndTimeMs;
                mHasWindowEndTimeConstraint = true;
                return this;
            }

            public Builder setExpiresAfterWindowEndTime(boolean expiresAfterWindowEndTime) {
                mExpiresAfterWindowEndTime = expiresAfterWindowEndTime;
                return this;
            }

            /**
             * Build the {@link OneOffInfo object} specified by this builder.
             *
             * @return the {@link OneOffInfo} object.
             */
            public OneOffInfo build() {
                return new OneOffInfo(this);
            }
        }
    }

    /** Specifies information regarding periodic tasks. */
    public static class PeriodicInfo implements TimingInfo {
        private final long mIntervalMs;
        private final long mFlexMs;
        private final boolean mHasFlex;
        private final boolean mExpiresAfterWindowEndTime;

        private PeriodicInfo(PeriodicInfo.Builder builder) {
            mIntervalMs = builder.mIntervalMs;
            mFlexMs = builder.mFlexMs;
            mHasFlex = builder.mHasFlex;
            mExpiresAfterWindowEndTime = builder.mExpiresAfterWindowEndTime;
        }

        /** @return the interval between occurrences of this task in milliseconds. */
        public long getIntervalMs() {
            return mIntervalMs;
        }

        /**
         * @return the flex time for this task. The task can execute at any time in a window of flex
         * length at the end of the period. It is reported in milliseconds.
         */
        public long getFlexMs() {
            return mFlexMs;
        }

        /** @return true whether this task has defined a flex time. False otherwise. */
        public boolean hasFlex() {
            return mHasFlex;
        }

        /**
         * @return whether this periodic task expires after {@link #getIntervalMs()} +
         * {@link #getFlexMs()}
         * False by default.
         */
        public boolean expiresAfterWindowEndTime() {
            return mExpiresAfterWindowEndTime;
        }

        /**
         * Checks if a periodic task expired.
         * @param scheduleTimeMs the time at which the task was scheduled.
         * @param intervalTimeMs the interval at which the periodic task was scheduled.
         * @param flexTimeMs the flex time of the task, either set by the caller or the default one.
         * @param currentTimeMs the current time to check for expiration.
         * @return true if the task expired and false otherwise.
         */
        public static boolean getExpirationStatus(
                long scheduleTimeMs, long intervalTimeMs, long flexTimeMs, long currentTimeMs) {
            // Whether the task is executed during the wanted time window is determined here. The
            // position of the current time in relation to the time window is calculated here.
            // This position is compared with the time window margins.
            // For example, if a task is scheduled at 6am with an interval of 5h and a flex of
            // 5min, the valid starting times in that day are: 10:55am to 11am, 3:55pm to 4pm and
            // 8:55pm to 9pm. For 7pm as the current time, the time in the interval window is 3h.
            // This is not inside a valid starting time, so the task is considered expired.
            // Similarly, for 8:58pm as the current time, the time in the interval window is 4h
            // and 58min, which fits in a valid interval window.
            // In the case of a flex value equal or bigger than the interval value, the task
            // never expires.
            long timeSinceScheduledMs = currentTimeMs - scheduleTimeMs;
            long deltaTimeComparedToWindowMs = timeSinceScheduledMs % intervalTimeMs;
            return deltaTimeComparedToWindowMs < intervalTimeMs - flexTimeMs
                    && flexTimeMs < intervalTimeMs;
        }

        @Override
        public void accept(TimingInfoVisitor visitor) {
            visitor.visit(this);
        }

        @Override
        public String toString() {
            StringBuilder sb = new StringBuilder("{");
            sb.append("intervalMs: ").append(mIntervalMs).append(", ");
            if (mHasFlex) {
                sb.append(", flexMs: ").append(mFlexMs).append(", ");
            }
            sb.append("expiresAfterWindowEndTime (+flex): ").append(mExpiresAfterWindowEndTime);
            sb.append("}");
            return sb.toString();
        }

        /** @return a new {@link OneOffInfo.Builder} object to set the values of the one-off task. */
        public static PeriodicInfo.Builder create() {
            return new PeriodicInfo.Builder();
        }

        /**
         * A helper builder to provide a way to build {@link OneOffInfo}.
         *
         * @see #create()
         */
        public static final class Builder {
            private long mIntervalMs;
            private long mFlexMs;
            // By default, a {@link PeriodicInfo} doesn't have a specified flex and the default
            // one will be used in the scheduler.
            private boolean mHasFlex;
            // By default, a {@link PeriodicInfo} doesn't have the expiration feature activated.
            private boolean mExpiresAfterWindowEndTime;

            public Builder setIntervalMs(long intervalMs) {
                mIntervalMs = intervalMs;
                return this;
            }

            public Builder setFlexMs(long flexMs) {
                mFlexMs = flexMs;
                mHasFlex = true;
                return this;
            }

            public Builder setExpiresAfterWindowEndTime(boolean expiresAfterWindowEndTime) {
                mExpiresAfterWindowEndTime = expiresAfterWindowEndTime;
                return this;
            }

            /**
             * Build the {@link PeriodicInfo object} specified by this builder.
             *
             * @return the {@link PeriodicInfo} object.
             */
            public PeriodicInfo build() {
                return new PeriodicInfo(this);
            }
        }
    }

    @IntDef({NetworkType.NONE, NetworkType.ANY, NetworkType.UNMETERED})
    @Retention(RetentionPolicy.SOURCE)
    public @interface NetworkType {
        /**
         * This task has no requirements for network connectivity. Default.
         *
         * @see NetworkType
         */
        int NONE = 0;

        /**
         * This task requires network connectivity.
         *
         * @see NetworkType
         */
        int ANY = 1;

        /**
         * This task requires network connectivity that is unmetered.
         *
         * @see NetworkType
         */
        int UNMETERED = 2;
    }

    /**
     * The task ID should be unique across all tasks. A list of such unique IDs exists in
     * {@link TaskIds}.
     */
    private final int mTaskId;

    /** The extras to provide to the {@link BackgroundTask} when it is run. */
    @NonNull private final PersistableBundle mExtras;

    /** The type of network the task requires to run. */
    @NetworkType private final int mRequiredNetworkType;

    /** Whether the task requires charging to run. */
    private final boolean mRequiresCharging;

    /** Whether the task is being scheduled to fulfill an explicit user request. */
    private final boolean mUserInitiated;

    /** Whether or not to persist this task across device reboots. */
    private final boolean mIsPersisted;

    /** Whether this task should override any preexisting tasks with the same task id. */
    private final boolean mUpdateCurrent;

    /** Task information regarding a type of task. */
    private final TimingInfo mTimingInfo;

    private TaskInfo(Builder builder) {
        mTaskId = builder.mTaskId;
        mExtras = builder.mExtras == null ? new PersistableBundle() : builder.mExtras;
        mRequiredNetworkType = builder.mRequiredNetworkType;
        mRequiresCharging = builder.mRequiresCharging;
        mUserInitiated = builder.mUserInitiated;
        mIsPersisted = builder.mIsPersisted;
        mUpdateCurrent = builder.mUpdateCurrent;
        mTimingInfo = builder.mTimingInfo;

        // User-initiated tasks cannot have end time constraint.
        if (mTimingInfo instanceof OneOffInfo) {
            OneOffInfo oneOffInfo = (OneOffInfo) mTimingInfo;
            assert !mUserInitiated || !oneOffInfo.hasWindowEndTimeConstraint();
        }
    }

    /** @return the unique ID of this task. */
    public int getTaskId() {
        return mTaskId;
    }

    /** @return the extras that will be provided to the {@link BackgroundTask}. */
    @NonNull
    public PersistableBundle getExtras() {
        return mExtras;
    }

    /** @return the type of network the task requires to run. */
    @NetworkType
    public int getRequiredNetworkType() {
        return mRequiredNetworkType;
    }

    /** @return whether the task requires charging to run. */
    public boolean requiresCharging() {
        return mRequiresCharging;
    }

    /** @return Whether the task is being scheduled to fulfill an explicit user request. */
    public boolean isUserInitiated() {
        return mUserInitiated;
    }

    /** @return whether or not to persist this task across device reboots. */
    public boolean isPersisted() {
        return mIsPersisted;
    }

    /** @return whether this task should override any preexisting tasks with the same task id. */
    public boolean shouldUpdateCurrent() {
        return mUpdateCurrent;
    }

    /** @return Whether or not this task is a periodic task. */
    @Deprecated
    public boolean isPeriodic() {
        return mTimingInfo instanceof PeriodicInfo;
    }

    /**
     * This is part of a {@link TaskInfo} iff the task is a one-off task.
     *
     * @return the specific data if it is a one-off tasks and null otherwise.
     */
    @Deprecated
    public OneOffInfo getOneOffInfo() {
        if (mTimingInfo instanceof OneOffInfo) return (OneOffInfo) mTimingInfo;
        return null;
    }

    /**
     * This is part of a {@link TaskInfo} iff the task is a periodic task.
     *
     * @return the specific data that if it is a periodic tasks and null otherwise.
     */
    @Deprecated
    public PeriodicInfo getPeriodicInfo() {
        if (mTimingInfo instanceof PeriodicInfo) return (PeriodicInfo) mTimingInfo;
        return null;
    }

    /** @return the specific data based on the type of task. */
    public TimingInfo getTimingInfo() {
        return mTimingInfo;
    }

    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        sb.append("{");
        sb.append("taskId: ").append(mTaskId);
        sb.append(", extras: ").append(mExtras);
        sb.append(", requiredNetworkType: ").append(mRequiredNetworkType);
        sb.append(", requiresCharging: ").append(mRequiresCharging);
        sb.append(", userInitiated: ").append(mUserInitiated);
        sb.append(", isPersisted: ").append(mIsPersisted);
        sb.append(", updateCurrent: ").append(mUpdateCurrent);
        sb.append(", timingInfo: ").append(mTimingInfo);
        sb.append("}");
        return sb.toString();
    }

    /**
     * Creates a task that holds all information necessary to schedule it.
     *
     * @param taskId the unique task ID for this task. Should be listed in {@link TaskIds}.
     * @param timingInfo the task information specific to each type of task.
     * @return the builder which can be used to continue configuration and {@link Builder#build()}.
     * @see TaskIds
     */
    public static Builder createTask(int taskId, TimingInfo timingInfo) {
        return new Builder(taskId).setTimingInfo(timingInfo);
    }

    /**
     * Schedule a one-off task to execute within a deadline. If windowEndTimeMs is 0, the task will
     * run as soon as possible. For executing a task within a time window, see
     * {@link #createOneOffTask(int, long, long)}.
     *
     * @param taskId the unique task ID for this task. Should be listed in {@link TaskIds}.
     * @param windowEndTimeMs the end of the window that the task can begin executing as a delta in
     * milliseconds from now. Note that the task begins executing at this point even if the
     * prerequisite conditions are not met.
     * @return the builder which can be used to continue configuration and {@link Builder#build()}.
     * @see TaskIds
     *
     * @deprecated the {@see #createTask(int, Class, TimingInfo)} method should be used instead.
     * This method requires an additional step for the caller: the creation of the specific
     * {@link TimingInfo} object with the wanted properties.
     */
    @Deprecated
    public static Builder createOneOffTask(int taskId, long windowEndTimeMs) {
        TimingInfo oneOffInfo = OneOffInfo.create().setWindowEndTimeMs(windowEndTimeMs).build();
        return new Builder(taskId).setTimingInfo(oneOffInfo);
    }

    /**
     * Schedule a one-off task to execute within a time window. For executing a task within a
     * deadline, see {@link #createOneOffTask(int, long)},
     *
     * @param taskId the unique task ID for this task. Should be listed in {@link TaskIds}.
     * @param windowStartTimeMs the start of the window that the task can begin executing as a delta
     * in milliseconds from now.
     * @param windowEndTimeMs the end of the window that the task can begin executing as a delta in
     * milliseconds from now. Note that the task begins executing at this point even if the
     * prerequisite conditions are not met.
     * @return the builder which can be used to continue configuration and {@link Builder#build()}.
     * @see TaskIds
     *
     * @deprecated the {@see #createTask(int, Class, TimingInfo)} method should be used instead.
     * This method requires an additional step for the caller: the creation of the specific
     * {@link TimingInfo} object with the wanted properties.
     */
    @Deprecated
    public static Builder createOneOffTask(
            int taskId, long windowStartTimeMs, long windowEndTimeMs) {
        TimingInfo oneOffInfo =
                OneOffInfo.create()
                        .setWindowStartTimeMs(windowStartTimeMs)
                        .setWindowEndTimeMs(windowEndTimeMs)
                        .build();
        return new Builder(taskId).setTimingInfo(oneOffInfo);
    }

    /**
     * Schedule a periodic task that will recur at the specified interval, without the need to
     * be rescheduled. The task will continue to recur until
     * {@link BackgroundTaskScheduler#cancel(Context, int)} is invoked with the task ID from this
     * {@link TaskInfo}.
     * The flex time specifies how close to the end of the interval you are willing to execute.
     * Instead of executing at the exact interval, the task will execute at the interval or up to
     * flex milliseconds before.
     *
     * @param taskId the unique task ID for this task. Should be listed in {@link TaskIds}.
     * @param intervalMs the interval between occurrences of this task in milliseconds.
     * @param flexMs the flex time for this task. The task can execute at any time in a window of
     * flex
     * length at the end of the period. It is reported in milliseconds.
     * @return the builder which can be used to continue configuration and {@link Builder#build()}.
     * @see TaskIds
     *
     * @deprecated the {@see #createTask(int, TimingInfo)} method should be used instead.
     * This method requires an additional step for the caller: the creation of the specific
     * {@link TimingInfo} object with the wanted properties.
     */
    @Deprecated
    public static Builder createPeriodicTask(int taskId, long intervalMs, long flexMs) {
        TimingInfo periodicInfo =
                PeriodicInfo.create().setIntervalMs(intervalMs).setFlexMs(flexMs).build();
        return new Builder(taskId).setTimingInfo(periodicInfo);
    }

    /**
     * A helper builder to provide a way to build {@link TaskInfo}. To create a {@link Builder}
     * use the createTask method on {@link TaskInfo}.
     *
     * @see @createTask(int, Class, TimingInfo)
     */
    public static final class Builder {
        private final int mTaskId;

        private PersistableBundle mExtras;
        @NetworkType private int mRequiredNetworkType;
        private boolean mRequiresCharging;
        private boolean mUserInitiated;
        private boolean mIsPersisted;
        private boolean mUpdateCurrent;
        private TimingInfo mTimingInfo;

        Builder(int taskId) {
            mTaskId = taskId;
        }

        Builder setTimingInfo(TimingInfo timingInfo) {
            mTimingInfo = timingInfo;
            return this;
        }

        /**
         * Set the optional extra values necessary for this task. Must only ever contain simple
         * values supported by {@link android.os.BaseBundle}. All other values are thrown away.
         * If the extras for this builder are not set, or set to null, the resulting
         * {@link TaskInfo} will have an empty bundle (i.e. not null).
         *
         * @param bundle the bundle of extra values necessary for this task.
         * @return this {@link Builder}.
         */
        public Builder setExtras(PersistableBundle bundle) {
            mExtras = bundle;
            return this;
        }

        /**
         * Set the type of network the task requires to run.
         *
         * @param networkType the {@link NetworkType} required for this task.
         * @return this {@link Builder}.
         */
        public Builder setRequiredNetworkType(@NetworkType int networkType) {
            mRequiredNetworkType = networkType;
            return this;
        }

        /**
         * Set whether the task requires charging to run.
         *
         * @param requiresCharging true if this task requires charging.
         * @return this {@link Builder}.
         */
        public Builder setRequiresCharging(boolean requiresCharging) {
            mRequiresCharging = requiresCharging;
            return this;
        }

        /**
         * Set whether the task is being scheduled to fulfill an explicit user request.
         * @param userInitiated Whether the task is user initiated.
         * @return this {@link Builder}.
         */
        public Builder setUserInitiated(boolean userInitiated) {
            mUserInitiated = userInitiated;
            return this;
        }

        /**
         * Set whether or not to persist this task across device reboots.
         *
         * @param isPersisted true if this task should be persisted across reboots.
         * @return this {@link Builder}.
         */
        public Builder setIsPersisted(boolean isPersisted) {
            mIsPersisted = isPersisted;
            return this;
        }

        /**
         * Set whether this task should override any preexisting tasks with the same task id.
         *
         * @param updateCurrent true if this task should overwrite a currently existing task with
         *                      the same ID, if it exists.
         * @return this {@link Builder}.
         */
        public Builder setUpdateCurrent(boolean updateCurrent) {
            mUpdateCurrent = updateCurrent;
            return this;
        }

        /**
         * Build the {@link TaskInfo object} specified by this builder.
         *
         * @return the {@link TaskInfo} object.
         */
        public TaskInfo build() {
            return new TaskInfo(this);
        }
    }
}