diff --git a/RELEASENOTES.md b/RELEASENOTES.md index eb04bdb5e4..40a14057db 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -14,6 +14,10 @@ * Move `Player.addListener(EventListener)` and `Player.removeListener(EventListener)` out of `Player` into subclasses. * Android 12 compatibility: + * Keep `DownloadService` started and in the foreground whilst waiting for + requirements to be met on Android 12. This is necessary due to new + [foreground service launch restrictions](https://developer.android.com/about/versions/12/foreground-services). + `DownloadService.getScheduler` will not be called on Android 12 devices. * Disable platform transcoding when playing content URIs on Android 12. * Add `ExoPlayer.setVideoChangeFrameRateStrategy` to allow disabling of calls from the player to `Surface.setFrameRate`. This is useful for @@ -23,6 +27,14 @@ * `SubtitleView` no longer implements `TextOutput`. `SubtitleView` implements `Player.Listener`, so can be registered to a player with `Player.addListener`. +* Downloads and caching: + * Modify `DownloadService` behavior when `DownloadService.getScheduler` + returns `null`, or returns a `Scheduler` that does not support the + requirements for downloads to continue. In both cases, `DownloadService` + will now remain started and in the foreground whilst waiting for + requirements to be met. + * Modify `DownlaodService` behavior when running on Android 12 and above. + See the "Android 12 compatibility" section above. * RTSP: * Support RFC4566 SDP attribute field grammar ([#9430](https://github.com/google/ExoPlayer/issues/9430)). 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 f0f22af1ac..ea9e6f8569 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 @@ -28,6 +28,7 @@ import android.os.Looper; import androidx.annotation.Nullable; import androidx.annotation.StringRes; import com.google.android.exoplayer2.scheduler.Requirements; +import com.google.android.exoplayer2.scheduler.Requirements.RequirementFlags; import com.google.android.exoplayer2.scheduler.Scheduler; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Log; @@ -36,6 +37,7 @@ import com.google.android.exoplayer2.util.Util; import java.util.HashMap; import java.util.List; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.checkerframework.checker.nullness.qual.RequiresNonNull; /** A {@link Service} for downloading media. */ public abstract class DownloadService extends Service { @@ -179,8 +181,7 @@ public abstract class DownloadService extends Service { @StringRes private final int channelNameResourceId; @StringRes private final int channelDescriptionResourceId; - private @MonotonicNonNull Scheduler scheduler; - private @MonotonicNonNull DownloadManager downloadManager; + private @MonotonicNonNull DownloadManagerHelper downloadManagerHelper; private int lastStartId; private boolean startedInForeground; private boolean taskRemoved; @@ -191,8 +192,7 @@ public abstract class DownloadService extends Service { * Creates a DownloadService. * *

If {@code foregroundNotificationId} is {@link #FOREGROUND_NOTIFICATION_ID_NONE} then the - * service will only ever run in the background. No foreground notification will be displayed and - * {@link #getScheduler()} will not be called. + * service will only ever run in the background, and no foreground notification will be displayed. * *

If {@code foregroundNotificationId} is not {@link #FOREGROUND_NOTIFICATION_ID_NONE} then the * service will run in the foreground. The foreground notification will be updated at least as @@ -583,22 +583,19 @@ public abstract class DownloadService extends Service { @Nullable DownloadManagerHelper downloadManagerHelper = downloadManagerHelpers.get(clazz); if (downloadManagerHelper == null) { boolean foregroundAllowed = foregroundNotificationUpdater != null; - @Nullable Scheduler scheduler = foregroundAllowed ? getScheduler() : null; - if (scheduler != null) { - this.scheduler = scheduler; - } - downloadManager = getDownloadManager(); + // See https://developer.android.com/about/versions/12/foreground-services. + boolean canStartForegroundServiceFromBackground = Util.SDK_INT < 31; + @Nullable + Scheduler scheduler = + foregroundAllowed && canStartForegroundServiceFromBackground ? getScheduler() : null; + DownloadManager downloadManager = getDownloadManager(); downloadManager.resumeDownloads(); downloadManagerHelper = new DownloadManagerHelper( getApplicationContext(), downloadManager, foregroundAllowed, scheduler, clazz); downloadManagerHelpers.put(clazz, downloadManagerHelper); - } else { - if (downloadManagerHelper.scheduler != null) { - scheduler = downloadManagerHelper.scheduler; - } - downloadManager = downloadManagerHelper.downloadManager; } + this.downloadManagerHelper = downloadManagerHelper; downloadManagerHelper.attachService(this); } @@ -618,7 +615,8 @@ public abstract class DownloadService extends Service { if (intentAction == null) { intentAction = ACTION_INIT; } - DownloadManager downloadManager = Assertions.checkNotNull(this.downloadManager); + DownloadManager downloadManager = + Assertions.checkNotNull(downloadManagerHelper).downloadManager; switch (intentAction) { case ACTION_INIT: case ACTION_RESTART: @@ -666,21 +664,6 @@ public abstract class DownloadService extends Service { if (requirements == null) { Log.e(TAG, "Ignored SET_REQUIREMENTS: Missing " + KEY_REQUIREMENTS + " extra"); } else { - 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; @@ -696,7 +679,7 @@ public abstract class DownloadService extends Service { isStopped = false; if (downloadManager.isIdle()) { - stop(); + onIdle(); } return START_STICKY; } @@ -709,9 +692,7 @@ public abstract class DownloadService extends Service { @Override public void onDestroy() { isDestroyed = true; - DownloadManagerHelper downloadManagerHelper = - Assertions.checkNotNull(downloadManagerHelpers.get(getClass())); - downloadManagerHelper.detachService(this); + Assertions.checkNotNull(downloadManagerHelper).detachService(this); if (foregroundNotificationUpdater != null) { foregroundNotificationUpdater.stopPeriodicUpdates(); } @@ -733,14 +714,35 @@ public abstract class DownloadService extends Service { protected abstract DownloadManager getDownloadManager(); /** - * Returns a {@link Scheduler} to restart the service when requirements allowing downloads to take - * place are met. If {@code null}, the service will only be restarted if the process is still in - * memory when the requirements are met. + * Returns a {@link Scheduler} to restart the service when requirements for downloads to continue + * are met. * - *

This method is not called for services whose {@code foregroundNotificationId} is set to - * {@link #FOREGROUND_NOTIFICATION_ID_NONE}. Such services will only be restarted if the process - * is still in memory and considered non-idle, meaning that it's either in the foreground or was - * backgrounded within the last few minutes. + *

This method is not called on all devices or for all service configurations. When it is + * called, it's called only once in the life cycle of the process. If a service has unfinished + * downloads that cannot make progress due to unmet requirements, it will behave according to the + * first matching case below: + * + *

*/ @Nullable protected abstract Scheduler getScheduler(); @@ -758,7 +760,7 @@ public abstract class DownloadService extends Service { * @return The foreground notification to display. */ protected abstract Notification getForegroundNotification( - List downloads, @Requirements.RequirementFlags int notMetRequirements); + List downloads, @RequirementFlags int notMetRequirements); /** * Invalidates the current foreground notification and causes {@link @@ -813,10 +815,21 @@ public abstract class DownloadService extends Service { return isStopped; } - private void stop() { + private void onIdle() { if (foregroundNotificationUpdater != null) { + // Whether the service remains started or not, we don't need periodic notification updates + // when the DownloadManager is idle. foregroundNotificationUpdater.stopPeriodicUpdates(); } + + if (!Assertions.checkNotNull(downloadManagerHelper).updateScheduler()) { + // We failed to schedule the service to restart when requirements that the DownloadManager is + // waiting for are met, so remain started. + return; + } + + // Stop the service, either because the DownloadManager is not waiting for requirements to be + // met, or because we've scheduled the service to be restarted when they are. if (Util.SDK_INT < 28 && taskRemoved) { // See [Internal: b/74248644]. stopSelf(); isStopped = true; @@ -887,9 +900,10 @@ public abstract class DownloadService extends Service { } private void update() { - List downloads = Assertions.checkNotNull(downloadManager).getCurrentDownloads(); - @Requirements.RequirementFlags - int notMetRequirements = downloadManager.getNotMetRequirements(); + DownloadManager downloadManager = + Assertions.checkNotNull(downloadManagerHelper).downloadManager; + List downloads = downloadManager.getCurrentDownloads(); + @RequirementFlags int notMetRequirements = downloadManager.getNotMetRequirements(); Notification notification = getForegroundNotification(downloads, notMetRequirements); if (!notificationDisplayed) { startForeground(notificationId, notification); @@ -914,7 +928,9 @@ public abstract class DownloadService extends Service { private final boolean foregroundAllowed; @Nullable private final Scheduler scheduler; private final Class serviceClass; + @Nullable private DownloadService downloadService; + private @MonotonicNonNull Requirements scheduledRequirements; private DownloadManagerHelper( Context context, @@ -949,8 +965,46 @@ public abstract class DownloadService extends Service { public void detachService(DownloadService downloadService) { Assertions.checkState(this.downloadService == downloadService); this.downloadService = null; - if (scheduler != null && !downloadManager.isWaitingForRequirements()) { - scheduler.cancel(); + } + + /** + * Schedules or cancels restarting the service, as needed for the current state. + * + * @return True if the DownloadManager is not waiting for requirements, or if it is waiting for + * requirements and the service has been successfully scheduled to be restarted when they + * are met. False if the DownloadManager is waiting for requirements and the service has not + * been scheduled for restart. + */ + public boolean updateScheduler() { + boolean waitingForRequirements = downloadManager.isWaitingForRequirements(); + if (scheduler == null) { + return !waitingForRequirements; + } + + if (!waitingForRequirements) { + cancelScheduler(); + return true; + } + + Requirements requirements = downloadManager.getRequirements(); + Requirements supportedRequirements = scheduler.getSupportedRequirements(requirements); + if (!supportedRequirements.equals(requirements)) { + cancelScheduler(); + return false; + } + + if (!schedulerNeedsUpdate(requirements)) { + return true; + } + + String servicePackage = context.getPackageName(); + if (scheduler.schedule(requirements, servicePackage, ACTION_RESTART)) { + scheduledRequirements = requirements; + return true; + } else { + Log.w(TAG, "Failed to schedule restart"); + cancelScheduler(); + return false; } } @@ -989,10 +1043,18 @@ public abstract class DownloadService extends Service { @Override public final void onIdle(DownloadManager downloadManager) { if (downloadService != null) { - downloadService.stop(); + downloadService.onIdle(); } } + @Override + public void onRequirementsStateChanged( + DownloadManager downloadManager, + Requirements requirements, + @RequirementFlags int notMetRequirements) { + updateScheduler(); + } + @Override public void onWaitingForRequirementsChanged( DownloadManager downloadManager, boolean waitingForRequirements) { @@ -1006,23 +1068,42 @@ public abstract class DownloadService extends Service { for (int i = 0; i < downloads.size(); i++) { if (downloads.get(i).state == Download.STATE_QUEUED) { restartService(); - break; + return; } } } - updateScheduler(); } // Internal methods. + private boolean schedulerNeedsUpdate(Requirements requirements) { + return !Util.areEqual(scheduledRequirements, requirements); + } + + @RequiresNonNull("scheduler") + private void cancelScheduler() { + Requirements canceledRequirements = new Requirements(/* requirements= */ 0); + if (schedulerNeedsUpdate(canceledRequirements)) { + scheduler.cancel(); + scheduledRequirements = canceledRequirements; + } + } + private boolean serviceMayNeedRestart() { return downloadService == null || downloadService.isStopped(); } private void restartService() { if (foregroundAllowed) { - Intent intent = getIntent(context, serviceClass, DownloadService.ACTION_RESTART); - Util.startForegroundService(context, intent); + try { + Intent intent = getIntent(context, serviceClass, DownloadService.ACTION_RESTART); + Util.startForegroundService(context, intent); + } catch (IllegalStateException e) { + // The process is running in the background, and is not allowed to start a foreground + // service due to foreground service launch restrictions + // (https://developer.android.com/about/versions/12/foreground-services). + Log.w(TAG, "Failed to restart (foreground launch restriction)"); + } } else { // The service is background only. Use ACTION_INIT rather than ACTION_RESTART because // ACTION_RESTART is handled as though KEY_FOREGROUND is set to true. @@ -1032,25 +1113,9 @@ public abstract class DownloadService extends Service { } catch (IllegalStateException e) { // The process is classed as idle by the platform. Starting a background service is not // allowed in this state. - Log.w(TAG, "Failed to restart DownloadService (process is idle)."); + Log.w(TAG, "Failed to restart (process is idle)"); } } } - - private void updateScheduler() { - if (scheduler == null) { - return; - } - if (downloadManager.isWaitingForRequirements()) { - String servicePackage = context.getPackageName(); - Requirements requirements = downloadManager.getRequirements(); - boolean success = scheduler.schedule(requirements, servicePackage, ACTION_RESTART); - if (!success) { - Log.e(TAG, "Scheduling downloads failed."); - } - } else { - scheduler.cancel(); - } - } } }