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