diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaStyleNotificationHelper.java b/libraries/session/src/main/java/androidx/media3/session/MediaStyleNotificationHelper.java index d03ac70036..0623064635 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaStyleNotificationHelper.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaStyleNotificationHelper.java @@ -15,8 +15,12 @@ */ package androidx.media3.session; +import static android.Manifest.permission.MEDIA_CONTENT_CONTROL; import static androidx.core.app.NotificationCompat.COLOR_DEFAULT; +import static androidx.media3.common.util.Assertions.checkArgument; +import static androidx.media3.common.util.Assertions.checkNotNull; +import android.annotation.SuppressLint; import android.app.Notification; import android.app.PendingIntent; import android.os.Build; @@ -25,13 +29,16 @@ import android.support.v4.media.session.MediaSessionCompat; import android.view.View; import android.widget.RemoteViews; import androidx.annotation.DoNotInline; +import androidx.annotation.DrawableRes; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; +import androidx.annotation.RequiresPermission; import androidx.core.app.NotificationBuilderWithBuilderAccessor; -import androidx.media3.common.util.Assertions; import androidx.media3.common.util.NullableType; import androidx.media3.common.util.UnstableApi; +import androidx.media3.common.util.Util; import com.google.errorprone.annotations.CanIgnoreReturnValue; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** * Class containing media specfic {@link androidx.core.app.NotificationCompat.Style styles} that you @@ -103,10 +110,14 @@ public class MediaStyleNotificationHelper { private static final int MAX_MEDIA_BUTTONS_IN_COMPACT = 3; private static final int MAX_MEDIA_BUTTONS = 5; - /* package */ MediaSession session; - /* package */ boolean showCancelButton; + /* package */ final MediaSession session; + + private boolean showCancelButton; /* package */ int @NullableType [] actionsToShowInCompact; - /* package */ @Nullable PendingIntent cancelButtonIntent; + @Nullable /* package */ PendingIntent cancelButtonIntent; + /* package */ @MonotonicNonNull CharSequence remoteDeviceName; + /* package */ int remoteDeviceIconRes; + @Nullable /* package */ PendingIntent remoteDeviceIntent; /** * Creates a new instance with a {@link MediaSession} to this Notification to provide additional @@ -172,9 +183,52 @@ public class MediaStyleNotificationHelper { return this; } + /** + * For media notifications associated with playback on a remote device, provide device + * information that will replace the default values for the output switcher chip on the media + * control, as well as an intent to use when the output switcher chip is tapped, on devices + * where this is supported. + * + *
Most apps should integrate with {@link android.media.MediaRouter2} instead. This method is + * only intended for system applications to provide information and/or functionality that would + * otherwise be unavailable to the default output switcher because the media originated on a + * remote device. + * + *
Also note that this method is a no-op when running on API 33 or lower. + * + * @param deviceName The name of the remote device to display. + * @param iconResource Icon resource, of size 12, representing the device. + * @param chipIntent PendingIntent to send when the output switcher is tapped. May be {@code + * null}, in which case the output switcher will be disabled. This intent should open an + * {@link android.app.Activity} or it will be ignored. + */ + @CanIgnoreReturnValue + @RequiresPermission(MEDIA_CONTENT_CONTROL) + public MediaStyle setRemotePlaybackInfo( + CharSequence deviceName, + @DrawableRes int iconResource, + @Nullable PendingIntent chipIntent) { + checkArgument(deviceName != null); + this.remoteDeviceName = deviceName; + this.remoteDeviceIconRes = iconResource; + this.remoteDeviceIntent = chipIntent; + return this; + } + @Override public void apply(NotificationBuilderWithBuilderAccessor builder) { - if (Build.VERSION.SDK_INT >= 21) { + if (Util.SDK_INT >= 34 && remoteDeviceName != null) { + Api21Impl.setMediaStyle( + builder.getBuilder(), + Api21Impl.fillInMediaStyle( + Api34Impl.setRemotePlaybackInfo( + Api21Impl.createMediaStyle(), + remoteDeviceName, + remoteDeviceIconRes, + remoteDeviceIntent), + actionsToShowInCompact, + session)); + } else if (Util.SDK_INT >= 21) { Api21Impl.setMediaStyle( builder.getBuilder(), Api21Impl.fillInMediaStyle( @@ -191,7 +245,7 @@ public class MediaStyleNotificationHelper { @Nullable @SuppressWarnings("nullness:override.return") // NotificationCompat doesn't annotate @Nullable public RemoteViews makeContentView(NotificationBuilderWithBuilderAccessor builder) { - if (Build.VERSION.SDK_INT >= 21) { + if (Util.SDK_INT >= 21) { // No custom content view required return null; } @@ -265,7 +319,7 @@ public class MediaStyleNotificationHelper { @Nullable @SuppressWarnings("nullness:override.return") // NotificationCompat doesn't annotate @Nullable public RemoteViews makeBigContentView(NotificationBuilderWithBuilderAccessor builder) { - if (Build.VERSION.SDK_INT >= 21) { + if (Util.SDK_INT >= 21) { // No custom content view required return null; } @@ -352,8 +406,18 @@ public class MediaStyleNotificationHelper { @Override public void apply(NotificationBuilderWithBuilderAccessor builder) { - - if (Build.VERSION.SDK_INT >= 24) { + if (Util.SDK_INT >= 34 && remoteDeviceName != null) { + Api21Impl.setMediaStyle( + builder.getBuilder(), + Api21Impl.fillInMediaStyle( + Api34Impl.setRemotePlaybackInfo( + Api24Impl.createDecoratedMediaCustomViewStyle(), + remoteDeviceName, + remoteDeviceIconRes, + remoteDeviceIntent), + actionsToShowInCompact, + session)); + } else if (Util.SDK_INT >= 24) { Api21Impl.setMediaStyle( builder.getBuilder(), Api21Impl.fillInMediaStyle( @@ -370,12 +434,12 @@ public class MediaStyleNotificationHelper { @Nullable @SuppressWarnings("nullness:override.return") // NotificationCompat doesn't annotate @Nullable public RemoteViews makeContentView(NotificationBuilderWithBuilderAccessor builder) { - if (Build.VERSION.SDK_INT >= 24) { + if (Util.SDK_INT >= 24) { // No custom content view required return null; } boolean hasContentView = mBuilder.getContentView() != null; - if (Build.VERSION.SDK_INT >= 21) { + if (Util.SDK_INT >= 21) { // If we are on L/M the media notification will only be colored if the expanded // version is of media style, so we have to create a custom view for the collapsed // version as well in that case. @@ -409,7 +473,7 @@ public class MediaStyleNotificationHelper { @Nullable @SuppressWarnings("nullness:override.return") // NotificationCompat doesn't annotate @Nullable public RemoteViews makeBigContentView(NotificationBuilderWithBuilderAccessor builder) { - if (Build.VERSION.SDK_INT >= 24) { + if (Util.SDK_INT >= 24) { // No custom big content view required return null; } @@ -423,7 +487,7 @@ public class MediaStyleNotificationHelper { } RemoteViews bigContentView = generateBigContentView(); buildIntoRemoteViews(bigContentView, innerView); - if (Build.VERSION.SDK_INT >= 21) { + if (Util.SDK_INT >= 21) { setBackgroundColor(bigContentView); } return bigContentView; @@ -440,7 +504,7 @@ public class MediaStyleNotificationHelper { @Nullable @SuppressWarnings("nullness:override.return") // NotificationCompat doesn't annotate @Nullable public RemoteViews makeHeadsUpContentView(NotificationBuilderWithBuilderAccessor builder) { - if (Build.VERSION.SDK_INT >= 24) { + if (Util.SDK_INT >= 24) { // No custom heads up content view required return null; } @@ -454,7 +518,7 @@ public class MediaStyleNotificationHelper { } RemoteViews headsUpContentView = generateBigContentView(); buildIntoRemoteViews(headsUpContentView, innerView); - if (Build.VERSION.SDK_INT >= 21) { + if (Util.SDK_INT >= 21) { setBackgroundColor(headsUpContentView); } return headsUpContentView; @@ -493,8 +557,8 @@ public class MediaStyleNotificationHelper { Notification.MediaStyle style, @Nullable int[] actionsToShowInCompact, MediaSession session) { - Assertions.checkNotNull(style); - Assertions.checkNotNull(session); + checkNotNull(style); + checkNotNull(session); if (actionsToShowInCompact != null) { setShowActionsInCompactView(style, actionsToShowInCompact); } @@ -518,4 +582,23 @@ public class MediaStyleNotificationHelper { return new Notification.DecoratedMediaCustomViewStyle(); } } + + @RequiresApi(34) + private static class Api34Impl { + + private Api34Impl() {} + + // MEDIA_CONTENT_CONTROL permission is required by setRemotePlaybackInfo + @CanIgnoreReturnValue + @SuppressLint({"MissingPermission"}) + @DoNotInline + public static Notification.MediaStyle setRemotePlaybackInfo( + Notification.MediaStyle style, + CharSequence remoteDeviceName, + @DrawableRes int remoteDeviceIconRes, + @Nullable PendingIntent remoteDeviceIntent) { + style.setRemotePlaybackInfo(remoteDeviceName, remoteDeviceIconRes, remoteDeviceIntent); + return style; + } + } }