diff --git a/demos/main/build.gradle b/demos/main/build.gradle
index b7a8666fe37..dd4fec385c6 100644
--- a/demos/main/build.gradle
+++ b/demos/main/build.gradle
@@ -70,6 +70,7 @@ dependencies {
implementation project(modulePrefix + 'library-hls')
implementation project(modulePrefix + 'library-smoothstreaming')
implementation project(modulePrefix + 'library-ui')
+ withExtensionsImplementation project(path: modulePrefix + 'extension-workmanager')
withExtensionsImplementation project(path: modulePrefix + 'extension-av1')
withExtensionsImplementation project(path: modulePrefix + 'extension-ffmpeg')
withExtensionsImplementation project(path: modulePrefix + 'extension-flac')
diff --git a/extensions/jobdispatcher/src/main/java/com/google/android/exoplayer2/ext/jobdispatcher/JobDispatcherScheduler.java b/extensions/jobdispatcher/src/main/java/com/google/android/exoplayer2/ext/jobdispatcher/JobDispatcherScheduler.java
index 8841f8355fc..a10f03f5e21 100644
--- a/extensions/jobdispatcher/src/main/java/com/google/android/exoplayer2/ext/jobdispatcher/JobDispatcherScheduler.java
+++ b/extensions/jobdispatcher/src/main/java/com/google/android/exoplayer2/ext/jobdispatcher/JobDispatcherScheduler.java
@@ -96,6 +96,27 @@ public boolean cancel() {
return result == FirebaseJobDispatcher.CANCEL_RESULT_SUCCESS;
}
+ @Override
+ public Requirements getSupportedRequirements(Requirements requirements) {
+ Requirements supportedRequirements = requirements;
+ if (requirements.isBatteryNotLowRequired()) {
+ Log.w(TAG, "Battery not low requirement not supported on the JobDispatcherScheduler "
+ + "Requirement removed.");
+ int newRequirements =
+ supportedRequirements.getRequirements() ^ Requirements.DEVICE_BATTERY_NOT_LOW;
+ supportedRequirements = new Requirements(newRequirements);
+ }
+
+ if (requirements.isStorageNotLowRequired()) {
+ Log.w(TAG, "Storage not low requirement not supported on the JobDispatcherScheduler "
+ + "Requirement removed.");
+ int newRequirements =
+ supportedRequirements.getRequirements() ^ Requirements.DEVICE_STORAGE_NOT_LOW;
+ supportedRequirements = new Requirements(newRequirements);
+ }
+ return supportedRequirements;
+ }
+
private static Job buildJob(
FirebaseJobDispatcher dispatcher,
Requirements requirements,
@@ -120,6 +141,14 @@ private static Job buildJob(
if (requirements.isChargingRequired()) {
builder.addConstraint(Constraint.DEVICE_CHARGING);
}
+
+ if (requirements.isBatteryNotLowRequired()) {
+ Log.w(TAG, "Battery not low requirement is not supported on the JobDispatcherScheduler");
+ }
+ if (requirements.isStorageNotLowRequired()) {
+ Log.w(TAG, "Storage not low requirement is not supported on the JobDispatcherScheduler");
+ }
+
builder.setLifetime(Lifetime.FOREVER).setReplaceCurrent(true);
Bundle extras = new Bundle();
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
index 97b132980d7..5e03534d3ca 100644
--- 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
@@ -87,6 +87,16 @@ private static Constraints buildConstraints(Requirements requirements) {
if (requirements.isIdleRequired() && Util.SDK_INT >= 23) {
setRequiresDeviceIdle(builder);
+ } else if (requirements.isIdleRequired()) {
+ Log.w(TAG, "Is idle requirements is only available on API 23 and up.");
+ }
+
+ if (requirements.isBatteryNotLowRequired()) {
+ builder.setRequiresBatteryNotLow(true);
+ }
+
+ if (requirements.isStorageNotLowRequired()) {
+ builder.setRequiresStorageNotLow(true);
}
return builder.build();
@@ -108,6 +118,19 @@ private static Data buildInputData(
return builder.build();
}
+ @Override
+ public Requirements getSupportedRequirements(Requirements requirements) {
+ Requirements supportedRequirements = requirements;
+ if (requirements.isIdleRequired() && Util.SDK_INT < 23) {
+ Log.w(TAG, "Is idle requirement not supported on the WorkManagerScheduler on API below 23. "
+ + "Requirement removed.");
+ int newRequirements =
+ supportedRequirements.getRequirements() ^ Requirements.DEVICE_IDLE;
+ supportedRequirements = new Requirements(newRequirements);
+ }
+ return supportedRequirements;
+ }
+
private static OneTimeWorkRequest buildWorkRequest(Constraints constraints, Data inputData) {
OneTimeWorkRequest.Builder builder = new OneTimeWorkRequest.Builder(SchedulerWorker.class);
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadService.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadService.java
index 51f7fa3765d..5d8983a1d8f 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadService.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadService.java
@@ -26,6 +26,7 @@
import android.os.Looper;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
+import com.google.android.exoplayer2.scheduler.PlatformScheduler;
import com.google.android.exoplayer2.scheduler.Requirements;
import com.google.android.exoplayer2.scheduler.Scheduler;
import com.google.android.exoplayer2.util.Assertions;
@@ -658,6 +659,10 @@ public int onStartCommand(@Nullable Intent intent, int flags, int startId) {
if (requirements == null) {
Log.e(TAG, "Ignored SET_REQUIREMENTS: Missing " + KEY_REQUIREMENTS + " extra");
} else {
+ @Nullable Scheduler scheduler = getScheduler();
+ if (scheduler != null) {
+ requirements = scheduler.getSupportedRequirements(requirements);
+ }
downloadManager.setRequirements(requirements);
}
break;
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/scheduler/PlatformScheduler.java b/library/core/src/main/java/com/google/android/exoplayer2/scheduler/PlatformScheduler.java
index c4861abdf34..7415021802c 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/scheduler/PlatformScheduler.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/scheduler/PlatformScheduler.java
@@ -86,6 +86,29 @@ public boolean cancel() {
return true;
}
+ @Override
+ public Requirements getSupportedRequirements(Requirements requirements) {
+ Requirements supportedRequirements = requirements;
+ if (Util.SDK_INT < 26) {
+ if (requirements.isBatteryNotLowRequired()) {
+ Log.w(TAG, "Battery not low requirement not supported on the PlatformScheduler"
+ + "on API below 26. Requirement removed.");
+ int newRequirements =
+ supportedRequirements.getRequirements() ^ Requirements.DEVICE_BATTERY_NOT_LOW;
+ supportedRequirements = new Requirements(newRequirements);
+ }
+
+ if (requirements.isStorageNotLowRequired()) {
+ Log.w(TAG, "Storage not low requirement not supported on the PlatformScheduler"
+ + "on API below 26. Requirement removed.");
+ int newRequirements =
+ supportedRequirements.getRequirements() ^ Requirements.DEVICE_STORAGE_NOT_LOW;
+ supportedRequirements = new Requirements(newRequirements);
+ }
+ }
+ return supportedRequirements;
+ }
+
// @RequiresPermission constructor annotation should ensure the permission is present.
@SuppressWarnings("MissingPermission")
private static JobInfo buildJobInfo(
@@ -103,6 +126,13 @@ private static JobInfo buildJobInfo(
}
builder.setRequiresDeviceIdle(requirements.isIdleRequired());
builder.setRequiresCharging(requirements.isChargingRequired());
+ if (Util.SDK_INT >= 26) {
+ builder.setRequiresStorageNotLow(requirements.isStorageNotLowRequired());
+ builder.setRequiresBatteryNotLow(requirements.isBatteryNotLowRequired());
+ } else if (requirements.isStorageNotLowRequired() || requirements.isBatteryNotLowRequired()) {
+ Log.w(TAG, "Storage not low and battery not low requirements are only available on API 26"
+ + " and up.");
+ }
builder.setPersisted(true);
PersistableBundle extras = new PersistableBundle();
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/scheduler/Requirements.java b/library/core/src/main/java/com/google/android/exoplayer2/scheduler/Requirements.java
index 8919a26720c..7b9e3f38e93 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/scheduler/Requirements.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/scheduler/Requirements.java
@@ -45,7 +45,7 @@ public final class Requirements implements Parcelable {
@Retention(RetentionPolicy.SOURCE)
@IntDef(
flag = true,
- value = {NETWORK, NETWORK_UNMETERED, DEVICE_IDLE, DEVICE_CHARGING})
+ value = {NETWORK, NETWORK_UNMETERED, DEVICE_IDLE, DEVICE_CHARGING, DEVICE_BATTERY_NOT_LOW, DEVICE_STORAGE_NOT_LOW})
public @interface RequirementFlags {}
/** Requirement that the device has network connectivity. */
@@ -56,6 +56,16 @@ public final class Requirements implements Parcelable {
public static final int DEVICE_IDLE = 1 << 2;
/** Requirement that the device is charging. */
public static final int DEVICE_CHARGING = 1 << 3;
+ /** Requirement that the storage is not low. */
+ public static final int DEVICE_STORAGE_NOT_LOW = 1 << 4;
+ /** Requirement that the battery is not low. */
+ public static final int DEVICE_BATTERY_NOT_LOW = 1 << 5;
+
+ /** Constant indicating the battery is not plugged in a power source */
+ private static final int BATTERY_PLUGGED_NONE = 0;
+ /** Constant when the battery is considered low (in percentage) */
+ private static final float BATTERY_LOW_PERCENTAGE = 0.15f;
+
@RequirementFlags private final int requirements;
@@ -94,6 +104,14 @@ public boolean isIdleRequired() {
return (requirements & DEVICE_IDLE) != 0;
}
+ public boolean isStorageNotLowRequired() {
+ return (requirements & DEVICE_STORAGE_NOT_LOW) != 0;
+ }
+
+ public boolean isBatteryNotLowRequired() {
+ return (requirements & DEVICE_BATTERY_NOT_LOW) != 0;
+ }
+
/**
* Returns whether the requirements are met.
*
@@ -119,6 +137,12 @@ public int getNotMetRequirements(Context context) {
if (isIdleRequired() && !isDeviceIdle(context)) {
notMetRequirements |= DEVICE_IDLE;
}
+ if (isBatteryNotLowRequired() && !isBatteryNotLow(context)) {
+ notMetRequirements |= DEVICE_BATTERY_NOT_LOW;
+ }
+ if (isStorageNotLowRequired() && !isStorageNotLow(context)) {
+ notMetRequirements |= DEVICE_STORAGE_NOT_LOW;
+ }
return notMetRequirements;
}
@@ -162,6 +186,54 @@ private boolean isDeviceIdle(Context context) {
: Util.SDK_INT >= 20 ? !powerManager.isInteractive() : !powerManager.isScreenOn();
}
+ /**
+ * Implementation taken from the the WorkManager source.
+ * @see BatteryNotLowTracker
+ */
+ private boolean isBatteryNotLow(Context context) {
+ IntentFilter intentFilter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED);
+ Intent intent = context.registerReceiver(null, intentFilter);
+ if (intent == null) {
+ return true;
+ }
+ int plugged = intent.getIntExtra(BatteryManager.EXTRA_PLUGGED, BATTERY_PLUGGED_NONE);
+ int status = intent.getIntExtra(BatteryManager.EXTRA_STATUS, -1);
+ int level = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1);
+ int scale = intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1);
+ float batteryPercentage = level / (float) scale;
+ return (plugged != BATTERY_PLUGGED_NONE
+ || status == BatteryManager.BATTERY_STATUS_UNKNOWN
+ || batteryPercentage > BATTERY_LOW_PERCENTAGE);
+ }
+
+ /**
+ * Implementation taken from the the WorkManager source.
+ * @see StorageNotLowTracker
+ */
+ private boolean isStorageNotLow(Context context) {
+ IntentFilter intentFilter = new IntentFilter();
+ intentFilter.addAction(Intent.ACTION_DEVICE_STORAGE_OK);
+ intentFilter.addAction(Intent.ACTION_DEVICE_STORAGE_LOW);
+ Intent intent = context.registerReceiver(null, intentFilter);
+ if (intent == null || intent.getAction() == null) {
+ // ACTION_DEVICE_STORAGE_LOW is a sticky broadcast that is removed when sufficient
+ // storage is available again. ACTION_DEVICE_STORAGE_OK is not sticky. So if we
+ // don't receive anything here, we can assume that the storage state is okay.
+ return true;
+ } else {
+ switch (intent.getAction()) {
+ case Intent.ACTION_DEVICE_STORAGE_OK:
+ return true;
+ case Intent.ACTION_DEVICE_STORAGE_LOW:
+ return false;
+ default:
+ // This should never happen because the intent filter is configured
+ // correctly.
+ return true;
+ }
+ }
+ }
+
private static boolean isInternetConnectivityValidated(ConnectivityManager connectivityManager) {
// It's possible to query NetworkCapabilities from API level 23, but RequirementsWatcher only
// fires an event to update its Requirements when NetworkCapabilities change from API level 24.
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/scheduler/RequirementsWatcher.java b/library/core/src/main/java/com/google/android/exoplayer2/scheduler/RequirementsWatcher.java
index 797b7f71709..07401e7b9a9 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/scheduler/RequirementsWatcher.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/scheduler/RequirementsWatcher.java
@@ -104,6 +104,13 @@ public int start() {
filter.addAction(Intent.ACTION_SCREEN_OFF);
}
}
+ if (requirements.isStorageNotLowRequired()) {
+ filter.addAction(Intent.ACTION_DEVICE_STORAGE_LOW);
+ filter.addAction(Intent.ACTION_DEVICE_STORAGE_OK);
+ }
+ if (requirements.isBatteryNotLowRequired()) {
+ filter.addAction(Intent.ACTION_BATTERY_CHANGED);
+ }
receiver = new DeviceStatusChangeReceiver();
context.registerReceiver(receiver, filter, null, handler);
return notMetRequirements;
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/scheduler/Scheduler.java b/library/core/src/main/java/com/google/android/exoplayer2/scheduler/Scheduler.java
index b5a6f404247..bcbd7420b0e 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/scheduler/Scheduler.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/scheduler/Scheduler.java
@@ -45,4 +45,13 @@ public interface Scheduler {
* @return Whether cancellation was successful.
*/
boolean cancel();
+
+ /**
+ * Checks if this {@link Scheduler} supports the provided {@link Requirements}. If all
+ * requirements are supported the same object is returned. If not all requirements are
+ * supported a new {@code Requirements} object is returned containing the supported requirements.
+ * @param requirements The requirements to check.
+ * @return The requirements supported by this scheduler.
+ */
+ Requirements getSupportedRequirements(Requirements requirements);
}