/*
* Copyright (C) 2017 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.media3.exoplayer.scheduler;
import static androidx.media3.common.util.Assertions.checkNotNull;
import android.app.job.JobInfo;
import android.app.job.JobParameters;
import android.app.job.JobScheduler;
import android.app.job.JobService;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.os.PersistableBundle;
import androidx.annotation.RequiresApi;
import androidx.annotation.RequiresPermission;
import androidx.media3.common.util.Log;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
/**
* A {@link Scheduler} that uses {@link JobScheduler}. To use this scheduler, you must add {@link
* PlatformSchedulerService} to your manifest:
*
* <pre>{@literal
* <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
* <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
*
* <service android:name="androidx.media3.exoplayer.scheduler.PlatformScheduler$PlatformSchedulerService"
* android:permission="android.permission.BIND_JOB_SERVICE"
* android:exported="true"/>
* }</pre>
*/
@RequiresApi(21)
@UnstableApi
public final class PlatformScheduler implements Scheduler {
private static final String TAG = "PlatformScheduler";
private static final String KEY_SERVICE_ACTION = "service_action";
private static final String KEY_SERVICE_PACKAGE = "service_package";
private static final String KEY_REQUIREMENTS = "requirements";
private static final int SUPPORTED_REQUIREMENTS =
Requirements.NETWORK
| Requirements.NETWORK_UNMETERED
| Requirements.DEVICE_IDLE
| Requirements.DEVICE_CHARGING
| (Util.SDK_INT >= 26 ? Requirements.DEVICE_STORAGE_NOT_LOW : 0);
private final int jobId;
private final ComponentName jobServiceComponentName;
private final JobScheduler jobScheduler;
/**
* @param context Any context.
* @param jobId An identifier for the jobs scheduled by this instance. If the same identifier was
* used by a previous instance, anything scheduled by the previous instance will be canceled
* by this instance if {@link #schedule(Requirements, String, String)} or {@link #cancel()}
* are called.
*/
@RequiresPermission(android.Manifest.permission.RECEIVE_BOOT_COMPLETED)
public PlatformScheduler(Context context, int jobId) {
context = context.getApplicationContext();
this.jobId = jobId;
jobServiceComponentName = new ComponentName(context, PlatformSchedulerService.class);
jobScheduler =
checkNotNull((JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE));
}
@Override
public boolean schedule(Requirements requirements, String servicePackage, String serviceAction) {
JobInfo jobInfo =
buildJobInfo(jobId, jobServiceComponentName, requirements, serviceAction, servicePackage);
int result = jobScheduler.schedule(jobInfo);
return result == JobScheduler.RESULT_SUCCESS;
}
@Override
public boolean cancel() {
jobScheduler.cancel(jobId);
return true;
}
@Override
public Requirements getSupportedRequirements(Requirements requirements) {
return requirements.filterRequirements(SUPPORTED_REQUIREMENTS);
}
// @RequiresPermission constructor annotation should ensure the permission is present.
@SuppressWarnings("MissingPermission")
private static JobInfo buildJobInfo(
int jobId,
ComponentName jobServiceComponentName,
Requirements requirements,
String serviceAction,
String servicePackage) {
Requirements filteredRequirements = requirements.filterRequirements(SUPPORTED_REQUIREMENTS);
if (!filteredRequirements.equals(requirements)) {
Log.w(
TAG,
"Ignoring unsupported requirements: "
+ (filteredRequirements.getRequirements() ^ requirements.getRequirements()));
}
JobInfo.Builder builder = new JobInfo.Builder(jobId, jobServiceComponentName);
if (requirements.isUnmeteredNetworkRequired()) {
builder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED);
} else if (requirements.isNetworkRequired()) {
builder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY);
}
builder.setRequiresDeviceIdle(requirements.isIdleRequired());
builder.setRequiresCharging(requirements.isChargingRequired());
if (Util.SDK_INT >= 26 && requirements.isStorageNotLowRequired()) {
builder.setRequiresStorageNotLow(true);
}
builder.setPersisted(true);
PersistableBundle extras = new PersistableBundle();
extras.putString(KEY_SERVICE_ACTION, serviceAction);
extras.putString(KEY_SERVICE_PACKAGE, servicePackage);
extras.putInt(KEY_REQUIREMENTS, requirements.getRequirements());
builder.setExtras(extras);
return builder.build();
}
/** A {@link JobService} that starts the target service if the requirements are met. */
public static final class PlatformSchedulerService extends JobService {
@Override
public boolean onStartJob(JobParameters params) {
PersistableBundle extras = params.getExtras();
Requirements requirements = new Requirements(extras.getInt(KEY_REQUIREMENTS));
int notMetRequirements = requirements.getNotMetRequirements(this);
if (notMetRequirements == 0) {
String serviceAction = checkNotNull(extras.getString(KEY_SERVICE_ACTION));
String servicePackage = checkNotNull(extras.getString(KEY_SERVICE_PACKAGE));
Intent intent = new Intent(serviceAction).setPackage(servicePackage);
Util.startForegroundService(this, intent);
} else {
Log.w(TAG, "Requirements not met: " + notMetRequirements);
jobFinished(params, /* wantsReschedule= */ true);
}
return false;
}
@Override
public boolean onStopJob(JobParameters params) {
return false;
}
}
}