diff --git a/RELEASENOTES.md b/RELEASENOTES.md
index c1b45b4b19..8b412ad6cb 100644
--- a/RELEASENOTES.md
+++ b/RELEASENOTES.md
@@ -142,6 +142,8 @@
directly instead.
* Update `CachedContentIndex` to use `SecureRandom` for generating the
initialization vector used to encrypt the cache contents.
+ * Add `Requirements.DEVICE_STORAGE_NOT_LOW`, which can be specified as a
+ requirement to a `DownloadManager` for it to proceed with downloading.
* Audio:
* Add a sample count parameter to `MediaCodecRenderer.processOutputBuffer`
and `AudioSink.handleBuffer` to allow batching multiple encoded frames
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 8841f8355f..f301f3e39d 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
@@ -65,6 +65,11 @@ public final class JobDispatcherScheduler implements Scheduler {
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;
private final String jobTag;
private final FirebaseJobDispatcher jobDispatcher;
@@ -96,24 +101,35 @@ public final class JobDispatcherScheduler implements Scheduler {
return result == FirebaseJobDispatcher.CANCEL_RESULT_SUCCESS;
}
+ @Override
+ public Requirements getSupportedRequirements(Requirements requirements) {
+ return requirements.filterRequirements(SUPPORTED_REQUIREMENTS);
+ }
+
private static Job buildJob(
FirebaseJobDispatcher dispatcher,
Requirements requirements,
String tag,
String servicePackage,
String serviceAction) {
+ Requirements filteredRequirements = requirements.filterRequirements(SUPPORTED_REQUIREMENTS);
+ if (!filteredRequirements.equals(requirements)) {
+ Log.w(
+ TAG,
+ "Ignoring unsupported requirements: "
+ + (filteredRequirements.getRequirements() ^ requirements.getRequirements()));
+ }
+
Job.Builder builder =
dispatcher
.newJobBuilder()
.setService(JobDispatcherSchedulerService.class) // the JobService that will be called
.setTag(tag);
-
if (requirements.isUnmeteredNetworkRequired()) {
builder.addConstraint(Constraint.ON_UNMETERED_NETWORK);
} else if (requirements.isNetworkRequired()) {
builder.addConstraint(Constraint.ON_ANY_NETWORK);
}
-
if (requirements.isIdleRequired()) {
builder.addConstraint(Constraint.DEVICE_IDLE);
}
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 97b132980d..e88b47c575 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
@@ -40,6 +40,12 @@ public final class WorkManagerScheduler implements Scheduler {
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
+ | (Util.SDK_INT >= 23 ? Requirements.DEVICE_IDLE : 0)
+ | Requirements.DEVICE_CHARGING
+ | Requirements.DEVICE_STORAGE_NOT_LOW;
private final String workName;
@@ -70,9 +76,21 @@ public final class WorkManagerScheduler implements Scheduler {
return true;
}
- private static Constraints buildConstraints(Requirements requirements) {
- Constraints.Builder builder = new Constraints.Builder();
+ @Override
+ public Requirements getSupportedRequirements(Requirements requirements) {
+ return requirements.filterRequirements(SUPPORTED_REQUIREMENTS);
+ }
+ private static Constraints buildConstraints(Requirements requirements) {
+ Requirements filteredRequirements = requirements.filterRequirements(SUPPORTED_REQUIREMENTS);
+ if (!filteredRequirements.equals(requirements)) {
+ Log.w(
+ TAG,
+ "Ignoring unsupported requirements: "
+ + (filteredRequirements.getRequirements() ^ requirements.getRequirements()));
+ }
+
+ Constraints.Builder builder = new Constraints.Builder();
if (requirements.isUnmeteredNetworkRequired()) {
builder.setRequiredNetworkType(NetworkType.UNMETERED);
} else if (requirements.isNetworkRequired()) {
@@ -80,13 +98,14 @@ public final class WorkManagerScheduler implements Scheduler {
} else {
builder.setRequiredNetworkType(NetworkType.NOT_REQUIRED);
}
-
+ if (Util.SDK_INT >= 23 && requirements.isIdleRequired()) {
+ setRequiresDeviceIdle(builder);
+ }
if (requirements.isChargingRequired()) {
builder.setRequiresCharging(true);
}
-
- if (requirements.isIdleRequired() && Util.SDK_INT >= 23) {
- setRequiresDeviceIdle(builder);
+ if (requirements.isStorageNotLowRequired()) {
+ builder.setRequiresStorageNotLow(true);
}
return builder.build();
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 0ee9a83260..1c980ca2ef 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
@@ -658,6 +658,22 @@ public abstract class DownloadService extends Service {
if (requirements == null) {
Log.e(TAG, "Ignored SET_REQUIREMENTS: Missing " + KEY_REQUIREMENTS + " extra");
} else {
+ @Nullable Scheduler scheduler = getScheduler();
+ if (scheduler != null) {
+ Requirements supportedRequirements = scheduler.getSupportedRequirements(requirements);
+ if (!supportedRequirements.equals(requirements)) {
+ Log.w(
+ TAG,
+ "Ignoring requirements not supported by the Scheduler: "
+ + (requirements.getRequirements() ^ supportedRequirements.getRequirements()));
+ // We need to make sure DownloadManager only uses requirements supported by the
+ // Scheduler. If we don't do this, DownloadManager can report itself as idle due to an
+ // unmet requirement that the Scheduler doesn't support. This can then lead to the
+ // service being destroyed, even though the Scheduler won't be able to restart it when
+ // the requirement is subsequently met.
+ requirements = supportedRequirements;
+ }
+ }
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 c4861abdf3..c8b6438fd8 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
@@ -50,6 +50,12 @@ public final class PlatformScheduler implements Scheduler {
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;
@@ -86,6 +92,11 @@ public final class PlatformScheduler implements Scheduler {
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(
@@ -94,8 +105,15 @@ public final class PlatformScheduler implements Scheduler {
Requirements requirements,
String serviceAction,
String servicePackage) {
- JobInfo.Builder builder = new JobInfo.Builder(jobId, jobServiceComponentName);
+ 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()) {
@@ -103,6 +121,9 @@ public final class PlatformScheduler implements Scheduler {
}
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();
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 8919a26720..334c1684bd 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
@@ -39,13 +39,13 @@ public final class Requirements implements Parcelable {
/**
* Requirement flags. Possible flag values are {@link #NETWORK}, {@link #NETWORK_UNMETERED},
- * {@link #DEVICE_IDLE} and {@link #DEVICE_CHARGING}.
+ * {@link #DEVICE_IDLE}, {@link #DEVICE_CHARGING} and {@link #DEVICE_STORAGE_NOT_LOW}.
*/
@Documented
@Retention(RetentionPolicy.SOURCE)
@IntDef(
flag = true,
- value = {NETWORK, NETWORK_UNMETERED, DEVICE_IDLE, DEVICE_CHARGING})
+ value = {NETWORK, NETWORK_UNMETERED, DEVICE_IDLE, DEVICE_CHARGING, DEVICE_STORAGE_NOT_LOW})
public @interface RequirementFlags {}
/** Requirement that the device has network connectivity. */
@@ -56,6 +56,11 @@ 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 device's internal storage is not low. Note that this requirement
+ * is not affected by the status of external storage.
+ */
+ public static final int DEVICE_STORAGE_NOT_LOW = 1 << 4;
@RequirementFlags private final int requirements;
@@ -74,6 +79,18 @@ public final class Requirements implements Parcelable {
return requirements;
}
+ /**
+ * Filters the requirements, returning the subset that are enabled by the provided filter.
+ *
+ * @param requirementsFilter The enabled {@link RequirementFlags}.
+ * @return The filtered requirements. If the filter does not cause a change in the requirements
+ * then this instance will be returned.
+ */
+ public Requirements filterRequirements(int requirementsFilter) {
+ int filteredRequirements = requirements & requirementsFilter;
+ return filteredRequirements == requirements ? this : new Requirements(filteredRequirements);
+ }
+
/** Returns whether network connectivity is required. */
public boolean isNetworkRequired() {
return (requirements & NETWORK) != 0;
@@ -94,6 +111,11 @@ public final class Requirements implements Parcelable {
return (requirements & DEVICE_IDLE) != 0;
}
+ /** Returns whether the device is required to not be low on internal storage. */
+ public boolean isStorageNotLowRequired() {
+ return (requirements & DEVICE_STORAGE_NOT_LOW) != 0;
+ }
+
/**
* Returns whether the requirements are met.
*
@@ -119,6 +141,9 @@ public final class Requirements implements Parcelable {
if (isIdleRequired() && !isDeviceIdle(context)) {
notMetRequirements |= DEVICE_IDLE;
}
+ if (isStorageNotLowRequired() && !isStorageNotLow(context)) {
+ notMetRequirements |= DEVICE_STORAGE_NOT_LOW;
+ }
return notMetRequirements;
}
@@ -145,8 +170,10 @@ public final class Requirements implements Parcelable {
}
private boolean isDeviceCharging(Context context) {
+ @Nullable
Intent batteryStatus =
- context.registerReceiver(null, new IntentFilter(Intent.ACTION_BATTERY_CHANGED));
+ context.registerReceiver(
+ /* receiver= */ null, new IntentFilter(Intent.ACTION_BATTERY_CHANGED));
if (batteryStatus == null) {
return false;
}
@@ -162,6 +189,12 @@ public final class Requirements implements Parcelable {
: Util.SDK_INT >= 20 ? !powerManager.isInteractive() : !powerManager.isScreenOn();
}
+ private boolean isStorageNotLow(Context context) {
+ return context.registerReceiver(
+ /* receiver= */ null, new IntentFilter(Intent.ACTION_DEVICE_STORAGE_LOW))
+ == null;
+ }
+
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 797b7f7170..9109242db1 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,10 @@ public final class RequirementsWatcher {
filter.addAction(Intent.ACTION_SCREEN_OFF);
}
}
+ if (requirements.isStorageNotLowRequired()) {
+ filter.addAction(Intent.ACTION_DEVICE_STORAGE_LOW);
+ filter.addAction(Intent.ACTION_DEVICE_STORAGE_OK);
+ }
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 b5a6f40424..c34c77b2cf 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,14 @@ public interface Scheduler {
* @return Whether cancellation was successful.
*/
boolean cancel();
+
+ /**
+ * Checks whether this {@link Scheduler} supports the provided {@link Requirements}. If all of the
+ * requirements are supported then the same {@link Requirements} instance is returned. If not then
+ * a new instance is returned containing the subset of the requirements that are supported.
+ *
+ * @param requirements The requirements to check.
+ * @return The supported requirements.
+ */
+ Requirements getSupportedRequirements(Requirements requirements);
}