Commit dfa4d55f by olly Committed by Oliver Woodman

Fix DownloadService resumption

- DownloadManagerHelper now passes all downloads to the
  DownloadService when the service is attached (and once
  the downloads are known). The service then starts the
  foreground notification updater if necessary. This fixes
  the ref'd issue.
- Don't call getScheduler() if the service is background
  only. This was already documented to be the case on the
  DownloadService constructor.
- If the service is started in the foreground on SDK level
  26 and higher, satisfy the condition to move the service
  to the foreground in onStartCommand rather than in stop().
  It's much more obviously correct, and should produce the
  same end result.

Issue: #6798
PiperOrigin-RevId: 290050024
parent c269ffe1
...@@ -4,14 +4,18 @@ ...@@ -4,14 +4,18 @@
* Add Java FLAC extractor * Add Java FLAC extractor
([#6406](https://github.com/google/ExoPlayer/issues/6406)). ([#6406](https://github.com/google/ExoPlayer/issues/6406)).
* Downloads:
* Merge downloads in `SegmentDownloader` to improve overall download
speed ([#5978](https://github.com/google/ExoPlayer/issues/5978)).
* Fix download resumption when the requirements for them to continue are
met ([#6733](https://github.com/google/ExoPlayer/issues/6733),
[#6798](https://github.com/google/ExoPlayer/issues/6798)).
* Fix `DownloadHelper.createMediaSource` to use `customCacheKey` when creating
`ProgressiveMediaSource` instances.
* Update `IcyDecoder` to try ISO-8859-1 decoding if UTF-8 decoding fails. * Update `IcyDecoder` to try ISO-8859-1 decoding if UTF-8 decoding fails.
Also change `IcyInfo.rawMetadata` from `String` to `byte[]` to allow Also change `IcyInfo.rawMetadata` from `String` to `byte[]` to allow
developers to handle data that's neither UTF-8 nor ISO-8859-1 developers to handle data that's neither UTF-8 nor ISO-8859-1
([#6753](https://github.com/google/ExoPlayer/issues/6753)). ([#6753](https://github.com/google/ExoPlayer/issues/6753)).
* Fix handling of network transitions in `RequirementsWatcher`
([#6733](https://github.com/google/ExoPlayer/issues/6733)). Incorrect handling
could previously cause downloads to be paused when they should have been able
to proceed.
* Fix handling of E-AC-3 streams that contain AC-3 syncframes * Fix handling of E-AC-3 streams that contain AC-3 syncframes
([#6602](https://github.com/google/ExoPlayer/issues/6602)). ([#6602](https://github.com/google/ExoPlayer/issues/6602)).
* Fix playback of TrueHD streams in Matroska * Fix playback of TrueHD streams in Matroska
......
...@@ -76,8 +76,8 @@ public final class JobDispatcherScheduler implements Scheduler { ...@@ -76,8 +76,8 @@ public final class JobDispatcherScheduler implements Scheduler {
* {@link #schedule(Requirements, String, String)} or {@link #cancel()} are called. * {@link #schedule(Requirements, String, String)} or {@link #cancel()} are called.
*/ */
public JobDispatcherScheduler(Context context, String jobTag) { public JobDispatcherScheduler(Context context, String jobTag) {
this.jobDispatcher = context = context.getApplicationContext();
new FirebaseJobDispatcher(new GooglePlayDriver(context.getApplicationContext())); this.jobDispatcher = new FirebaseJobDispatcher(new GooglePlayDriver(context));
this.jobTag = jobTag; this.jobTag = jobTag;
} }
......
...@@ -578,13 +578,13 @@ public abstract class DownloadService extends Service { ...@@ -578,13 +578,13 @@ public abstract class DownloadService extends Service {
NotificationUtil.IMPORTANCE_LOW); NotificationUtil.IMPORTANCE_LOW);
} }
Class<? extends DownloadService> clazz = getClass(); Class<? extends DownloadService> clazz = getClass();
DownloadManagerHelper downloadManagerHelper = downloadManagerHelpers.get(clazz); @Nullable DownloadManagerHelper downloadManagerHelper = downloadManagerHelpers.get(clazz);
if (downloadManagerHelper == null) { if (downloadManagerHelper == null) {
@Nullable Scheduler scheduler = foregroundNotificationUpdater == null ? null : getScheduler();
downloadManager = getDownloadManager(); downloadManager = getDownloadManager();
downloadManager.resumeDownloads(); downloadManager.resumeDownloads();
downloadManagerHelper = downloadManagerHelper =
new DownloadManagerHelper( new DownloadManagerHelper(getApplicationContext(), downloadManager, scheduler, clazz);
getApplicationContext(), downloadManager, getScheduler(), clazz);
downloadManagerHelpers.put(clazz, downloadManagerHelper); downloadManagerHelpers.put(clazz, downloadManagerHelper);
} else { } else {
downloadManager = downloadManagerHelper.downloadManager; downloadManager = downloadManagerHelper.downloadManager;
...@@ -600,9 +600,9 @@ public abstract class DownloadService extends Service { ...@@ -600,9 +600,9 @@ public abstract class DownloadService extends Service {
@Nullable String contentId = null; @Nullable String contentId = null;
if (intent != null) { if (intent != null) {
intentAction = intent.getAction(); intentAction = intent.getAction();
contentId = intent.getStringExtra(KEY_CONTENT_ID);
startedInForeground |= startedInForeground |=
intent.getBooleanExtra(KEY_FOREGROUND, false) || ACTION_RESTART.equals(intentAction); intent.getBooleanExtra(KEY_FOREGROUND, false) || ACTION_RESTART.equals(intentAction);
contentId = intent.getStringExtra(KEY_CONTENT_ID);
} }
// intentAction is null if the service is restarted or no action is specified. // intentAction is null if the service is restarted or no action is specified.
if (intentAction == null) { if (intentAction == null) {
...@@ -660,6 +660,10 @@ public abstract class DownloadService extends Service { ...@@ -660,6 +660,10 @@ public abstract class DownloadService extends Service {
break; break;
} }
if (Util.SDK_INT >= 26 && startedInForeground && foregroundNotificationUpdater != null) {
// From API level 26, services started in the foreground are required to show a notification.
foregroundNotificationUpdater.showNotificationIfNotAlready();
}
if (downloadManager.isIdle()) { if (downloadManager.isIdle()) {
stop(); stop();
} }
...@@ -701,21 +705,21 @@ public abstract class DownloadService extends Service { ...@@ -701,21 +705,21 @@ public abstract class DownloadService extends Service {
* Returns a {@link Scheduler} to restart the service when requirements allowing downloads to take * 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 * place are met. If {@code null}, the service will only be restarted if the process is still in
* memory when the requirements are met. * memory when the requirements are met.
*
* <p>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.
*/ */
protected abstract @Nullable Scheduler getScheduler(); @Nullable
protected abstract Scheduler getScheduler();
/** /**
* Returns a notification to be displayed when this service running in the foreground. This method * Returns a notification to be displayed when this service running in the foreground.
* is called when there is a download state change and periodically while there are active
* downloads. The periodic update interval can be set using {@link #DownloadService(int, long)}.
*
* <p>On API level 26 and above, this method may also be called just before the service stops,
* with an empty {@code downloads} array. The returned notification is used to satisfy system
* requirements for foreground services.
* *
* <p>Download services that do not wish to run in the foreground should be created by setting the * <p>Download services that do not wish to run in the foreground should be created by setting the
* {@code foregroundNotificationId} constructor argument to {@link * {@code foregroundNotificationId} constructor argument to {@link
* #FOREGROUND_NOTIFICATION_ID_NONE}. This method will not be called in this case, meaning it can * #FOREGROUND_NOTIFICATION_ID_NONE}. This method is not called for such services, meaning it can
* be implemented to throw {@link UnsupportedOperationException}. * be implemented to throw {@link UnsupportedOperationException}.
* *
* @param downloads The current downloads. * @param downloads The current downloads.
...@@ -754,13 +758,32 @@ public abstract class DownloadService extends Service { ...@@ -754,13 +758,32 @@ public abstract class DownloadService extends Service {
// Do nothing. // Do nothing.
} }
/**
* Called after the service is created, once the downloads are known.
*
* @param downloads The current downloads.
*/
private void notifyDownloads(List<Download> downloads) {
if (foregroundNotificationUpdater != null) {
for (int i = 0; i < downloads.size(); i++) {
if (needsForegroundNotification(downloads.get(i).state)) {
foregroundNotificationUpdater.startPeriodicUpdates();
break;
}
}
}
}
/**
* Called when the state of a download changes.
*
* @param download The state of the download.
*/
@SuppressWarnings("deprecation") @SuppressWarnings("deprecation")
private void notifyDownloadChanged(Download download) { private void notifyDownloadChanged(Download download) {
onDownloadChanged(download); onDownloadChanged(download);
if (foregroundNotificationUpdater != null) { if (foregroundNotificationUpdater != null) {
if (download.state == Download.STATE_DOWNLOADING if (needsForegroundNotification(download.state)) {
|| download.state == Download.STATE_REMOVING
|| download.state == Download.STATE_RESTARTING) {
foregroundNotificationUpdater.startPeriodicUpdates(); foregroundNotificationUpdater.startPeriodicUpdates();
} else { } else {
foregroundNotificationUpdater.invalidate(); foregroundNotificationUpdater.invalidate();
...@@ -768,6 +791,11 @@ public abstract class DownloadService extends Service { ...@@ -768,6 +791,11 @@ public abstract class DownloadService extends Service {
} }
} }
/**
* Called when a download is removed.
*
* @param download The last state of the download before it was removed.
*/
@SuppressWarnings("deprecation") @SuppressWarnings("deprecation")
private void notifyDownloadRemoved(Download download) { private void notifyDownloadRemoved(Download download) {
onDownloadRemoved(download); onDownloadRemoved(download);
...@@ -779,10 +807,6 @@ public abstract class DownloadService extends Service { ...@@ -779,10 +807,6 @@ public abstract class DownloadService extends Service {
private void stop() { private void stop() {
if (foregroundNotificationUpdater != null) { if (foregroundNotificationUpdater != null) {
foregroundNotificationUpdater.stopPeriodicUpdates(); foregroundNotificationUpdater.stopPeriodicUpdates();
// Make sure startForeground is called before stopping. Workaround for [Internal: b/69424260].
if (startedInForeground && Util.SDK_INT >= 26) {
foregroundNotificationUpdater.showNotificationIfNotAlready();
}
} }
if (Util.SDK_INT < 28 && taskRemoved) { // See [Internal: b/74248644]. if (Util.SDK_INT < 28 && taskRemoved) { // See [Internal: b/74248644].
stopSelf(); stopSelf();
...@@ -791,6 +815,12 @@ public abstract class DownloadService extends Service { ...@@ -791,6 +815,12 @@ public abstract class DownloadService extends Service {
} }
} }
private static boolean needsForegroundNotification(@Download.State int state) {
return state == Download.STATE_DOWNLOADING
|| state == Download.STATE_REMOVING
|| state == Download.STATE_RESTARTING;
}
private static Intent getIntent( private static Intent getIntent(
Context context, Class<? extends DownloadService> clazz, String action, boolean foreground) { Context context, Class<? extends DownloadService> clazz, String action, boolean foreground) {
return getIntent(context, clazz, action).putExtra(KEY_FOREGROUND, foreground); return getIntent(context, clazz, action).putExtra(KEY_FOREGROUND, foreground);
...@@ -884,6 +914,16 @@ public abstract class DownloadService extends Service { ...@@ -884,6 +914,16 @@ public abstract class DownloadService extends Service {
public void attachService(DownloadService downloadService) { public void attachService(DownloadService downloadService) {
Assertions.checkState(this.downloadService == null); Assertions.checkState(this.downloadService == null);
this.downloadService = downloadService; this.downloadService = downloadService;
if (downloadManager.isInitialized()) {
// The call to DownloadService.notifyDownloads is posted to avoid it being called directly
// from DownloadService.onCreate. This is a good idea because it may in turn call
// DownloadService.getForegroundNotification, and concrete subclass implementations may
// not anticipate the possibility of this method being called before their onCreate
// implementation has finished executing.
Util.createHandler()
.postAtFrontOfQueue(
() -> downloadService.notifyDownloads(downloadManager.getCurrentDownloads()));
}
} }
public void detachService(DownloadService downloadService) { public void detachService(DownloadService downloadService) {
...@@ -894,6 +934,15 @@ public abstract class DownloadService extends Service { ...@@ -894,6 +934,15 @@ public abstract class DownloadService extends Service {
} }
} }
// DownloadManager.Listener implementation.
@Override
public void onInitialized(DownloadManager downloadManager) {
if (downloadService != null) {
downloadService.notifyDownloads(downloadManager.getCurrentDownloads());
}
}
@Override @Override
public void onDownloadChanged(DownloadManager downloadManager, Download download) { public void onDownloadChanged(DownloadManager downloadManager, Download download) {
if (downloadService != null) { if (downloadService != null) {
...@@ -921,6 +970,10 @@ public abstract class DownloadService extends Service { ...@@ -921,6 +970,10 @@ public abstract class DownloadService extends Service {
Requirements requirements, Requirements requirements,
@Requirements.RequirementFlags int notMetRequirements) { @Requirements.RequirementFlags int notMetRequirements) {
boolean requirementsMet = notMetRequirements == 0; boolean requirementsMet = notMetRequirements == 0;
// TODO: Fix this logic to only start the service if the DownloadManager is actually going to
// make progress (in addition to the requirements being met, it also needs to be not paused
// and have some current downloads). Start in service in the foreground if the service has a
// ForegroundNotificationUpdater.
if (downloadService == null && requirementsMet) { if (downloadService == null && requirementsMet) {
try { try {
Intent intent = getIntent(context, serviceClass, DownloadService.ACTION_INIT); Intent intent = getIntent(context, serviceClass, DownloadService.ACTION_INIT);
...@@ -935,6 +988,12 @@ public abstract class DownloadService extends Service { ...@@ -935,6 +988,12 @@ public abstract class DownloadService extends Service {
} }
} }
// Internal methods.
// TODO: Fix callers to this method so that the scheduler is only enabled if the DownloadManager
// would actually make progress were the requirements met (or if it's not initialized yet, in
// which case we should be cautious until we know better). To implement this properly it'll be
// necessary to call this method from some additional places.
@RequiresNonNull("scheduler") @RequiresNonNull("scheduler")
private void setSchedulerEnabled(boolean enabled, Requirements requirements) { private void setSchedulerEnabled(boolean enabled, Requirements requirements) {
if (!enabled) { if (!enabled) {
......
...@@ -64,6 +64,7 @@ public final class PlatformScheduler implements Scheduler { ...@@ -64,6 +64,7 @@ public final class PlatformScheduler implements Scheduler {
*/ */
@RequiresPermission(android.Manifest.permission.RECEIVE_BOOT_COMPLETED) @RequiresPermission(android.Manifest.permission.RECEIVE_BOOT_COMPLETED)
public PlatformScheduler(Context context, int jobId) { public PlatformScheduler(Context context, int jobId) {
context = context.getApplicationContext();
this.jobId = jobId; this.jobId = jobId;
jobServiceComponentName = new ComponentName(context, PlatformSchedulerService.class); jobServiceComponentName = new ComponentName(context, PlatformSchedulerService.class);
jobScheduler = (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE); jobScheduler = (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE);
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment