chromium/chrome/android/java/src/org/chromium/chrome/browser/offlinepages/OfflineBackgroundTask.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.chrome.browser.offlinepages;

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

import androidx.annotation.VisibleForTesting;

import org.chromium.base.ApplicationStatus;
import org.chromium.base.Callback;
import org.chromium.base.Log;
import org.chromium.base.SysUtils;
import org.chromium.chrome.browser.device.DeviceConditions;
import org.chromium.components.background_task_scheduler.NativeBackgroundTask;
import org.chromium.components.background_task_scheduler.TaskIds;
import org.chromium.components.background_task_scheduler.TaskParameters;

/**
 * Handles servicing of background offlining requests coming via background_task_scheduler
 * component.
 */
public class OfflineBackgroundTask extends NativeBackgroundTask {
    private static final String TAG = "OfflineBkgrndTask";

    @Override
    protected @StartBeforeNativeResult int onStartTaskBeforeNativeLoaded(
            Context context, TaskParameters taskParameters, TaskFinishedCallback callback) {
        assert taskParameters.getTaskId() == TaskIds.OFFLINE_PAGES_BACKGROUND_JOB_ID;

        if (!checkConditions(context, taskParameters.getExtras())) {
            return StartBeforeNativeResult.RESCHEDULE;
        }

        return StartBeforeNativeResult.LOAD_NATIVE;
    }

    @Override
    protected void onStartTaskWithNative(
            Context context, TaskParameters taskParameters, TaskFinishedCallback callback) {
        assert taskParameters.getTaskId() == TaskIds.OFFLINE_PAGES_BACKGROUND_JOB_ID;

        if (!startScheduledProcessing(
                BackgroundSchedulerProcessor.getInstance(),
                context,
                taskParameters.getExtras(),
                wrapCallback(callback))) {
            callback.taskFinished(true);
            return;
        }

        // Set up backup scheduled task in case processing is killed before RequestCoordinator
        // has a chance to reschedule base on remaining work.
        BackgroundScheduler.getInstance()
                .scheduleBackup(
                        TaskExtrasPacker.unpackTriggerConditionsFromBundle(
                                taskParameters.getExtras()),
                        DateUtils.MINUTE_IN_MILLIS * 5);
    }

    @Override
    protected boolean onStopTaskBeforeNativeLoaded(Context context, TaskParameters taskParameters) {
        assert taskParameters.getTaskId() == TaskIds.OFFLINE_PAGES_BACKGROUND_JOB_ID;

        // Native didn't complete loading, but it was supposed to. Presume we need to reschedule.
        return true;
    }

    @Override
    protected boolean onStopTaskWithNative(Context context, TaskParameters taskParameters) {
        assert taskParameters.getTaskId() == TaskIds.OFFLINE_PAGES_BACKGROUND_JOB_ID;

        return BackgroundSchedulerProcessor.getInstance().stopScheduledProcessing();
    }

    /** Wraps the callback for code reuse */
    private Callback<Boolean> wrapCallback(final TaskFinishedCallback callback) {
        return new Callback<Boolean>() {
            @Override
            public void onResult(Boolean result) {
                callback.taskFinished(result);
            }
        };
    }

    /**
     * Starts scheduled processing and reports UMA. This method does not check for current
     * conditions and should be used together with {@link #checkConditions} to ensure that it
     * performs the tasks only when it is supposed to.
     *
     * @returns Whether processing will be carried out and completion will be indicated through a
     *     callback.
     */
    @VisibleForTesting
    static boolean startScheduledProcessing(
            BackgroundSchedulerProcessor bridge,
            Context context,
            PersistableBundle taskExtras,
            Callback<Boolean> callback) {
        // Gather UMA data to measure how often the user's machine is amenable to background
        // loading when we wake to do a task.
        DeviceConditions deviceConditions = DeviceConditions.getCurrent(context);
        return bridge.startScheduledProcessing(deviceConditions, callback);
    }

    /** @returns Whether conditions for running the tasks are met. */
    @VisibleForTesting
    static boolean checkConditions(Context context, PersistableBundle taskExtras) {
        TriggerConditions triggerConditions =
                TaskExtrasPacker.unpackTriggerConditionsFromBundle(taskExtras);

        DeviceConditions deviceConditions = DeviceConditions.getCurrent(context);
        if (!areBatteryConditionsMet(deviceConditions, triggerConditions)) {
            Log.d(TAG, "Battery percentage is lower than minimum to start processing");
            return false;
        }

        if (!isSvelteConditionsMet()) {
            Log.d(TAG, "Application visible on low-end device so deferring background processing");
            return false;
        }

        return true;
    }

    /** Whether battery conditions (on power and enough battery percentage) are met. */
    private static boolean areBatteryConditionsMet(
            DeviceConditions deviceConditions, TriggerConditions triggerConditions) {
        return deviceConditions.isPowerConnected()
                || (deviceConditions.getBatteryPercentage()
                        >= triggerConditions.getMinimumBatteryPercentage());
    }

    /** Whether there are no visible activities when on Svelte. */
    private static boolean isSvelteConditionsMet() {
        return !SysUtils.isLowEndDevice() || !ApplicationStatus.hasVisibleActivities();
    }
}