blob: 68380dfab7208d1f4afeb737d018371f1d17c165 [file] [log] [blame]
// Copyright 2017 The Chromium Authors. All rights reserved.
// 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.annotation.TargetApi;
import android.app.job.JobInfo;
import android.app.job.JobParameters;
import android.app.job.JobScheduler;
import android.content.ComponentName;
import android.content.Context;
import android.os.Build;
import android.os.Bundle;
import android.os.PersistableBundle;
import org.chromium.base.Log;
import org.chromium.base.ThreadUtils;
import org.chromium.base.VisibleForTesting;
import java.util.List;
/**
* An implementation of {@link BackgroundTaskSchedulerDelegate} that uses the system
* {@link JobScheduler} to schedule jobs.
*/
@TargetApi(Build.VERSION_CODES.LOLLIPOP_MR1)
class BackgroundTaskSchedulerJobService implements BackgroundTaskSchedulerDelegate {
private static final String TAG = "BkgrdTaskSchedulerJS";
private static final String BACKGROUND_TASK_CLASS_KEY = "_background_task_class";
@VisibleForTesting
static final String BACKGROUND_TASK_EXTRAS_KEY = "_background_task_extras";
static BackgroundTask getBackgroundTaskFromJobParameters(JobParameters jobParameters) {
String backgroundTaskClassName = getBackgroundTaskClassFromJobParameters(jobParameters);
return BackgroundTaskReflection.getBackgroundTaskFromClassName(backgroundTaskClassName);
}
private static String getBackgroundTaskClassFromJobParameters(JobParameters jobParameters) {
PersistableBundle extras = jobParameters.getExtras();
if (extras == null) return null;
return extras.getString(BACKGROUND_TASK_CLASS_KEY);
}
/**
* Retrieves the {@link TaskParameters} from the {@link JobParameters}, which are passed as
* one of the keys. Only values valid for {@link android.os.BaseBundle} are supported, and other
* values are stripped at the time when the task is scheduled.
*
* @param jobParameters the {@link JobParameters} to extract the {@link TaskParameters} from.
* @return the {@link TaskParameters} for the current job.
*/
static TaskParameters getTaskParametersFromJobParameters(JobParameters jobParameters) {
TaskParameters.Builder builder = TaskParameters.create(jobParameters.getJobId());
PersistableBundle jobExtras = jobParameters.getExtras();
PersistableBundle persistableTaskExtras =
jobExtras.getPersistableBundle(BACKGROUND_TASK_EXTRAS_KEY);
Bundle taskExtras = new Bundle();
taskExtras.putAll(persistableTaskExtras);
builder.addExtras(taskExtras);
return builder.build();
}
@VisibleForTesting
static JobInfo createJobInfoFromTaskInfo(Context context, TaskInfo taskInfo) {
PersistableBundle jobExtras = new PersistableBundle();
jobExtras.putString(BACKGROUND_TASK_CLASS_KEY, taskInfo.getBackgroundTaskClass().getName());
PersistableBundle persistableBundle = getTaskExtrasAsPersistableBundle(taskInfo);
jobExtras.putPersistableBundle(BACKGROUND_TASK_EXTRAS_KEY, persistableBundle);
JobInfo.Builder builder =
new JobInfo
.Builder(taskInfo.getTaskId(),
new ComponentName(context, BackgroundTaskJobService.class))
.setExtras(jobExtras)
.setPersisted(taskInfo.isPersisted())
.setRequiresCharging(taskInfo.requiresCharging())
.setRequiredNetworkType(getJobInfoNetworkTypeFromTaskNetworkType(
taskInfo.getRequiredNetworkType()));
if (taskInfo.isPeriodic()) {
builder = getPeriodicJobInfo(builder, taskInfo);
} else {
builder = getOneOffJobInfo(builder, taskInfo);
}
return builder.build();
}
private static JobInfo.Builder getOneOffJobInfo(JobInfo.Builder builder, TaskInfo taskInfo) {
TaskInfo.OneOffInfo oneOffInfo = taskInfo.getOneOffInfo();
if (oneOffInfo.hasWindowStartTimeConstraint()) {
builder = builder.setMinimumLatency(oneOffInfo.getWindowStartTimeMs());
}
return builder.setOverrideDeadline(oneOffInfo.getWindowEndTimeMs());
}
private static JobInfo.Builder getPeriodicJobInfo(JobInfo.Builder builder, TaskInfo taskInfo) {
TaskInfo.PeriodicInfo periodicInfo = taskInfo.getPeriodicInfo();
if (periodicInfo.hasFlex()) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
return builder.setPeriodic(periodicInfo.getIntervalMs(), periodicInfo.getFlexMs());
}
return builder.setPeriodic(periodicInfo.getIntervalMs());
}
return builder.setPeriodic(periodicInfo.getIntervalMs());
}
private static int getJobInfoNetworkTypeFromTaskNetworkType(
@TaskInfo.NetworkType int networkType) {
// The values are hard coded to represent the same as the network type from JobService.
return networkType;
}
private static PersistableBundle getTaskExtrasAsPersistableBundle(TaskInfo taskInfo) {
Bundle taskExtras = taskInfo.getExtras();
BundleToPersistableBundleConverter.Result convertedData =
BundleToPersistableBundleConverter.convert(taskExtras);
if (convertedData.hasErrors()) {
Log.w(TAG, "Failed converting extras to PersistableBundle: "
+ convertedData.getFailedKeysErrorString());
}
return convertedData.getPersistableBundle();
}
@Override
public boolean schedule(Context context, TaskInfo taskInfo) {
ThreadUtils.assertOnUiThread();
if (!BackgroundTaskReflection.hasParameterlessPublicConstructor(
taskInfo.getBackgroundTaskClass())) {
Log.e(TAG, "BackgroundTask " + taskInfo.getBackgroundTaskClass()
+ " has no parameterless public constructor.");
return false;
}
JobInfo jobInfo = createJobInfoFromTaskInfo(context, taskInfo);
JobScheduler jobScheduler =
(JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE);
if (!taskInfo.shouldUpdateCurrent() && hasPendingJob(jobScheduler, taskInfo.getTaskId())) {
return true;
}
// This can fail on heavily modified android builds. Catch so we don't crash.
try {
return jobScheduler.schedule(jobInfo) == JobScheduler.RESULT_SUCCESS;
} catch (Exception e) {
// Typically we don't catch RuntimeException, but this time we do want to catch it
// because we are worried about android as modified by device manufacturers.
Log.e(TAG, "Unable to schedule with Android.", e);
return false;
}
}
@Override
public void cancel(Context context, int taskId) {
ThreadUtils.assertOnUiThread();
JobScheduler jobScheduler =
(JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE);
try {
jobScheduler.cancel(taskId);
} catch (NullPointerException exception) {
Log.e(TAG, "Failed to cancel task: " + taskId);
}
}
private boolean hasPendingJob(JobScheduler jobScheduler, int jobId) {
List<JobInfo> pendingJobs = jobScheduler.getAllPendingJobs();
for (JobInfo pendingJob : pendingJobs) {
if (pendingJob.getId() == jobId) return true;
}
return false;
}
}