chromium/chrome/android/java/src/org/chromium/chrome/browser/background_sync/BackgroundSyncBackgroundTaskScheduler.java

// Copyright 2019 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.chrome.browser.background_sync;

import android.os.PersistableBundle;
import android.text.format.DateUtils;

import androidx.annotation.IntDef;
import androidx.annotation.VisibleForTesting;

import org.jni_zero.CalledByNative;
import org.jni_zero.NativeMethods;

import org.chromium.base.ContextUtils;
import org.chromium.base.ObserverList;
import org.chromium.base.TimeUtils;
import org.chromium.components.background_task_scheduler.BackgroundTaskSchedulerFactory;
import org.chromium.components.background_task_scheduler.TaskIds;
import org.chromium.components.background_task_scheduler.TaskInfo;

/**
 * The {@link BackgroundSyncBackgroundTaskScheduler} singleton is responsible
 * for scheduling and cancelling background tasks to wake Chrome up so that
 * Background Sync events ready to be fired can be fired.
 *
 * Thread model: This class is to be run on the UI thread only.
 */
public class BackgroundSyncBackgroundTaskScheduler {
    /** An observer interface for BackgroundSyncBackgroundTaskScheduler. */
    interface Observer {
        void oneOffTaskScheduledFor(@BackgroundSyncTask int taskType, long delay);

        void oneOffTaskCanceledFor(@BackgroundSyncTask int taskType);
    }

    /**
     * Any tasks scheduled using GCMNetworkManager directly to wake up Chrome
     * would use this TASK_TAG. We no longer use GCMNetworkManager directly, so
     * when these tasks are run, we rescheduling using
     * BackgroundSyncBackgroundTaskScheduler.
     */
    public static final String TASK_TAG = "BackgroundSync Event";

    /**
     * Denotes the one-off Background Sync Background Tasks scheduled through
     * this class.
     * ONE_SHOT_SYNC_CHROME_WAKE_UP is the task that processes one-shot
     * Background Sync registrations.
     * PERIODIC_SYNC_CHROME_WAKE_UP processes Periodic Background Sync
     * registrations.
     */
    @IntDef({
        BackgroundSyncTask.ONE_SHOT_SYNC_CHROME_WAKE_UP,
        BackgroundSyncTask.PERIODIC_SYNC_CHROME_WAKE_UP
    })
    public @interface BackgroundSyncTask {
        int ONE_SHOT_SYNC_CHROME_WAKE_UP = 0;
        int PERIODIC_SYNC_CHROME_WAKE_UP = 1;
    };

    // Keep in sync with the default min_sync_recovery_time of
    // BackgroundSyncParameters.
    private static final long MIN_SYNC_RECOVERY_TIME = DateUtils.MINUTE_IN_MILLIS * 6;

    // Bundle key for the timestamp of the soonest wakeup time expected for
    // this task.
    public static final String SOONEST_EXPECTED_WAKETIME = "SoonestWakeupTime";

    private static BackgroundSyncBackgroundTaskScheduler sInstance;

    private final ObserverList<Observer> mObservers = new ObserverList<>();

    @CalledByNative
    public static BackgroundSyncBackgroundTaskScheduler getInstance() {
        if (sInstance == null) sInstance = new BackgroundSyncBackgroundTaskScheduler();
        return sInstance;
    }

    @VisibleForTesting
    static boolean hasInstance() {
        return sInstance != null;
    }

    /**
     * Returns the appropriate TaskID to use based on the class of the Background
     * Sync task we're working with.
     *
     * @param taskType The Background Sync task to get the TaskID for.
     */
    @VisibleForTesting
    public static int getAppropriateTaskId(@BackgroundSyncTask int taskType) {
        switch (taskType) {
            case BackgroundSyncTask.ONE_SHOT_SYNC_CHROME_WAKE_UP:
                return TaskIds.BACKGROUND_SYNC_ONE_SHOT_JOB_ID;
            case BackgroundSyncTask.PERIODIC_SYNC_CHROME_WAKE_UP:
                return TaskIds.PERIODIC_BACKGROUND_SYNC_CHROME_WAKEUP_TASK_JOB_ID;
            default:
                assert false : "Incorrect Background Sync task type";
                return -1;
        }
    }

    /** @param observer The observer to add. */
    @VisibleForTesting
    public void addObserver(Observer observer) {
        mObservers.addObserver(observer);
    }

    /** @param observer The observer to remove. */
    public void removeObserver(Observer observer) {
        mObservers.removeObserver(observer);
    }

    /**
     * Cancels a Background Sync one-off task, if there's one scheduled.
     *
     * @param taskType The Background Sync task to cancel.
     */
    @VisibleForTesting
    @CalledByNative
    protected void cancelOneOffTask(@BackgroundSyncTask int taskType) {
        BackgroundTaskSchedulerFactory.getScheduler()
                .cancel(ContextUtils.getApplicationContext(), getAppropriateTaskId(taskType));

        for (Observer observer : mObservers) {
            observer.oneOffTaskCanceledFor(taskType);
        }
    }

    /**
     * Schedules a one-off background task to wake the browser up on network
     * connectivity and call into native code to fire ready (periodic) Background Sync
     * events.
     *
     * @param minDelayMs The minimum time to wait before waking the browser.
     * @param taskType   The Background Sync task to schedule.
     */
    @VisibleForTesting
    @CalledByNative
    protected boolean scheduleOneOffTask(@BackgroundSyncTask int taskType, long minDelayMs) {
        // Pack SOONEST_EXPECTED_WAKETIME in extras.
        PersistableBundle taskExtras = new PersistableBundle();
        taskExtras.putLong(SOONEST_EXPECTED_WAKETIME, System.currentTimeMillis() + minDelayMs);

        // We setWindowEndTime to Long.MAX_VALUE to wait a long time for network connectivity,
        // so that we can process the pending sync event. setExpiresAfterWindowEndTime ensures
        // that we never wake up Chrome without network connectivity.
        TaskInfo.TimingInfo timingInfo =
                TaskInfo.OneOffInfo.create()
                        .setWindowStartTimeMs(minDelayMs)
                        .setWindowEndTimeMs(TimeUtils.MILLISECONDS_PER_YEAR)
                        .setExpiresAfterWindowEndTime(true)
                        .build();
        TaskInfo taskInfo =
                TaskInfo.createTask(getAppropriateTaskId(taskType), timingInfo)
                        .setRequiredNetworkType(TaskInfo.NetworkType.ANY)
                        .setUpdateCurrent(true)
                        .setIsPersisted(true)
                        .setExtras(taskExtras)
                        .build();
        // This will overwrite any existing task with this ID.
        boolean didSchedule =
                BackgroundTaskSchedulerFactory.getScheduler()
                        .schedule(ContextUtils.getApplicationContext(), taskInfo);

        for (Observer observer : mObservers) {
            observer.oneOffTaskScheduledFor(taskType, minDelayMs);
        }

        return didSchedule;
    }

    /**
     * Method for rescheduling a background task to wake up Chrome for processing Background Sync
     * events in the event of an OS upgrade or Google Play Services upgrade.
     *
     * @param taskType The Background Sync task to reschedule.
     */
    public void reschedule(@BackgroundSyncTask int taskType) {
        // TODO(crbug.com/40256221): Investigate if this can be deleted.
        scheduleOneOffTask(taskType, MIN_SYNC_RECOVERY_TIME);
    }

    @NativeMethods
    interface Natives {
        /**
         * Chrome currently disables BackgroundSyncManager if Google Play Services aren't up-to-date
         * at startup. Disable this check for tests, since we mock out interaction with GCM.
         * This method can be removed once our test devices start updating Google Play Services
         * before tests are run. https://crbug.com/514449
         * @param disabled disable or enable the version check for Google Play Services.
         */
        void setPlayServicesVersionCheckDisabledForTests(boolean disabled);
    }
}