From 3f16730763dab6d16fe751b16187e70c31bbe34c Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 20 Aug 2021 13:26:08 +0100 Subject: [PATCH] Support generating notifications for paused downloads - Android 12 will not allow our download service to be restarted from the background when conditions that allow downloads to continue are met. As an interim (and possibly permanent) solution, we'll keep the service in the foreground if there are unfinished downloads that would continue if conditions were met. - Keeping the service in the foreground requires a foreground notification. Hence we need to be able to generate a meaningful notification for this state. PiperOrigin-RevId: 391969986 --- .../exoplayer2/demo/DemoDownloadService.java | 7 +- .../exoplayer2/offline/DownloadService.java | 10 +- .../dash/offline/DownloadServiceDashTest.java | 5 +- .../ui/DownloadNotificationHelper.java | 113 +++++++++++++----- library/ui/src/main/res/values/strings.xml | 6 + 5 files changed, 105 insertions(+), 36 deletions(-) diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoDownloadService.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoDownloadService.java index c462c14c75..87d45148fe 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoDownloadService.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoDownloadService.java @@ -25,6 +25,7 @@ import com.google.android.exoplayer2.offline.Download; import com.google.android.exoplayer2.offline.DownloadManager; import com.google.android.exoplayer2.offline.DownloadService; import com.google.android.exoplayer2.scheduler.PlatformScheduler; +import com.google.android.exoplayer2.scheduler.Requirements; import com.google.android.exoplayer2.ui.DownloadNotificationHelper; import com.google.android.exoplayer2.util.NotificationUtil; import com.google.android.exoplayer2.util.Util; @@ -66,14 +67,16 @@ public class DemoDownloadService extends DownloadService { @Override @NonNull - protected Notification getForegroundNotification(@NonNull List downloads) { + protected Notification getForegroundNotification( + @NonNull List downloads, @Requirements.RequirementFlags int notMetRequirements) { return DemoUtil.getDownloadNotificationHelper(/* context= */ this) .buildProgressNotification( /* context= */ this, R.drawable.ic_download, /* contentIntent= */ null, /* message= */ null, - downloads); + downloads, + notMetRequirements); } /** 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 527c51ea83..25ca27bdce 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 @@ -747,13 +747,15 @@ public abstract class DownloadService extends Service { * be implemented to throw {@link UnsupportedOperationException}. * * @param downloads The current downloads. + * @param notMetRequirements Any requirements for downloads that are not currently met. * @return The foreground notification to display. */ - protected abstract Notification getForegroundNotification(List downloads); + protected abstract Notification getForegroundNotification( + List downloads, @Requirements.RequirementFlags int notMetRequirements); /** * Invalidates the current foreground notification and causes {@link - * #getForegroundNotification(List)} to be invoked again if the service isn't stopped. + * #getForegroundNotification(List, int)} to be invoked again if the service isn't stopped. */ protected final void invalidateForegroundNotification() { if (foregroundNotificationUpdater != null && !isDestroyed) { @@ -908,7 +910,9 @@ public abstract class DownloadService extends Service { private void update() { List downloads = Assertions.checkNotNull(downloadManager).getCurrentDownloads(); - startForeground(notificationId, getForegroundNotification(downloads)); + @Requirements.RequirementFlags + int notMetRequirements = downloadManager.getNotMetRequirements(); + startForeground(notificationId, getForegroundNotification(downloads, notMetRequirements)); notificationDisplayed = true; if (periodicUpdatesStarted) { handler.removeCallbacksAndMessages(null); diff --git a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadServiceDashTest.java b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadServiceDashTest.java index 98a7f6e887..e8a71798be 100644 --- a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadServiceDashTest.java +++ b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadServiceDashTest.java @@ -35,6 +35,7 @@ import com.google.android.exoplayer2.offline.DownloadRequest; import com.google.android.exoplayer2.offline.DownloadService; import com.google.android.exoplayer2.offline.StreamKey; import com.google.android.exoplayer2.robolectric.TestDownloadManagerListener; +import com.google.android.exoplayer2.scheduler.Requirements; import com.google.android.exoplayer2.scheduler.Scheduler; import com.google.android.exoplayer2.testutil.DummyMainThread; import com.google.android.exoplayer2.testutil.FakeDataSet; @@ -139,7 +140,9 @@ public class DownloadServiceDashTest { } @Override - protected Notification getForegroundNotification(List downloads) { + protected Notification getForegroundNotification( + List downloads, + @Requirements.RequirementFlags int notMetRequirements) { throw new UnsupportedOperationException(); } }; diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/DownloadNotificationHelper.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/DownloadNotificationHelper.java index 83da4d54a8..fa7244b250 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/DownloadNotificationHelper.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/DownloadNotificationHelper.java @@ -24,6 +24,7 @@ import androidx.annotation.StringRes; import androidx.core.app.NotificationCompat; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.offline.Download; +import com.google.android.exoplayer2.scheduler.Requirements; import java.util.List; /** Helper for creating download notifications. */ @@ -42,6 +43,21 @@ public final class DownloadNotificationHelper { new NotificationCompat.Builder(context.getApplicationContext(), channelId); } + /** + * @deprecated Use {@link #buildProgressNotification(Context, int, PendingIntent, String, List, + * int)}. + */ + @Deprecated + public Notification buildProgressNotification( + Context context, + @DrawableRes int smallIcon, + @Nullable PendingIntent contentIntent, + @Nullable String message, + List downloads) { + return buildProgressNotification( + context, smallIcon, contentIntent, message, downloads, /* notMetRequirements= */ 0); + } + /** * Returns a progress notification for the given downloads. * @@ -50,6 +66,7 @@ public final class DownloadNotificationHelper { * @param contentIntent An optional content intent to send when the notification is clicked. * @param message An optional message to display on the notification. * @param downloads The downloads. + * @param notMetRequirements Any requirements for downloads that are not currently met. * @return The notification. */ public Notification buildProgressNotification( @@ -57,52 +74,88 @@ public final class DownloadNotificationHelper { @DrawableRes int smallIcon, @Nullable PendingIntent contentIntent, @Nullable String message, - List downloads) { + List downloads, + @Requirements.RequirementFlags int notMetRequirements) { float totalPercentage = 0; int downloadTaskCount = 0; boolean allDownloadPercentagesUnknown = true; boolean haveDownloadedBytes = false; - boolean haveDownloadTasks = false; - boolean haveRemoveTasks = false; + boolean haveDownloadingTasks = false; + boolean haveQueuedTasks = false; + boolean haveRemovingTasks = false; for (int i = 0; i < downloads.size(); i++) { Download download = downloads.get(i); - if (download.state == Download.STATE_REMOVING) { - haveRemoveTasks = true; - continue; + switch (download.state) { + case Download.STATE_REMOVING: + haveRemovingTasks = true; + break; + case Download.STATE_QUEUED: + haveQueuedTasks = true; + break; + case Download.STATE_RESTARTING: + case Download.STATE_DOWNLOADING: + haveDownloadingTasks = true; + float downloadPercentage = download.getPercentDownloaded(); + if (downloadPercentage != C.PERCENTAGE_UNSET) { + allDownloadPercentagesUnknown = false; + totalPercentage += downloadPercentage; + } + haveDownloadedBytes |= download.getBytesDownloaded() > 0; + downloadTaskCount++; + break; + // Terminal states aren't expected, but if we encounter them we do nothing. + case Download.STATE_STOPPED: + case Download.STATE_COMPLETED: + case Download.STATE_FAILED: + default: + break; } - if (download.state != Download.STATE_RESTARTING - && download.state != Download.STATE_DOWNLOADING) { - continue; - } - haveDownloadTasks = true; - float downloadPercentage = download.getPercentDownloaded(); - if (downloadPercentage != C.PERCENTAGE_UNSET) { - allDownloadPercentagesUnknown = false; - totalPercentage += downloadPercentage; - } - haveDownloadedBytes |= download.getBytesDownloaded() > 0; - downloadTaskCount++; } - int titleStringId = - haveDownloadTasks - ? R.string.exo_download_downloading - : (haveRemoveTasks ? R.string.exo_download_removing : NULL_STRING_ID); - int progress = 0; - boolean indeterminate = true; - if (haveDownloadTasks) { - progress = (int) (totalPercentage / downloadTaskCount); - indeterminate = allDownloadPercentagesUnknown && haveDownloadedBytes; + int titleStringId; + boolean showProgress = true; + if (haveDownloadingTasks) { + titleStringId = R.string.exo_download_downloading; + } else if (haveQueuedTasks && notMetRequirements != 0) { + showProgress = false; + if ((notMetRequirements & Requirements.NETWORK_UNMETERED) != 0) { + // Note: This assumes that "unmetered" == "WiFi", since it provides a clearer message that's + // correct in the majority of cases. + titleStringId = R.string.exo_download_paused_for_wifi; + } else if ((notMetRequirements & Requirements.NETWORK) != 0) { + titleStringId = R.string.exo_download_paused_for_network; + } else { + titleStringId = R.string.exo_download_paused; + } + } else if (haveRemovingTasks) { + titleStringId = R.string.exo_download_removing; + } else { + // There are either no downloads, or all downloads are in terminal states. + titleStringId = NULL_STRING_ID; } + + int maxProgress = 0; + int currentProgress = 0; + boolean indeterminateProgress = false; + if (showProgress) { + maxProgress = 100; + if (haveDownloadingTasks) { + currentProgress = (int) (totalPercentage / downloadTaskCount); + indeterminateProgress = allDownloadPercentagesUnknown && haveDownloadedBytes; + } else { + indeterminateProgress = true; + } + } + return buildNotification( context, smallIcon, contentIntent, message, titleStringId, - /* maxProgress= */ 100, - progress, - indeterminate, + maxProgress, + currentProgress, + indeterminateProgress, /* ongoing= */ true, /* showWhen= */ false); } diff --git a/library/ui/src/main/res/values/strings.xml b/library/ui/src/main/res/values/strings.xml index a11d04073f..0dde445c16 100644 --- a/library/ui/src/main/res/values/strings.xml +++ b/library/ui/src/main/res/values/strings.xml @@ -91,6 +91,12 @@ Download failed Removing downloads + + Downloads paused + + Downloads waiting for network + + Downloads waiting for WiFi Video