diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 04bf514c12..33cab06819 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,5 +1,9 @@ # Release notes # +### 2.10.4 ### + +* Offline: Add Scheduler implementation which uses WorkManager. + ### 2.10.3 ### * Display last frame when seeking to end of stream diff --git a/core_settings.gradle b/core_settings.gradle index 4d90fa962a..38889e1a21 100644 --- a/core_settings.gradle +++ b/core_settings.gradle @@ -38,6 +38,7 @@ include modulePrefix + 'extension-vp9' include modulePrefix + 'extension-rtmp' include modulePrefix + 'extension-leanback' include modulePrefix + 'extension-jobdispatcher' +include modulePrefix + 'extension-workmanager' project(modulePrefix + 'library').projectDir = new File(rootDir, 'library/all') project(modulePrefix + 'library-core').projectDir = new File(rootDir, 'library/core') @@ -60,3 +61,4 @@ project(modulePrefix + 'extension-vp9').projectDir = new File(rootDir, 'extensio project(modulePrefix + 'extension-rtmp').projectDir = new File(rootDir, 'extensions/rtmp') project(modulePrefix + 'extension-leanback').projectDir = new File(rootDir, 'extensions/leanback') project(modulePrefix + 'extension-jobdispatcher').projectDir = new File(rootDir, 'extensions/jobdispatcher') +project(modulePrefix + 'extension-workmanager').projectDir = new File(rootDir, 'extensions/workmanager') diff --git a/extensions/workmanager/README.md b/extensions/workmanager/README.md new file mode 100644 index 0000000000..bd2dbc71ad --- /dev/null +++ b/extensions/workmanager/README.md @@ -0,0 +1,22 @@ +# ExoPlayer WorkManager extension + +This extension provides a Scheduler implementation which uses [WorkManager][]. + +[WorkManager]: https://developer.android.com/topic/libraries/architecture/workmanager.html + +## Getting the extension + +The easiest way to use the extension is to add it as a gradle dependency: + +```gradle +implementation 'com.google.android.exoplayer:extension-workmanager:2.X.X' +``` + +where `2.X.X` is the version, which must match the version of the ExoPlayer +library being used. + +Alternatively, you can clone the ExoPlayer repository and depend on the module +locally. Instructions for doing this can be found in ExoPlayer's +[top level README][]. + +[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md diff --git a/extensions/workmanager/build.gradle b/extensions/workmanager/build.gradle new file mode 100644 index 0000000000..9065855a3f --- /dev/null +++ b/extensions/workmanager/build.gradle @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2019 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. + */ +apply from: '../../constants.gradle' +apply plugin: 'com.android.library' + +android { + compileSdkVersion project.ext.compileSdkVersion + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + defaultConfig { + minSdkVersion project.ext.minSdkVersion + targetSdkVersion project.ext.targetSdkVersion + } + + testOptions.unitTests.includeAndroidResources = true +} + +dependencies { + implementation project(modulePrefix + 'library-core') + implementation 'androidx.work:work-runtime:2.0.1' +} + +ext { + javadocTitle = 'WorkManager extension' +} +apply from: '../../javadoc_library.gradle' + +ext { + releaseArtifact = 'extension-workmanager' + releaseDescription = 'WorkManager extension for ExoPlayer.' +} +apply from: '../../publish.gradle' diff --git a/extensions/workmanager/src/main/AndroidManifest.xml b/extensions/workmanager/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..1daf50bd00 --- /dev/null +++ b/extensions/workmanager/src/main/AndroidManifest.xml @@ -0,0 +1,18 @@ + + + + diff --git a/extensions/workmanager/src/main/java/com/google/android/exoplayer2/ext/workmanager/WorkManagerScheduler.java b/extensions/workmanager/src/main/java/com/google/android/exoplayer2/ext/workmanager/WorkManagerScheduler.java new file mode 100644 index 0000000000..01801c9897 --- /dev/null +++ b/extensions/workmanager/src/main/java/com/google/android/exoplayer2/ext/workmanager/WorkManagerScheduler.java @@ -0,0 +1,161 @@ +/* + * Copyright (C) 2019 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 com.google.android.exoplayer2.ext.workmanager; + +import android.annotation.TargetApi; +import android.content.Context; +import android.content.Intent; +import androidx.work.Constraints; +import androidx.work.Data; +import androidx.work.ExistingWorkPolicy; +import androidx.work.NetworkType; +import androidx.work.OneTimeWorkRequest; +import androidx.work.WorkManager; +import androidx.work.Worker; +import androidx.work.WorkerParameters; +import com.google.android.exoplayer2.scheduler.Requirements; +import com.google.android.exoplayer2.scheduler.Scheduler; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.Log; +import com.google.android.exoplayer2.util.Util; + +/** A {@link Scheduler} that uses {@link WorkManager}. */ +public final class WorkManagerScheduler implements Scheduler { + + private static final boolean DEBUG = false; + private static final String TAG = "WorkManagerScheduler"; + 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 final String workName; + + /** + * @param workName A name for work scheduled by this instance. If the same name 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. + */ + public WorkManagerScheduler(String workName) { + this.workName = workName; + } + + @Override + public boolean schedule(Requirements requirements, String servicePackage, String serviceAction) { + Constraints constraints = buildConstraints(requirements); + Data inputData = buildInputData(requirements, servicePackage, serviceAction); + OneTimeWorkRequest workRequest = buildWorkRequest(constraints, inputData); + logd("Scheduling work: " + workName); + WorkManager.getInstance().enqueueUniqueWork(workName, ExistingWorkPolicy.REPLACE, workRequest); + return true; + } + + @Override + public boolean cancel() { + logd("Canceling work: " + workName); + WorkManager.getInstance().cancelUniqueWork(workName); + return true; + } + + private static Constraints buildConstraints(Requirements requirements) { + Constraints.Builder builder = new Constraints.Builder(); + + if (requirements.isUnmeteredNetworkRequired()) { + builder.setRequiredNetworkType(NetworkType.UNMETERED); + } else if (requirements.isNetworkRequired()) { + builder.setRequiredNetworkType(NetworkType.CONNECTED); + } else { + builder.setRequiredNetworkType(NetworkType.NOT_REQUIRED); + } + + if (requirements.isChargingRequired()) { + builder.setRequiresCharging(true); + } + + if (requirements.isIdleRequired() && Util.SDK_INT >= 23) { + setRequiresDeviceIdle(builder); + } + + return builder.build(); + } + + @TargetApi(23) + private static void setRequiresDeviceIdle(Constraints.Builder builder) { + builder.setRequiresDeviceIdle(true); + } + + private static Data buildInputData( + Requirements requirements, String servicePackage, String serviceAction) { + Data.Builder builder = new Data.Builder(); + + builder.putInt(KEY_REQUIREMENTS, requirements.getRequirements()); + builder.putString(KEY_SERVICE_PACKAGE, servicePackage); + builder.putString(KEY_SERVICE_ACTION, serviceAction); + + return builder.build(); + } + + private static OneTimeWorkRequest buildWorkRequest(Constraints constraints, Data inputData) { + OneTimeWorkRequest.Builder builder = new OneTimeWorkRequest.Builder(SchedulerWorker.class); + + builder.setConstraints(constraints); + builder.setInputData(inputData); + + return builder.build(); + } + + private static void logd(String message) { + if (DEBUG) { + Log.d(TAG, message); + } + } + + /** A {@link Worker} that starts the target service if the requirements are met. */ + // This class needs to be public so that WorkManager can instantiate it. + public static final class SchedulerWorker extends Worker { + + private final WorkerParameters workerParams; + private final Context context; + + public SchedulerWorker(Context context, WorkerParameters workerParams) { + super(context, workerParams); + this.workerParams = workerParams; + this.context = context; + } + + @Override + public Result doWork() { + logd("SchedulerWorker is started"); + Data inputData = workerParams.getInputData(); + Assertions.checkNotNull(inputData, "Work started without input data."); + Requirements requirements = new Requirements(inputData.getInt(KEY_REQUIREMENTS, 0)); + if (requirements.checkRequirements(context)) { + logd("Requirements are met"); + String serviceAction = inputData.getString(KEY_SERVICE_ACTION); + String servicePackage = inputData.getString(KEY_SERVICE_PACKAGE); + Assertions.checkNotNull(serviceAction, "Service action missing."); + Assertions.checkNotNull(servicePackage, "Service package missing."); + Intent intent = new Intent(serviceAction).setPackage(servicePackage); + logd("Starting service action: " + serviceAction + " package: " + servicePackage); + Util.startForegroundService(context, intent); + return Result.success(); + } else { + logd("Requirements are not met"); + return Result.retry(); + } + } + } +}