diff --git a/RELEASENOTES.md b/RELEASENOTES.md
index f9ce2de3a1..5ebf5ef7ad 100644
--- a/RELEASENOTES.md
+++ b/RELEASENOTES.md
@@ -115,6 +115,11 @@
playback if a suitable device is connected within a configurable timeout
(default is 5 minutes).
* Downloads:
+ * Declare "data sync" foreground service type for `DownloadService` for
+ Android 14 compatibility. When using this service, the app also needs to
+ add `dataSync` as `foregroundServiceType` in the manifest and add the
+ `FOREGROUND_SERVICE_DATA_SYNC` permission
+ ([#11239](https://github.com/google/ExoPlayer/issues/11239)).
* OkHttp Extension:
* Cronet Extension:
* RTMP Extension:
diff --git a/demos/main/src/main/AndroidManifest.xml b/demos/main/src/main/AndroidManifest.xml
index 10ce807160..63266bfbd4 100644
--- a/demos/main/src/main/AndroidManifest.xml
+++ b/demos/main/src/main/AndroidManifest.xml
@@ -23,6 +23,7 @@
+
@@ -94,7 +95,8 @@
+ android:exported="false"
+ android:foregroundServiceType="dataSync">
diff --git a/libraries/common/src/main/java/androidx/media3/common/util/Util.java b/libraries/common/src/main/java/androidx/media3/common/util/Util.java
index 6d53487af0..8e76c0bada 100644
--- a/libraries/common/src/main/java/androidx/media3/common/util/Util.java
+++ b/libraries/common/src/main/java/androidx/media3/common/util/Util.java
@@ -36,6 +36,8 @@ import static java.lang.Math.min;
import android.Manifest.permission;
import android.annotation.SuppressLint;
import android.app.Activity;
+import android.app.Notification;
+import android.app.Service;
import android.app.UiModeManager;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
@@ -298,6 +300,36 @@ public final class Util {
}
}
+ /**
+ * Sets the notification required for a foreground service.
+ *
+ * @param service The foreground {@link Service}.
+ * @param notificationId The notification id.
+ * @param notification The {@link Notification}.
+ * @param foregroundServiceType The foreground service type defined in {@link
+ * android.content.pm.ServiceInfo}.
+ * @param foregroundServiceManifestType The required foreground service type string for the {@code
+ * } element in the manifest.
+ */
+ @UnstableApi
+ public static void setForegroundServiceNotification(
+ Service service,
+ int notificationId,
+ Notification notification,
+ int foregroundServiceType,
+ String foregroundServiceManifestType) {
+ if (Util.SDK_INT >= 29) {
+ Api29.startForeground(
+ service,
+ notificationId,
+ notification,
+ foregroundServiceType,
+ foregroundServiceManifestType);
+ } else {
+ service.startForeground(notificationId, notification);
+ }
+ }
+
/**
* Checks whether it's necessary to request the {@link permission#READ_EXTERNAL_STORAGE}
* permission read the specified {@link Uri}s, requesting the permission if necessary.
@@ -3356,4 +3388,29 @@ public final class Util {
return resources.getDrawable(res, context.getTheme());
}
}
+
+ @RequiresApi(29)
+ private static class Api29 {
+
+ @DoNotInline
+ public static void startForeground(
+ Service mediaSessionService,
+ int notificationId,
+ Notification notification,
+ int foregroundServiceType,
+ String foregroundServiceManifestType) {
+ try {
+ // startForeground() will throw if the service's foregroundServiceType is not defined.
+ mediaSessionService.startForeground(notificationId, notification, foregroundServiceType);
+ } catch (RuntimeException e) {
+ Log.e(
+ TAG,
+ "The service must be declared with a foregroundServiceType that includes "
+ + foregroundServiceManifestType);
+ throw e;
+ }
+ }
+
+ private Api29() {}
+ }
}
diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/offline/DownloadService.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/offline/DownloadService.java
index b66d7996b5..9f80fa02e5 100644
--- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/offline/DownloadService.java
+++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/offline/DownloadService.java
@@ -17,11 +17,13 @@ package androidx.media3.exoplayer.offline;
import static androidx.media3.exoplayer.offline.Download.STOP_REASON_NONE;
+import android.annotation.SuppressLint;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
+import android.content.pm.ServiceInfo;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
@@ -910,6 +912,7 @@ public abstract class DownloadService extends Service {
}
}
+ @SuppressLint("InlinedApi") // Using compile time constant FOREGROUND_SERVICE_TYPE_DATA_SYNC
private void update() {
DownloadManager downloadManager =
Assertions.checkNotNull(downloadManagerHelper).downloadManager;
@@ -917,7 +920,12 @@ public abstract class DownloadService extends Service {
@RequirementFlags int notMetRequirements = downloadManager.getNotMetRequirements();
Notification notification = getForegroundNotification(downloads, notMetRequirements);
if (!notificationDisplayed) {
- startForeground(notificationId, notification);
+ Util.setForegroundServiceNotification(
+ /* service= */ DownloadService.this,
+ notificationId,
+ notification,
+ ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC,
+ "dataSync");
notificationDisplayed = true;
} else {
// Update the notification via NotificationManager rather than by repeatedly calling
diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaNotificationManager.java b/libraries/session/src/main/java/androidx/media3/session/MediaNotificationManager.java
index a65bbf2837..a0c9e065a9 100644
--- a/libraries/session/src/main/java/androidx/media3/session/MediaNotificationManager.java
+++ b/libraries/session/src/main/java/androidx/media3/session/MediaNotificationManager.java
@@ -19,6 +19,7 @@ import static android.app.Service.STOP_FOREGROUND_DETACH;
import static android.app.Service.STOP_FOREGROUND_REMOVE;
import static androidx.media3.common.util.Assertions.checkStateNotNull;
+import android.annotation.SuppressLint;
import android.app.Notification;
import android.content.Intent;
import android.content.pm.ServiceInfo;
@@ -346,14 +347,15 @@ import java.util.concurrent.TimeoutException;
}
}
+ @SuppressLint("InlinedApi") // Using compile time constant FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK
private void startForeground(MediaNotification mediaNotification) {
ContextCompat.startForegroundService(mediaSessionService, startSelfIntent);
- if (Util.SDK_INT >= 29) {
- Api29.startForeground(mediaSessionService, mediaNotification);
- } else {
- mediaSessionService.startForeground(
- mediaNotification.notificationId, mediaNotification.notification);
- }
+ Util.setForegroundServiceNotification(
+ mediaSessionService,
+ mediaNotification.notificationId,
+ mediaNotification.notification,
+ ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK,
+ "mediaPlayback");
startedInForeground = true;
}
@@ -380,29 +382,4 @@ import java.util.concurrent.TimeoutException;
private Api24() {}
}
-
- @RequiresApi(29)
- private static class Api29 {
-
- @DoNotInline
- public static void startForeground(
- MediaSessionService mediaSessionService, MediaNotification mediaNotification) {
- try {
- // startForeground() will throw if the service's foregroundServiceType is not defined in the
- // manifest to include mediaPlayback.
- mediaSessionService.startForeground(
- mediaNotification.notificationId,
- mediaNotification.notification,
- ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK);
- } catch (RuntimeException e) {
- Log.e(
- TAG,
- "The service must be declared with a foregroundServiceType that includes "
- + " mediaPlayback");
- throw e;
- }
- }
-
- private Api29() {}
- }
}