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); }