Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Stuck downloads when changing network conditions #6798

Closed
daveoxley opened this issue Dec 23, 2019 · 9 comments
Closed

Stuck downloads when changing network conditions #6798

daveoxley opened this issue Dec 23, 2019 · 9 comments
Assignees
Labels

Comments

@daveoxley
Copy link

daveoxley commented Dec 23, 2019

Issue description

DownloadService race condition causing downloads to not resume when network requirements are met after previously not being met.

The issue appears to be that DownloadManagerHelper.onDownloadChanged is called before the new service is attached to the DownloadManagerHelper. Therefore DownloadService.notifyDownloadChanged is never called and startForeground is never called on the new service. The app process is then killed after roughly 10 seconds as the scheduler has been cancelled.

Reproduction steps

Using the exoplayer app:

  • Start downloading some content
  • Click back to exit the app. Downloading notification should be shown.
  • Turn off network. Downloading notification should be hidden.
  • Wait approximately 2 minutes.
  • Turn on network. Downloading notification is not reshown when it should be.

Link to test content

Tested using WV: Clear SD & HD (MP4,H264) in the sample app.

Version of ExoPlayer being used

2.10.7. Also reproduced the issue on 2.11.1.

Device(s) and version(s) of Android being used

Pixel 3XL Android 10 (QQ1A.191205.008)

@ojw28
Copy link
Contributor

ojw28 commented Jan 2, 2020

This is probably a duplicate of #6733. Marking as such, but please do test with the dev-v2 branch to confirm that the issue is fixed.

@ojw28 ojw28 closed this as completed Jan 2, 2020
@ojw28 ojw28 added the duplicate label Jan 2, 2020
@daveoxley
Copy link
Author

@ojw28 Unfortunately this issue still exists on the latest dev-v2 branch (Tested on 826083d). The below patch fixes the issue and I'll put together a better change and submit a pr later. Any chance you could reopen this issue please?

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 db10517b6..8c4220c35 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
@@ -32,6 +32,8 @@ import com.google.android.exoplayer2.util.Assertions;
 import com.google.android.exoplayer2.util.Log;
 import com.google.android.exoplayer2.util.NotificationUtil;
 import com.google.android.exoplayer2.util.Util;
+
+import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
 
@@ -856,6 +858,7 @@ public abstract class DownloadService extends Service {
     @Nullable private final Scheduler scheduler;
     private final Class<? extends DownloadService> serviceClass;
     @Nullable private DownloadService downloadService;
+    private final List<Download> list = new ArrayList<>();
 
     private DownloadManagerHelper(
         Context context,
@@ -877,6 +880,10 @@ public abstract class DownloadService extends Service {
     public void attachService(DownloadService downloadService) {
       Assertions.checkState(this.downloadService == null);
       this.downloadService = downloadService;
+      for (Download download : list) {
+        downloadService.notifyDownloadChanged(download);
+      }
+      list.clear();
     }
 
     public void detachService(DownloadService downloadService, boolean unschedule) {
@@ -891,6 +898,8 @@ public abstract class DownloadService extends Service {
     public void onDownloadChanged(DownloadManager downloadManager, Download download) {
       if (downloadService != null) {
         downloadService.notifyDownloadChanged(download);
+      } else {
+        list.add(download);
       }
     }
 

@ojw28
Copy link
Contributor

ojw28 commented Jan 5, 2020

Thanks for checking! I see what's going on now, and yes you're absolutely right.

What's happening in this case is that the DownloadManager is calling DownloadManagerHelper. onRequirementsStateChanged, which is then failing to restart the service because the process is in the background (the "wait approximately 2 minutes" part of the reproduction instructions are important, else the process is still classified as being in the foreground). DownloadManagerHelper.onDownloadChanged is then called as the DownloadManager resumes its downloads. At some point later the Scheduler then restarts the service, which does work because it starts it as a foreground service.

Your PR will cause the DownloadManagerHelper.onDownloadChanged events to be processed when the Scheduler restarts the service, but it's a relatively uncontrolled fix because there aren't particularly strong guarantees on when this will happen. There are also some related issues, such as what happens if there's a Scheduler and foregroundNotificationId == FOREGROUND_NOTIFICATION_ID_NONE. I'd like to spend a bit of time this week seeing if there's a more controlled fix, that'll also solve this type of related issue at the same time.

ojw28 added a commit that referenced this issue Jan 13, 2020
As discovered whilst investigating #6798, there are cases
where these methods are not correctly. They were added as
convenience methods that could be overridden by concrete
DownloadService implementations, but since they don't work
properly it's preferable to require application code to
listen to their DownloadManager directly instead.

Notes:

- The original proposal to fix #6798 stored the state change
events in memory until they could be delivered. This approach
is not ideal because the events still end up being delivered
later than they should be. We also want to fix the root cause
in a different way that does not require doing this.
- This change does not fix #6798. It's a preparatory step.

Issue: #6798
PiperOrigin-RevId: 289418555
ojw28 added a commit that referenced this issue Jan 13, 2020
Issue: #6798
PiperOrigin-RevId: 289424582
ojw28 added a commit that referenced this issue Jan 16, 2020
- 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
@ojw28
Copy link
Contributor

ojw28 commented Jan 16, 2020

This should be fixed in dev-v2 now. Could you give it another try and verify?

Note that there's still some additional work to be done, which should speed up resumption in the case that the process is still in memory (and also fix this case if getScheduler is implemented to return null). See the TODOs added in 51f2723 if you're interested in what's left to do.

ojw28 added a commit that referenced this issue Jan 16, 2020
This fixes an issue where a DownloadService implementation
that allows foreground but doesn't provide a scheduler would
not be restarted in the case that it was still in memory but
classed as idle by the platform.

It also speeds up service restart in the case that a
scheduler is provided.

Issue: #6798
PiperOrigin-RevId: 290068960
ojw28 added a commit that referenced this issue Jan 17, 2020
- Add additional Listener methods to DownloadManager, to inform of
  changes to whether the downloads are paused or waiting for requirements.

- Only schedule the Scheduler if we really are waiting for requirements.

- Only restart the service if we're no longer waiting for requirements,
  and if there are queued downloads that will now be restarted.
  Previously the service would be restarted whenever the requirements
  were met, regardless of whether there was any work to do.

- Restart service if it might be stopping, as well as if it's already
  stopped. Also restart service if there's a download state change to a
  state for which the service should be started, if.

Issue: #6798
PiperOrigin-RevId: 290270547
ojw28 added a commit that referenced this issue Jan 17, 2020
As discovered whilst investigating #6798, there are cases
where these methods are not correctly. They were added as
convenience methods that could be overridden by concrete
DownloadService implementations, but since they don't work
properly it's preferable to require application code to
listen to their DownloadManager directly instead.

Notes:

- The original proposal to fix #6798 stored the state change
events in memory until they could be delivered. This approach
is not ideal because the events still end up being delivered
later than they should be. We also want to fix the root cause
in a different way that does not require doing this.
- This change does not fix #6798. It's a preparatory step.

Issue: #6798
PiperOrigin-RevId: 289418555
ojw28 added a commit that referenced this issue Jan 17, 2020
Issue: #6798
PiperOrigin-RevId: 289424582
ojw28 added a commit that referenced this issue Jan 17, 2020
- 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
ojw28 added a commit that referenced this issue Jan 17, 2020
This fixes an issue where a DownloadService implementation
that allows foreground but doesn't provide a scheduler would
not be restarted in the case that it was still in memory but
classed as idle by the platform.

It also speeds up service restart in the case that a
scheduler is provided.

Issue: #6798
PiperOrigin-RevId: 290068960
ojw28 added a commit that referenced this issue Jan 17, 2020
- Add additional Listener methods to DownloadManager, to inform of
  changes to whether the downloads are paused or waiting for requirements.

- Only schedule the Scheduler if we really are waiting for requirements.

- Only restart the service if we're no longer waiting for requirements,
  and if there are queued downloads that will now be restarted.
  Previously the service would be restarted whenever the requirements
  were met, regardless of whether there was any work to do.

- Restart service if it might be stopping, as well as if it's already
  stopped. Also restart service if there's a download state change to a
  state for which the service should be started, if.

Issue: #6798
PiperOrigin-RevId: 290270547
@ojw28
Copy link
Contributor

ojw28 commented Jan 17, 2020

Cleanup is complete, and the changes are in dev-v2 and dev-v2-r2.11.2. The changes were fairly extensive, so I'd really appreciate it if someone has a chance to try it out.

@ojw28 ojw28 closed this as completed Jan 17, 2020
@daveoxley
Copy link
Author

@ojw28 I can still make it go wrong by following the original steps using the demo app. Tested dev-v2-r2.11.2 (ce1ec1d). It doesn't appear to happen as often any more but it still happens.

@ojw28
Copy link
Contributor

ojw28 commented Jan 27, 2020

How many times do you generally have to try for it to reproduce? I haven't managed to reproduce the problem yet (using the same version you were using).

@ojw28
Copy link
Contributor

ojw28 commented Jan 27, 2020

A couple of additional:

  1. I know it's an annoying question (heh, sorry), but are you 100% certain you were using that build?
  2. Does it make a difference whether the demo app's process is killed between the second to last and last reproduction steps? I'm wondering if you're only seeing it reproduce sometimes because it always happens in one case or the other (i.e. process is still alive vs process is killed). Just before the final reproduction step, it should be possible for you to query whether the process is still alive or not using adb shell ps -A | grep com.google.android.exoplayer2.demo, or something along those lines.

@google google locked and limited conversation to collaborators Mar 18, 2020
@ojw28
Copy link
Contributor

ojw28 commented Jun 3, 2020

It doesn't appear to happen as often any more but it still happens.

We do now have reproduction steps for one case in which downloads fail to resume when network connectivity is restored. We're tracking this using #7453.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Projects
None yet
Development

No branches or pull requests

2 participants