PlatformScheduler.java

/*
 * 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;
    }
  }
}