Commit 3f167307 by olly Committed by Oliver Woodman

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
parent 03d0b34a
...@@ -25,6 +25,7 @@ import com.google.android.exoplayer2.offline.Download; ...@@ -25,6 +25,7 @@ import com.google.android.exoplayer2.offline.Download;
import com.google.android.exoplayer2.offline.DownloadManager; import com.google.android.exoplayer2.offline.DownloadManager;
import com.google.android.exoplayer2.offline.DownloadService; import com.google.android.exoplayer2.offline.DownloadService;
import com.google.android.exoplayer2.scheduler.PlatformScheduler; 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.ui.DownloadNotificationHelper;
import com.google.android.exoplayer2.util.NotificationUtil; import com.google.android.exoplayer2.util.NotificationUtil;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
...@@ -66,14 +67,16 @@ public class DemoDownloadService extends DownloadService { ...@@ -66,14 +67,16 @@ public class DemoDownloadService extends DownloadService {
@Override @Override
@NonNull @NonNull
protected Notification getForegroundNotification(@NonNull List<Download> downloads) { protected Notification getForegroundNotification(
@NonNull List<Download> downloads, @Requirements.RequirementFlags int notMetRequirements) {
return DemoUtil.getDownloadNotificationHelper(/* context= */ this) return DemoUtil.getDownloadNotificationHelper(/* context= */ this)
.buildProgressNotification( .buildProgressNotification(
/* context= */ this, /* context= */ this,
R.drawable.ic_download, R.drawable.ic_download,
/* contentIntent= */ null, /* contentIntent= */ null,
/* message= */ null, /* message= */ null,
downloads); downloads,
notMetRequirements);
} }
/** /**
......
...@@ -747,13 +747,15 @@ public abstract class DownloadService extends Service { ...@@ -747,13 +747,15 @@ public abstract class DownloadService extends Service {
* be implemented to throw {@link UnsupportedOperationException}. * be implemented to throw {@link UnsupportedOperationException}.
* *
* @param downloads The current downloads. * @param downloads The current downloads.
* @param notMetRequirements Any requirements for downloads that are not currently met.
* @return The foreground notification to display. * @return The foreground notification to display.
*/ */
protected abstract Notification getForegroundNotification(List<Download> downloads); protected abstract Notification getForegroundNotification(
List<Download> downloads, @Requirements.RequirementFlags int notMetRequirements);
/** /**
* Invalidates the current foreground notification and causes {@link * 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() { protected final void invalidateForegroundNotification() {
if (foregroundNotificationUpdater != null && !isDestroyed) { if (foregroundNotificationUpdater != null && !isDestroyed) {
...@@ -908,7 +910,9 @@ public abstract class DownloadService extends Service { ...@@ -908,7 +910,9 @@ public abstract class DownloadService extends Service {
private void update() { private void update() {
List<Download> downloads = Assertions.checkNotNull(downloadManager).getCurrentDownloads(); List<Download> downloads = Assertions.checkNotNull(downloadManager).getCurrentDownloads();
startForeground(notificationId, getForegroundNotification(downloads)); @Requirements.RequirementFlags
int notMetRequirements = downloadManager.getNotMetRequirements();
startForeground(notificationId, getForegroundNotification(downloads, notMetRequirements));
notificationDisplayed = true; notificationDisplayed = true;
if (periodicUpdatesStarted) { if (periodicUpdatesStarted) {
handler.removeCallbacksAndMessages(null); handler.removeCallbacksAndMessages(null);
......
...@@ -35,6 +35,7 @@ import com.google.android.exoplayer2.offline.DownloadRequest; ...@@ -35,6 +35,7 @@ import com.google.android.exoplayer2.offline.DownloadRequest;
import com.google.android.exoplayer2.offline.DownloadService; import com.google.android.exoplayer2.offline.DownloadService;
import com.google.android.exoplayer2.offline.StreamKey; import com.google.android.exoplayer2.offline.StreamKey;
import com.google.android.exoplayer2.robolectric.TestDownloadManagerListener; 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.scheduler.Scheduler;
import com.google.android.exoplayer2.testutil.DummyMainThread; import com.google.android.exoplayer2.testutil.DummyMainThread;
import com.google.android.exoplayer2.testutil.FakeDataSet; import com.google.android.exoplayer2.testutil.FakeDataSet;
...@@ -139,7 +140,9 @@ public class DownloadServiceDashTest { ...@@ -139,7 +140,9 @@ public class DownloadServiceDashTest {
} }
@Override @Override
protected Notification getForegroundNotification(List<Download> downloads) { protected Notification getForegroundNotification(
List<Download> downloads,
@Requirements.RequirementFlags int notMetRequirements) {
throw new UnsupportedOperationException(); throw new UnsupportedOperationException();
} }
}; };
......
...@@ -24,6 +24,7 @@ import androidx.annotation.StringRes; ...@@ -24,6 +24,7 @@ import androidx.annotation.StringRes;
import androidx.core.app.NotificationCompat; import androidx.core.app.NotificationCompat;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.offline.Download; import com.google.android.exoplayer2.offline.Download;
import com.google.android.exoplayer2.scheduler.Requirements;
import java.util.List; import java.util.List;
/** Helper for creating download notifications. */ /** Helper for creating download notifications. */
...@@ -43,6 +44,21 @@ public final class DownloadNotificationHelper { ...@@ -43,6 +44,21 @@ public final class DownloadNotificationHelper {
} }
/** /**
* @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<Download> downloads) {
return buildProgressNotification(
context, smallIcon, contentIntent, message, downloads, /* notMetRequirements= */ 0);
}
/**
* Returns a progress notification for the given downloads. * Returns a progress notification for the given downloads.
* *
* @param context A context. * @param context A context.
...@@ -50,6 +66,7 @@ public final class DownloadNotificationHelper { ...@@ -50,6 +66,7 @@ public final class DownloadNotificationHelper {
* @param contentIntent An optional content intent to send when the notification is clicked. * @param contentIntent An optional content intent to send when the notification is clicked.
* @param message An optional message to display on the notification. * @param message An optional message to display on the notification.
* @param downloads The downloads. * @param downloads The downloads.
* @param notMetRequirements Any requirements for downloads that are not currently met.
* @return The notification. * @return The notification.
*/ */
public Notification buildProgressNotification( public Notification buildProgressNotification(
...@@ -57,52 +74,88 @@ public final class DownloadNotificationHelper { ...@@ -57,52 +74,88 @@ public final class DownloadNotificationHelper {
@DrawableRes int smallIcon, @DrawableRes int smallIcon,
@Nullable PendingIntent contentIntent, @Nullable PendingIntent contentIntent,
@Nullable String message, @Nullable String message,
List<Download> downloads) { List<Download> downloads,
@Requirements.RequirementFlags int notMetRequirements) {
float totalPercentage = 0; float totalPercentage = 0;
int downloadTaskCount = 0; int downloadTaskCount = 0;
boolean allDownloadPercentagesUnknown = true; boolean allDownloadPercentagesUnknown = true;
boolean haveDownloadedBytes = false; boolean haveDownloadedBytes = false;
boolean haveDownloadTasks = false; boolean haveDownloadingTasks = false;
boolean haveRemoveTasks = false; boolean haveQueuedTasks = false;
boolean haveRemovingTasks = false;
for (int i = 0; i < downloads.size(); i++) { for (int i = 0; i < downloads.size(); i++) {
Download download = downloads.get(i); Download download = downloads.get(i);
if (download.state == Download.STATE_REMOVING) { switch (download.state) {
haveRemoveTasks = true; case Download.STATE_REMOVING:
continue; haveRemovingTasks = true;
} break;
if (download.state != Download.STATE_RESTARTING case Download.STATE_QUEUED:
&& download.state != Download.STATE_DOWNLOADING) { haveQueuedTasks = true;
continue; 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;
} }
haveDownloadTasks = true; }
float downloadPercentage = download.getPercentDownloaded();
if (downloadPercentage != C.PERCENTAGE_UNSET) { int titleStringId;
allDownloadPercentagesUnknown = false; boolean showProgress = true;
totalPercentage += downloadPercentage; 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;
} }
haveDownloadedBytes |= download.getBytesDownloaded() > 0; } else if (haveRemovingTasks) {
downloadTaskCount++; titleStringId = R.string.exo_download_removing;
} else {
// There are either no downloads, or all downloads are in terminal states.
titleStringId = NULL_STRING_ID;
} }
int titleStringId = int maxProgress = 0;
haveDownloadTasks int currentProgress = 0;
? R.string.exo_download_downloading boolean indeterminateProgress = false;
: (haveRemoveTasks ? R.string.exo_download_removing : NULL_STRING_ID); if (showProgress) {
int progress = 0; maxProgress = 100;
boolean indeterminate = true; if (haveDownloadingTasks) {
if (haveDownloadTasks) { currentProgress = (int) (totalPercentage / downloadTaskCount);
progress = (int) (totalPercentage / downloadTaskCount); indeterminateProgress = allDownloadPercentagesUnknown && haveDownloadedBytes;
indeterminate = allDownloadPercentagesUnknown && haveDownloadedBytes; } else {
indeterminateProgress = true;
}
} }
return buildNotification( return buildNotification(
context, context,
smallIcon, smallIcon,
contentIntent, contentIntent,
message, message,
titleStringId, titleStringId,
/* maxProgress= */ 100, maxProgress,
progress, currentProgress,
indeterminate, indeterminateProgress,
/* ongoing= */ true, /* ongoing= */ true,
/* showWhen= */ false); /* showWhen= */ false);
} }
......
...@@ -91,6 +91,12 @@ ...@@ -91,6 +91,12 @@
<string name="exo_download_failed">Download failed</string> <string name="exo_download_failed">Download failed</string>
<!-- Shown in a notification or UI component to indicate downloads are being removed from the device. [CHAR LIMIT=40] --> <!-- Shown in a notification or UI component to indicate downloads are being removed from the device. [CHAR LIMIT=40] -->
<string name="exo_download_removing">Removing downloads</string> <string name="exo_download_removing">Removing downloads</string>
<!-- Shown in a notification or UI component to indicate downloads are paused. [CHAR LIMIT=40] -->
<string name="exo_download_paused">Downloads paused</string>
<!-- Shown in a notification or UI component to indicate downloads are paused waiting for network connectivity. [CHAR LIMIT=40] -->
<string name="exo_download_paused_for_network">Downloads waiting for network</string>
<!-- Shown in a notification or UI component to indicate downloads are paused waiting for WiFi connectivity. [CHAR LIMIT=40] -->
<string name="exo_download_paused_for_wifi">Downloads waiting for WiFi</string>
<!-- The title of a track selection view for video tracks. [CHAR LIMIT=20] --> <!-- The title of a track selection view for video tracks. [CHAR LIMIT=20] -->
<string name="exo_track_selection_title_video">Video</string> <string name="exo_track_selection_title_video">Video</string>
......
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