diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 9cca7eaf4c..90d2fd8576 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -37,6 +37,8 @@ frames is dequeued without reading the 'end of stream' sample. ([#11079](https://github.com/google/ExoPlayer/issues/11079)). * Add `Builder` for `DeviceInfo` and deprecate existing constructor. + * Add `DeviceInfo.routingControllerId` to specify the routing controller + ID for remote playbacks. * Session: * Deprecate 4 volume-controlling methods in `Player` and add overloaded methods which allow users to specify volume flags: diff --git a/api.txt b/api.txt index 92b67b5971..1981a65991 100644 --- a/api.txt +++ b/api.txt @@ -181,6 +181,7 @@ package androidx.media3.common { field @IntRange(from=0) public final int maxVolume; field @IntRange(from=0) public final int minVolume; field @androidx.media3.common.DeviceInfo.PlaybackType public final int playbackType; + field @Nullable public final String routingControllerId; } public static final class DeviceInfo.Builder { @@ -188,6 +189,7 @@ package androidx.media3.common { method public androidx.media3.common.DeviceInfo build(); method public androidx.media3.common.DeviceInfo.Builder setMaxVolume(@IntRange(from=0) int); method public androidx.media3.common.DeviceInfo.Builder setMinVolume(@IntRange(from=0) int); + method public androidx.media3.common.DeviceInfo.Builder setRoutingControllerId(@Nullable String); } @IntDef({androidx.media3.common.DeviceInfo.PLAYBACK_TYPE_LOCAL, androidx.media3.common.DeviceInfo.PLAYBACK_TYPE_REMOTE}) @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) @java.lang.annotation.Target(java.lang.annotation.ElementType.TYPE_USE) public static @interface DeviceInfo.PlaybackType { diff --git a/libraries/common/src/main/java/androidx/media3/common/DeviceInfo.java b/libraries/common/src/main/java/androidx/media3/common/DeviceInfo.java index 5a8e76c120..62daf34a41 100644 --- a/libraries/common/src/main/java/androidx/media3/common/DeviceInfo.java +++ b/libraries/common/src/main/java/androidx/media3/common/DeviceInfo.java @@ -17,6 +17,7 @@ package androidx.media3.common; import static java.lang.annotation.ElementType.TYPE_USE; +import android.media.MediaRouter2; import android.os.Bundle; import androidx.annotation.IntDef; import androidx.annotation.IntRange; @@ -57,6 +58,7 @@ public final class DeviceInfo implements Bundleable { private int minVolume; private int maxVolume; + @Nullable private String routingControllerId; /** * Creates the builder. @@ -93,6 +95,28 @@ public final class DeviceInfo implements Bundleable { return this; } + /** + * Sets the {@linkplain MediaRouter2.RoutingController#getId() routing controller id} of the + * associated {@link MediaRouter2.RoutingController}. + * + *

This id allows mapping this device information to a routing controller, which provides + * information about the media route and allows controlling its volume. + * + *

The set value must be null if {@link DeviceInfo#playbackType} is {@link + * #PLAYBACK_TYPE_LOCAL}. + * + * @param routingControllerId The {@linkplain MediaRouter2.RoutingController#getId() routing + * controller id} of the associated {@link MediaRouter2.RoutingController}, or null to leave + * it unspecified. + * @return This builder. + */ + @CanIgnoreReturnValue + public Builder setRoutingControllerId(@Nullable String routingControllerId) { + Assertions.checkArgument(playbackType != PLAYBACK_TYPE_LOCAL || routingControllerId == null); + this.routingControllerId = routingControllerId; + return this; + } + /** Builds the {@link DeviceInfo}. */ public DeviceInfo build() { Assertions.checkArgument(minVolume <= maxVolume); @@ -108,6 +132,15 @@ public final class DeviceInfo implements Bundleable { /** The maximum volume that the device supports, or {@code 0} if unspecified. */ @IntRange(from = 0) public final int maxVolume; + /** + * The {@linkplain MediaRouter2.RoutingController#getId() routing controller id} of the associated + * {@link MediaRouter2.RoutingController}, or null if unset or {@link #playbackType} is {@link + * #PLAYBACK_TYPE_LOCAL}. + * + *

This id allows mapping this device information to a routing controller, which provides + * information about the media route and allows controlling its volume. + */ + @Nullable public final String routingControllerId; /** * @deprecated Use {@link Builder} instead. @@ -125,6 +158,7 @@ public final class DeviceInfo implements Bundleable { this.playbackType = builder.playbackType; this.minVolume = builder.minVolume; this.maxVolume = builder.maxVolume; + this.routingControllerId = builder.routingControllerId; } @Override @@ -138,7 +172,8 @@ public final class DeviceInfo implements Bundleable { DeviceInfo other = (DeviceInfo) obj; return playbackType == other.playbackType && minVolume == other.minVolume - && maxVolume == other.maxVolume; + && maxVolume == other.maxVolume + && Util.areEqual(routingControllerId, other.routingControllerId); } @Override @@ -147,6 +182,7 @@ public final class DeviceInfo implements Bundleable { result = 31 * result + playbackType; result = 31 * result + minVolume; result = 31 * result + maxVolume; + result = 31 * result + (routingControllerId == null ? 0 : routingControllerId.hashCode()); return result; } @@ -155,6 +191,7 @@ public final class DeviceInfo implements Bundleable { private static final String FIELD_PLAYBACK_TYPE = Util.intToStringMaxRadix(0); private static final String FIELD_MIN_VOLUME = Util.intToStringMaxRadix(1); private static final String FIELD_MAX_VOLUME = Util.intToStringMaxRadix(2); + private static final String FIELD_ROUTING_CONTROLLER_ID = Util.intToStringMaxRadix(3); @UnstableApi @Override @@ -169,6 +206,9 @@ public final class DeviceInfo implements Bundleable { if (maxVolume != 0) { bundle.putInt(FIELD_MAX_VOLUME, maxVolume); } + if (routingControllerId != null) { + bundle.putString(FIELD_ROUTING_CONTROLLER_ID, routingControllerId); + } return bundle; } @@ -180,9 +220,11 @@ public final class DeviceInfo implements Bundleable { bundle.getInt(FIELD_PLAYBACK_TYPE, /* defaultValue= */ PLAYBACK_TYPE_LOCAL); int minVolume = bundle.getInt(FIELD_MIN_VOLUME, /* defaultValue= */ 0); int maxVolume = bundle.getInt(FIELD_MAX_VOLUME, /* defaultValue= */ 0); + @Nullable String routingControllerId = bundle.getString(FIELD_ROUTING_CONTROLLER_ID); return new DeviceInfo.Builder(playbackType) .setMinVolume(minVolume) .setMaxVolume(maxVolume) + .setRoutingControllerId(routingControllerId) .build(); }; } diff --git a/libraries/common/src/test/java/androidx/media3/common/DeviceInfoTest.java b/libraries/common/src/test/java/androidx/media3/common/DeviceInfoTest.java index b6cd9cb035..8c742f0524 100644 --- a/libraries/common/src/test/java/androidx/media3/common/DeviceInfoTest.java +++ b/libraries/common/src/test/java/androidx/media3/common/DeviceInfoTest.java @@ -31,6 +31,7 @@ public class DeviceInfoTest { new DeviceInfo.Builder(DeviceInfo.PLAYBACK_TYPE_REMOTE) .setMinVolume(1) .setMaxVolume(9) + .setRoutingControllerId("route") .build(); assertThat(DeviceInfo.CREATOR.fromBundle(deviceInfo.toBundle())).isEqualTo(deviceInfo); diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplLegacy.java b/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplLegacy.java index 0e8659e322..88e0658bba 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplLegacy.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplLegacy.java @@ -1453,7 +1453,8 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; controllerCompat.getFlags(), controllerCompat.isSessionReady(), controllerCompat.getRatingType(), - getInstance().getTimeDiffMs()); + getInstance().getTimeDiffMs(), + getRoutingControllerId(controllerCompat)); Pair<@NullableType Integer, @NullableType Integer> reasons = calculateDiscontinuityAndTransitionReason( legacyPlayerInfo, @@ -1642,6 +1643,22 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; listeners.flushEvents(); } + @Nullable + private static String getRoutingControllerId(MediaControllerCompat controllerCompat) { + if (Util.SDK_INT < 30) { + return null; + } + android.media.session.MediaController fwkController = + (android.media.session.MediaController) controllerCompat.getMediaController(); + @Nullable + android.media.session.MediaController.PlaybackInfo playbackInfo = + fwkController.getPlaybackInfo(); + if (playbackInfo == null) { + return null; + } + return playbackInfo.getVolumeControlId(); + } + private static void ignoreFuture(Future unused) { // Ignore return value of the future because legacy session cannot get result back. } @@ -1816,7 +1833,8 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; long sessionFlags, boolean isSessionReady, @RatingCompat.Style int ratingType, - long timeDiffMs) { + long timeDiffMs, + @Nullable String routingControllerId) { QueueTimeline currentTimeline; MediaMetadata mediaMetadata; int currentMediaItemIndex; @@ -1963,7 +1981,8 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; newLegacyPlayerInfo.mediaMetadataCompat, timeDiffMs); boolean isPlaying = MediaUtils.convertToIsPlaying(newLegacyPlayerInfo.playbackStateCompat); - DeviceInfo deviceInfo = MediaUtils.convertToDeviceInfo(newLegacyPlayerInfo.playbackInfoCompat); + DeviceInfo deviceInfo = + MediaUtils.convertToDeviceInfo(newLegacyPlayerInfo.playbackInfoCompat, routingControllerId); int deviceVolume = MediaUtils.convertToDeviceVolume(newLegacyPlayerInfo.playbackInfoCompat); boolean deviceMuted = MediaUtils.convertToIsDeviceMuted(newLegacyPlayerInfo.playbackInfoCompat); long seekBackIncrementMs = oldControllerInfo.playerInfo.seekBackIncrementMs; diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaUtils.java b/libraries/session/src/main/java/androidx/media3/session/MediaUtils.java index 5f6adff951..785a2ff604 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaUtils.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaUtils.java @@ -1331,7 +1331,8 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; /** Converts {@link MediaControllerCompat.PlaybackInfo} to {@link DeviceInfo}. */ public static DeviceInfo convertToDeviceInfo( - @Nullable MediaControllerCompat.PlaybackInfo playbackInfoCompat) { + @Nullable MediaControllerCompat.PlaybackInfo playbackInfoCompat, + @Nullable String routingControllerId) { if (playbackInfoCompat == null) { return DeviceInfo.UNKNOWN; } @@ -1341,6 +1342,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; ? DeviceInfo.PLAYBACK_TYPE_REMOTE : DeviceInfo.PLAYBACK_TYPE_LOCAL) .setMaxVolume(playbackInfoCompat.getMaxVolume()) + .setRoutingControllerId(routingControllerId) .build(); } diff --git a/libraries/session/src/main/java/androidx/media3/session/PlayerWrapper.java b/libraries/session/src/main/java/androidx/media3/session/PlayerWrapper.java index 8086c31d83..25f131ce6e 100644 --- a/libraries/session/src/main/java/androidx/media3/session/PlayerWrapper.java +++ b/libraries/session/src/main/java/androidx/media3/session/PlayerWrapper.java @@ -1028,7 +1028,9 @@ import java.util.List; Handler handler = new Handler(getApplicationLooper()); int currentVolume = getDeviceVolumeWithCommandCheck(); int legacyVolumeFlag = C.VOLUME_FLAG_SHOW_UI; - return new VolumeProviderCompat(volumeControlType, getDeviceInfo().maxVolume, currentVolume) { + DeviceInfo deviceInfo = getDeviceInfo(); + return new VolumeProviderCompat( + volumeControlType, deviceInfo.maxVolume, currentVolume, deviceInfo.routingControllerId) { @Override public void onSetVolumeTo(int volume) { postOrRun( diff --git a/libraries/test_session_common/src/main/aidl/androidx/media3/test/session/common/IRemoteMediaSessionCompat.aidl b/libraries/test_session_common/src/main/aidl/androidx/media3/test/session/common/IRemoteMediaSessionCompat.aidl index 196306d789..b51384d961 100644 --- a/libraries/test_session_common/src/main/aidl/androidx/media3/test/session/common/IRemoteMediaSessionCompat.aidl +++ b/libraries/test_session_common/src/main/aidl/androidx/media3/test/session/common/IRemoteMediaSessionCompat.aidl @@ -29,7 +29,7 @@ interface IRemoteMediaSessionCompat { Bundle getSessionToken(String sessionTag); void release(String sessionTag); void setPlaybackToLocal(String sessionTag, int stream); - void setPlaybackToRemote(String sessionTag, int volumeControl, int maxVolume, int currentVolume); + void setPlaybackToRemote(String sessionTag, int volumeControl, int maxVolume, int currentVolume, @nullable String routingControllerId); void setPlaybackState(String sessionTag, in Bundle stateBundle); void setMetadata(String sessionTag, in Bundle metadataBundle); void setQueue(String sessionTag, in Bundle queueBundle); diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerListenerWithMediaSessionCompatTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerListenerWithMediaSessionCompatTest.java index 299ed7f7c2..b88dff7b8b 100644 --- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerListenerWithMediaSessionCompatTest.java +++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerListenerWithMediaSessionCompatTest.java @@ -246,7 +246,8 @@ public class MediaControllerListenerWithMediaSessionCompatTest { session.setPlaybackToRemote( /* volumeControl= */ VolumeProviderCompat.VOLUME_CONTROL_ABSOLUTE, /* maxVolume= */ 100, - /* currentVolume= */ 50); + /* currentVolume= */ 50, + /* routingSessionId= */ "route"); MediaController controller = controllerTestRule.createController(session.getSessionToken()); CountDownLatch latch = new CountDownLatch(2); AtomicReference audioAttributesParamRef = new AtomicReference<>(); @@ -305,15 +306,18 @@ public class MediaControllerListenerWithMediaSessionCompatTest { } }; threadTestRule.getHandler().postAndSync(() -> controller.addListener(listener)); + String testRoutingSessionId = Util.SDK_INT >= 30 ? "route" : null; session.setPlaybackToRemote( /* volumeControl= */ VolumeProviderCompat.VOLUME_CONTROL_ABSOLUTE, /* maxVolume= */ 100, - /* currentVolume= */ 50); + /* currentVolume= */ 50, + testRoutingSessionId); assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); assertThat(deviceInfoParamRef.get().playbackType).isEqualTo(DeviceInfo.PLAYBACK_TYPE_REMOTE); assertThat(deviceInfoParamRef.get().maxVolume).isEqualTo(100); + assertThat(deviceInfoParamRef.get().routingControllerId).isEqualTo(testRoutingSessionId); assertThat(deviceInfoGetterRef.get()).isEqualTo(deviceInfoParamRef.get()); assertThat(deviceInfoOnEventsRef.get()).isEqualTo(deviceInfoGetterRef.get()); assertThat(getEventsAsList(onEvents.get())).contains(Player.EVENT_DEVICE_VOLUME_CHANGED); @@ -348,7 +352,8 @@ public class MediaControllerListenerWithMediaSessionCompatTest { session.setPlaybackToRemote( /* volumeControl= */ VolumeProviderCompat.VOLUME_CONTROL_ABSOLUTE, /* maxVolume= */ 100, - /* currentVolume= */ 50); + /* currentVolume= */ 50, + /* routingSessionId= */ "route"); assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); assertThat(deviceVolumeParam.get()).isEqualTo(50); diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerWithMediaSessionCompatTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerWithMediaSessionCompatTest.java index 4df4269499..ee779997b8 100644 --- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerWithMediaSessionCompatTest.java +++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerWithMediaSessionCompatTest.java @@ -1548,6 +1548,7 @@ public class MediaControllerWithMediaSessionCompatTest { int volumeControlType = VolumeProviderCompat.VOLUME_CONTROL_ABSOLUTE; int maxVolume = 100; int currentVolume = 45; + String routingSessionId = Util.SDK_INT >= 30 ? "route" : null; AtomicReference deviceInfoRef = new AtomicReference<>(); CountDownLatch latchForDeviceInfo = new CountDownLatch(1); @@ -1572,11 +1573,12 @@ public class MediaControllerWithMediaSessionCompatTest { MediaController controller = controllerTestRule.createController(session.getSessionToken()); threadTestRule.getHandler().postAndSync(() -> controller.addListener(listener)); - session.setPlaybackToRemote(volumeControlType, maxVolume, currentVolume); + session.setPlaybackToRemote(volumeControlType, maxVolume, currentVolume, routingSessionId); assertThat(latchForDeviceInfo.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); assertThat(latchForDeviceVolume.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); assertThat(deviceInfoRef.get().maxVolume).isEqualTo(maxVolume); + assertThat(deviceInfoRef.get().routingControllerId).isEqualTo(routingSessionId); } @Test @@ -1588,7 +1590,8 @@ public class MediaControllerWithMediaSessionCompatTest { session.setPlaybackToRemote( VolumeProviderCompat.VOLUME_CONTROL_ABSOLUTE, /* maxVolume= */ 100, - /* currentVolume= */ 45); + /* currentVolume= */ 45, + /* routingSessionId= */ "route"); int testLocalStreamType = AudioManager.STREAM_ALARM; AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); diff --git a/libraries/test_session_current/src/main/java/androidx/media3/session/MediaSessionCompatProviderService.java b/libraries/test_session_current/src/main/java/androidx/media3/session/MediaSessionCompatProviderService.java index aa4072ad5b..be08fe3b9a 100644 --- a/libraries/test_session_current/src/main/java/androidx/media3/session/MediaSessionCompatProviderService.java +++ b/libraries/test_session_current/src/main/java/androidx/media3/session/MediaSessionCompatProviderService.java @@ -120,11 +120,15 @@ public class MediaSessionCompatProviderService extends Service { @Override public void setPlaybackToRemote( - String sessionTag, int volumeControl, int maxVolume, int currentVolume) + String sessionTag, + int volumeControl, + int maxVolume, + int currentVolume, + @Nullable String routingControllerId) throws RemoteException { MediaSessionCompat session = sessionMap.get(sessionTag); session.setPlaybackToRemote( - new VolumeProviderCompat(volumeControl, maxVolume, currentVolume) { + new VolumeProviderCompat(volumeControl, maxVolume, currentVolume, routingControllerId) { @Override public void onSetVolumeTo(int volume) { setCurrentVolume(volume); diff --git a/libraries/test_session_current/src/main/java/androidx/media3/session/RemoteMediaSessionCompat.java b/libraries/test_session_current/src/main/java/androidx/media3/session/RemoteMediaSessionCompat.java index 870fd32b0c..e190bc39dc 100644 --- a/libraries/test_session_current/src/main/java/androidx/media3/session/RemoteMediaSessionCompat.java +++ b/libraries/test_session_current/src/main/java/androidx/media3/session/RemoteMediaSessionCompat.java @@ -114,12 +114,13 @@ public class RemoteMediaSessionCompat { } /** - * Since we cannot pass VolumeProviderCompat directly, we pass volumeControl, maxVolume, - * currentVolume instead. + * Since we cannot pass VolumeProviderCompat directly, we pass the individual parameters instead. */ - public void setPlaybackToRemote(int volumeControl, int maxVolume, int currentVolume) + public void setPlaybackToRemote( + int volumeControl, int maxVolume, int currentVolume, @Nullable String routingControllerId) throws RemoteException { - binder.setPlaybackToRemote(sessionTag, volumeControl, maxVolume, currentVolume); + binder.setPlaybackToRemote( + sessionTag, volumeControl, maxVolume, currentVolume, routingControllerId); } public void setPlaybackState(PlaybackStateCompat state) throws RemoteException {